diff options
Diffstat (limited to 'toolkit/content')
428 files changed, 81676 insertions, 0 deletions
diff --git a/toolkit/content/.eslintrc.js b/toolkit/content/.eslintrc.js new file mode 100644 index 0000000000..ea5a72ce67 --- /dev/null +++ b/toolkit/content/.eslintrc.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.exports = { + env: { + "mozilla/browser-window": true, + }, + + plugins: ["mozilla"], + + rules: { + // XXX Bug 1358949 - This should be reduced down - probably to 20 or to + // be removed & synced with the mozilla/recommended value. + complexity: ["error", 48], + }, +}; diff --git a/toolkit/content/TopLevelVideoDocument.js b/toolkit/content/TopLevelVideoDocument.js new file mode 100644 index 0000000000..f4f80d1e73 --- /dev/null +++ b/toolkit/content/TopLevelVideoDocument.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Hide our variables from the web content, even though the spec allows them +// (and the DOM) to be accessible (see bug 1474832) +{ + // <video> is used for top-level audio documents as well + let videoElement = document.getElementsByTagName("video")[0]; + + let setFocusToVideoElement = function(e) { + // We don't want to retarget focus if it goes to the controls in + // the video element. Because they're anonymous content, the target + // will be the video element in that case. Avoid calling .focus() + // for those events: + if (e && e.target == videoElement) { + return; + } + videoElement.focus(); + }; + + // Redirect focus to the video element whenever the document receives + // focus. + document.addEventListener("focus", setFocusToVideoElement, true); + + // Focus on the video in the newly created document. + setFocusToVideoElement(); + + // Opt out of moving focus away if the DOM tree changes (from add-on or web content) + let observer = new MutationObserver(() => { + observer.disconnect(); + document.removeEventListener("focus", setFocusToVideoElement, true); + }); + observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); + + // Handle fullscreen mode + document.addEventListener("keypress", ev => { + // Maximize the standalone video when pressing F11, + // but ignore audio elements + if ( + ev.key == "F11" && + videoElement.videoWidth != 0 && + videoElement.videoHeight != 0 + ) { + // If we're in browser fullscreen mode, it means the user pressed F11 + // while browser chrome or another tab had focus. + // Don't break leaving that mode, so do nothing here. + if (window.fullScreen) { + return; + } + + // If we're not in browser fullscreen mode, prevent entering into that, + // so we don't end up there after pressing Esc. + ev.preventDefault(); + ev.stopPropagation(); + + if (!document.fullscreenElement) { + videoElement.requestFullscreen(); + } else { + document.exitFullscreen(); + } + } + }); +} diff --git a/toolkit/content/about.js b/toolkit/content/about.js new file mode 100644 index 0000000000..93436e659a --- /dev/null +++ b/toolkit/content/about.js @@ -0,0 +1,32 @@ +/* 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/. */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// get release notes and vendor URL from prefs +var releaseNotesURL = Services.urlFormatter.formatURLPref( + "app.releaseNotesURL" +); +if (releaseNotesURL != "about:blank") { + var relnotes = document.getElementById("releaseNotesURL"); + relnotes.setAttribute("href", releaseNotesURL); + relnotes.parentNode.removeAttribute("hidden"); +} + +var vendorURL = Services.urlFormatter.formatURLPref("app.vendorURL"); +if (vendorURL != "about:blank") { + var vendor = document.getElementById("vendorURL"); + vendor.setAttribute("href", vendorURL); +} + +// insert the version of the XUL application (!= XULRunner platform version) +var versionNum = Services.appinfo.version; +var version = document.getElementById("version"); +version.textContent += " " + versionNum; + +// append user agent +var ua = navigator.userAgent; +if (ua) { + document.getElementById("buildID").textContent += " " + ua; +} diff --git a/toolkit/content/aboutAbout.html b/toolkit/content/aboutAbout.html new file mode 100644 index 0000000000..b5fb77a388 --- /dev/null +++ b/toolkit/content/aboutAbout.html @@ -0,0 +1,26 @@ + +<!DOCTYPE html> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" > + <title data-l10n-id="about-about-title"></title> + <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css"> + <link rel="localization" href="toolkit/about/aboutAbout.ftl"> + <script src="chrome://global/content/aboutAbout.js"></script> + +</head> + +<body> + <div class="container"> + <h1 data-l10n-id="about-about-title"></h1> + <p><em data-l10n-id="about-about-note"></em></p> + <ul id="abouts" class="columns"></ul> + </div> +</body> +</html> diff --git a/toolkit/content/aboutAbout.js b/toolkit/content/aboutAbout.js new file mode 100644 index 0000000000..013aaec744 --- /dev/null +++ b/toolkit/content/aboutAbout.js @@ -0,0 +1,24 @@ +/* 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/. */ + +const { AboutPagesUtils } = ChromeUtils.import( + "resource://gre/modules/AboutPagesUtils.jsm" +); + +var gContainer; +window.onload = function() { + gContainer = document.getElementById("abouts"); + AboutPagesUtils.visibleAboutUrls.forEach(createProtocolListing); +}; + +function createProtocolListing(aUrl) { + var li = document.createElement("li"); + var link = document.createElement("a"); + var text = document.createTextNode(aUrl); + + link.href = aUrl; + link.appendChild(text); + li.appendChild(link); + gContainer.appendChild(li); +} diff --git a/toolkit/content/aboutGlean.css b/toolkit/content/aboutGlean.css new file mode 100644 index 0000000000..c560df750e --- /dev/null +++ b/toolkit/content/aboutGlean.css @@ -0,0 +1,5 @@ +/* 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/. */ + +@import url("chrome://global/skin/in-content/common.css"); diff --git a/toolkit/content/aboutGlean.html b/toolkit/content/aboutGlean.html new file mode 100644 index 0000000000..9206b77a0a --- /dev/null +++ b/toolkit/content/aboutGlean.html @@ -0,0 +1,48 @@ +<!-- 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/. --> + +<!DOCTYPE html> + +<html> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'"/> + <title data-l10n-id="about-glean-page-title"></title> + <link rel="stylesheet" href="chrome://global/content/aboutGlean.css" + type="text/css"/> + + <script src="chrome://global/content/aboutGlean.js"></script> + <link rel="localization" href="branding/brand.ftl"/> + <link rel="localization" href="toolkit/about/aboutGlean.ftl"/> + </head> + + <body> + <section class="main-content"> + <div id="description"> + <p data-l10n-id="about-glean-description"> + <a data-l10n-name="glean-sdk-doc-link" href="https://mozilla.github.io/glean/book/index.html"></a> + <a data-l10n-name="fog-debug-doc-link" href="https://firefox-source-docs.mozilla.org/toolkit/components/glean/testing.html"></a> + </p> + <p data-l10n-id="about-glean-warning"></p> + </div> + <div id="controls"> + <div id="tag-pings-section"> + <label for="tag-pings" data-l10n-id="tag-pings-label"></label> + <input type="text" name="tag-pings" id="tag-pings"> + </div> + <div id="log-pings-section"> + <label for="log-pings" data-l10n-id="log-pings-label"></label> + <input type="checkbox" name="log-pings" id="log-pings"> + </div> + <div id="send-pings-section"> + <label for="send-pings" data-l10n-id="send-pings-label"></label> + <input type="text" name="send-pings" id="send-pings"> + </div> + <div id="submit"> + <button id="controls-submit" data-l10n-id="controls-button-label"></button> + </div> + </div> + </section> + </body> + +</html> diff --git a/toolkit/content/aboutGlean.js b/toolkit/content/aboutGlean.js new file mode 100644 index 0000000000..8ff0b8852a --- /dev/null +++ b/toolkit/content/aboutGlean.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function onLoad() { + document.getElementById("controls-submit").addEventListener("click", () => { + let tag = document.getElementById("tag-pings").value; + let log = document.getElementById("log-pings").checked; + let send = document.getElementById("send-pings").value; + let FOG = Cc["@mozilla.org/toolkit/glean;1"].createInstance(Ci.nsIFOG); + FOG.setLogPings(log); + FOG.setTagPings(tag); + FOG.sendPing(send); + }); +} + +window.addEventListener("load", onLoad); diff --git a/toolkit/content/aboutMozilla.css b/toolkit/content/aboutMozilla.css new file mode 100644 index 0000000000..6babca7f26 --- /dev/null +++ b/toolkit/content/aboutMozilla.css @@ -0,0 +1,35 @@ +/* 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/. */ + +html { + background: maroon radial-gradient( circle, #a01010 0%, #800000 80%) center center / cover no-repeat; + color: white; + font-style: italic; + text-rendering: optimizeLegibility; + min-height: 100%; +} + +#moztext { + margin-top: 15%; + font-size: 1.1em; + font-family: serif; + text-align: center; + line-height: 1.5; +} + +#from { + font-size: 1.95em; + font-family: serif; + text-align: end; +} + +em { + font-size: 1.3em; + line-height: 0; +} + +a { + text-decoration: none; + color: white; +} diff --git a/toolkit/content/aboutNetworking.html b/toolkit/content/aboutNetworking.html new file mode 100644 index 0000000000..870799b20a --- /dev/null +++ b/toolkit/content/aboutNetworking.html @@ -0,0 +1,287 @@ +<!-- 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/. --> + + +<!DOCTYPE html> + +<html> + <head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'"> + <title data-l10n-id="about-networking-title"></title> + <link rel="stylesheet" href="chrome://global/skin/aboutNetworking.css"> + <script src="chrome://global/content/aboutNetworking.js"></script> + <link rel="localization" href="toolkit/about/aboutNetworking.ftl"> + </head> + <body id="body"> + <div id="categories"> + <div class="category category-no-icon" selected="true" id="category-http"> + <span class="category-name" data-l10n-id="about-networking-http"></span> + </div> + <div class="category category-no-icon" id="category-sockets"> + <span class="category-name" data-l10n-id="about-networking-sockets"></span> + </div> + <div class="category category-no-icon" id="category-dns"> + <span class="category-name" data-l10n-id="about-networking-dns"></span> + </div> + <div class="category category-no-icon" id="category-websockets"> + <span class="category-name" data-l10n-id="about-networking-websockets"></span> + </div> + <hr> + <div class="category category-no-icon" id="category-dnslookuptool"> + <span class="category-name" data-l10n-id="about-networking-dns-lookup"></span> + </div> + <div class="category category-no-icon" id="category-logging"> + <span class="category-name" data-l10n-id="about-networking-logging"></span> + </div> + <div class="category category-no-icon" id="category-rcwn"> + <span class="category-name" data-l10n-id="about-networking-rcwn"></span> + </div> + <div class="category category-no-icon" id="category-networkid"> + <span class="category-name" data-l10n-id="about-networking-networkid"></span> + </div> + </div> + <div class="main-content"> + <div class="header"> + <div id="sectionTitle" class="header-name" data-l10n-id="about-networking-http"></div> + <div id="refreshDiv" class="toggle-container-with-text"> + <button id="refreshButton" data-l10n-id="about-networking-refresh"></button> + <input id="autorefcheck" type="checkbox" name="Autorefresh" role="checkbox"> + <label for="autorefcheck" data-l10n-id="about-networking-auto-refresh"></label> + </div> + </div> + + <div id="http" class="tab active"> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-hostname"></th> + <th data-l10n-id="about-networking-port"></th> + <th data-l10n-id="about-networking-http-version"></th> + <th data-l10n-id="about-networking-ssl"></th> + <th data-l10n-id="about-networking-active"></th> + <th data-l10n-id="about-networking-idle"></th> + </tr> + </thead> + <tbody id="http_content"></tbody> + </table> + </div> + + <div id="sockets" class="tab" hidden="true"> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-host"></th> + <th data-l10n-id="about-networking-port"></th> + <th data-l10n-id="about-networking-tcp"></th> + <th data-l10n-id="about-networking-active"></th> + <th data-l10n-id="about-networking-sent"></th> + <th data-l10n-id="about-networking-received"></th> + </tr> + </thead> + <tbody id="sockets_content"></tbody> + </table> + </div> + + <div id="dns" class="tab" hidden="true"> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-dns-suffix"></th> + </tr> + </thead> + <tbody id="dns_suffix_content"></tbody> + </table> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-dns-trr-url"></th> + </tr> + </thead> + <tbody id="dns_trr_url"></tbody> + </table> + <br><br> + <button id="clearDNSCache" data-l10n-id="about-networking-dns-clear-cache-button"></button> + <br><br> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-hostname"></th> + <th data-l10n-id="about-networking-family"></th> + <th data-l10n-id="about-networking-trr"></th> + <th data-l10n-id="about-networking-addresses"></th> + <th data-l10n-id="about-networking-expires"></th> + <th data-l10n-id="about-networking-originAttributesSuffix"></th> + </tr> + </thead> + <tbody id="dns_content"></tbody> + </table> + </div> + + <div id="websockets" class="tab" hidden="true"> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-hostname"></th> + <th data-l10n-id="about-networking-ssl"></th> + <th data-l10n-id="about-networking-messages-sent"></th> + <th data-l10n-id="about-networking-messages-received"></th> + <th data-l10n-id="about-networking-bytes-sent"></th> + <th data-l10n-id="about-networking-bytes-received"></th> + </tr> + </thead> + <tbody id="websockets_content"></tbody> + </table> + </div> + + <div id="dnslookuptool" class="tab" hidden="true"> + <label data-l10n-id="about-networking-dns-domain"></label> + <input type="text" name="host" id="host"> + <button id="dnsLookupButton" data-l10n-id="about-networking-dns-lookup-button"></button> + <hr> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-dns-lookup-table-column"></th> + </tr> + </thead> + <tbody id="dnslookuptool_content"></tbody> + </table> + <hr> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-dns-https-rr-lookup-table-column"></th> + </tr> + </thead> + <tbody id="https_rr_content"></tbody> + </table> + </div> + + <div id="rcwn" class="tab" hidden="true"> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-rcwn-status"></th> + <th data-l10n-id="about-networking-total-network-requests"></th> + <th data-l10n-id="about-networking-rcwn-cache-won-count"></th> + <th data-l10n-id="about-networking-rcwn-net-won-count"></th> + </tr> + </thead> + <tbody id="rcwn_content"> + <tr> + <td id="rcwn_status"> </td> + <td id="total_req_count"> </td> + <td id="rcwn_cache_won_count"> </td> + <td id="rcwn_cache_net_count"> </td> + </tr> + </tbody> + </table> + + <br><br> + + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-rcwn-operation"></th> + <th data-l10n-id="about-networking-rcwn-avg-short"></th> + <th data-l10n-id="about-networking-rcwn-avg-long"></th> + <th data-l10n-id="about-networking-rcwn-std-dev-long"></th> + </tr> + </thead> + <tbody id="cacheperf_content"> + <tr> + <td data-l10n-id="about-networking-rcwn-perf-open"></td> + <td id="rcwn_perfstats_open_avgShort"> </td> + <td id="rcwn_perfstats_open_avgLong"> </td> + <td id="rcwn_perfstats_open_stddevLong"> </td> + </tr> + <tr> + <td data-l10n-id="about-networking-rcwn-perf-read"></td> + <td id="rcwn_perfstats_read_avgShort"> </td> + <td id="rcwn_perfstats_read_avgLong"> </td> + <td id="rcwn_perfstats_read_stddevLong"> </td> + </tr> + <tr> + <td data-l10n-id="about-networking-rcwn-perf-write"></td> + <td id="rcwn_perfstats_write_avgShort"> </td> + <td id="rcwn_perfstats_write_avgLong"> </td> + <td id="rcwn_perfstats_write_stddevLong"> </td> + </tr> + <tr> + <td data-l10n-id="about-networking-rcwn-perf-entry-open"></td> + <td id="rcwn_perfstats_entryopen_avgShort"> </td> + <td id="rcwn_perfstats_entryopen_avgLong"> </td> + <td id="rcwn_perfstats_entryopen_stddevLong"> </td> + </tr> + </tbody> + </table> + + <br><br> + + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-rcwn-cache-slow"></th> + <th data-l10n-id="about-networking-rcwn-cache-not-slow"></th> + </tr> + </thead> + <tbody> + <tr> + <td id="rcwn_cache_slow"> </td> + <td id="rcwn_cache_not_slow"> </td> + </tr> + </tbody> + </table> + </div> + + <div id="logging" class="tab" hidden="true"> + <div> + <button id="start-logging-button" data-l10n-id="about-networking-start-logging"></button> + <button id="stop-logging-button" data-l10n-id="about-networking-stop-logging"></button> + </div> + <br> + <br> + <div> + <label data-l10n-id="about-networking-current-log-file"></label> + <div id="current-log-file"></div><br> + <input type="text" name="log-file" id="log-file"> + <button id="set-log-file-button" data-l10n-id="about-networking-set-log-file"></button> + </div> + <div> + <label data-l10n-id="about-networking-current-log-modules"></label> + <div id="current-log-modules"></div><br> + <input type="text" name="log-modules" id="log-modules" value="timestamp,sync,nsHttp:5,cache2:5,nsSocketTransport:5,nsHostResolver:5"> + <button id="set-log-modules-button" data-l10n-id="about-networking-set-log-modules"></button> + </div> + <br> + <br> + + <p id="log-tutorial" data-l10n-id="about-networking-log-tutorial"> + <a data-l10n-name="logging" href="https://developer.mozilla.org/docs/Mozilla/Debugging/HTTP_logging"></a> + </p> + </div> + + <div id="networkid" class="tab" hidden="true"> + <table> + <thead> + <tr> + <th data-l10n-id="about-networking-networkid-is-up"></th> + <th data-l10n-id="about-networking-networkid-status-known"></th> + <th data-l10n-id="about-networking-networkid-id"></th> + </tr> + </thead> + <tbody id="networkid_content"> + <tr> + <td id="networkid_isUp"> </td> + <td id="networkid_statusKnown"> </td> + <td id="networkid_id"> </td> + </tr> + </tbody> + </table> + </div> + + </div> + </body> +</html> diff --git a/toolkit/content/aboutNetworking.js b/toolkit/content/aboutNetworking.js new file mode 100644 index 0000000000..25ee33507a --- /dev/null +++ b/toolkit/content/aboutNetworking.js @@ -0,0 +1,593 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const FileUtils = ChromeUtils.import("resource://gre/modules/FileUtils.jsm") + .FileUtils; +const gEnv = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment +); +const gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService( + Ci.nsIDashboard +); +const gDirServ = Cc["@mozilla.org/file/directory_service;1"].getService( + Ci.nsIDirectoryServiceProvider +); +const gNetLinkSvc = + Cc["@mozilla.org/network/network-link-service;1"] && + Cc["@mozilla.org/network/network-link-service;1"].getService( + Ci.nsINetworkLinkService + ); +const gDNSService = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); + +const gRequestNetworkingData = { + http: gDashboard.requestHttpConnections, + sockets: gDashboard.requestSockets, + dns: gDashboard.requestDNSInfo, + websockets: gDashboard.requestWebsocketConnections, + dnslookuptool: () => {}, + logging: () => {}, + rcwn: gDashboard.requestRcwnStats, + networkid: displayNetworkID, +}; +const gDashboardCallbacks = { + http: displayHttp, + sockets: displaySockets, + dns: displayDns, + websockets: displayWebsockets, + rcwn: displayRcwnStats, +}; + +const REFRESH_INTERVAL_MS = 3000; + +function col(element) { + let col = document.createElement("td"); + let content = document.createTextNode(element); + col.appendChild(content); + return col; +} + +function displayHttp(data) { + let cont = document.getElementById("http_content"); + let parent = cont.parentNode; + let new_cont = document.createElement("tbody"); + new_cont.setAttribute("id", "http_content"); + + for (let i = 0; i < data.connections.length; i++) { + let row = document.createElement("tr"); + row.appendChild(col(data.connections[i].host)); + row.appendChild(col(data.connections[i].port)); + row.appendChild(col(data.connections[i].httpVersion)); + row.appendChild(col(data.connections[i].ssl)); + row.appendChild(col(data.connections[i].active.length)); + row.appendChild(col(data.connections[i].idle.length)); + new_cont.appendChild(row); + } + + parent.replaceChild(new_cont, cont); +} + +function displaySockets(data) { + let cont = document.getElementById("sockets_content"); + let parent = cont.parentNode; + let new_cont = document.createElement("tbody"); + new_cont.setAttribute("id", "sockets_content"); + + for (let i = 0; i < data.sockets.length; i++) { + let row = document.createElement("tr"); + row.appendChild(col(data.sockets[i].host)); + row.appendChild(col(data.sockets[i].port)); + row.appendChild(col(data.sockets[i].tcp)); + row.appendChild(col(data.sockets[i].active)); + row.appendChild(col(data.sockets[i].sent)); + row.appendChild(col(data.sockets[i].received)); + new_cont.appendChild(row); + } + + parent.replaceChild(new_cont, cont); +} + +function displayDns(data) { + let suffixContent = document.getElementById("dns_suffix_content"); + let suffixParent = suffixContent.parentNode; + let suffixes = []; + try { + suffixes = gNetLinkSvc.dnsSuffixList; // May throw + } catch (e) {} + let suffix_tbody = document.createElement("tbody"); + suffix_tbody.id = "dns_suffix_content"; + for (let suffix of suffixes) { + let row = document.createElement("tr"); + row.appendChild(col(suffix)); + suffix_tbody.appendChild(row); + } + suffixParent.replaceChild(suffix_tbody, suffixContent); + + let trr_url_tbody = document.createElement("tbody"); + trr_url_tbody.id = "dns_trr_url"; + let trr_url = document.createElement("tr"); + trr_url.appendChild(col(gDNSService.currentTrrURI)); + trr_url_tbody.appendChild(trr_url); + let prevURL = document.getElementById("dns_trr_url"); + prevURL.parentNode.replaceChild(trr_url_tbody, prevURL); + + let cont = document.getElementById("dns_content"); + let parent = cont.parentNode; + let new_cont = document.createElement("tbody"); + new_cont.setAttribute("id", "dns_content"); + + for (let i = 0; i < data.entries.length; i++) { + let row = document.createElement("tr"); + row.appendChild(col(data.entries[i].hostname)); + row.appendChild(col(data.entries[i].family)); + row.appendChild(col(data.entries[i].trr)); + let column = document.createElement("td"); + + for (let j = 0; j < data.entries[i].hostaddr.length; j++) { + column.appendChild(document.createTextNode(data.entries[i].hostaddr[j])); + column.appendChild(document.createElement("br")); + } + + row.appendChild(column); + row.appendChild(col(data.entries[i].expiration)); + row.appendChild(col(data.entries[i].originAttributesSuffix)); + new_cont.appendChild(row); + } + + parent.replaceChild(new_cont, cont); +} + +function displayWebsockets(data) { + let cont = document.getElementById("websockets_content"); + let parent = cont.parentNode; + let new_cont = document.createElement("tbody"); + new_cont.setAttribute("id", "websockets_content"); + + for (let i = 0; i < data.websockets.length; i++) { + let row = document.createElement("tr"); + row.appendChild(col(data.websockets[i].hostport)); + row.appendChild(col(data.websockets[i].encrypted)); + row.appendChild(col(data.websockets[i].msgsent)); + row.appendChild(col(data.websockets[i].msgreceived)); + row.appendChild(col(data.websockets[i].sentsize)); + row.appendChild(col(data.websockets[i].receivedsize)); + new_cont.appendChild(row); + } + + parent.replaceChild(new_cont, cont); +} + +function displayRcwnStats(data) { + let status = Services.prefs.getBoolPref("network.http.rcwn.enabled"); + let linkType = Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN; + try { + linkType = gNetLinkSvc.linkType; + } catch (e) {} + if ( + !( + linkType == Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN || + linkType == Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET || + linkType == Ci.nsINetworkLinkService.LINK_TYPE_USB || + linkType == Ci.nsINetworkLinkService.LINK_TYPE_WIFI + ) + ) { + status = false; + } + + let cacheWon = data.rcwnCacheWonCount; + let netWon = data.rcwnNetWonCount; + let total = data.totalNetworkRequests; + let cacheSlow = data.cacheSlowCount; + let cacheNotSlow = data.cacheNotSlowCount; + + document.getElementById("rcwn_status").innerText = status; + document.getElementById("total_req_count").innerText = total; + document.getElementById("rcwn_cache_won_count").innerText = cacheWon; + document.getElementById("rcwn_cache_net_count").innerText = netWon; + document.getElementById("rcwn_cache_slow").innerText = cacheSlow; + document.getElementById("rcwn_cache_not_slow").innerText = cacheNotSlow; + + // Keep in sync with CachePerfStats::EDataType in CacheFileUtils.h + const perfStatTypes = ["open", "read", "write", "entryopen"]; + + const perfStatFieldNames = ["avgShort", "avgLong", "stddevLong"]; + + for (let typeIndex in perfStatTypes) { + for (let statFieldIndex in perfStatFieldNames) { + document.getElementById( + "rcwn_perfstats_" + + perfStatTypes[typeIndex] + + "_" + + perfStatFieldNames[statFieldIndex] + ).innerText = + data.perfStats[typeIndex][perfStatFieldNames[statFieldIndex]]; + } + } +} + +function displayNetworkID() { + try { + let linkIsUp = gNetLinkSvc.isLinkUp; + let linkStatusKnown = gNetLinkSvc.linkStatusKnown; + let networkID = gNetLinkSvc.networkID; + + document.getElementById("networkid_isUp").innerText = linkIsUp; + document.getElementById( + "networkid_statusKnown" + ).innerText = linkStatusKnown; + document.getElementById("networkid_id").innerText = networkID; + } catch (e) { + document.getElementById("networkid_isUp").innerText = "<unknown>"; + document.getElementById("networkid_statusKnown").innerText = "<unknown>"; + document.getElementById("networkid_id").innerText = "<unknown>"; + } +} + +function requestAllNetworkingData() { + for (let id in gRequestNetworkingData) { + requestNetworkingDataForTab(id); + } +} + +function requestNetworkingDataForTab(id) { + gRequestNetworkingData[id](gDashboardCallbacks[id]); +} + +let gInited = false; +function init() { + if (gInited) { + return; + } + gInited = true; + gDashboard.enableLogging = true; + + requestAllNetworkingData(); + + let autoRefresh = document.getElementById("autorefcheck"); + if (autoRefresh.checked) { + setAutoRefreshInterval(autoRefresh); + } + + autoRefresh.addEventListener("click", function() { + let refrButton = document.getElementById("refreshButton"); + if (this.checked) { + setAutoRefreshInterval(this); + refrButton.disabled = "disabled"; + } else { + clearInterval(this.interval); + refrButton.disabled = null; + } + }); + + let refr = document.getElementById("refreshButton"); + refr.addEventListener("click", requestAllNetworkingData); + if (document.getElementById("autorefcheck").checked) { + refr.disabled = "disabled"; + } + + // Event delegation on #categories element + let menu = document.getElementById("categories"); + menu.addEventListener("click", function click(e) { + if (e.target && e.target.parentNode == menu) { + show(e.target); + } + }); + + let dnsLookupButton = document.getElementById("dnsLookupButton"); + dnsLookupButton.addEventListener("click", function() { + doLookup(); + }); + + let clearDNSCache = document.getElementById("clearDNSCache"); + clearDNSCache.addEventListener("click", function() { + gDNSService.clearCache(true); + }); + + let setLogButton = document.getElementById("set-log-file-button"); + setLogButton.addEventListener("click", setLogFile); + + let setModulesButton = document.getElementById("set-log-modules-button"); + setModulesButton.addEventListener("click", setLogModules); + + let startLoggingButton = document.getElementById("start-logging-button"); + startLoggingButton.addEventListener("click", startLogging); + + let stopLoggingButton = document.getElementById("stop-logging-button"); + stopLoggingButton.addEventListener("click", stopLogging); + + try { + let file = gDirServ.getFile("TmpD", {}); + file.append("log.txt"); + document.getElementById("log-file").value = file.path; + } catch (e) { + console.error(e); + } + + // Update the value of the log file. + updateLogFile(); + + // Update the active log modules + updateLogModules(); + + // If we can't set the file and the modules at runtime, + // the start and stop buttons wouldn't really do anything. + if (setLogButton.disabled && setModulesButton.disabled) { + startLoggingButton.disabled = true; + stopLoggingButton.disabled = true; + } + + if (location.hash) { + let sectionButton = document.getElementById( + "category-" + location.hash.substring(1) + ); + if (sectionButton) { + sectionButton.click(); + } + } +} + +function updateLogFile() { + let logPath = ""; + + // Try to get the environment variable for the log file + logPath = gEnv.get("MOZ_LOG_FILE") || gEnv.get("NSPR_LOG_FILE"); + let currentLogFile = document.getElementById("current-log-file"); + let setLogFileButton = document.getElementById("set-log-file-button"); + + // If the log file was set from an env var, we disable the ability to set it + // at runtime. + if (logPath.length) { + currentLogFile.innerText = logPath; + setLogFileButton.disabled = true; + } else { + // There may be a value set by a pref. + currentLogFile.innerText = gDashboard.getLogPath(); + } +} + +function updateLogModules() { + // Try to get the environment variable for the log file + let logModules = + gEnv.get("MOZ_LOG") || + gEnv.get("MOZ_LOG_MODULES") || + gEnv.get("NSPR_LOG_MODULES"); + let currentLogModules = document.getElementById("current-log-modules"); + let setLogModulesButton = document.getElementById("set-log-modules-button"); + if (logModules.length) { + currentLogModules.innerText = logModules; + // If the log modules are set by an environment variable at startup, do not + // allow changing them throught a pref. It would be difficult to figure out + // which ones are enabled and which ones are not. The user probably knows + // what he they are doing. + setLogModulesButton.disabled = true; + } else { + let activeLogModules = []; + try { + if (Services.prefs.getBoolPref("logging.config.add_timestamp")) { + activeLogModules.push("timestamp"); + } + } catch (e) {} + try { + if (Services.prefs.getBoolPref("logging.config.sync")) { + activeLogModules.push("sync"); + } + } catch (e) {} + + let children = Services.prefs.getBranch("logging.").getChildList(""); + + for (let pref of children) { + if (pref.startsWith("config.")) { + continue; + } + + try { + let value = Services.prefs.getIntPref(`logging.${pref}`); + activeLogModules.push(`${pref}:${value}`); + } catch (e) { + console.error(e); + } + } + + currentLogModules.innerText = activeLogModules.join(","); + } +} + +function setLogFile() { + let setLogButton = document.getElementById("set-log-file-button"); + if (setLogButton.disabled) { + // There's no point trying since it wouldn't work anyway. + return; + } + let logFile = document.getElementById("log-file").value.trim(); + Services.prefs.setCharPref("logging.config.LOG_FILE", logFile); + updateLogFile(); +} + +function clearLogModules() { + // Turn off all the modules. + let children = Services.prefs.getBranch("logging.").getChildList(""); + for (let pref of children) { + if (!pref.startsWith("config.")) { + Services.prefs.clearUserPref(`logging.${pref}`); + } + } + Services.prefs.clearUserPref("logging.config.add_timestamp"); + Services.prefs.clearUserPref("logging.config.sync"); + updateLogModules(); +} + +function setLogModules() { + let setLogModulesButton = document.getElementById("set-log-modules-button"); + if (setLogModulesButton.disabled) { + // The modules were set via env var, so we shouldn't try to change them. + return; + } + + let modules = document.getElementById("log-modules").value.trim(); + + // Clear previously set log modules. + clearLogModules(); + + let logModules = modules.split(","); + for (let module of logModules) { + if (module == "timestamp") { + Services.prefs.setBoolPref("logging.config.add_timestamp", true); + } else if (module == "rotate") { + // XXX: rotate is not yet supported. + } else if (module == "append") { + // XXX: append is not yet supported. + } else if (module == "sync") { + Services.prefs.setBoolPref("logging.config.sync", true); + } else { + let lastColon = module.lastIndexOf(":"); + let key = module.slice(0, lastColon); + let value = parseInt(module.slice(lastColon + 1), 10); + Services.prefs.setIntPref(`logging.${key}`, value); + } + } + + updateLogModules(); +} + +function startLogging() { + setLogFile(); + setLogModules(); +} + +function stopLogging() { + clearLogModules(); + // clear the log file as well + Services.prefs.clearUserPref("logging.config.LOG_FILE"); + updateLogFile(); +} + +function show(button) { + let current_tab = document.querySelector(".active"); + let category = button.getAttribute("id").substring("category-".length); + let content = document.getElementById(category); + if (current_tab == content) { + return; + } + current_tab.classList.remove("active"); + current_tab.hidden = true; + content.classList.add("active"); + content.hidden = false; + + let current_button = document.querySelector("[selected=true]"); + current_button.removeAttribute("selected"); + button.setAttribute("selected", "true"); + + let autoRefresh = document.getElementById("autorefcheck"); + if (autoRefresh.checked) { + clearInterval(autoRefresh.interval); + setAutoRefreshInterval(autoRefresh); + } + + let title = document.getElementById("sectionTitle"); + title.textContent = button.children[0].textContent; + location.hash = category; +} + +function setAutoRefreshInterval(checkBox) { + let active_tab = document.querySelector(".active"); + checkBox.interval = setInterval(function() { + requestNetworkingDataForTab(active_tab.id); + }, REFRESH_INTERVAL_MS); +} + +// We use the pageshow event instead of onload. This is needed because sometimes +// the page is loaded via session-restore/bfcache. In such cases we need to call +// init() to keep the page behaviour consistent with the ticked checkboxes. +// Mostly the issue is with the autorefresh checkbox. +window.addEventListener("pageshow", function() { + init(); +}); + +function doLookup() { + let host = document.getElementById("host").value; + if (host) { + try { + gDashboard.requestDNSLookup(host, displayDNSLookup); + } catch (e) {} + try { + gDashboard.requestDNSHTTPSRRLookup(host, displayHTTPSRRLookup); + } catch (e) {} + } +} + +function displayDNSLookup(data) { + let cont = document.getElementById("dnslookuptool_content"); + let parent = cont.parentNode; + let new_cont = document.createElement("tbody"); + new_cont.setAttribute("id", "dnslookuptool_content"); + + if (data.answer) { + for (let address of data.address) { + let row = document.createElement("tr"); + row.appendChild(col(address)); + new_cont.appendChild(row); + } + } else { + new_cont.appendChild(col(data.error)); + } + + parent.replaceChild(new_cont, cont); +} + +function displayHTTPSRRLookup(data) { + let cont = document.getElementById("https_rr_content"); + let parent = cont.parentNode; + let new_cont = document.createElement("tbody"); + new_cont.setAttribute("id", "https_rr_content"); + + if (data.answer) { + for (let record of data.records) { + let row = document.createElement("tr"); + let alpn = record.alpn ? `alpn="${record.alpn.alpn}" ` : ""; + let noDefaultAlpn = record.noDefaultAlpn ? "noDefaultAlpn " : ""; + let port = record.port ? `port="${record.port.port}" ` : ""; + let echConfig = record.echConfig + ? `echConfig="${record.echConfig.echConfig}" ` + : ""; + let ODoHConfig = record.ODoHConfig + ? `ODoHConfig="${record.ODoHConfig.ODoHConfig}" ` + : ""; + let ipv4hint = ""; + let ipv6hint = ""; + if (record.ipv4Hint) { + let ipv4Str = ""; + for (let addr of record.ipv4Hint.address) { + ipv4Str += `${addr}, `; + } + // Remove ", " at the end. + ipv4Str = ipv4Str.slice(0, -2); + ipv4hint = `ipv4hint="${ipv4Str}" `; + } + if (record.ipv6Hint) { + let ipv6Str = ""; + for (let addr of record.ipv6Hint.address) { + ipv6Str += `${addr}, `; + } + // Remove ", " at the end. + ipv6Str = ipv6Str.slice(0, -2); + ipv6hint = `ipv6hint="${ipv6Str}" `; + } + + let str = `${record.priority} ${record.targetName} `; + str += `(${alpn}${noDefaultAlpn}${port}`; + str += `${ipv4hint}${echConfig}${ipv6hint}`; + str += `${ODoHConfig})`; + row.appendChild(col(str)); + new_cont.appendChild(row); + } + } else { + new_cont.appendChild(col(data.error)); + } + + parent.replaceChild(new_cont, cont); +} diff --git a/toolkit/content/aboutProfiles.js b/toolkit/content/aboutProfiles.js new file mode 100644 index 0000000000..36b730f068 --- /dev/null +++ b/toolkit/content/aboutProfiles.js @@ -0,0 +1,416 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "ProfileService", + "@mozilla.org/toolkit/profile-service;1", + "nsIToolkitProfileService" +); + +async function flush() { + try { + ProfileService.flush(); + rebuildProfileList(); + } catch (e) { + let [title, msg, button] = await document.l10n.formatValues([ + { id: "profiles-flush-fail-title" }, + { + id: + e.result == Cr.NS_ERROR_DATABASE_CHANGED + ? "profiles-flush-conflict" + : "profiles-flush-failed", + }, + { id: "profiles-flush-restart-button" }, + ]); + + const PS = Ci.nsIPromptService; + let result = Services.prompt.confirmEx( + window, + title, + msg, + PS.BUTTON_POS_0 * PS.BUTTON_TITLE_CANCEL + + PS.BUTTON_POS_1 * PS.BUTTON_TITLE_IS_STRING, + null, + button, + null, + null, + {} + ); + if (result == 1) { + restart(false); + } + } +} + +function rebuildProfileList() { + let parent = document.getElementById("profiles"); + while (parent.firstChild) { + parent.firstChild.remove(); + } + + let defaultProfile; + try { + defaultProfile = ProfileService.defaultProfile; + } catch (e) {} + + let currentProfile = ProfileService.currentProfile; + + for (let profile of ProfileService.profiles) { + let isCurrentProfile = profile == currentProfile; + let isInUse = isCurrentProfile; + if (!isInUse) { + try { + let lock = profile.lock({}); + lock.unlock(); + } catch (e) { + if ( + e.result != Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && + e.result != Cr.NS_ERROR_FILE_NOT_DIRECTORY && + e.result != Cr.NS_ERROR_FILE_NOT_FOUND + ) { + isInUse = true; + } + } + } + display({ + profile, + isDefault: profile == defaultProfile, + isCurrentProfile, + isInUse, + }); + } +} + +function display(profileData) { + let parent = document.getElementById("profiles"); + + let div = document.createElement("div"); + parent.appendChild(div); + + let name = document.createElement("h2"); + + div.appendChild(name); + document.l10n.setAttributes(name, "profiles-name", { + name: profileData.profile.name, + }); + + if (profileData.isCurrentProfile) { + let currentProfile = document.createElement("h3"); + document.l10n.setAttributes(currentProfile, "profiles-current-profile"); + div.appendChild(currentProfile); + } else if (profileData.isInUse) { + let currentProfile = document.createElement("h3"); + document.l10n.setAttributes(currentProfile, "profiles-in-use-profile"); + div.appendChild(currentProfile); + } + + let table = document.createElement("table"); + div.appendChild(table); + + let tbody = document.createElement("tbody"); + table.appendChild(tbody); + + function createItem(title, value, dir = false) { + let tr = document.createElement("tr"); + tbody.appendChild(tr); + + let th = document.createElement("th"); + th.setAttribute("class", "column"); + document.l10n.setAttributes(th, title); + tr.appendChild(th); + + let td = document.createElement("td"); + tr.appendChild(td); + + if (dir) { + td.appendChild(document.createTextNode(value.path)); + + if (value.exists()) { + let button = document.createElement("button"); + button.setAttribute("class", "opendir"); + document.l10n.setAttributes(button, "profiles-opendir"); + + td.appendChild(button); + + button.addEventListener("click", function(e) { + value.reveal(); + }); + } + } else { + document.l10n.setAttributes(td, value); + } + } + + createItem( + "profiles-is-default", + profileData.isDefault ? "profiles-yes" : "profiles-no" + ); + + createItem("profiles-rootdir", profileData.profile.rootDir, true); + + if (profileData.profile.localDir.path != profileData.profile.rootDir.path) { + createItem("profiles-localdir", profileData.profile.localDir, true); + } + + let renameButton = document.createElement("button"); + document.l10n.setAttributes(renameButton, "profiles-rename"); + renameButton.onclick = function() { + renameProfile(profileData.profile); + }; + div.appendChild(renameButton); + + if (!profileData.isInUse) { + let removeButton = document.createElement("button"); + document.l10n.setAttributes(removeButton, "profiles-remove"); + removeButton.onclick = function() { + removeProfile(profileData.profile); + }; + + div.appendChild(removeButton); + } + + if (!profileData.isDefault) { + let defaultButton = document.createElement("button"); + document.l10n.setAttributes(defaultButton, "profiles-set-as-default"); + defaultButton.onclick = function() { + defaultProfile(profileData.profile); + }; + div.appendChild(defaultButton); + } + + if (!profileData.isInUse) { + let runButton = document.createElement("button"); + document.l10n.setAttributes(runButton, "profiles-launch-profile"); + runButton.onclick = function() { + openProfile(profileData.profile); + }; + div.appendChild(runButton); + } + + let sep = document.createElement("hr"); + div.appendChild(sep); +} + +// This is called from the createProfileWizard.xhtml dialog. +function CreateProfile(profile) { + // The wizard created a profile, just make it the default. + defaultProfile(profile); +} + +function createProfileWizard() { + // This should be rewritten in HTML eventually. + window.browsingContext.topChromeWindow.openDialog( + "chrome://mozapps/content/profile/createProfileWizard.xhtml", + "", + "centerscreen,chrome,modal,titlebar", + ProfileService, + { CreateProfile } + ); +} + +async function renameProfile(profile) { + let newName = { value: profile.name }; + let [title, msg] = await document.l10n.formatValues([ + { id: "profiles-rename-profile-title" }, + { id: "profiles-rename-profile", args: { name: profile.name } }, + ]); + + if (Services.prompt.prompt(window, title, msg, newName, null, { value: 0 })) { + newName = newName.value; + + if (newName == profile.name) { + return; + } + + try { + profile.name = newName; + } catch (e) { + let [title, msg] = await document.l10n.formatValues([ + { id: "profiles-invalid-profile-name-title" }, + { id: "profiles-invalid-profile-name", args: { name: newName } }, + ]); + + Services.prompt.alert(window, title, msg); + return; + } + + flush(); + } +} + +async function removeProfile(profile) { + let deleteFiles = false; + + if (profile.rootDir.exists()) { + let [ + title, + msg, + dontDeleteStr, + deleteStr, + ] = await document.l10n.formatValues([ + { id: "profiles-delete-profile-title" }, + { + id: "profiles-delete-profile-confirm", + args: { dir: profile.rootDir.path }, + }, + { id: "profiles-dont-delete-files" }, + { id: "profiles-delete-files" }, + ]); + let buttonPressed = Services.prompt.confirmEx( + window, + title, + msg, + Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 + + Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 + + Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_2, + dontDeleteStr, + null, + deleteStr, + null, + { value: 0 } + ); + if (buttonPressed == 1) { + return; + } + + if (buttonPressed == 2) { + deleteFiles = true; + } + } + + // If we are deleting the default profile we must choose a different one. + let isDefault = false; + try { + isDefault = ProfileService.defaultProfile == profile; + } catch (e) {} + + if (isDefault) { + for (let p of ProfileService.profiles) { + if (profile == p) { + continue; + } + + if (isDefault) { + try { + ProfileService.defaultProfile = p; + } catch (e) { + // This can happen on dev-edition if a non-default profile is in use. + // In such a case the next time that dev-edition is started it will + // find no default profile and just create a new one. + } + } + + break; + } + } + + try { + profile.removeInBackground(deleteFiles); + } catch (e) { + let [title, msg] = await document.l10n.formatValues([ + { id: "profiles-delete-profile-failed-title" }, + { id: "profiles-delete-profile-failed-message" }, + ]); + + Services.prompt.alert(window, title, msg); + return; + } + + flush(); +} + +async function defaultProfile(profile) { + try { + ProfileService.defaultProfile = profile; + flush(); + } catch (e) { + // This can happen on dev-edition. + let [title, msg] = await document.l10n.formatValues([ + { id: "profiles-cannot-set-as-default-title" }, + { id: "profiles-cannot-set-as-default-message" }, + ]); + + Services.prompt.alert(window, title, msg); + } +} + +function openProfile(profile) { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + + if (cancelQuit.data) { + return; + } + + Services.startup.createInstanceWithProfile(profile); +} + +function restart(safeMode) { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + + if (cancelQuit.data) { + return; + } + + let flags = Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart; + + if (safeMode) { + Services.startup.restartInSafeMode(flags); + } else { + Services.startup.quit(flags); + } +} + +window.addEventListener( + "DOMContentLoaded", + function() { + let createButton = document.getElementById("create-button"); + createButton.addEventListener("click", createProfileWizard); + + let restartSafeModeButton = document.getElementById( + "restart-in-safe-mode-button" + ); + if (!Services.policies || Services.policies.isAllowed("safeMode")) { + restartSafeModeButton.addEventListener("click", () => { + restart(true); + }); + } else { + restartSafeModeButton.setAttribute("disabled", "true"); + } + + let restartNormalModeButton = document.getElementById("restart-button"); + restartNormalModeButton.addEventListener("click", () => { + restart(false); + }); + + if (ProfileService.isListOutdated) { + document.getElementById("owned").hidden = true; + } else { + document.getElementById("conflict").hidden = true; + rebuildProfileList(); + } + }, + { once: true } +); diff --git a/toolkit/content/aboutProfiles.xhtml b/toolkit/content/aboutProfiles.xhtml new file mode 100644 index 0000000000..56a43b3dd1 --- /dev/null +++ b/toolkit/content/aboutProfiles.xhtml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + + +<!DOCTYPE html> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" /> + <title data-l10n-id="profiles-title"></title> + <link rel="icon" type="image/png" id="favicon" href="chrome://branding/content/icon32.png" /> + <link rel="stylesheet" href="chrome://mozapps/skin/aboutProfiles.css" type="text/css" /> + <script src="chrome://global/content/aboutProfiles.js" /> + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="toolkit/about/aboutProfiles.ftl" /> +</head> + <body id="body" class="wide-container"> + <div id="action-box" class="notice-box"> + <h3 data-l10n-id="profiles-restart-title"></h3> + <button id="restart-in-safe-mode-button" data-l10n-id="profiles-restart-in-safe-mode"></button> + <button id="restart-button" data-l10n-id="profiles-restart-normal"></button> + </div> + + <h1 data-l10n-id="profiles-title"></h1> + + <div id="conflict"> + <p data-l10n-id="profiles-conflict" /> + </div> + <div id="owned"> + <div data-l10n-id="profiles-subtitle"></div> + + <div> + <button id="create-button" data-l10n-id="profiles-create"></button> + </div> + + <div id="profiles" class="tab"></div> + </div> + </body> +</html> diff --git a/toolkit/content/aboutRights-unbranded.xhtml b/toolkit/content/aboutRights-unbranded.xhtml new file mode 100644 index 0000000000..44c4c350b2 --- /dev/null +++ b/toolkit/content/aboutRights-unbranded.xhtml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; +]> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml"> + +<head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" /> + <title data-l10n-id="rights-title"></title> + <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css"/> + <link rel="localization" href="branding/brand.ftl"/> + <link rel="localization" href="toolkit/about/aboutRights.ftl"/> +</head> + +<body id="your-rights" class="aboutPageWideContainer"> +<div class="container"> + <h1 data-l10n-id="rights-title"></h1> + + <p data-l10n-id="rights-intro"></p> + + <ul> + <li data-l10n-id="rights-intro-point-1"><a href="http://www.mozilla.org/MPL/" data-l10n-name="mozilla-public-license-link"></a></li> + <!-- Point 2 discusses Mozilla trademarks, and isn't needed when the build is unbranded. + - Point 4 discusses privacy policy, unbranded builds get a placeholder (for the vendor to replace) + - Point 5 discusses web service terms, unbranded builds gets a placeholder (for the vendor to replace) --> + <li data-l10n-id="rights-intro-point-4-unbranded"></li> + <li data-l10n-id="rights-intro-point-5-unbranded"><a href="about:rights#webservices" id="showWebServices" data-l10n-name="mozilla-website-services-link"></a></li> + </ul> + + <div id="webservices-container"> + <a name="webservices"/> + <h3 data-l10n-id="rights-webservices-header"></h3> + + <p data-l10n-id="rights-webservices-unbranded"></p> + + <ol> + <!-- Terms only apply to official builds, unbranded builds get a placeholder. --> + <li data-l10n-id="rights-webservices-term-unbranded"></li> + </ol> + </div> +</div> + +</body> +<script src="chrome://global/content/aboutRights.js"/> +</html> diff --git a/toolkit/content/aboutRights.js b/toolkit/content/aboutRights.js new file mode 100644 index 0000000000..1defd21978 --- /dev/null +++ b/toolkit/content/aboutRights.js @@ -0,0 +1,42 @@ +/* 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 servicesDiv = document.getElementById("webservices-container"); +servicesDiv.style.display = "none"; + +function showServices() { + servicesDiv.style.display = ""; +} + +// Fluent replaces the children of the element being overlayed which prevents us +// from putting an event handler directly on the children. +let rightsIntro = + document.querySelector("[data-l10n-id=rights-intro-point-5]") || + document.querySelector("[data-l10n-id=rights-intro-point-5-unbranded]"); +rightsIntro.addEventListener("click", event => { + if (event.target.id == "showWebServices") { + showServices(); + } +}); + +var disablingServicesDiv = document.getElementById( + "disabling-webservices-container" +); + +function showDisablingServices() { + disablingServicesDiv.style.display = ""; +} + +if (disablingServicesDiv != null) { + disablingServicesDiv.style.display = "none"; + // Same issue here with Fluent replacing the children affecting the event listeners. + let rightsWebServices = document.querySelector( + "[data-l10n-id=rights-webservices]" + ); + rightsWebServices.addEventListener("click", event => { + if (event.target.id == "showDisablingWebServices") { + showDisablingServices(); + } + }); +} diff --git a/toolkit/content/aboutRights.xhtml b/toolkit/content/aboutRights.xhtml new file mode 100644 index 0000000000..23dfe8512e --- /dev/null +++ b/toolkit/content/aboutRights.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; +]> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml"> + +<head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" /> + <title data-l10n-id="rights-title"></title> + <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css"/> + <link rel="stylesheet" href="chrome://global/skin/aboutRights.css" type="text/css"/> + <link rel="localization" href="branding/brand.ftl"/> + <link rel="localization" href="toolkit/about/aboutRights.ftl"/> +</head> + +<body id="your-rights"> +<div class="container"> + <div class="rights-header"> + <div> + <h1 data-l10n-id="rights-title"></h1> + + <p data-l10n-id="rights-intro"></p> + </div> + </div> + + <ul> + <li data-l10n-id="rights-intro-point-1"><a href="http://www.mozilla.org/MPL/" data-l10n-name="mozilla-public-license-link"></a></li> + <!-- Point 2 discusses Mozilla trademarks, and isn't needed when the build is unbranded. + - Point 4 discusses privacy policy, unbranded builds get a placeholder (for the vendor to replace) + - Point 5 discusses web service terms, unbranded builds gets a placeholder (for the vendor to replace) --> + <li data-l10n-id="rights-intro-point-2"><a href="http://www.mozilla.org/foundation/trademarks/policy.html" data-l10n-name="mozilla-trademarks-link"></a></li> + <li data-l10n-id="rights-intro-point-3"></li> + <li data-l10n-id="rights-intro-point-4"><a href="https://www.mozilla.org/legal/privacy/firefox.html" data-l10n-name="mozilla-privacy-policy-link"></a></li> + <li data-l10n-id="rights-intro-point-5"><a href="about:rights#webservices" id="showWebServices" data-l10n-name="mozilla-service-terms-link"></a></li> + <li data-l10n-id="rights-intro-point-6"></li> + </ul> + + <div id="webservices-container"> + <a name="webservices"/> + <h3 data-l10n-id="rights-webservices-header"></h3> + + <p data-l10n-id="rights-webservices"><a href="about:rights#disabling-webservices" id="showDisablingWebServices" data-l10n-name="mozilla-disable-service-link"></a></p> + + <div id="disabling-webservices-container" style="margin-left:40px;"> + <a name="disabling-webservices"/> + <p data-l10n-id="rights-safebrowsing"></p> + <ul> + <li data-l10n-id="rights-safebrowsing-term-1"></li> + <li data-l10n-id="rights-safebrowsing-term-2"></li> + <li data-l10n-id="rights-safebrowsing-term-3"></li> + <li data-l10n-id="rights-safebrowsing-term-4"></li> + </ul> + + <p data-l10n-id="rights-locationawarebrowsing"></p> + <ul> + <li data-l10n-id="rights-locationawarebrowsing-term-1"></li> + <li data-l10n-id="rights-locationawarebrowsing-term-2"></li> + <li data-l10n-id="rights-locationawarebrowsing-term-3"></li> + <li data-l10n-id="rights-locationawarebrowsing-term-4"></li> + </ul> + </div> + + <ol> + <!-- Terms only apply to official builds, unbranded builds get a placeholder. --> + <li data-l10n-id="rights-webservices-term-1"></li> + <li data-l10n-id="rights-webservices-term-2"></li> + <li data-l10n-id="rights-webservices-term-3"></li> + <li data-l10n-id="rights-webservices-term-4"></li> + <li data-l10n-id="rights-webservices-term-5"></li> + <li data-l10n-id="rights-webservices-term-6"></li> + <li data-l10n-id="rights-webservices-term-7"></li> + </ol> + </div> +</div> + +</body> +<script src="chrome://global/content/aboutRights.js"/> +</html> diff --git a/toolkit/content/aboutServiceWorkers.js b/toolkit/content/aboutServiceWorkers.js new file mode 100644 index 0000000000..c5457b7ec8 --- /dev/null +++ b/toolkit/content/aboutServiceWorkers.js @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var gSWM; +var gSWCount = 0; + +function init() { + let enabled = Services.prefs.getBoolPref("dom.serviceWorkers.enabled"); + if (!enabled) { + let div = document.getElementById("warning_not_enabled"); + div.classList.add("active"); + return; + } + + gSWM = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + if (!gSWM) { + dump( + "AboutServiceWorkers: Failed to get the ServiceWorkerManager service!\n" + ); + return; + } + + let data = gSWM.getAllRegistrations(); + if (!data) { + dump("AboutServiceWorkers: Failed to retrieve the registrations.\n"); + return; + } + + let length = data.length; + if (!length) { + let div = document.getElementById("warning_no_serviceworkers"); + div.classList.add("active"); + return; + } + + let ps = undefined; + try { + ps = Cc["@mozilla.org/push/Service;1"].getService(Ci.nsIPushService); + } catch (e) { + dump("Could not acquire PushService\n"); + } + + for (let i = 0; i < length; ++i) { + let info = data.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo); + if (!info) { + dump( + "AboutServiceWorkers: Invalid nsIServiceWorkerRegistrationInfo interface.\n" + ); + continue; + } + + display(info, ps); + } +} + +async function display(info, pushService) { + let parent = document.getElementById("serviceworkers"); + + let div = document.createElement("div"); + parent.appendChild(div); + + let title = document.createElement("h2"); + document.l10n.setAttributes(title, "origin-title", { + originTitle: info.principal.origin, + }); + div.appendChild(title); + + let list = document.createElement("ul"); + div.appendChild(list); + + function createItem(l10nId, value, makeLink) { + let item = document.createElement("li"); + list.appendChild(item); + let bold = document.createElement("strong"); + bold.setAttribute("data-l10n-name", "item-label"); + item.appendChild(bold); + // Falsey values like "" are still valid values, so check exactly against + // undefined for the cases where the caller did not provide any value. + if (value === undefined) { + document.l10n.setAttributes(item, l10nId); + } else if (makeLink) { + let link = document.createElement("a"); + link.setAttribute("target", "_blank"); + link.setAttribute("data-l10n-name", "link"); + link.setAttribute("href", value); + item.appendChild(link); + document.l10n.setAttributes(item, l10nId, { url: value }); + } else { + document.l10n.setAttributes(item, l10nId, { name: value }); + } + return item; + } + + createItem("scope", info.scope); + createItem("script-spec", info.scriptSpec, true); + let currentWorkerURL = info.activeWorker ? info.activeWorker.scriptSpec : ""; + createItem("current-worker-url", currentWorkerURL, true); + let activeCacheName = info.activeWorker ? info.activeWorker.cacheName : ""; + createItem("active-cache-name", activeCacheName); + let waitingCacheName = info.waitingWorker ? info.waitingWorker.cacheName : ""; + createItem("waiting-cache-name", waitingCacheName); + + let pushItem = createItem("push-end-point-waiting"); + if (pushService) { + pushService.getSubscription( + info.scope, + info.principal, + (status, pushRecord) => { + if (Components.isSuccessCode(status)) { + document.l10n.setAttributes(pushItem, "push-end-point-result", { + name: JSON.stringify(pushRecord), + }); + } else { + dump("about:serviceworkers - retrieving push registration failed\n"); + } + } + ); + } + + let updateButton = document.createElement("button"); + document.l10n.setAttributes(updateButton, "update-button"); + updateButton.onclick = function() { + gSWM.propagateSoftUpdate(info.principal.originAttributes, info.scope); + }; + div.appendChild(updateButton); + + let unregisterButton = document.createElement("button"); + document.l10n.setAttributes(unregisterButton, "unregister-button"); + div.appendChild(unregisterButton); + + let loadingMessage = document.createElement("span"); + document.l10n.setAttributes(loadingMessage, "waiting"); + loadingMessage.classList.add("inactive"); + div.appendChild(loadingMessage); + + unregisterButton.onclick = function() { + let cb = { + unregisterSucceeded() { + parent.removeChild(div); + + if (!--gSWCount) { + let div = document.getElementById("warning_no_serviceworkers"); + div.classList.add("active"); + } + }, + + async unregisterFailed() { + let [alertMsg] = await document.l10n.formatValues([ + { id: "unregister-error" }, + ]); + alert(alertMsg); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIServiceWorkerUnregisterCallback", + ]), + }; + + loadingMessage.classList.remove("inactive"); + gSWM.propagateUnregister(info.principal, cb, info.scope); + }; + + let sep = document.createElement("hr"); + div.appendChild(sep); + + ++gSWCount; +} + +window.addEventListener( + "DOMContentLoaded", + function() { + init(); + }, + { once: true } +); diff --git a/toolkit/content/aboutServiceWorkers.xhtml b/toolkit/content/aboutServiceWorkers.xhtml new file mode 100644 index 0000000000..6bf38033e3 --- /dev/null +++ b/toolkit/content/aboutServiceWorkers.xhtml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + + +<!DOCTYPE html [ +<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" /> + <title data-l10n-id="about-service-workers-title"></title> + <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css" /> + <link rel="stylesheet" href="chrome://mozapps/skin/aboutServiceWorkers.css" type="text/css" /> + <link rel="localization" href="toolkit/about/aboutServiceWorkers.ftl"/> + <link rel="localization" href="branding/brand.ftl"/> + <script src="chrome://global/content/aboutServiceWorkers.js" /> + </head> + <body id="body"> + <div id="warning_not_enabled" class="warningBackground"> + <div class="warningMessage" data-l10n-id="about-service-workers-warning-not-enabled"></div> + </div> + + <div id="warning_no_serviceworkers" class="warningBackground"> + <div class="warningMessage" data-l10n-id="about-service-workers-warning-no-service-workers"></div> + </div> + + <div id="serviceworkers" class="tab active"> + <h1 data-l10n-id="about-service-workers-main-title"></h1> + </div> + </body> +</html> diff --git a/toolkit/content/aboutSupport.js b/toolkit/content/aboutSupport.js new file mode 100644 index 0000000000..a7917b9f5d --- /dev/null +++ b/toolkit/content/aboutSupport.js @@ -0,0 +1,1872 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { Troubleshoot } = ChromeUtils.import( + "resource://gre/modules/Troubleshoot.jsm" +); +const { ResetProfile } = ChromeUtils.import( + "resource://gre/modules/ResetProfile.jsm" +); +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "PluralForm", + "resource://gre/modules/PluralForm.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "PlacesDBUtils", + "resource://gre/modules/PlacesDBUtils.jsm" +); + +function showThirdPartyModules() { + return ( + AppConstants.platform == "win" && + Services.prefs.getBoolPref("browser.enableAboutThirdParty") + ); +} + +window.addEventListener("load", function onload(event) { + try { + window.removeEventListener("load", onload); + Troubleshoot.snapshot(async function(snapshot) { + for (let prop in snapshotFormatters) { + await snapshotFormatters[prop](snapshot[prop]); + } + }); + populateActionBox(); + setupEventListeners(); + if (showThirdPartyModules()) { + $("third-party-modules").hidden = false; + $("third-party-modules-table").hidden = false; + } + } catch (e) { + Cu.reportError( + "stack of load error for about:support: " + e + ": " + e.stack + ); + } +}); + +function prefsTable(data) { + return sortedArrayFromObject(data).map(function([name, value]) { + return $.new("tr", [ + $.new("td", name, "pref-name"), + // Very long preference values can cause users problems when they + // copy and paste them into some text editors. Long values generally + // aren't useful anyway, so truncate them to a reasonable length. + $.new("td", String(value).substr(0, 120), "pref-value"), + ]); + }); +} + +// Fluent uses lisp-case IDs so this converts +// the SentenceCase info IDs to lisp-case. +const FLUENT_IDENT_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]*$/; +function toFluentID(str) { + if (!FLUENT_IDENT_REGEX.test(str)) { + return null; + } + return str + .toString() + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .toLowerCase(); +} + +// Each property in this object corresponds to a property in Troubleshoot.jsm's +// snapshot data. Each function is passed its property's corresponding data, +// and it's the function's job to update the page with it. +var snapshotFormatters = { + async application(data) { + $("application-box").textContent = data.name; + $("useragent-box").textContent = data.userAgent; + $("os-box").textContent = data.osVersion; + if (AppConstants.platform == "macosx") { + $("rosetta-box").textContent = data.rosetta; + } + $("binary-box").textContent = Services.dirsvc.get( + "XREExeF", + Ci.nsIFile + ).path; + $("supportLink").href = data.supportURL; + let version = AppConstants.MOZ_APP_VERSION_DISPLAY; + if (data.vendor) { + version += " (" + data.vendor + ")"; + } + $("version-box").textContent = version; + $("buildid-box").textContent = data.buildID; + $("distributionid-box").textContent = data.distributionID; + if (data.updateChannel) { + $("updatechannel-box").textContent = data.updateChannel; + } + if (AppConstants.MOZ_UPDATER && AppConstants.platform != "android") { + $("update-dir-box").textContent = Services.dirsvc.get( + "UpdRootD", + Ci.nsIFile + ).path; + } + $("profile-dir-box").textContent = Services.dirsvc.get( + "ProfD", + Ci.nsIFile + ).path; + + try { + let launcherStatusTextId = "launcher-process-status-unknown"; + switch (data.launcherProcessState) { + case 0: + case 1: + case 2: + launcherStatusTextId = + "launcher-process-status-" + data.launcherProcessState; + break; + } + + document.l10n.setAttributes( + $("launcher-process-box"), + launcherStatusTextId + ); + } catch (e) {} + + const STATUS_STRINGS = { + experimentControl: "fission-status-experiment-control", + experimentTreatment: "fission-status-experiment-treatment", + disabledByE10sEnv: "fission-status-disabled-by-e10s-env", + enabledByEnv: "fission-status-enabled-by-env", + disabledBySafeMode: "fission-status-disabled-by-safe-mode", + enabledByDefault: "fission-status-enabled-by-default", + disabledByDefault: "fission-status-disabled-by-default", + enabledByUserPref: "fission-status-enabled-by-user-pref", + disabledByUserPref: "fission-status-disabled-by-user-pref", + disabledByE10sOther: "fission-status-disabled-by-e10s-other", + }; + + let statusTextId = STATUS_STRINGS[data.fissionDecisionStatus]; + + document.l10n.setAttributes( + $("multiprocess-box-process-count"), + "multi-process-windows", + { + remoteWindows: data.numRemoteWindows, + totalWindows: data.numTotalWindows, + } + ); + document.l10n.setAttributes( + $("fission-box-process-count"), + "fission-windows", + { + fissionWindows: data.numFissionWindows, + totalWindows: data.numTotalWindows, + } + ); + document.l10n.setAttributes($("fission-box-status"), statusTextId); + + if (Services.policies) { + let policiesStrId = ""; + let aboutPolicies = "about:policies"; + switch (data.policiesStatus) { + case Services.policies.INACTIVE: + policiesStrId = "policies-inactive"; + break; + + case Services.policies.ACTIVE: + policiesStrId = "policies-active"; + aboutPolicies += "#active"; + break; + + default: + policiesStrId = "policies-error"; + aboutPolicies += "#errors"; + break; + } + + if (data.policiesStatus != Services.policies.INACTIVE) { + let activePolicies = $.new("a", null, null, { + href: aboutPolicies, + }); + document.l10n.setAttributes(activePolicies, policiesStrId); + $("policies-status").appendChild(activePolicies); + } else { + document.l10n.setAttributes($("policies-status"), policiesStrId); + } + } else { + $("policies-status-row").hidden = true; + } + + let keyLocationServiceGoogleFound = data.keyLocationServiceGoogleFound + ? "found" + : "missing"; + document.l10n.setAttributes( + $("key-location-service-google-box"), + keyLocationServiceGoogleFound + ); + + let keySafebrowsingGoogleFound = data.keySafebrowsingGoogleFound + ? "found" + : "missing"; + document.l10n.setAttributes( + $("key-safebrowsing-google-box"), + keySafebrowsingGoogleFound + ); + + let keyMozillaFound = data.keyMozillaFound ? "found" : "missing"; + document.l10n.setAttributes($("key-mozilla-box"), keyMozillaFound); + + $("safemode-box").textContent = data.safeMode; + }, + + crashes(data) { + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + + let daysRange = Troubleshoot.kMaxCrashAge / (24 * 60 * 60 * 1000); + document.l10n.setAttributes($("crashes-title"), "report-crash-for-days", { + days: daysRange, + }); + let reportURL; + try { + reportURL = Services.prefs.getCharPref("breakpad.reportURL"); + // Ignore any non http/https urls + if (!/^https?:/i.test(reportURL)) { + reportURL = null; + } + } catch (e) {} + if (!reportURL) { + $("crashes-noConfig").style.display = "block"; + $("crashes-noConfig").classList.remove("no-copy"); + return; + } + $("crashes-allReports").style.display = "block"; + + if (data.pending > 0) { + document.l10n.setAttributes( + $("crashes-allReportsWithPending"), + "pending-reports", + { reports: data.pending } + ); + } + + let dateNow = new Date(); + $.append( + $("crashes-tbody"), + data.submitted.map(function(crash) { + let date = new Date(crash.date); + let timePassed = dateNow - date; + let formattedDateStrId; + let formattedDateStrArgs; + if (timePassed >= 24 * 60 * 60 * 1000) { + let daysPassed = Math.round(timePassed / (24 * 60 * 60 * 1000)); + formattedDateStrId = "crashes-time-days"; + formattedDateStrArgs = { days: daysPassed }; + } else if (timePassed >= 60 * 60 * 1000) { + let hoursPassed = Math.round(timePassed / (60 * 60 * 1000)); + formattedDateStrId = "crashes-time-hours"; + formattedDateStrArgs = { hours: hoursPassed }; + } else { + let minutesPassed = Math.max(Math.round(timePassed / (60 * 1000)), 1); + formattedDateStrId = "crashes-time-minutes"; + formattedDateStrArgs = { minutes: minutesPassed }; + } + return $.new("tr", [ + $.new("td", [ + $.new("a", crash.id, null, { href: reportURL + crash.id }), + ]), + $.new("td", null, null, { + "data-l10n-id": formattedDateStrId, + "data-l10n-args": formattedDateStrArgs, + }), + ]); + }) + ); + }, + + addons(data) { + $.append( + $("addons-tbody"), + data.map(function(addon) { + return $.new("tr", [ + $.new("td", addon.name), + $.new("td", addon.type), + $.new("td", addon.version), + $.new("td", addon.isActive), + $.new("td", addon.id), + ]); + }) + ); + }, + + securitySoftware(data) { + if (!AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) { + $("security-software-title").hidden = true; + $("security-software-table").hidden = true; + return; + } + + $("security-software-antivirus").textContent = data.registeredAntiVirus; + $("security-software-antispyware").textContent = data.registeredAntiSpyware; + $("security-software-firewall").textContent = data.registeredFirewall; + }, + + features(data) { + $.append( + $("features-tbody"), + data.map(function(feature) { + return $.new("tr", [ + $.new("td", feature.name), + $.new("td", feature.version), + $.new("td", feature.id), + ]); + }) + ); + }, + + async processes(data) { + async function buildEntry(name, value) { + let entryName = + (await document.l10n.formatValue( + `process-type-${name.toLowerCase()}` + )) || name; + + $("processes-tbody").appendChild( + $.new("tr", [$.new("td", entryName), $.new("td", value)]) + ); + } + + let remoteProcessesCount = Object.values(data.remoteTypes).reduce( + (a, b) => a + b, + 0 + ); + document.querySelector( + "#remoteprocesses-row a" + ).textContent = remoteProcessesCount; + + // Display the regular "web" process type first in the list, + // and with special formatting. + if (data.remoteTypes.web) { + await buildEntry( + "web", + `${data.remoteTypes.web} / ${data.maxWebContentProcesses}` + ); + delete data.remoteTypes.web; + } + + for (let remoteProcessType in data.remoteTypes) { + await buildEntry(remoteProcessType, data.remoteTypes[remoteProcessType]); + } + }, + + async experimentalFeatures(data) { + if (!data) { + return; + } + let titleL10nIds = data.map(([titleL10nId]) => titleL10nId); + let titleL10nObjects = await document.l10n.formatMessages(titleL10nIds); + if (titleL10nObjects.length != data.length) { + throw Error("Missing localized title strings in experimental features"); + } + for (let i = 0; i < titleL10nObjects.length; i++) { + let localizedTitle = titleL10nObjects[i].attributes.find( + a => a.name == "label" + ).value; + data[i] = [localizedTitle, data[i][1], data[i][2]]; + } + + $.append( + $("experimental-features-tbody"), + data.map(function([title, pref, value]) { + return $.new("tr", [ + $.new("td", `${title} (${pref})`, "pref-name"), + $.new("td", value, "pref-value"), + ]); + }) + ); + }, + + environmentVariables(data) { + if (!data) { + return; + } + $.append( + $("environment-variables-tbody"), + Object.entries(data).map(([name, value]) => { + return $.new("tr", [ + $.new("td", name, "pref-name"), + $.new("td", value, "pref-value"), + ]); + }) + ); + }, + + modifiedPreferences(data) { + $.append($("prefs-tbody"), prefsTable(data)); + }, + + lockedPreferences(data) { + $.append($("locked-prefs-tbody"), prefsTable(data)); + }, + + printingPreferences(data) { + if (AppConstants.platform == "android") { + return; + } + const tbody = $("support-printing-prefs-tbody"); + $.append(tbody, prefsTable(data)); + $("support-printing-clear-settings-button").addEventListener( + "click", + function() { + for (let name in data) { + Services.prefs.clearUserPref(name); + } + tbody.textContent = ""; + } + ); + }, + + async graphics(data) { + function localizedMsg(msg) { + if (typeof msg == "object" && msg.key) { + return document.l10n.formatValue(msg.key, msg.args); + } + let msgId = toFluentID(msg); + if (msgId) { + return document.l10n.formatValue(msgId); + } + return ""; + } + + // Read APZ info out of data.info, stripping it out in the process. + let apzInfo = []; + let formatApzInfo = function(info) { + let out = []; + for (let type of [ + "Wheel", + "Touch", + "Drag", + "Keyboard", + "Autoscroll", + "Zooming", + ]) { + let key = "Apz" + type + "Input"; + + if (!(key in info)) { + continue; + } + + delete info[key]; + + out.push(toFluentID(type.toLowerCase() + "Enabled")); + } + + return out; + }; + + // Create a <tr> element with key and value columns. + // + // @key Text in the key column. Localized automatically, unless starts with "#". + // @value Fluent ID for text in the value column, or array of children. + function buildRow(key, value) { + let title = key[0] == "#" ? key.substr(1) : key; + let keyStrId = toFluentID(key); + let valueStrId = Array.isArray(value) ? null : toFluentID(value); + let td = $.new("td", value); + td.style["white-space"] = "pre-wrap"; + if (valueStrId) { + document.l10n.setAttributes(td, valueStrId); + } + + let th = $.new("th", title, "column"); + if (!key.startsWith("#")) { + document.l10n.setAttributes(th, keyStrId); + } + return $.new("tr", [th, td]); + } + + // @where The name in "graphics-<name>-tbody", of the element to append to. + // @trs Array of row elements. + function addRows(where, trs) { + $.append($("graphics-" + where + "-tbody"), trs); + } + + // Build and append a row. + // + // @where The name in "graphics-<name>-tbody", of the element to append to. + function addRow(where, key, value) { + addRows(where, [buildRow(key, value)]); + } + if ("info" in data) { + apzInfo = formatApzInfo(data.info); + + let trs = sortedArrayFromObject(data.info).map(function([prop, val]) { + let td = $.new("td", String(val)); + td.style["word-break"] = "break-all"; + return $.new("tr", [$.new("th", prop, "column"), td]); + }); + addRows("diagnostics", trs); + + delete data.info; + } + + let windowUtils = window.windowUtils; + let gpuProcessPid = windowUtils.gpuProcessPid; + + if (gpuProcessPid != -1) { + let gpuProcessKillButton = null; + if (AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_DEV_EDITION) { + gpuProcessKillButton = $.new("button"); + + gpuProcessKillButton.addEventListener("click", function() { + windowUtils.terminateGPUProcess(); + }); + + document.l10n.setAttributes( + gpuProcessKillButton, + "gpu-process-kill-button" + ); + } + + addRow("diagnostics", "gpu-process-pid", [new Text(gpuProcessPid)]); + if (gpuProcessKillButton) { + addRow("diagnostics", "gpu-process", [gpuProcessKillButton]); + } + } + + if ( + (AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_DEV_EDITION) && + AppConstants.platform != "macosx" + ) { + let gpuDeviceResetButton = $.new("button"); + + gpuDeviceResetButton.addEventListener("click", function() { + windowUtils.triggerDeviceReset(); + }); + + document.l10n.setAttributes( + gpuDeviceResetButton, + "gpu-device-reset-button" + ); + addRow("diagnostics", "gpu-device-reset", [gpuDeviceResetButton]); + } + + // graphics-failures-tbody tbody + if ("failures" in data) { + // If indices is there, it should be the same length as failures, + // (see Troubleshoot.jsm) but we check anyway: + if ("indices" in data && data.failures.length == data.indices.length) { + let combined = []; + for (let i = 0; i < data.failures.length; i++) { + let assembled = assembleFromGraphicsFailure(i, data); + combined.push(assembled); + } + combined.sort(function(a, b) { + if (a.index < b.index) { + return -1; + } + if (a.index > b.index) { + return 1; + } + return 0; + }); + $.append( + $("graphics-failures-tbody"), + combined.map(function(val) { + return $.new("tr", [ + $.new("th", val.header, "column"), + $.new("td", val.message), + ]); + }) + ); + delete data.indices; + } else { + $.append($("graphics-failures-tbody"), [ + $.new("tr", [ + $.new("th", "LogFailure", "column"), + $.new( + "td", + data.failures.map(function(val) { + return $.new("p", val); + }) + ), + ]), + ]); + } + delete data.failures; + } else { + $("graphics-failures-tbody").style.display = "none"; + } + + // Add a new row to the table, and take the key (or keys) out of data. + // + // @where Table section to add to. + // @key Data key to use. + // @colKey The localization key to use, if different from key. + async function addRowFromKey(where, key, colKey) { + if (!(key in data)) { + return; + } + colKey = colKey || key; + + let value; + let messageKey = key + "Message"; + if (messageKey in data) { + value = await localizedMsg(data[messageKey]); + delete data[messageKey]; + } else { + value = data[key]; + } + delete data[key]; + + if (value) { + addRow(where, colKey, [new Text(value)]); + } + } + + // graphics-features-tbody + let compositor = ""; + if (data.windowLayerManagerRemote) { + compositor = data.windowLayerManagerType; + if (data.windowUsingAdvancedLayers) { + compositor += " (Advanced Layers)"; + } + } else { + let noOMTCString = await document.l10n.formatValue("main-thread-no-omtc"); + compositor = "BasicLayers (" + noOMTCString + ")"; + } + addRow("features", "compositing", [new Text(compositor)]); + delete data.windowLayerManagerRemote; + delete data.windowLayerManagerType; + delete data.numTotalWindows; + delete data.numAcceleratedWindows; + delete data.numAcceleratedWindowsMessage; + delete data.windowUsingAdvancedLayers; + + addRow( + "features", + "asyncPanZoom", + apzInfo.length + ? [ + new Text( + ( + await document.l10n.formatValues( + apzInfo.map(id => { + return { id }; + }) + ) + ).join("; ") + ), + ] + : "apz-none" + ); + let featureKeys = [ + "webgl1WSIInfo", + "webgl1Renderer", + "webgl1Version", + "webgl1DriverExtensions", + "webgl1Extensions", + "webgl2WSIInfo", + "webgl2Renderer", + "webgl2Version", + "webgl2DriverExtensions", + "webgl2Extensions", + ["supportsHardwareH264", "hardware-h264"], + ["direct2DEnabled", "#Direct2D"], + ["windowProtocol", "graphics-window-protocol"], + ["desktopEnvironment", "graphics-desktop-environment"], + "usesTiling", + "contentUsesTiling", + "offMainThreadPaintEnabled", + "offMainThreadPaintWorkerCount", + "targetFrameRate", + ]; + for (let feature of featureKeys) { + if (Array.isArray(feature)) { + await addRowFromKey("features", feature[0], feature[1]); + continue; + } + await addRowFromKey("features", feature); + } + + if ("directWriteEnabled" in data) { + let message = data.directWriteEnabled; + if ("directWriteVersion" in data) { + message += " (" + data.directWriteVersion + ")"; + } + await addRow("features", "#DirectWrite", [new Text(message)]); + delete data.directWriteEnabled; + delete data.directWriteVersion; + } + + // Adapter tbodies. + let adapterKeys = [ + ["adapterDescription", "gpu-description"], + ["adapterVendorID", "gpu-vendor-id"], + ["adapterDeviceID", "gpu-device-id"], + ["driverVendor", "gpu-driver-vendor"], + ["driverVersion", "gpu-driver-version"], + ["driverDate", "gpu-driver-date"], + ["adapterDrivers", "gpu-drivers"], + ["adapterSubsysID", "gpu-subsys-id"], + ["adapterRAM", "gpu-ram"], + ]; + + function showGpu(id, suffix) { + function get(prop) { + return data[prop + suffix]; + } + + let trs = []; + for (let [prop, key] of adapterKeys) { + let value = get(prop); + if (value === undefined || value === "") { + continue; + } + trs.push(buildRow(key, [new Text(value)])); + } + + if (!trs.length) { + $("graphics-" + id + "-tbody").style.display = "none"; + return; + } + + let active = "yes"; + if ("isGPU2Active" in data && (suffix == "2") != data.isGPU2Active) { + active = "no"; + } + + addRow(id, "gpu-active", active); + addRows(id, trs); + } + showGpu("gpu-1", ""); + showGpu("gpu-2", "2"); + + // Remove adapter keys. + for (let [prop /* key */] of adapterKeys) { + delete data[prop]; + delete data[prop + "2"]; + } + delete data.isGPU2Active; + + let featureLog = data.featureLog; + delete data.featureLog; + + if (featureLog.features.length) { + for (let feature of featureLog.features) { + let trs = []; + for (let entry of feature.log) { + let contents; + if (!entry.hasOwnProperty("message")) { + // This is a default entry. + contents = entry.status + " by " + entry.type; + } else if (entry.message.length && entry.message[0] == "#") { + // This is a failure ID. See nsIGfxInfo.idl. + let m = /#BLOCKLIST_FEATURE_FAILURE_BUG_(\d+)/.exec(entry.message); + if (m) { + let bugSpan = $.new("span"); + + let bugHref = $.new("a"); + bugHref.href = + "https://bugzilla.mozilla.org/show_bug.cgi?id=" + m[1]; + bugHref.setAttribute("data-l10n-name", "bug-link"); + bugSpan.append(bugHref); + document.l10n.setAttributes(bugSpan, "support-blocklisted-bug", { + bugNumber: m[1], + }); + + contents = [bugSpan]; + } else { + let unknownFailure = $.new("span"); + document.l10n.setAttributes(unknownFailure, "unknown-failure", { + failureCode: entry.message.substr(1), + }); + contents = [unknownFailure]; + } + } else { + contents = + entry.status + " by " + entry.type + ": " + entry.message; + } + + trs.push($.new("tr", [$.new("td", contents)])); + } + addRow("decisions", "#" + feature.name, [$.new("table", trs)]); + } + } else { + $("graphics-decisions-tbody").style.display = "none"; + } + + if (featureLog.fallbacks.length) { + for (let fallback of featureLog.fallbacks) { + addRow("workarounds", "#" + fallback.name, [ + new Text(fallback.message), + ]); + } + } else { + $("graphics-workarounds-tbody").style.display = "none"; + } + + let crashGuards = data.crashGuards; + delete data.crashGuards; + + if (crashGuards.length) { + for (let guard of crashGuards) { + let resetButton = $.new("button"); + let onClickReset = function() { + Services.prefs.setIntPref(guard.prefName, 0); + resetButton.removeEventListener("click", onClickReset); + resetButton.disabled = true; + }; + + document.l10n.setAttributes(resetButton, "reset-on-next-restart"); + resetButton.addEventListener("click", onClickReset); + + addRow("crashguards", guard.type + "CrashGuard", [resetButton]); + } + } else { + $("graphics-crashguards-tbody").style.display = "none"; + } + + // Now that we're done, grab any remaining keys in data and drop them into + // the diagnostics section. + for (let key in data) { + let value = data[key]; + addRow("diagnostics", key, [new Text(value)]); + } + }, + + media(data) { + function insertBasicInfo(key, value) { + function createRow(key, value) { + let th = $.new("th", null, "column"); + document.l10n.setAttributes(th, key); + let td = $.new("td", value); + td.style["white-space"] = "pre-wrap"; + td.colSpan = 8; + return $.new("tr", [th, td]); + } + $.append($("media-info-tbody"), [createRow(key, value)]); + } + + function createDeviceInfoRow(device) { + let deviceInfo = Ci.nsIAudioDeviceInfo; + + let states = {}; + states[deviceInfo.STATE_DISABLED] = "Disabled"; + states[deviceInfo.STATE_UNPLUGGED] = "Unplugged"; + states[deviceInfo.STATE_ENABLED] = "Enabled"; + + let preferreds = {}; + preferreds[deviceInfo.PREF_NONE] = "None"; + preferreds[deviceInfo.PREF_MULTIMEDIA] = "Multimedia"; + preferreds[deviceInfo.PREF_VOICE] = "Voice"; + preferreds[deviceInfo.PREF_NOTIFICATION] = "Notification"; + preferreds[deviceInfo.PREF_ALL] = "All"; + + let formats = {}; + formats[deviceInfo.FMT_S16LE] = "S16LE"; + formats[deviceInfo.FMT_S16BE] = "S16BE"; + formats[deviceInfo.FMT_F32LE] = "F32LE"; + formats[deviceInfo.FMT_F32BE] = "F32BE"; + + function toPreferredString(preferred) { + if (preferred == deviceInfo.PREF_NONE) { + return preferreds[deviceInfo.PREF_NONE]; + } else if (preferred & deviceInfo.PREF_ALL) { + return preferreds[deviceInfo.PREF_ALL]; + } + let str = ""; + for (let pref of [ + deviceInfo.PREF_MULTIMEDIA, + deviceInfo.PREF_VOICE, + deviceInfo.PREF_NOTIFICATION, + ]) { + if (preferred & pref) { + str += " " + preferreds[pref]; + } + } + return str; + } + + function toFromatString(dev) { + let str = "default: " + formats[dev.defaultFormat] + ", support:"; + for (let fmt of [ + deviceInfo.FMT_S16LE, + deviceInfo.FMT_S16BE, + deviceInfo.FMT_F32LE, + deviceInfo.FMT_F32BE, + ]) { + if (dev.supportedFormat & fmt) { + str += " " + formats[fmt]; + } + } + return str; + } + + function toRateString(dev) { + return ( + "default: " + + dev.defaultRate + + ", support: " + + dev.minRate + + " - " + + dev.maxRate + ); + } + + function toLatencyString(dev) { + return dev.minLatency + " - " + dev.maxLatency; + } + + return $.new("tr", [ + $.new("td", device.name), + $.new("td", device.groupId), + $.new("td", device.vendor), + $.new("td", states[device.state]), + $.new("td", toPreferredString(device.preferred)), + $.new("td", toFromatString(device)), + $.new("td", device.maxChannels), + $.new("td", toRateString(device)), + $.new("td", toLatencyString(device)), + ]); + } + + function insertDeviceInfo(side, devices) { + let rows = []; + for (let dev of devices) { + rows.push(createDeviceInfoRow(dev)); + } + $.append($("media-" + side + "-devices-tbody"), rows); + } + + function insertEnumerateDatabase() { + if ( + !Services.prefs.getBoolPref("media.mediacapabilities.from-database") + ) { + $("media-capabilities-tbody").style.display = "none"; + return; + } + let button = $("enumerate-database-button"); + if (button) { + button.addEventListener("click", function(event) { + let { KeyValueService } = ChromeUtils.import( + "resource://gre/modules/kvstore.jsm" + ); + let currProfDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + currProfDir.append("mediacapabilities"); + let path = currProfDir.path; + + function enumerateDatabase(name) { + KeyValueService.getOrCreate(path, name) + .then(database => { + return database.enumerate(); + }) + .then(enumerator => { + var logs = []; + logs.push(`${name}:`); + while (enumerator.hasMoreElements()) { + const { key, value } = enumerator.getNext(); + logs.push(`${key}: ${value}`); + } + $("enumerate-database-result").textContent += + logs.join("\n") + "\n"; + }) + .catch(err => { + $("enumerate-database-result").textContent += `${name}:\n`; + }); + } + + $("enumerate-database-result").style.display = "block"; + $("enumerate-database-result").classList.remove("no-copy"); + $("enumerate-database-result").textContent = ""; + + enumerateDatabase("video/av1"); + enumerateDatabase("video/vp8"); + enumerateDatabase("video/vp9"); + enumerateDatabase("video/avc"); + enumerateDatabase("video/theora"); + }); + } + } + + function roundtripAudioLatency() { + insertBasicInfo("roundtrip-latency", "..."); + window.windowUtils + .defaultDevicesRoundTripLatency() + .then(latency => { + var latencyString = `${(latency[0] * 1000).toFixed(2)}ms (${( + latency[1] * 1000 + ).toFixed(2)})`; + data.defaultDevicesRoundTripLatency = latencyString; + document.querySelector( + 'th[data-l10n-id="roundtrip-latency"]' + ).nextSibling.textContent = latencyString; + }) + .catch(e => {}); + } + + // Basic information + insertBasicInfo("audio-backend", data.currentAudioBackend); + insertBasicInfo("max-audio-channels", data.currentMaxAudioChannels); + insertBasicInfo("sample-rate", data.currentPreferredSampleRate); + + if (AppConstants.platform == "macosx") { + var micStatus = {}; + let permission = Cc["@mozilla.org/ospermissionrequest;1"].getService( + Ci.nsIOSPermissionRequest + ); + permission.getAudioCapturePermissionState(micStatus); + if (micStatus.value == permission.PERMISSION_STATE_AUTHORIZED) { + roundtripAudioLatency(); + } + } else { + roundtripAudioLatency(); + } + + // Output devices information + insertDeviceInfo("output", data.audioOutputDevices); + + // Input devices information + insertDeviceInfo("input", data.audioInputDevices); + + // Media Capabilitites + insertEnumerateDatabase(); + }, + + remoteAgent(data) { + if (!AppConstants.ENABLE_REMOTE_AGENT) { + return; + } + $("remote-debugging-accepting-connections").textContent = data.listening; + $("remote-debugging-url").textContent = data.url; + }, + + accessibility(data) { + $("a11y-activated").textContent = data.isActive; + $("a11y-force-disabled").textContent = data.forceDisabled || 0; + + let a11yHandlerUsed = $("a11y-handler-used"); + if (a11yHandlerUsed) { + a11yHandlerUsed.textContent = data.handlerUsed; + } + + let a11yInstantiator = $("a11y-instantiator"); + if (a11yInstantiator) { + a11yInstantiator.textContent = data.instantiator; + } + }, + + startupCache(data) { + $("startup-cache-disk-cache-path").textContent = data.DiskCachePath; + $("startup-cache-ignore-disk-cache").textContent = data.IgnoreDiskCache; + $("startup-cache-found-disk-cache-on-init").textContent = + data.FoundDiskCacheOnInit; + $("startup-cache-wrote-to-disk-cache").textContent = data.WroteToDiskCache; + }, + + libraryVersions(data) { + let trs = [ + $.new("tr", [ + $.new("th", ""), + $.new("th", null, null, { "data-l10n-id": "min-lib-versions" }), + $.new("th", null, null, { "data-l10n-id": "loaded-lib-versions" }), + ]), + ]; + sortedArrayFromObject(data).forEach(function([name, val]) { + trs.push( + $.new("tr", [ + $.new("td", name), + $.new("td", val.minVersion), + $.new("td", val.version), + ]) + ); + }); + $.append($("libversions-tbody"), trs); + }, + + userJS(data) { + if (!data.exists) { + return; + } + let userJSFile = Services.dirsvc.get("PrefD", Ci.nsIFile); + userJSFile.append("user.js"); + $("prefs-user-js-link").href = Services.io.newFileURI(userJSFile).spec; + $("prefs-user-js-section").style.display = ""; + // Clear the no-copy class + $("prefs-user-js-section").className = ""; + }, + + sandbox(data) { + if (!AppConstants.MOZ_SANDBOX) { + return; + } + + let tbody = $("sandbox-tbody"); + for (let key in data) { + // Simplify the display a little in the common case. + if ( + key === "hasPrivilegedUserNamespaces" && + data[key] === data.hasUserNamespaces + ) { + continue; + } + if (key === "syscallLog") { + // Not in this table. + continue; + } + let keyStrId = toFluentID(key); + let th = $.new("th", null, "column"); + document.l10n.setAttributes(th, keyStrId); + tbody.appendChild($.new("tr", [th, $.new("td", data[key])])); + } + + if ("syscallLog" in data) { + let syscallBody = $("sandbox-syscalls-tbody"); + let argsHead = $("sandbox-syscalls-argshead"); + for (let syscall of data.syscallLog) { + if (argsHead.colSpan < syscall.args.length) { + argsHead.colSpan = syscall.args.length; + } + let procTypeStrId = toFluentID(syscall.procType); + let cells = [ + $.new("td", syscall.index, "integer"), + $.new("td", syscall.msecAgo / 1000), + $.new("td", syscall.pid, "integer"), + $.new("td", syscall.tid, "integer"), + $.new("td", null, null, { + "data-l10n-id": "sandbox-proc-type-" + procTypeStrId, + }), + $.new("td", syscall.syscall, "integer"), + ]; + for (let arg of syscall.args) { + cells.push($.new("td", arg, "integer")); + } + syscallBody.appendChild($.new("tr", cells)); + } + } + }, + + intl(data) { + $("intl-locale-requested").textContent = JSON.stringify( + data.localeService.requested + ); + $("intl-locale-available").textContent = JSON.stringify( + data.localeService.available + ); + $("intl-locale-supported").textContent = JSON.stringify( + data.localeService.supported + ); + $("intl-locale-regionalprefs").textContent = JSON.stringify( + data.localeService.regionalPrefs + ); + $("intl-locale-default").textContent = JSON.stringify( + data.localeService.defaultLocale + ); + + $("intl-osprefs-systemlocales").textContent = JSON.stringify( + data.osPrefs.systemLocales + ); + $("intl-osprefs-regionalprefs").textContent = JSON.stringify( + data.osPrefs.regionalPrefsLocales + ); + }, + + thirdPartyModules(aData) { + if (!showThirdPartyModules()) { + return; + } + + if (!aData || !aData.length) { + $("third-party-modules-no-data").hidden = false; + return; + } + + const createElementWithLabel = (tag, label) => + label + ? $.new(tag, label) + : $.new(tag, "", "", { + "data-l10n-id": "support-third-party-modules-no-value", + }); + const createLoadStatusElement = (tag, status) => { + const labelLoadStatus = [ + "support-third-party-modules-status-loaded", + "support-third-party-modules-status-blocked", + "support-third-party-modules-status-redirected", + ]; + return status >= 0 && status < labelLoadStatus.length + ? $.new(tag, "", "", { "data-l10n-id": labelLoadStatus[status] }) + : $.new(tag, status); + }; + + const iconUp = "chrome://global/skin/icons/arrow-up-12.svg"; + const iconDown = "chrome://global/skin/icons/arrow-dropdown-12.svg"; + const iconFolder = "chrome://global/skin/icons/findFile.svg"; + const iconUnsigned = + "chrome://global/skin/icons/connection-mixed-active-loaded.svg"; + const outerTHead = $("third-party-modules-thead"); + const outerTBody = $("third-party-modules-tbody"); + + outerTBody.addEventListener("click", event => { + const btnOpenDir = event.target.closest("button"); + if (!btnOpenDir || !btnOpenDir.fileObj) { + return; + } + btnOpenDir.fileObj.reveal(); + }); + + for (const module of aData) { + const btnOpenDir = $.new( + "button", + [ + $.new("img", "", "third-party-svg-common", { + src: iconFolder, + "data-l10n-id": "support-third-party-modules-folder-icon", + }), + ], + "third-party-button third-party-button-open-dir", + { + "data-l10n-id": "support-third-party-modules-button-open", + } + ); + btnOpenDir.fileObj = module.dllFile; + + const innerTBody = $.new("tbody", [], null); + for (const event of module.events) { + innerTBody.appendChild( + $.new("tr", [ + $.new("td", event.processIdAndType), + createElementWithLabel("td", event.threadName), + $.new("td", event.baseAddress), + $.new("td", event.processUptimeMS), + // loadDurationMS can be empty (not zero) when a module is loaded + // very early in the process. processUptimeMS always has a value. + createElementWithLabel("td", event.loadDurationMS), + createLoadStatusElement("td", event.loadStatus), + ]) + ); + } + + const detailRow = $.new( + "tr", + [ + $.new( + "td", + [ + $.new("table", [ + $.new("thead", [ + $.new("th", "", "", { + "data-l10n-id": "support-third-party-modules-process", + }), + $.new("th", "", "", { + "data-l10n-id": "support-third-party-modules-thread", + }), + $.new("th", "", "", { + "data-l10n-id": "support-third-party-modules-base", + }), + $.new("th", "", "", { + "data-l10n-id": "support-third-party-modules-uptime", + }), + $.new("th", "", "", { + "data-l10n-id": "support-third-party-modules-duration", + }), + $.new("th", "", "", { + "data-l10n-id": "support-third-party-modules-status", + }), + ]), + innerTBody, + ]), + ], + "", + { colspan: outerTHead.children.length + 1 } + ), + ], + "", + { hidden: true } + ); + + const imgUpDown = $.new("img", "", "third-party-svg-common", { + src: iconDown, + "data-l10n-id": "support-third-party-modules-down-icon", + }); + const btnExpandCollapse = $.new( + "button", + [imgUpDown], + "third-party-button", + { + "data-l10n-id": "support-third-party-modules-expand", + } + ); + btnExpandCollapse.addEventListener("click", () => { + if (detailRow.hidden) { + detailRow.hidden = false; + imgUpDown.src = iconUp; + document.l10n.setAttributes( + imgUpDown, + "support-third-party-modules-up-icon" + ); + document.l10n.setAttributes( + btnExpandCollapse, + "support-third-party-modules-collapse" + ); + } else { + detailRow.hidden = true; + imgUpDown.src = iconDown; + document.l10n.setAttributes( + imgUpDown, + "support-third-party-modules-down-icon" + ); + document.l10n.setAttributes( + btnExpandCollapse, + "support-third-party-modules-expand" + ); + } + }); + + const vendorInfoCell = $.new("td", [ + createElementWithLabel("span", module.signedBy || module.companyName), + ]); + if (!module.signedBy) { + vendorInfoCell.prepend( + $.new( + "img", + "", + "third-party-svg-common third-party-image-unsigned", + { + src: iconUnsigned, + "data-l10n-id": "support-third-party-modules-unsigned-icon", + } + ) + ); + } + + outerTBody.appendChild( + $.new("tr", [ + $.new("td", [ + document.createTextNode(module.dllFile.leafName), + btnOpenDir, + ]), + createElementWithLabel("td", module.fileVersion), + vendorInfoCell, + $.new( + "td", + module.events.length, + "third-party-modules-column-occurrence" + ), + $.new("td", [btnExpandCollapse], "third-party-modules-column-expand"), + ]) + ); + outerTBody.appendChild(detailRow); + } + }, +}; + +var $ = document.getElementById.bind(document); + +$.new = function $_new(tag, textContentOrChildren, className, attributes) { + let elt = document.createElement(tag); + if (className) { + elt.className = className; + } + if (attributes) { + if (attributes["data-l10n-id"]) { + let args = attributes.hasOwnProperty("data-l10n-args") + ? attributes["data-l10n-args"] + : undefined; + document.l10n.setAttributes(elt, attributes["data-l10n-id"], args); + delete attributes["data-l10n-id"]; + if (args) { + delete attributes["data-l10n-args"]; + } + } + + for (let attrName in attributes) { + elt.setAttribute(attrName, attributes[attrName]); + } + } + if (Array.isArray(textContentOrChildren)) { + this.append(elt, textContentOrChildren); + } else if (!attributes || !attributes["data-l10n-id"]) { + elt.textContent = String(textContentOrChildren); + } + return elt; +}; + +$.append = function $_append(parent, children) { + children.forEach(c => parent.appendChild(c)); +}; + +function assembleFromGraphicsFailure(i, data) { + // Only cover the cases we have today; for example, we do not have + // log failures that assert and we assume the log level is 1/error. + let message = data.failures[i]; + let index = data.indices[i]; + let what = ""; + if (message.search(/\[GFX1-\]: \(LF\)/) == 0) { + // Non-asserting log failure - the message is substring(14) + what = "LogFailure"; + message = message.substring(14); + } else if (message.search(/\[GFX1-\]: /) == 0) { + // Non-asserting - the message is substring(9) + what = "Error"; + message = message.substring(9); + } else if (message.search(/\[GFX1\]: /) == 0) { + // Asserting - the message is substring(8) + what = "Assert"; + message = message.substring(8); + } + let assembled = { + index, + header: "(#" + index + ") " + what, + message, + }; + return assembled; +} + +function sortedArrayFromObject(obj) { + let tuples = []; + for (let prop in obj) { + tuples.push([prop, obj[prop]]); + } + tuples.sort(([prop1, v1], [prop2, v2]) => prop1.localeCompare(prop2)); + return tuples; +} + +function copyRawDataToClipboard(button) { + if (button) { + button.disabled = true; + } + try { + Troubleshoot.snapshot(async function(snapshot) { + if (button) { + button.disabled = false; + } + let str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + str.data = JSON.stringify(snapshot, undefined, 2); + let transferable = Cc[ + "@mozilla.org/widget/transferable;1" + ].createInstance(Ci.nsITransferable); + transferable.init(getLoadContext()); + transferable.addDataFlavor("text/unicode"); + transferable.setTransferData("text/unicode", str, str.data.length * 2); + Services.clipboard.setData( + transferable, + null, + Ci.nsIClipboard.kGlobalClipboard + ); + if (AppConstants.platform == "android") { + // Present a snackbar notification. + var { Snackbars } = ChromeUtils.import( + "resource://gre/modules/Snackbars.jsm" + ); + let rawDataCopiedString = await document.l10n.formatValue( + "raw-data-copied" + ); + Snackbars.show(rawDataCopiedString, Snackbars.LENGTH_SHORT); + } + }); + } catch (err) { + if (button) { + button.disabled = false; + } + throw err; + } +} + +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +async function copyContentsToClipboard() { + // Get the HTML and text representations for the important part of the page. + let contentsDiv = $("contents").cloneNode(true); + // Remove the items we don't want to copy from the clone: + contentsDiv.querySelectorAll(".no-copy, [hidden]").forEach(n => n.remove()); + let dataHtml = contentsDiv.innerHTML; + let dataText = createTextForElement(contentsDiv); + + // We can't use plain strings, we have to use nsSupportsString. + let supportsStringClass = Cc["@mozilla.org/supports-string;1"]; + let ssHtml = supportsStringClass.createInstance(Ci.nsISupportsString); + let ssText = supportsStringClass.createInstance(Ci.nsISupportsString); + + let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + transferable.init(getLoadContext()); + + // Add the HTML flavor. + transferable.addDataFlavor("text/html"); + ssHtml.data = dataHtml; + transferable.setTransferData("text/html", ssHtml, dataHtml.length * 2); + + // Add the plain text flavor. + transferable.addDataFlavor("text/unicode"); + ssText.data = dataText; + transferable.setTransferData("text/unicode", ssText, dataText.length * 2); + + // Store the data into the clipboard. + Services.clipboard.setData( + transferable, + null, + Services.clipboard.kGlobalClipboard + ); + + if (AppConstants.platform == "android") { + // Present a snackbar notification. + var { Snackbars } = ChromeUtils.import( + "resource://gre/modules/Snackbars.jsm" + ); + let textCopiedString = await document.l10n.formatValue("text-copied"); + Snackbars.show(textCopiedString, Snackbars.LENGTH_SHORT); + } +} + +// Return the plain text representation of an element. Do a little bit +// of pretty-printing to make it human-readable. +function createTextForElement(elem) { + let serializer = new Serializer(); + let text = serializer.serialize(elem); + + // Actual CR/LF pairs are needed for some Windows text editors. + if (AppConstants.platform == "win") { + text = text.replace(/\n/g, "\r\n"); + } + + return text; +} + +function Serializer() {} + +Serializer.prototype = { + serialize(rootElem) { + this._lines = []; + this._startNewLine(); + this._serializeElement(rootElem); + this._startNewLine(); + return this._lines.join("\n").trim() + "\n"; + }, + + // The current line is always the line that writing will start at next. When + // an element is serialized, the current line is updated to be the line at + // which the next element should be written. + get _currentLine() { + return this._lines.length ? this._lines[this._lines.length - 1] : null; + }, + + set _currentLine(val) { + return (this._lines[this._lines.length - 1] = val); + }, + + _serializeElement(elem) { + // table + if (elem.localName == "table") { + this._serializeTable(elem); + return; + } + + // all other elements + + let hasText = false; + for (let child of elem.childNodes) { + if (child.nodeType == Node.TEXT_NODE) { + let text = this._nodeText(child); + this._appendText(text); + hasText = hasText || !!text.trim(); + } else if (child.nodeType == Node.ELEMENT_NODE) { + this._serializeElement(child); + } + } + + // For headings, draw a "line" underneath them so they stand out. + let isHeader = /^h[0-9]+$/.test(elem.localName); + if (isHeader) { + let headerText = (this._currentLine || "").trim(); + if (headerText) { + this._startNewLine(); + this._appendText("-".repeat(headerText.length)); + } + } + + // Add a blank line underneath elements but only if they contain text. + if (hasText && (isHeader || "p" == elem.localName)) { + this._startNewLine(); + this._startNewLine(); + } + }, + + _startNewLine(lines) { + let currLine = this._currentLine; + if (currLine) { + // The current line is not empty. Trim it. + this._currentLine = currLine.trim(); + if (!this._currentLine) { + // The current line became empty. Discard it. + this._lines.pop(); + } + } + this._lines.push(""); + }, + + _appendText(text, lines) { + this._currentLine += text; + }, + + _isHiddenSubHeading(th) { + return th.parentNode.parentNode.style.display == "none"; + }, + + _serializeTable(table) { + // Collect the table's column headings if in fact there are any. First + // check thead. If there's no thead, check the first tr. + let colHeadings = {}; + let tableHeadingElem = table.querySelector("thead"); + if (!tableHeadingElem) { + tableHeadingElem = table.querySelector("tr"); + } + if (tableHeadingElem) { + let tableHeadingCols = tableHeadingElem.querySelectorAll("th,td"); + // If there's a contiguous run of th's in the children starting from the + // rightmost child, then consider them to be column headings. + for (let i = tableHeadingCols.length - 1; i >= 0; i--) { + let col = tableHeadingCols[i]; + if (col.localName != "th" || col.classList.contains("title-column")) { + break; + } + colHeadings[i] = this._nodeText(col).trim(); + } + } + let hasColHeadings = !!Object.keys(colHeadings).length; + if (!hasColHeadings) { + tableHeadingElem = null; + } + + let trs = table.querySelectorAll("table > tr, tbody > tr"); + let startRow = + tableHeadingElem && tableHeadingElem.localName == "tr" ? 1 : 0; + + if (startRow >= trs.length) { + // The table's empty. + return; + } + + if (hasColHeadings) { + // Use column headings. Print each tr as a multi-line chunk like: + // Heading 1: Column 1 value + // Heading 2: Column 2 value + for (let i = startRow; i < trs.length; i++) { + let children = trs[i].querySelectorAll("td"); + for (let j = 0; j < children.length; j++) { + let text = ""; + if (colHeadings[j]) { + text += colHeadings[j] + ": "; + } + text += this._nodeText(children[j]).trim(); + this._appendText(text); + this._startNewLine(); + } + this._startNewLine(); + } + return; + } + + // Don't use column headings. Assume the table has only two columns and + // print each tr in a single line like: + // Column 1 value: Column 2 value + for (let i = startRow; i < trs.length; i++) { + let children = trs[i].querySelectorAll("th,td"); + let rowHeading = this._nodeText(children[0]).trim(); + if (children[0].classList.contains("title-column")) { + if (!this._isHiddenSubHeading(children[0])) { + this._appendText(rowHeading); + } + } else if (children.length == 1) { + // This is a single-cell row. + this._appendText(rowHeading); + } else { + let childTables = trs[i].querySelectorAll("table"); + if (childTables.length) { + // If we have child tables, don't use nodeText - its trs are already + // queued up from querySelectorAll earlier. + this._appendText(rowHeading + ": "); + } else { + this._appendText( + rowHeading + ": " + this._nodeText(children[1]).trim() + ); + } + } + this._startNewLine(); + } + this._startNewLine(); + }, + + _nodeText(node) { + return node.textContent.replace(/\s+/g, " "); + }, +}; + +function openProfileDirectory() { + // Get the profile directory. + let currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile); + let profileDir = currProfD.path; + + // Show the profile directory. + let nsLocalFile = Components.Constructor( + "@mozilla.org/file/local;1", + "nsIFile", + "initWithPath" + ); + new nsLocalFile(profileDir).reveal(); +} + +/** + * Profile reset is only supported for the default profile if the appropriate migrator exists. + */ +function populateActionBox() { + if (ResetProfile.resetSupported()) { + $("reset-box").style.display = "block"; + } + if (!Services.appinfo.inSafeMode && AppConstants.platform !== "android") { + $("safe-mode-box").style.display = "block"; + + if (Services.policies && !Services.policies.isAllowed("safeMode")) { + $("restart-in-safe-mode-button").setAttribute("disabled", "true"); + } + } +} + +// Prompt user to restart the browser in safe mode +function safeModeRestart() { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + + if (!cancelQuit.data) { + Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit); + } +} +/** + * Set up event listeners for buttons. + */ +function setupEventListeners() { + let button = $("reset-box-button"); + if (button) { + button.addEventListener("click", function(event) { + ResetProfile.openConfirmationDialog(window); + }); + } + button = $("clear-startup-cache-button"); + if (button) { + button.addEventListener("click", async function(event) { + const [ + promptTitle, + promptBody, + restartButtonLabel, + ] = await document.l10n.formatValues([ + { id: "startup-cache-dialog-title" }, + { id: "startup-cache-dialog-body" }, + { id: "restart-button-label" }, + ]); + const buttonFlags = + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + + Services.prompt.BUTTON_POS_0_DEFAULT; + const result = Services.prompt.confirmEx( + window, + promptTitle, + promptBody, + buttonFlags, + restartButtonLabel, + null, + null, + null, + {} + ); + if (result !== 0) { + return; + } + Services.appinfo.invalidateCachesOnRestart(); + Services.startup.quit( + Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit + ); + }); + } + button = $("restart-in-safe-mode-button"); + if (button) { + button.addEventListener("click", function(event) { + if ( + Services.obs + .enumerateObservers("restart-in-safe-mode") + .hasMoreElements() + ) { + Services.obs.notifyObservers(null, "restart-in-safe-mode"); + } else { + safeModeRestart(); + } + }); + } + if (AppConstants.MOZ_UPDATER) { + button = $("update-dir-button"); + if (button) { + button.addEventListener("click", function(event) { + // Get the update directory. + let updateDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile); + if (!updateDir.exists()) { + updateDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + let updateDirPath = updateDir.path; + // Show the update directory. + let nsLocalFile = Components.Constructor( + "@mozilla.org/file/local;1", + "nsIFile", + "initWithPath" + ); + new nsLocalFile(updateDirPath).reveal(); + }); + } + button = $("show-update-history-button"); + if (button) { + button.addEventListener("click", function(event) { + window.browsingContext.topChromeWindow.openDialog( + "chrome://mozapps/content/update/history.xhtml", + "Update:History", + "centerscreen,resizable=no,titlebar,modal" + ); + }); + } + } + button = $("verify-place-integrity-button"); + if (button) { + button.addEventListener("click", function(event) { + PlacesDBUtils.checkAndFixDatabase().then(tasksStatusMap => { + let logs = []; + for (let [key, value] of tasksStatusMap) { + logs.push(`> Task: ${key}`); + let prefix = value.succeeded ? "+ " : "- "; + logs = logs.concat(value.logs.map(m => `${prefix}${m}`)); + } + $("verify-place-result").style.display = "block"; + $("verify-place-result").classList.remove("no-copy"); + $("verify-place-result").textContent = logs.join("\n"); + }); + }); + } + + $("copy-raw-data-to-clipboard").addEventListener("click", function(event) { + copyRawDataToClipboard(this); + }); + $("copy-to-clipboard").addEventListener("click", function(event) { + copyContentsToClipboard(); + }); + $("profile-dir-button").addEventListener("click", function(event) { + openProfileDirectory(); + }); +} diff --git a/toolkit/content/aboutSupport.xhtml b/toolkit/content/aboutSupport.xhtml new file mode 100644 index 0000000000..fe72e41ea2 --- /dev/null +++ b/toolkit/content/aboutSupport.xhtml @@ -0,0 +1,810 @@ +<?xml version="1.0" encoding="UTF-8"?> + +# 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/. + +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> %brandDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" /> + <title data-l10n-id="page-title"/> + + <link rel="icon" type="image/png" id="favicon" + href="chrome://branding/content/icon32.png"/> + <link rel="stylesheet" href="chrome://global/skin/aboutSupport.css" + type="text/css"/> + + <script src="chrome://global/content/aboutSupport.js"/> + <link rel="localization" href="branding/brand.ftl"/> + <link rel="localization" href="toolkit/about/aboutSupport.ftl"/> + <link rel="localization" href="toolkit/global/resetProfile.ftl"/> + <link rel="localization" href="toolkit/global/processTypes.ftl"/> +#ifndef ANDROID + <link rel="localization" href="toolkit/featuregates/features.ftl"/> +#endif + </head> + + <body class="wide-container"> + +#ifndef ANDROID + <div id="action-box" class="notice-box"> + <div id="reset-box"> + <h3 data-l10n-id="refresh-profile"/> + <button id="reset-box-button" data-l10n-id="refresh-profile-button"/> + </div> + <div id="safe-mode-box"> + <h3 data-l10n-id="safe-mode-title"/> + <button id="restart-in-safe-mode-button" data-l10n-id="restart-in-safe-mode-label"/> + </div> + <div id="clear-startup-cache-box"> + <h3 data-l10n-id="clear-startup-cache-title"/> + <button id="clear-startup-cache-button" data-l10n-id="clear-startup-cache-label"/> + </div> + </div> +#endif + + <h1 data-l10n-id="page-title"/> + + <div class="page-subtitle" data-l10n-id="page-subtitle"> + <a id="supportLink" data-l10n-name="support-link"></a> + </div> + + <div> + <button id="copy-raw-data-to-clipboard" data-l10n-id="copy-raw-data-to-clipboard-label"/> + <button id="copy-to-clipboard" data-l10n-id="copy-text-to-clipboard-label"/> + </div> + + <div id="contents"> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="app-basics-title"/> + + <table> + <tbody> + <tr> + <th class="column" data-l10n-id="app-basics-name"/> + + <td id="application-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-version"/> + + <td id="version-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-build-id"/> + <td id="buildid-box"></td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-distribution-id"/> + <td id="distributionid-box"></td> + </tr> + +#ifndef ANDROID +#ifdef MOZ_UPDATER + <tr id="update-row" class="no-copy"> + <th class="column" data-l10n-id="app-basics-update-dir"/> + + <td> + <button id="update-dir-button" data-l10n-id="show-dir-label"/> + <span id="update-dir-box" dir="ltr"> + </span> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-update-history"/> + + <td> + <button id="show-update-history-button" data-l10n-id="app-basics-show-update-history"/> + </td> + </tr> +#endif +#endif + +#ifdef MOZ_UPDATER + <tr> + <th class="column" data-l10n-id="app-basics-update-channel"/> + <td id="updatechannel-box"></td> + </tr> +#endif + + <tr> + <th class="column" data-l10n-id="app-basics-user-agent"/> + + <td id="useragent-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-os"/> + + <td id="os-box"> + </td> + </tr> + +#ifdef XP_MACOSX + <tr> + <th class="column" data-l10n-id="app-basics-rosetta"/> + + <td id="rosetta-box"> + </td> + </tr> +#endif + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-binary"/> + + <td id="binary-box" dir="ltr"> + </td> + </tr> + + <tr id="profile-row" class="no-copy"> + <th class="column" data-l10n-id="app-basics-profile-dir"/> + + <td> + <button id="profile-dir-button" data-l10n-id="show-dir-label"/> + <span id="profile-dir-box" dir="ltr"> + </span> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-enabled-plugins"/> + + <td> + <a href="about:plugins">about:plugins</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-build-config"/> + + <td> + <a href="about:buildconfig">about:buildconfig</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-memory-use"/> + + <td> + <a href="about:memory">about:memory</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-performance"/> + + <td> + <a href="about:performance">about:performance</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-service-workers"/> + + <td> + <a href="about:serviceworkers">about:serviceworkers</a> + </td> + </tr> + +#if defined(XP_WIN) && defined(MOZ_LAUNCHER_PROCESS) + <tr> + <th class="column" data-l10n-id="app-basics-launcher-process-status"/> + + <td id="launcher-process-box"> + </td> + </tr> +#endif + + <tr> + <th class="column" data-l10n-id="app-basics-multi-process-support"/> + + <td id="multiprocess-box"> + <span id="multiprocess-box-process-count"/> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-fission-support"/> + + <td id="fission-box"> + <span id="fission-box-process-count"/> + <span id="fission-box-status"/> + </td> + </tr> + + <tr id="remoteprocesses-row"> + <th class="column" data-l10n-id="app-basics-remote-processes-count"/> + + <td> + <a href="#remote-processes"></a> + </td> + </tr> + + <tr id="policies-status-row"> + <th class="column" data-l10n-id="app-basics-enterprise-policies"/> + + <td id="policies-status"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-location-service-key-google"/> + + <td id="key-location-service-google-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-safebrowsing-key-google"/> + + <td id="key-safebrowsing-google-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-key-mozilla"/> + + <td id="key-mozilla-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-safe-mode"/> + + <td id="safemode-box"> + </td> + </tr> + +#ifndef ANDROID + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-profiles"/> + + <td> + <a href="about:profiles">about:profiles</a> + </td> + </tr> +#endif + + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> +#ifdef MOZ_CRASHREPORTER + + <h2 class="major-section" id="crashes-title" data-l10n-id="crashes-title"/> + + <table id="crashes-table"> + <thead> + <tr> + <th data-l10n-id="crashes-id"/> + <th data-l10n-id="crashes-send-date"/> + </tr> + </thead> + <tbody id="crashes-tbody"> + </tbody> + </table> + <p id="crashes-allReports" class="hidden no-copy"> + <a href="about:crashes" id="crashes-allReportsWithPending" + class="block" data-l10n-id="crashes-all-reports"/> + </p> + <p id="crashes-noConfig" class="hidden no-copy" data-l10n-id="crashes-no-config"/> + +#endif + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="features-title"/> + + <table id="features-table"> + <thead> + <tr> + <th data-l10n-id="features-name"/> + <th data-l10n-id="features-version"/> + <th data-l10n-id="features-id"/> + </tr> + </thead> + <tbody id="features-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="processes-title" id="remote-processes"/> + + <table id="remote-processes-table"> + <thead> + <tr> + <th data-l10n-id="processes-type"/> + <th data-l10n-id="processes-count"/> + </tr> + </thead> + <tbody id="processes-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="support-addons-title"/> + + <table> + <thead> + <tr> + <th data-l10n-id="support-addons-name"/> + <th data-l10n-id="support-addons-type"/> + <th data-l10n-id="support-addons-version"/> + <th data-l10n-id="support-addons-enabled"/> + <th data-l10n-id="support-addons-id"/> + </tr> + </thead> + <tbody id="addons-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" id="security-software-title" data-l10n-id="security-software-title"/> + + <table id="security-software-table"> + <thead> + <tr> + <th data-l10n-id="security-software-type"/> + <th data-l10n-id="security-software-name"/> + </tr> + </thead> + <tbody> + <tr> + <th class="column" data-l10n-id="security-software-antivirus"/> + + <td id="security-software-antivirus"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="security-software-antispyware"/> + + <td id="security-software-antispyware"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="security-software-firewall"/> + + <td id="security-software-firewall"> + </td> + </tr> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="graphics-title"/> + + <table> + <tbody id="graphics-features-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-features-title"/> + </tr> + </tbody> + + <tbody id="graphics-gpu-1-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-gpu1-title"/> + </tr> + </tbody> + + <tbody id="graphics-gpu-2-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-gpu2-title"/> + </tr> + </tbody> + + <tbody id="graphics-diagnostics-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-diagnostics-title"/> + </tr> + </tbody> + + <tbody id="graphics-decisions-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-decision-log-title"/> + </tr> + </tbody> + + <tbody id="graphics-crashguards-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-crash-guards-title"/> + </tr> + </tbody> + + <tbody id="graphics-workarounds-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-workarounds-title"/> + </tr> + </tbody> + + <tbody id="graphics-failures-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-failure-log-title"/> + </tr> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="media-title"/> + <table> + <tbody id="media-info-tbody"> + </tbody> + + <tbody id="media-output-devices-tbody"> + <tr> + <th colspan="9" class="title-column" data-l10n-id="media-output-devices-title"/> + </tr> + <tr> + <th data-l10n-id="media-device-name"/> + <th data-l10n-id="media-device-group"/> + <th data-l10n-id="media-device-vendor"/> + <th data-l10n-id="media-device-state"/> + <th data-l10n-id="media-device-preferred"/> + <th data-l10n-id="media-device-format"/> + <th data-l10n-id="media-device-channels"/> + <th data-l10n-id="media-device-rate"/> + <th data-l10n-id="media-device-latency"/> + </tr> + </tbody> + + <tbody id="media-input-devices-tbody"> + <tr> + <th colspan="9" class="title-column" data-l10n-id="media-input-devices-title"/> + </tr> + <tr> + <th data-l10n-id="media-device-name"/> + <th data-l10n-id="media-device-group"/> + <th data-l10n-id="media-device-vendor"/> + <th data-l10n-id="media-device-state"/> + <th data-l10n-id="media-device-preferred"/> + <th data-l10n-id="media-device-format"/> + <th data-l10n-id="media-device-channels"/> + <th data-l10n-id="media-device-rate"/> + <th data-l10n-id="media-device-latency"/> + </tr> + </tbody> + + <tbody id="media-capabilities-tbody"> + <tr> + <th colspan="9" class="title-column" data-l10n-id="media-capabilities-title"/> + </tr> + <tr> + <td colspan="9"> + <button id="enumerate-database-button" data-l10n-id="media-capabilities-enumerate"/> + <pre id="enumerate-database-result" class="hidden no-copy"></pre> + </td> + </tr> + </tbody> + + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="environment-variables-title"/> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name" data-l10n-id="environment-variables-name"/> + + <th class="value" data-l10n-id="environment-variables-value"/> + </thead> + + <tbody id="environment-variables-tbody"> + </tbody> + </table> + + +#ifndef ANDROID + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="experimental-features-title"/> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name" data-l10n-id="experimental-features-name"/> + + <th class="value" data-l10n-id="experimental-features-value"/> + </thead> + + <tbody id="experimental-features-tbody"> + </tbody> + </table> +#endif + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="modified-key-prefs-title"/> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name" data-l10n-id="modified-prefs-name"/> + + <th class="value" data-l10n-id="modified-prefs-value"/> + </thead> + + <tbody id="prefs-tbody"> + </tbody> + </table> + + <section id="prefs-user-js-section" class="hidden no-copy"> + <h3 data-l10n-id="user-js-title"/> + <p data-l10n-id="user-js-description"> + <a id="prefs-user-js-link" data-l10n-name="user-js-link"></a> + </p> + </section> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="locked-key-prefs-title"/> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name" data-l10n-id="locked-prefs-name"/> + + <th class="value" data-l10n-id="locked-prefs-value"/> + </thead> + + <tbody id="locked-prefs-tbody"> + </tbody> + </table> + +#ifndef ANDROID + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="place-database-title"/> + + <table> + <tr class="no-copy"> + <th class="column" data-l10n-id="place-database-integrity"/> + + <td> + <button id="verify-place-integrity-button" data-l10n-id="place-database-verify-integrity"/> + <pre id="verify-place-result" class="hidden no-copy"></pre> + </td> + </tr> + </table> +#endif + + <!-- - - - - - - - - - - - - - - - - - - - - --> + <h2 class="major-section" data-l10n-id="a11y-title"/> + + <table> + <tbody> + <tr> + <th class="column" data-l10n-id="a11y-activated"/> + + <td id="a11y-activated"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="a11y-force-disabled"/> + + <td id="a11y-force-disabled"> + </td> + </tr> +#if defined(XP_WIN) + <tr> + <th class="column" data-l10n-id="a11y-handler-used"/> + + <td id="a11y-handler-used"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="a11y-instantiator"/> + + <td id="a11y-instantiator"> + </td> + </tr> +#endif + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + <h2 class="major-section" data-l10n-id="library-version-title"/> + + <table> + <tbody id="libversions-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + +#if defined(MOZ_SANDBOX) + <h2 class="major-section" id="sandbox" data-l10n-id="sandbox-title"/> + + <table> + <tbody id="sandbox-tbody"> + </tbody> + </table> + +#if defined(XP_LINUX) + <h4 data-l10n-id="sandbox-sys-call-log-title"/> + <table> + <thead> + <tr> + <th data-l10n-id="sandbox-sys-call-index"/> + <th data-l10n-id="sandbox-sys-call-age"/> + <th data-l10n-id="sandbox-sys-call-pid"/> + <th data-l10n-id="sandbox-sys-call-tid"/> + <th data-l10n-id="sandbox-sys-call-proc-type"/> + <th data-l10n-id="sandbox-sys-call-number"/> + <th id="sandbox-syscalls-argshead" data-l10n-id="sandbox-sys-call-args"/> + </tr> + </thead> + <tbody id="sandbox-syscalls-tbody"> + </tbody> + </table> +#endif +#endif + + <h2 class="major-section" data-l10n-id="startup-cache-title"/> + + <table> + <tbody> + <tr> + <th class="column" data-l10n-id="startup-cache-disk-cache-path"/> + + <td id="startup-cache-disk-cache-path"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="startup-cache-ignore-disk-cache"/> + + <td id="startup-cache-ignore-disk-cache"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="startup-cache-found-disk-cache-on-init"/> + + <td id="startup-cache-found-disk-cache-on-init"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="startup-cache-wrote-to-disk-cache"/> + + <td id="startup-cache-wrote-to-disk-cache"> + </td> + </tr> + </tbody> + </table> + + <h2 class="major-section" data-l10n-id="intl-title"/> + + <table> + <tbody id="intl-localeservice-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="intl-app-title"/> + </tr> + <tr> + <th class="column" data-l10n-id="intl-locales-requested"/> + <td id="intl-locale-requested"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-locales-available"/> + <td id="intl-locale-available"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-locales-supported"/> + <td id="intl-locale-supported"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-regional-prefs"/> + <td id="intl-locale-regionalprefs"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-locales-default"/> + <td id="intl-locale-default"> + </td> + </tr> + </tbody> + <tbody id="intl-ospreferences-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="intl-os-title"/> + </tr> + <tr> + <th class="column" data-l10n-id="intl-os-prefs-system-locales"/> + <td id="intl-osprefs-systemlocales"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-regional-prefs"/> + <td id="intl-osprefs-regionalprefs"> + </td> + </tr> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + +#if defined(ENABLE_REMOTE_AGENT) + <h2 class="major-section" data-l10n-id="remote-debugging-title"/> + + <table> + <tbody> + <tr> + <th class="column" data-l10n-id="remote-debugging-accepting-connections"/> + <td id="remote-debugging-accepting-connections"></td> + </tr> + <tr> + <th class="column" data-l10n-id="remote-debugging-url"/> + <td id="remote-debugging-url"></td> + </tr> + </tbody> + </table> +#endif + +#ifndef ANDROID + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="support-printing-title"/> + + <table> + <tr class="no-copy"> + <th class="column" data-l10n-id="support-printing-troubleshoot"/> + <td> + <button id="support-printing-clear-settings-button" data-l10n-id="support-printing-clear-settings-button"/> + </td> + </tr> + </table> + + <h3 data-l10n-id="support-printing-modified-settings"/> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name" data-l10n-id="support-printing-prefs-name"/> + + <th class="value" data-l10n-id="support-printing-prefs-value"/> + </thead> + + <tbody id="support-printing-prefs-tbody"> + </tbody> + </table> +#endif + +#ifdef XP_WIN + <h2 id="third-party-modules" class="major-section" hidden="true" + data-l10n-id="support-third-party-modules-title"/> + <table id="third-party-modules-table" class="prefs-table" hidden="true"> + <thead id="third-party-modules-thead"> + <th data-l10n-id="support-third-party-modules-module"/> + <th class="third-party-modules-column-version" + data-l10n-id="support-third-party-modules-version"/> + <th data-l10n-id="support-third-party-modules-vendor"/> + <th class="third-party-modules-header-occurrence" colspan="2" + data-l10n-id="support-third-party-modules-occurrence"/> + </thead> + <tbody id="third-party-modules-tbody"> + <tr><td id="third-party-modules-no-data" colspan="5" hidden="true" + data-l10n-id="support-third-party-modules-empty"></td></tr> + </tbody> + </table> +#endif + + </div> + + </body> + +</html> diff --git a/toolkit/content/aboutTelemetry.css b/toolkit/content/aboutTelemetry.css new file mode 100644 index 0000000000..e589a3962b --- /dev/null +++ b/toolkit/content/aboutTelemetry.css @@ -0,0 +1,344 @@ +/* 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/. */ + +@import url("chrome://global/skin/in-content/common.css"); + +body { + display: flex; +} + +/* This is needed to make the sidebar not hide the last button but breaks printing by hiding overflowing content */ +@media not print { + html, body { + height: 100%; + } +} + +#categories { + padding-top: 0; + overflow-y: auto; + margin-bottom: 42px; + user-select: none; +} + +.main-content.search > section > *:not(.data) { + display: none; +} + +.main-content { + flex: 1; + line-height: 1.6; +} + +#home-section { + font-size: 18px; +} + +#category-raw { + background-color: var(--in-content-page-background); + position: absolute; + bottom: 0; + inset-inline-start: 0; +} + +.heading { + display: flex; + flex-direction: column; + font-size: 17px; + font-weight: 600; + pointer-events: none; + padding: 12px 8px; +} + +.header { + display: flex; +} + +.header select { + margin-inline-start: 4px; +} + +#sectionTitle { + flex-grow: 1; +} + +#sectionFilters { + display: flex; + align-items: center; +} + +#stores { + padding-block: 5px; + padding-inline-start: 5px; +} + +#ping-type { + flex-grow: 1; + text-align: center; + pointer-events: all; + cursor: pointer; +} + +#older-ping, +#newer-ping, +#ping-date { + pointer-events: all; + user-select: none; + cursor: pointer; + text-align: center; +} + +.dropdown { + background-image: url(chrome://global/skin/icons/arrow-dropdown-16.svg); + background-position: right 8px center; + background-repeat: no-repeat; + -moz-context-properties: fill; + fill: currentColor; +} + +.dropdown:dir(rtl) { + background-position-x: left 8px; +} + +#controls { + display: flex; + margin-top: 4px; + justify-content: space-between; +} + +.category:not(.has-data) { + display: none; +} + +.category { + cursor: pointer; + display: flex; + flex-direction: column; + min-height: 42px; +} + +#categories > .category.category-no-icon { + margin-inline-start: 0; + margin-inline-end: 0; + width: auto; +} + +.category-name { + padding: 9px 0; + vertical-align: middle; +} + +.category-subsection { + color: var(--in-content-text-color); + padding: 8px 0; + padding-inline-start: 16px; + display: none; +} + +.category-subsection.selected { + color: inherit; +} + +.category-subsection::first-letter { + text-transform: uppercase; +} + +.category.selected > .category-subsection { + display: block; +} + +.category-name { + pointer-events: none; +} + +section:not(.active) { + display: none; +} + +#ping-explanation > span { + cursor: pointer; + border-bottom-width: 2px; + border-bottom-style: solid; +} + +#no-search-results { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + align-items: center; + flex-direction: column; +} + +#no-search-results-text { + font-size: 17px; + margin-bottom: 2em; +} + +.no-search-results-image { + background-image: url("chrome://browser/skin/preferences/no-search-results.svg"); + width: 380px; + height: 293px; +} + +.hidden { + display: none !important; +} + +#ping-picker { + min-width: 300px; + position: fixed; + z-index: 2; + top: 32px; + border-radius: 2px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.25); + display: flex; + padding: 24px; + flex-direction: column; + background-color: var(--in-content-box-background); + border: 1px solid var(--in-content-box-border-color); + margin: 12px 0; + inset-inline-start: 12px; +} + +#ping-picker .title { + margin: 4px 0; +} + +#ping-source-picker { + margin-inline-start: 5px; + margin-bottom: 10px; +} + +#ping-source-archive-container.disabled { + opacity: 0.5; +} + +.stack-title { + font-size: medium; + font-weight: bold; + text-decoration: underline; +} + +#histograms { + overflow: hidden; +} + +.histogram { + float: inline-start; + white-space: nowrap; + padding: 10px; + position: relative; /* required for position:absolute of the contained .copy-node */ + padding-block: 12px; + padding-inline: 20px; + border: 1px solid var(--in-content-box-border-color); + border-radius: 2px; + margin-bottom: 24px; + margin-inline-end: 24px; + min-height: 17.5em; +} + +.histogram-title { + text-overflow: ellipsis; + width: 100%; + white-space: nowrap; + overflow: hidden; + font-size: 17px +} + +.histogram-stats { + font-size: 13px; +} + +.keyed-histogram { + white-space: nowrap; + position: relative; /* required for position:absolute of the contained .copy-node */ + overflow: hidden; + margin-bottom: 1em; +} + +.keyed-scalar, +.sub-section { + margin-bottom: 1em; +} + +.keyed-title { + text-overflow: ellipsis; + margin: 12px 0; + font-size: 17px; + white-space: nowrap; +} + +.bar { + font-size: 17px; + width: 2em; + margin: 2px; + text-align: center; + float: inline-start; + font-family: monospace; +} + +.bar-inner { + background-color: #0a84ff; + border: 1px solid #0060df; + border-radius: 2px; +} + +.bar:nth-child(even) .long-label { + margin-bottom: 1em; +} + +th, td, table { + text-align: start; + word-break: break-all; + border-collapse: collapse; +} + +table { + table-layout: fixed; + width: 100%; + font-size: 15px; +} + +td { + padding-bottom: 0.25em; + border-bottom: 1px solid var(--in-content-box-border-color); +} + +tr:not(:first-child):hover { + background-color: rgba(0, 0, 0, 0.05); +} + +th { + font-size: 13px; + white-space: nowrap; + padding: 0.5em 0; +} + +caption { + text-align: start; + font-size: 22px; + margin-block: 0.5em; + margin-inline: 0; +} + +.copy-node { + visibility: hidden; + position: absolute; + bottom: 1px; + inset-inline-end: 1px; +} + +.histogram:hover .copy-node { + visibility: visible; +} + +#raw-ping-data { + font-size: 15px; +} + +.clearfix { + clear: both; +} diff --git a/toolkit/content/aboutTelemetry.js b/toolkit/content/aboutTelemetry.js new file mode 100644 index 0000000000..aeb8a31d7c --- /dev/null +++ b/toolkit/content/aboutTelemetry.js @@ -0,0 +1,2739 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { BrowserUtils } = ChromeUtils.import( + "resource://gre/modules/BrowserUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { TelemetryTimestamps } = ChromeUtils.import( + "resource://gre/modules/TelemetryTimestamps.jsm" +); +const { TelemetryController } = ChromeUtils.import( + "resource://gre/modules/TelemetryController.jsm" +); +const { TelemetryArchive } = ChromeUtils.import( + "resource://gre/modules/TelemetryArchive.jsm" +); +const { TelemetrySend } = ChromeUtils.import( + "resource://gre/modules/TelemetrySend.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "AppConstants", + "resource://gre/modules/AppConstants.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "Preferences", + "resource://gre/modules/Preferences.jsm" +); + +const Telemetry = Services.telemetry; +const bundle = Services.strings.createBundle( + "chrome://global/locale/aboutTelemetry.properties" +); +const brandBundle = Services.strings.createBundle( + "chrome://branding/locale/brand.properties" +); + +// Maximum height of a histogram bar (in em for html, in chars for text) +const MAX_BAR_HEIGHT = 8; +const MAX_BAR_CHARS = 25; +const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner"; +const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; +const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql"; +const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl"; +const DEFAULT_SYMBOL_SERVER_URI = "https://symbols.mozilla.org/symbolicate/v4"; +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +// ms idle before applying the filter (allow uninterrupted typing) +const FILTER_IDLE_TIMEOUT = 500; + +const isWindows = Services.appinfo.OS == "WINNT"; +const EOL = isWindows ? "\r\n" : "\n"; + +// This is the ping object currently displayed in the page. +var gPingData = null; + +// Cached value of document's RTL mode +var documentRTLMode = ""; + +/** + * Helper function for determining whether the document direction is RTL. + * Caches result of check on first invocation. + */ +function isRTL() { + if (!documentRTLMode) { + documentRTLMode = window.getComputedStyle(document.body).direction; + } + return documentRTLMode == "rtl"; +} + +function isFlatArray(obj) { + if (!Array.isArray(obj)) { + return false; + } + return !obj.some(e => typeof e == "object"); +} + +/** + * This is a helper function for explodeObject. + */ +function flattenObject(obj, map, path, array) { + for (let k of Object.keys(obj)) { + let newPath = [...path, array ? "[" + k + "]" : k]; + let v = obj[k]; + if (!v || typeof v != "object") { + map.set(newPath.join("."), v); + } else if (isFlatArray(v)) { + map.set(newPath.join("."), "[" + v.join(", ") + "]"); + } else { + flattenObject(v, map, newPath, Array.isArray(v)); + } + } +} + +/** + * This turns a JSON object into a "flat" stringified form. + * + * For an object like {a: "1", b: {c: "2", d: "3"}} it returns a Map of the + * form Map(["a","1"], ["b.c", "2"], ["b.d", "3"]). + */ +function explodeObject(obj) { + let map = new Map(); + flattenObject(obj, map, []); + return map; +} + +function filterObject(obj, filterOut) { + let ret = {}; + for (let k of Object.keys(obj)) { + if (!filterOut.includes(k)) { + ret[k] = obj[k]; + } + } + return ret; +} + +/** + * This turns a JSON object into a "flat" stringified form, separated into top-level sections. + * + * For an object like: + * { + * a: {b: "1"}, + * c: {d: "2", e: {f: "3"}} + * } + * it returns a Map of the form: + * Map([ + * ["a", Map(["b","1"])], + * ["c", Map([["d", "2"], ["e.f", "3"]])] + * ]) + */ +function sectionalizeObject(obj) { + let map = new Map(); + for (let k of Object.keys(obj)) { + map.set(k, explodeObject(obj[k])); + } + return map; +} + +/** + * Obtain the main DOMWindow for the current context. + */ +function getMainWindow() { + return window.browsingContext.topChromeWindow; +} + +/** + * Obtain the DOMWindow that can open a preferences pane. + * + * This is essentially "get the browser chrome window" with the added check + * that the supposed browser chrome window is capable of opening a preferences + * pane. + * + * This may return null if we can't find the browser chrome window. + */ +function getMainWindowWithPreferencesPane() { + let mainWindow = getMainWindow(); + if (mainWindow && "openPreferences" in mainWindow) { + return mainWindow; + } + return null; +} + +/** + * Remove all child nodes of a document node. + */ +function removeAllChildNodes(node) { + while (node.hasChildNodes()) { + node.removeChild(node.lastChild); + } +} + +var Settings = { + SETTINGS: [ + // data upload + { + pref: PREF_FHR_UPLOAD_ENABLED, + defaultPrefValue: false, + }, + // extended "Telemetry" recording + { + pref: PREF_TELEMETRY_ENABLED, + defaultPrefValue: false, + }, + ], + + attachObservers() { + for (let s of this.SETTINGS) { + let setting = s; + Preferences.observe(setting.pref, this.render, this); + } + + let elements = document.getElementsByClassName("change-data-choices-link"); + for (let el of elements) { + el.parentElement.addEventListener("click", function(event) { + if (event.target.localName === "a") { + if (AppConstants.platform == "android") { + var { EventDispatcher } = ChromeUtils.import( + "resource://gre/modules/Messaging.jsm" + ); + EventDispatcher.instance.sendRequest({ + type: "Settings:Show", + resource: "preferences_privacy", + }); + } else { + // Show the data choices preferences on desktop. + let mainWindow = getMainWindowWithPreferencesPane(); + mainWindow.openPreferences("privacy-reports"); + } + } + }); + } + }, + + detachObservers() { + for (let setting of this.SETTINGS) { + Preferences.ignore(setting.pref, this.render, this); + } + }, + + /** + * Updates the button & text at the top of the page to reflect Telemetry state. + */ + render() { + let settingsExplanation = document.getElementById("settings-explanation"); + let extendedEnabled = Services.telemetry.canRecordExtended; + + let channel = extendedEnabled ? "prerelease" : "release"; + let uploadcase = TelemetrySend.sendingEnabled() ? "enabled" : "disabled"; + + document.l10n.setAttributes( + settingsExplanation, + "about-telemetry-settings-explanation", + { channel, uploadcase } + ); + + this.attachObservers(); + }, +}; + +var PingPicker = { + viewCurrentPingData: null, + _archivedPings: null, + TYPE_ALL: "all", + + attachObservers() { + let pingSourceElements = document.getElementsByName("choose-ping-source"); + for (let el of pingSourceElements) { + el.addEventListener("change", () => this.onPingSourceChanged()); + } + + let displays = document.getElementsByName("choose-ping-display"); + for (let el of displays) { + el.addEventListener("change", () => this.onPingDisplayChanged()); + } + + document + .getElementById("show-subsession-data") + .addEventListener("change", () => { + this._updateCurrentPingData(); + }); + + document.getElementById("choose-ping-id").addEventListener("change", () => { + this._updateArchivedPingData(); + }); + document + .getElementById("choose-ping-type") + .addEventListener("change", () => { + this.filterDisplayedPings(); + }); + + document + .getElementById("newer-ping") + .addEventListener("click", () => this._movePingIndex(-1)); + document + .getElementById("older-ping") + .addEventListener("click", () => this._movePingIndex(1)); + + let pingPickerNeedHide = false; + let pingPicker = document.getElementById("ping-picker"); + pingPicker.addEventListener( + "mouseenter", + () => (pingPickerNeedHide = false) + ); + pingPicker.addEventListener( + "mouseleave", + () => (pingPickerNeedHide = true) + ); + document.addEventListener("click", ev => { + if (pingPickerNeedHide) { + pingPicker.classList.add("hidden"); + } + }); + document + .getElementById("stores") + .addEventListener("change", () => displayPingData(gPingData)); + Array.from(document.querySelectorAll(".change-ping")).forEach(el => { + el.addEventListener("click", event => { + if (!pingPicker.classList.contains("hidden")) { + pingPicker.classList.add("hidden"); + } else { + pingPicker.classList.remove("hidden"); + event.stopPropagation(); + } + }); + }); + }, + + onPingSourceChanged() { + this.update(); + }, + + onPingDisplayChanged() { + this.update(); + }, + + render() { + // Display the type and controls if the ping is not current + let pingDate = document.getElementById("ping-date"); + let pingType = document.getElementById("ping-type"); + let controls = document.getElementById("controls"); + let pingExplanation = document.getElementById("ping-explanation"); + + if (!this.viewCurrentPingData) { + let pingName = this._getSelectedPingName(); + // Change sidebar heading text. + pingDate.textContent = pingName; + pingDate.setAttribute("title", pingName); + let pingTypeText = this._getSelectedPingType(); + controls.classList.remove("hidden"); + pingType.textContent = pingTypeText; + document.l10n.setAttributes( + pingExplanation, + "about-telemetry-ping-details", + { timestamp: pingTypeText, name: pingName } + ); + } else { + // Change sidebar heading text. + controls.classList.add("hidden"); + document.l10n.setAttributes( + pingType, + "about-telemetry-current-data-sidebar" + ); + // Change home page text. + document.l10n.setAttributes( + pingExplanation, + "about-telemetry-data-details-current" + ); + } + + GenericSubsection.deleteAllSubSections(); + }, + + async update() { + let viewCurrent = document.getElementById("ping-source-current").checked; + let currentChanged = viewCurrent !== this.viewCurrentPingData; + this.viewCurrentPingData = viewCurrent; + + // If we have no archived pings, disable the ping archive selection. + // This can happen on new profiles or if the ping archive is disabled. + let archivedPingList = await TelemetryArchive.promiseArchivedPingList(); + let sourceArchived = document.getElementById("ping-source-archive"); + let sourceArchivedContainer = document.getElementById( + "ping-source-archive-container" + ); + let archivedDisabled = !archivedPingList.length; + sourceArchived.disabled = archivedDisabled; + sourceArchivedContainer.classList.toggle("disabled", archivedDisabled); + + if (currentChanged) { + if (this.viewCurrentPingData) { + document.getElementById("current-ping-picker").hidden = false; + document.getElementById("archived-ping-picker").hidden = true; + this._updateCurrentPingData(); + } else { + document.getElementById("current-ping-picker").hidden = true; + await this._updateArchivedPingList(archivedPingList); + document.getElementById("archived-ping-picker").hidden = false; + } + } + }, + + _updateCurrentPingData() { + const subsession = document.getElementById("show-subsession-data").checked; + let ping = TelemetryController.getCurrentPingData(subsession); + if (!ping) { + return; + } + + let stores = Telemetry.getAllStores(); + let getData = { + histograms: Telemetry.getSnapshotForHistograms, + keyedHistograms: Telemetry.getSnapshotForKeyedHistograms, + scalars: Telemetry.getSnapshotForScalars, + keyedScalars: Telemetry.getSnapshotForKeyedScalars, + }; + + let data = {}; + for (const [name, fn] of Object.entries(getData)) { + for (const store of stores) { + if (!data[store]) { + data[store] = {}; + } + let measurement = fn(store, /* clear */ false, /* filterTest */ true); + let processes = Object.keys(measurement); + + for (const process of processes) { + if (!data[store][process]) { + data[store][process] = {}; + } + + data[store][process][name] = measurement[process]; + } + } + } + ping.payload.stores = data; + + // Delete the unused data from the payload of the current ping. + // It's included in the above `stores` attribute. + for (const data of Object.values(ping.payload.processes)) { + delete data.scalars; + delete data.keyedScalars; + delete data.histograms; + delete data.keyedHistograms; + } + delete ping.payload.histograms; + delete ping.payload.keyedHistograms; + + // augment ping payload with event telemetry + let eventSnapshot = Telemetry.snapshotEvents( + Telemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + for (let process of Object.keys(eventSnapshot)) { + if (process in ping.payload.processes) { + ping.payload.processes[process].events = eventSnapshot[process].filter( + e => !e[1].startsWith("telemetry.test") + ); + } + } + + // augment "current ping payload" with origin telemetry + const originSnapshot = Telemetry.getOriginSnapshot(false /* clear */); + ping.payload.origins = originSnapshot; + + displayPingData(ping, true); + }, + + _updateArchivedPingData() { + let id = this._getSelectedPingId(); + let res = Promise.resolve(); + if (id) { + res = TelemetryArchive.promiseArchivedPingById(id).then(ping => + displayPingData(ping, true) + ); + } + return res; + }, + + async _updateArchivedPingList(pingList) { + // The archived ping list is sorted in ascending timestamp order, + // but descending is more practical for the operations we do here. + pingList.reverse(); + this._archivedPings = pingList; + // Render the archive data. + this._renderPingList(); + // Update the displayed ping. + await this._updateArchivedPingData(); + }, + + _renderPingList() { + let pingSelector = document.getElementById("choose-ping-id"); + Array.from(pingSelector.children).forEach(child => + removeAllChildNodes(child) + ); + + let pingTypes = new Set(); + pingTypes.add(this.TYPE_ALL); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + + for (let p of this._archivedPings) { + pingTypes.add(p.type); + const pingDate = new Date(p.timestampCreated); + const datetimeText = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeStyle: "medium", + }).format(pingDate); + const pingName = `${datetimeText}, ${p.type}`; + + let option = document.createElement("option"); + let content = document.createTextNode(pingName); + option.appendChild(content); + option.setAttribute("value", p.id); + option.dataset.type = p.type; + option.dataset.date = datetimeText; + + pingDate.setHours(0, 0, 0, 0); + if (pingDate.getTime() === today.getTime()) { + pingSelector.children[0].appendChild(option); + } else if (pingDate.getTime() === yesterday.getTime()) { + pingSelector.children[1].appendChild(option); + } else { + pingSelector.children[2].appendChild(option); + } + } + this._renderPingTypes(pingTypes); + }, + + _renderPingTypes(pingTypes) { + let pingTypeSelector = document.getElementById("choose-ping-type"); + removeAllChildNodes(pingTypeSelector); + pingTypes.forEach(type => { + let option = document.createElement("option"); + option.appendChild(document.createTextNode(type)); + option.setAttribute("value", type); + pingTypeSelector.appendChild(option); + }); + }, + + _movePingIndex(offset) { + if (this.viewCurrentPingData) { + return; + } + let typeSelector = document.getElementById("choose-ping-type"); + let type = typeSelector.selectedOptions.item(0).value; + + let id = this._getSelectedPingId(); + let index = this._archivedPings.findIndex(p => p.id == id); + let newIndex = Math.min( + Math.max(0, index + offset), + this._archivedPings.length - 1 + ); + + let pingList; + if (offset > 0) { + pingList = this._archivedPings.slice(newIndex); + } else { + pingList = this._archivedPings.slice(0, newIndex); + pingList.reverse(); + } + + let ping = pingList.find(p => { + return type == this.TYPE_ALL || p.type == type; + }); + + if (ping) { + this.selectPing(ping); + this._updateArchivedPingData(); + } + }, + + selectPing(ping) { + let pingSelector = document.getElementById("choose-ping-id"); + // Use some() to break if we find the ping. + Array.from(pingSelector.children).some(group => { + return Array.from(group.children).some(option => { + if (option.value == ping.id) { + option.selected = true; + return true; + } + return false; + }); + }); + }, + + filterDisplayedPings() { + let pingSelector = document.getElementById("choose-ping-id"); + let typeSelector = document.getElementById("choose-ping-type"); + let type = typeSelector.selectedOptions.item(0).value; + let first = true; + Array.from(pingSelector.children).forEach(group => { + Array.from(group.children).forEach(option => { + if (first && option.dataset.type == type) { + option.selected = true; + first = false; + } + option.hidden = type != this.TYPE_ALL && option.dataset.type != type; + // Arrow keys should only iterate over visible options + option.disabled = option.hidden; + }); + }); + this._updateArchivedPingData(); + }, + + _getSelectedPingName() { + let pingSelector = document.getElementById("choose-ping-id"); + let selected = pingSelector.selectedOptions.item(0); + return selected.dataset.date; + }, + + _getSelectedPingType() { + let pingSelector = document.getElementById("choose-ping-id"); + let selected = pingSelector.selectedOptions.item(0); + return selected.dataset.type; + }, + + _getSelectedPingId() { + let pingSelector = document.getElementById("choose-ping-id"); + let selected = pingSelector.selectedOptions.item(0); + return selected.getAttribute("value"); + }, + + _showRawPingData() { + show(document.getElementById("category-raw")); + }, + + _showStructuredPingData() { + show(document.getElementById("category-home")); + }, +}; + +var GeneralData = { + /** + * Renders the general data + */ + render(aPing) { + setHasData("general-data-section", true); + let generalDataSection = document.getElementById("general-data"); + removeAllChildNodes(generalDataSection); + + const headings = [ + "about-telemetry-names-header", + "about-telemetry-values-header", + ]; + + // The payload & environment parts are handled by other renderers. + let ignoreSections = ["payload", "environment"]; + let data = explodeObject(filterObject(aPing, ignoreSections)); + + const table = GenericTable.render(data, headings); + generalDataSection.appendChild(table); + }, +}; + +var EnvironmentData = { + /** + * Renders the environment data + */ + render(ping) { + let dataDiv = document.getElementById("environment-data"); + removeAllChildNodes(dataDiv); + const hasData = !!ping.environment; + setHasData("environment-data-section", hasData); + if (!hasData) { + return; + } + + let ignore = ["addons"]; + let env = filterObject(ping.environment, ignore); + let sections = sectionalizeObject(env); + GenericSubsection.render(sections, dataDiv, "environment-data-section"); + + // We use specialized rendering here to make the addon and plugin listings + // more readable. + this.createAddonSection(dataDiv, ping); + }, + + renderPersona(addonObj, addonSection, sectionTitle) { + let table = document.createElement("table"); + table.setAttribute("id", sectionTitle); + this.appendAddonSubsectionTitle(sectionTitle, table); + this.appendRow(table, "persona", addonObj.persona); + addonSection.appendChild(table); + }, + + renderActivePlugins(addonObj, addonSection, sectionTitle) { + let table = document.createElement("table"); + table.setAttribute("id", sectionTitle); + this.appendAddonSubsectionTitle(sectionTitle, table); + + for (let plugin of addonObj) { + let data = explodeObject(plugin); + this.appendHeadingName(table, data.get("name")); + + for (let [key, value] of data) { + this.appendRow(table, key, value); + } + } + + addonSection.appendChild(table); + }, + + renderAddonsObject(addonObj, addonSection, sectionTitle) { + let table = document.createElement("table"); + table.setAttribute("id", sectionTitle); + this.appendAddonSubsectionTitle(sectionTitle, table); + + for (let id of Object.keys(addonObj)) { + let addon = addonObj[id]; + this.appendHeadingName(table, addon.name || id); + this.appendAddonID(table, id); + let data = explodeObject(addon); + + for (let [key, value] of data) { + this.appendRow(table, key, value); + } + } + + addonSection.appendChild(table); + }, + + renderKeyValueObject(addonObj, addonSection, sectionTitle) { + let data = explodeObject(addonObj); + let table = GenericTable.render(data); + table.setAttribute("class", sectionTitle); + this.appendAddonSubsectionTitle(sectionTitle, table); + addonSection.appendChild(table); + }, + + appendAddonID(table, addonID) { + this.appendRow(table, "id", addonID); + }, + + appendHeadingName(table, name) { + let headings = document.createElement("tr"); + this.appendColumn(headings, "th", name); + headings.cells[0].colSpan = 2; + table.appendChild(headings); + }, + + appendAddonSubsectionTitle(section, table) { + let caption = document.createElement("caption"); + caption.appendChild(document.createTextNode(section)); + table.appendChild(caption); + }, + + createAddonSection(dataDiv, ping) { + if (!ping || !("environment" in ping) || !("addons" in ping.environment)) { + return; + } + let addonSection = document.createElement("div"); + addonSection.setAttribute("class", "subsection-data subdata"); + let addons = ping.environment.addons; + this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons"); + this.renderActivePlugins( + addons.activePlugins, + addonSection, + "activePlugins" + ); + this.renderKeyValueObject(addons.theme, addonSection, "theme"); + this.renderAddonsObject( + addons.activeGMPlugins, + addonSection, + "activeGMPlugins" + ); + this.renderPersona(addons, addonSection, "persona"); + + let hasAddonData = !!Object.keys(ping.environment.addons).length; + let s = GenericSubsection.renderSubsectionHeader( + "addons", + hasAddonData, + "environment-data-section" + ); + s.appendChild(addonSection); + dataDiv.appendChild(s); + }, + + appendRow(table, id, value) { + let row = document.createElement("tr"); + row.id = id; + this.appendColumn(row, "td", id); + this.appendColumn(row, "td", value); + table.appendChild(row); + }, + /** + * Helper function for appending a column to the data table. + * + * @param aRowElement Parent row element + * @param aColType Column's tag name + * @param aColText Column contents + */ + appendColumn(aRowElement, aColType, aColText) { + let colElement = document.createElement(aColType); + let colTextElement = document.createTextNode(aColText); + colElement.appendChild(colTextElement); + aRowElement.appendChild(colElement); + }, +}; + +var SlowSQL = { + /** + * Render slow SQL statistics + */ + render: function SlowSQL_render(aPing) { + // We can add the debug SQL data to the current ping later. + // However, we need to be careful to never send that debug data + // out due to privacy concerns. + // We want to show the actual ping data for archived pings, + // so skip this there. + + let debugSlowSql = + PingPicker.viewCurrentPingData && + Preferences.get(PREF_DEBUG_SLOW_SQL, false); + let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL; + if (!slowSql) { + setHasData("slow-sql-section", false); + return; + } + + let { mainThread, otherThreads } = debugSlowSql + ? Telemetry.debugSlowSQL + : aPing.payload.slowSQL; + + let mainThreadCount = Object.keys(mainThread).length; + let otherThreadCount = Object.keys(otherThreads).length; + if (mainThreadCount == 0 && otherThreadCount == 0) { + setHasData("slow-sql-section", false); + return; + } + + setHasData("slow-sql-section", true); + if (debugSlowSql) { + document.getElementById("sql-warning").hidden = false; + } + + let slowSqlDiv = document.getElementById("slow-sql-tables"); + removeAllChildNodes(slowSqlDiv); + + // Main thread + if (mainThreadCount > 0) { + let table = document.createElement("table"); + this.renderTableHeader(table, "main"); + this.renderTable(table, mainThread); + slowSqlDiv.appendChild(table); + } + + // Other threads + if (otherThreadCount > 0) { + let table = document.createElement("table"); + this.renderTableHeader(table, "other"); + this.renderTable(table, otherThreads); + slowSqlDiv.appendChild(table); + } + }, + + /** + * Creates a header row for a Slow SQL table + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param aTable Parent table element + * @param aTitle Table's title + */ + renderTableHeader: function SlowSQL_renderTableHeader(aTable, threadType) { + let caption = document.createElement("caption"); + if (threadType == "main") { + document.l10n.setAttributes(caption, "about-telemetry-slow-sql-main"); + } + + if (threadType == "other") { + document.l10n.setAttributes(caption, "about-telemetry-slow-sql-other"); + } + aTable.appendChild(caption); + + let headings = document.createElement("tr"); + document.l10n.setAttributes( + this.appendColumn(headings, "th"), + "about-telemetry-slow-sql-hits" + ); + document.l10n.setAttributes( + this.appendColumn(headings, "th"), + "about-telemetry-slow-sql-average" + ); + document.l10n.setAttributes( + this.appendColumn(headings, "th"), + "about-telemetry-slow-sql-statement" + ); + aTable.appendChild(headings); + }, + + /** + * Fills out the table body + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param aTable Parent table element + * @param aSql SQL stats object + */ + renderTable: function SlowSQL_renderTable(aTable, aSql) { + for (let [sql, [hitCount, totalTime]] of Object.entries(aSql)) { + let averageTime = totalTime / hitCount; + + let sqlRow = document.createElement("tr"); + + this.appendColumn(sqlRow, "td", hitCount + "\t"); + this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t"); + this.appendColumn(sqlRow, "td", sql + "\n"); + + aTable.appendChild(sqlRow); + } + }, + + /** + * Helper function for appending a column to a Slow SQL table. + * + * @param aRowElement Parent row element + * @param aColType Column's tag name + * @param aColText Column contents + */ + appendColumn: function SlowSQL_appendColumn( + aRowElement, + aColType, + aColText = "" + ) { + let colElement = document.createElement(aColType); + if (aColText) { + let colTextElement = document.createTextNode(aColText); + colElement.appendChild(colTextElement); + } + aRowElement.appendChild(colElement); + return colElement; + }, +}; + +var StackRenderer = { + /** + * Outputs the memory map associated with this hang report + * + * @param aDiv Output div + */ + renderMemoryMap: async function StackRenderer_renderMemoryMap( + aDiv, + memoryMap + ) { + let memoryMapTitleElement = document.createElement("span"); + document.l10n.setAttributes( + memoryMapTitleElement, + "about-telemetry-memory-map-title" + ); + aDiv.appendChild(memoryMapTitleElement); + aDiv.appendChild(document.createElement("br")); + + for (let currentModule of memoryMap) { + aDiv.appendChild(document.createTextNode(currentModule.join(" "))); + aDiv.appendChild(document.createElement("br")); + } + + aDiv.appendChild(document.createElement("br")); + }, + + /** + * Outputs the raw PCs from the hang's stack + * + * @param aDiv Output div + * @param aStack Array of PCs from the hang stack + */ + renderStack: function StackRenderer_renderStack(aDiv, aStack) { + let stackTitleElement = document.createElement("span"); + document.l10n.setAttributes( + stackTitleElement, + "about-telemetry-stack-title" + ); + aDiv.appendChild(stackTitleElement); + let stackText = " " + aStack.join(" "); + aDiv.appendChild(document.createTextNode(stackText)); + + aDiv.appendChild(document.createElement("br")); + aDiv.appendChild(document.createElement("br")); + }, + renderStacks: function StackRenderer_renderStacks( + aPrefix, + aStacks, + aMemoryMap, + aRenderHeader + ) { + let div = document.getElementById(aPrefix); + removeAllChildNodes(div); + + let fetchE = document.getElementById(aPrefix + "-fetch-symbols"); + if (fetchE) { + fetchE.hidden = false; + } + let hideE = document.getElementById(aPrefix + "-hide-symbols"); + if (hideE) { + hideE.hidden = true; + } + + if (!aStacks.length) { + return; + } + + setHasData(aPrefix + "-section", true); + + this.renderMemoryMap(div, aMemoryMap); + + for (let i = 0; i < aStacks.length; ++i) { + let stack = aStacks[i]; + aRenderHeader(i); + this.renderStack(div, stack); + } + }, + + /** + * Renders the title of the stack: e.g. "Late Write #1" or + * "Hang Report #1 (6 seconds)". + * + * @param aFormatArgs formating args to be passed to formatStringFromName. + */ + renderHeader: function StackRenderer_renderHeader(aPrefix, aFormatArgs) { + let div = document.getElementById(aPrefix); + + let titleElement = document.createElement("span"); + titleElement.className = "stack-title"; + + let titleText = bundle.formatStringFromName( + aPrefix + "-title", + aFormatArgs + ); + titleElement.appendChild(document.createTextNode(titleText)); + + div.appendChild(titleElement); + div.appendChild(document.createElement("br")); + }, +}; + +var RawPayloadData = { + /** + * Renders the raw pyaload. + */ + render(aPing) { + setHasData("raw-payload-section", true); + let pre = document.getElementById("raw-payload-data"); + pre.textContent = JSON.stringify(aPing.payload, null, 2); + }, + + attachObservers() { + document + .getElementById("payload-json-viewer") + .addEventListener("click", e => { + openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2)); + }); + }, +}; + +function SymbolicationRequest( + aPrefix, + aRenderHeader, + aMemoryMap, + aStacks, + aDurations = null +) { + this.prefix = aPrefix; + this.renderHeader = aRenderHeader; + this.memoryMap = aMemoryMap; + this.stacks = aStacks; + this.durations = aDurations; +} +/** + * A callback for onreadystatechange. It replaces the numeric stack with + * the symbolicated one returned by the symbolication server. + */ +SymbolicationRequest.prototype.handleSymbolResponse = async function SymbolicationRequest_handleSymbolResponse() { + if (this.symbolRequest.readyState != 4) { + return; + } + + let fetchElement = document.getElementById(this.prefix + "-fetch-symbols"); + fetchElement.hidden = true; + let hideElement = document.getElementById(this.prefix + "-hide-symbols"); + hideElement.hidden = false; + let div = document.getElementById(this.prefix); + removeAllChildNodes(div); + let errorMessage = await document.l10n.formatValue( + "about-telemetry-error-fetching-symbols" + ); + + if (this.symbolRequest.status != 200) { + div.appendChild(document.createTextNode(errorMessage)); + return; + } + + let jsonResponse = {}; + try { + jsonResponse = JSON.parse(this.symbolRequest.responseText); + } catch (e) { + div.appendChild(document.createTextNode(errorMessage)); + return; + } + + for (let i = 0; i < jsonResponse.length; ++i) { + let stack = jsonResponse[i]; + this.renderHeader(i, this.durations); + + for (let symbol of stack) { + div.appendChild(document.createTextNode(symbol)); + div.appendChild(document.createElement("br")); + } + div.appendChild(document.createElement("br")); + } +}; +/** + * Send a request to the symbolication server to symbolicate this stack. + */ +SymbolicationRequest.prototype.fetchSymbols = function SymbolicationRequest_fetchSymbols() { + let symbolServerURI = Preferences.get( + PREF_SYMBOL_SERVER_URI, + DEFAULT_SYMBOL_SERVER_URI + ); + let request = { memoryMap: this.memoryMap, stacks: this.stacks, version: 3 }; + let requestJSON = JSON.stringify(request); + + this.symbolRequest = new XMLHttpRequest(); + this.symbolRequest.open("POST", symbolServerURI, true); + this.symbolRequest.setRequestHeader("Content-type", "application/json"); + this.symbolRequest.setRequestHeader("Content-length", requestJSON.length); + this.symbolRequest.setRequestHeader("Connection", "close"); + this.symbolRequest.onreadystatechange = this.handleSymbolResponse.bind(this); + this.symbolRequest.send(requestJSON); +}; + +var CapturedStacks = { + symbolRequest: null, + + render: function CapturedStacks_render(payload) { + // Retrieve captured stacks from telemetry payload. + let capturedStacks = + "processes" in payload && "parent" in payload.processes + ? payload.processes.parent.capturedStacks + : false; + let hasData = + capturedStacks && capturedStacks.stacks && !!capturedStacks.stacks.length; + setHasData("captured-stacks-section", hasData); + if (!hasData) { + return; + } + + let stacks = capturedStacks.stacks; + let memoryMap = capturedStacks.memoryMap; + let captures = capturedStacks.captures; + + StackRenderer.renderStacks("captured-stacks", stacks, memoryMap, index => + this.renderCaptureHeader(index, captures) + ); + }, + + renderCaptureHeader: function CaptureStacks_renderCaptureHeader( + index, + captures + ) { + let key = captures[index][0]; + let cardinality = captures[index][2]; + StackRenderer.renderHeader("captured-stacks", [key, cardinality]); + }, +}; + +var Histogram = { + /** + * Renders a single Telemetry histogram + * + * @param aParent Parent element + * @param aName Histogram name + * @param aHgram Histogram information + * @param aOptions Object with render options + * * exponential: bars follow logarithmic scale + */ + render: function Histogram_render(aParent, aName, aHgram, aOptions) { + let options = aOptions || {}; + let hgram = this.processHistogram(aHgram, aName); + + let outerDiv = document.createElement("div"); + outerDiv.className = "histogram"; + outerDiv.id = aName; + + let divTitle = document.createElement("div"); + divTitle.classList.add("histogram-title"); + divTitle.appendChild(document.createTextNode(aName)); + outerDiv.appendChild(divTitle); + + let divStats = document.createElement("div"); + divStats.classList.add("histogram-stats"); + + let histogramStatsArgs = { + sampleCount: hgram.sample_count, + prettyAverage: hgram.pretty_average, + sum: hgram.sum, + }; + + document.l10n.setAttributes( + divStats, + "about-telemetry-histogram-stats", + histogramStatsArgs + ); + + if (isRTL()) { + hgram.values.reverse(); + } + + let textData = this.renderValues(outerDiv, hgram, options); + + // The 'Copy' button contains the textual data, copied to clipboard on click + let copyButton = document.createElement("button"); + copyButton.className = "copy-node"; + document.l10n.setAttributes(copyButton, "about-telemetry-histogram-copy"); + + copyButton.addEventListener("click", async function() { + let divStatsString = await document.l10n.formatValue( + "about-telemetry-histogram-stats", + histogramStatsArgs + ); + copyButton.histogramText = + aName + EOL + divStatsString + EOL + EOL + textData; + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(this.histogramText); + }); + outerDiv.appendChild(copyButton); + + aParent.appendChild(outerDiv); + return outerDiv; + }, + + processHistogram(aHgram, aName) { + const values = Object.keys(aHgram.values).map(k => aHgram.values[k]); + if (!values.length) { + // If we have no values collected for this histogram, just return + // zero values so we still render it. + return { + values: [], + pretty_average: 0, + max: 0, + sample_count: 0, + sum: 0, + }; + } + + const sample_count = values.reduceRight((a, b) => a + b); + const average = Math.round((aHgram.sum * 10) / sample_count) / 10; + const max_value = Math.max(...values); + + const labelledValues = Object.keys(aHgram.values).map(k => [ + Number(k), + aHgram.values[k], + ]); + + let result = { + values: labelledValues, + pretty_average: average, + max: max_value, + sample_count, + sum: aHgram.sum, + }; + + return result; + }, + + /** + * Return a non-negative, logarithmic representation of a non-negative number. + * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3 + * + * @param aNumber Non-negative number + */ + getLogValue(aNumber) { + return Math.max(0, Math.log10(aNumber) + 1); + }, + + /** + * Create histogram HTML bars, also returns a textual representation + * Both aMaxValue and aSumValues must be positive. + * Values are assumed to use 0 as baseline. + * + * @param aDiv Outer parent div + * @param aHgram The histogram data + * @param aOptions Object with render options (@see #render) + */ + renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) { + let text = ""; + // If the last label is not the longest string, alignment will break a little + let labelPadTo = 0; + if (aHgram.values.length) { + labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length; + } + let maxBarValue = aOptions.exponential + ? this.getLogValue(aHgram.max) + : aHgram.max; + + for (let [label, value] of aHgram.values) { + label = String(label); + let barValue = aOptions.exponential ? this.getLogValue(value) : value; + + // Create a text representation: <right-aligned-label> |<bar-of-#><value> <percentage> + text += + EOL + + " ".repeat(Math.max(0, labelPadTo - label.length)) + + label + // Right-aligned label + " |" + + "#".repeat(Math.round((MAX_BAR_CHARS * barValue) / maxBarValue)) + // Bar + " " + + value + // Value + " " + + Math.round((100 * value) / aHgram.sample_count) + + "%"; // Percentage + + // Construct the HTML labels + bars + let belowEm = + Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10; + let aboveEm = MAX_BAR_HEIGHT - belowEm; + + let barDiv = document.createElement("div"); + barDiv.className = "bar"; + barDiv.style.paddingTop = aboveEm + "em"; + + // Add value label or an nbsp if no value + barDiv.appendChild(document.createTextNode(value ? value : "\u00A0")); + + // Create the blue bar + let bar = document.createElement("div"); + bar.className = "bar-inner"; + bar.style.height = belowEm + "em"; + barDiv.appendChild(bar); + + // Add a special class to move the text down to prevent text overlap + if (label.length > 3) { + bar.classList.add("long-label"); + } + // Add bucket label + barDiv.appendChild(document.createTextNode(label)); + + aDiv.appendChild(barDiv); + } + + return text.substr(EOL.length); // Trim the EOL before the first line + }, +}; + +var Search = { + HASH_SEARCH: "search=", + + // A list of ids of sections that do not support search. + blacklist: ["late-writes-section", "raw-payload-section"], + + // Pass if: all non-empty array items match (case-sensitive) + isPassText(subject, filter) { + for (let item of filter) { + if (item.length && !subject.includes(item)) { + return false; // mismatch and not a spurious space + } + } + return true; + }, + + isPassRegex(subject, filter) { + return filter.test(subject); + }, + + chooseFilter(filterText) { + let filter = filterText.toString(); + // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx) + let isPassFunc; // filter function, set once, then applied to all elements + filter = filter.trim(); + if (filter[0] != "/") { + // Plain text: case insensitive, AND if multi-string + isPassFunc = this.isPassText; + filter = filter.toLowerCase().split(" "); + } else { + isPassFunc = this.isPassRegex; + var r = filter.match(/^\/(.*)\/(i?)$/); + try { + filter = RegExp(r[1], r[2]); + } catch (e) { + // Incomplete or bad RegExp - always no match + isPassFunc = function() { + return false; + }; + } + } + return [isPassFunc, filter]; + }, + + filterElements(elements, filterText) { + let [isPassFunc, filter] = this.chooseFilter(filterText); + let allElementHidden = true; + + let needLowerCase = isPassFunc === this.isPassText; + for (let element of elements) { + let subject = needLowerCase ? element.id.toLowerCase() : element.id; + element.hidden = !isPassFunc(subject, filter); + if (allElementHidden && !element.hidden) { + allElementHidden = false; + } + } + return allElementHidden; + }, + + filterKeyedElements(keyedElements, filterText) { + let [isPassFunc, filter] = this.chooseFilter(filterText); + let allElementsHidden = true; + + let needLowerCase = isPassFunc === this.isPassText; + keyedElements.forEach(keyedElement => { + let subject = needLowerCase + ? keyedElement.key.id.toLowerCase() + : keyedElement.key.id; + if (!isPassFunc(subject, filter)) { + // If the keyedHistogram's name is not matched + let allKeyedElementsHidden = true; + for (let element of keyedElement.datas) { + let subject = needLowerCase ? element.id.toLowerCase() : element.id; + let match = isPassFunc(subject, filter); + element.hidden = !match; + if (match) { + allKeyedElementsHidden = false; + } + } + if (allElementsHidden && !allKeyedElementsHidden) { + allElementsHidden = false; + } + keyedElement.key.hidden = allKeyedElementsHidden; + } else { + // If the keyedHistogram's name is matched + allElementsHidden = false; + keyedElement.key.hidden = false; + for (let element of keyedElement.datas) { + element.hidden = false; + } + } + }); + return allElementsHidden; + }, + + searchHandler(e) { + if (this.idleTimeout) { + clearTimeout(this.idleTimeout); + } + this.idleTimeout = setTimeout( + () => Search.search(e.target.value), + FILTER_IDLE_TIMEOUT + ); + }, + + search(text, sectionParam = null) { + let section = sectionParam; + if (!section) { + let sectionId = document + .querySelector(".category.selected") + .getAttribute("value"); + section = document.getElementById(sectionId); + } + if (Search.blacklist.includes(section.id)) { + return false; + } + let noSearchResults = true; + if (section.id === "home-section") { + return this.homeSearch(text); + } else if (section.id === "histograms-section") { + let histograms = section.getElementsByClassName("histogram"); + noSearchResults = this.filterElements(histograms, text); + } else if (section.id === "keyed-histograms-section") { + let keyedElements = []; + let keyedHistograms = section.getElementsByClassName("keyed-histogram"); + for (let key of keyedHistograms) { + let datas = key.getElementsByClassName("histogram"); + keyedElements.push({ key, datas }); + } + noSearchResults = this.filterKeyedElements(keyedElements, text); + } else if (section.id === "keyed-scalars-section") { + let keyedElements = []; + let keyedScalars = section.getElementsByClassName("keyed-scalar"); + for (let key of keyedScalars) { + let datas = key.querySelector("table").rows; + keyedElements.push({ key, datas }); + } + noSearchResults = this.filterKeyedElements(keyedElements, text); + } else if (section.querySelector(".sub-section")) { + let keyedSubSections = []; + let subsections = section.querySelectorAll(".sub-section"); + for (let section of subsections) { + let datas = section.querySelector("table").rows; + keyedSubSections.push({ key: section, datas }); + } + noSearchResults = this.filterKeyedElements(keyedSubSections, text); + } else { + let tables = section.querySelectorAll("table"); + for (let table of tables) { + noSearchResults = this.filterElements(table.rows, text); + if (table.caption) { + table.caption.hidden = noSearchResults; + } + } + } + + changeUrlSearch(text); + + if (!sectionParam) { + // If we are not searching in all section. + this.updateNoResults(text, noSearchResults); + } + return noSearchResults; + }, + + updateNoResults(text, noSearchResults) { + document + .getElementById("no-search-results") + .classList.toggle("hidden", !noSearchResults); + if (noSearchResults) { + let section = document.querySelector(".category.selected > span"); + let searchResultsText = document.getElementById("no-search-results-text"); + if (section.parentElement.id === "category-home") { + document.l10n.setAttributes( + searchResultsText, + "about-telemetry-no-search-results-all", + { searchTerms: text } + ); + } else { + let sectionName = section.textContent.trim(); + text === "" + ? document.l10n.setAttributes( + searchResultsText, + "about-telemetry-no-data-to-display", + { sectionName } + ) + : document.l10n.setAttributes( + searchResultsText, + "about-telemetry-no-search-results", + { sectionName, currentSearchText: text } + ); + } + } + }, + + resetHome() { + document.getElementById("main").classList.remove("search"); + document.getElementById("no-search-results").classList.add("hidden"); + adjustHeaderState(); + Array.from(document.querySelectorAll("section")).forEach(section => { + section.classList.toggle("active", section.id == "home-section"); + }); + }, + + homeSearch(text) { + changeUrlSearch(text); + removeSearchSectionTitles(); + if (text === "") { + this.resetHome(); + return; + } + document.getElementById("main").classList.add("search"); + adjustHeaderState(text); + let noSearchResults = true; + Array.from(document.querySelectorAll("section")).forEach(section => { + if (section.id == "home-section" || section.id == "raw-payload-section") { + section.classList.remove("active"); + return; + } + section.classList.add("active"); + let sectionHidden = this.search(text, section); + if (!sectionHidden) { + let sectionTitle = document.querySelector( + `.category[value="${section.id}"] .category-name` + ).textContent; + let sectionDataDiv = document.querySelector( + `#${section.id}.has-data.active .data` + ); + let titleDiv = document.createElement("h1"); + titleDiv.classList.add("data", "search-section-title"); + titleDiv.textContent = sectionTitle; + section.insertBefore(titleDiv, sectionDataDiv); + noSearchResults = false; + } else { + // Hide all subsections if the section is hidden + let subsections = section.querySelectorAll(".sub-section"); + for (let subsection of subsections) { + subsection.hidden = true; + } + } + }); + this.updateNoResults(text, noSearchResults); + }, +}; + +/* + * Helper function to render JS objects with white space between top level elements + * so that they look better in the browser + * @param aObject JavaScript object or array to render + * @return String + */ +function RenderObject(aObject) { + let output = ""; + if (Array.isArray(aObject)) { + if (!aObject.length) { + return "[]"; + } + output = "[" + JSON.stringify(aObject[0]); + for (let i = 1; i < aObject.length; i++) { + output += ", " + JSON.stringify(aObject[i]); + } + return output + "]"; + } + let keys = Object.keys(aObject); + if (!keys.length) { + return "{}"; + } + output = '{"' + keys[0] + '":\u00A0' + JSON.stringify(aObject[keys[0]]); + for (let i = 1; i < keys.length; i++) { + output += ', "' + keys[i] + '":\u00A0' + JSON.stringify(aObject[keys[i]]); + } + return output + "}"; +} + +var GenericSubsection = { + addSubSectionToSidebar(id, title) { + let category = document.querySelector("#categories > [value=" + id + "]"); + category.classList.add("has-subsection"); + let subCategory = document.createElement("div"); + subCategory.classList.add("category-subsection"); + subCategory.setAttribute("value", id + "-" + title); + subCategory.addEventListener("click", ev => { + let section = ev.target; + showSubSection(section); + }); + subCategory.appendChild(document.createTextNode(title)); + category.appendChild(subCategory); + }, + + render(data, dataDiv, sectionID) { + for (let [title, sectionData] of data) { + let hasData = sectionData.size > 0; + let s = this.renderSubsectionHeader(title, hasData, sectionID); + s.appendChild(this.renderSubsectionData(title, sectionData)); + dataDiv.appendChild(s); + } + }, + + renderSubsectionHeader(title, hasData, sectionID) { + this.addSubSectionToSidebar(sectionID, title); + let section = document.createElement("div"); + section.setAttribute("id", sectionID + "-" + title); + section.classList.add("sub-section"); + if (hasData) { + section.classList.add("has-subdata"); + } + return section; + }, + + renderSubsectionData(title, data) { + // Create data container + let dataDiv = document.createElement("div"); + dataDiv.setAttribute("class", "subsection-data subdata"); + // Instanciate the data + let table = GenericTable.render(data); + let caption = document.createElement("caption"); + caption.textContent = title; + table.appendChild(caption); + dataDiv.appendChild(table); + + return dataDiv; + }, + + deleteAllSubSections() { + let subsections = document.querySelectorAll(".category-subsection"); + subsections.forEach(el => { + el.parentElement.removeChild(el); + }); + }, +}; + +var GenericTable = { + // Returns a table with key and value headers + defaultHeadings() { + return ["about-telemetry-keys-header", "about-telemetry-values-header"]; + }, + + /** + * Returns a n-column table. + * @param rows An array of arrays, each containing data to render + * for one row. + * @param headings The column header strings. + */ + render(rows, headings = this.defaultHeadings()) { + let table = document.createElement("table"); + this.renderHeader(table, headings); + this.renderBody(table, rows); + return table; + }, + + /** + * Create the table header. + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param table Table element + * @param headings Array of column header strings. + */ + renderHeader(table, headings) { + let headerRow = document.createElement("tr"); + table.appendChild(headerRow); + + for (let i = 0; i < headings.length; ++i) { + let column = document.createElement("th"); + document.l10n.setAttributes(column, headings[i]); + headerRow.appendChild(column); + } + }, + + /** + * Create the table body + * Tabs & newlines added to cells to make it easier to copy-paste. + * + * @param table Table element + * @param rows An array of arrays, each containing data to render + * for one row. + */ + renderBody(table, rows) { + for (let row of rows) { + row = row.map(value => { + // use .valueOf() to unbox Number, String, etc. objects + if ( + value && + typeof value == "object" && + typeof value.valueOf() == "object" + ) { + return RenderObject(value); + } + return value; + }); + + let newRow = document.createElement("tr"); + newRow.id = row[0]; + table.appendChild(newRow); + + for (let i = 0; i < row.length; ++i) { + let suffix = i == row.length - 1 ? "\n" : "\t"; + let field = document.createElement("td"); + field.appendChild(document.createTextNode(row[i] + suffix)); + newRow.appendChild(field); + } + } + }, +}; + +var KeyedHistogram = { + render(parent, id, keyedHistogram) { + let outerDiv = document.createElement("div"); + outerDiv.className = "keyed-histogram"; + outerDiv.id = id; + + let divTitle = document.createElement("div"); + divTitle.classList.add("keyed-title"); + divTitle.appendChild(document.createTextNode(id)); + outerDiv.appendChild(divTitle); + + for (let [name, hgram] of Object.entries(keyedHistogram)) { + Histogram.render(outerDiv, name, hgram); + } + + parent.appendChild(outerDiv); + return outerDiv; + }, +}; + +var AddonDetails = { + /** + * Render the addon details section as a series of headers followed by key/value tables + * @param aPing A ping object to render the data from. + */ + render(aPing) { + let addonSection = document.getElementById("addon-details"); + removeAllChildNodes(addonSection); + let addonDetails = aPing.payload.addonDetails; + const hasData = addonDetails && !!Object.keys(addonDetails).length; + setHasData("addon-details-section", hasData); + if (!hasData) { + return; + } + + for (let provider in addonDetails) { + let providerSection = document.createElement("caption"); + document.l10n.setAttributes( + providerSection, + "about-telemetry-addon-provider", + { addonProvider: provider } + ); + let headingStrings = [ + "about-telemetry-addon-table-id", + "about-telemetry-addon-table-details", + ]; + let table = GenericTable.render( + explodeObject(addonDetails[provider]), + headingStrings + ); + table.appendChild(providerSection); + addonSection.appendChild(table); + } + }, +}; + +class Section { + static renderContent(data, process, div, section) { + if (data && Object.keys(data).length) { + let s = GenericSubsection.renderSubsectionHeader(process, true, section); + let heading = document.createElement("h2"); + document.l10n.setAttributes(heading, "about-telemetry-process", { + process, + }); + s.appendChild(heading); + + this.renderData(data, s); + + div.appendChild(s); + let separator = document.createElement("div"); + separator.classList.add("clearfix"); + div.appendChild(separator); + } + } + + /** + * Make parent process the first one, content process the second + * then sort processes alphabetically + */ + static processesComparator(a, b) { + if (a === "parent" || (a === "content" && b !== "parent")) { + return -1; + } else if (b === "parent" || b === "content") { + return 1; + } else if (a < b) { + return -1; + } else if (a > b) { + return 1; + } + return 0; + } + + /** + * Render sections + */ + static renderSection(divName, section, aPayload) { + let div = document.getElementById(divName); + removeAllChildNodes(div); + + let data = {}; + let hasData = false; + let selectedStore = getSelectedStore(); + + let payload = aPayload.stores; + + let isCurrentPayload = !!payload; + + // Sort processes + let sortedProcesses = isCurrentPayload + ? Object.keys(payload[selectedStore]).sort(this.processesComparator) + : Object.keys(aPayload.processes).sort(this.processesComparator); + + // Render content by process + for (const process of sortedProcesses) { + data = isCurrentPayload + ? this.dataFiltering(payload, selectedStore, process) + : this.archivePingDataFiltering(aPayload, process); + hasData = hasData || data !== {}; + this.renderContent(data, process, div, section, this.renderData); + } + setHasData(section, hasData); + } +} + +class Scalars extends Section { + /** + * Return data from the current ping + */ + static dataFiltering(payload, selectedStore, process) { + return payload[selectedStore][process].scalars; + } + + /** + * Return data from an archived ping + */ + static archivePingDataFiltering(payload, process) { + return payload.processes[process].scalars; + } + + static renderData(data, div) { + const scalarsHeadings = [ + "about-telemetry-names-header", + "about-telemetry-values-header", + ]; + let scalarsTable = GenericTable.render( + explodeObject(data), + scalarsHeadings + ); + div.appendChild(scalarsTable); + } + + /** + * Render the scalar data - if present - from the payload in a simple key-value table. + * @param aPayload A payload object to render the data from. + */ + static render(aPayload) { + const divName = "scalars"; + const section = "scalars-section"; + this.renderSection(divName, section, aPayload); + } +} + +class KeyedScalars extends Section { + /** + * Return data from the current ping + */ + static dataFiltering(payload, selectedStore, process) { + return payload[selectedStore][process].keyedScalars; + } + + /** + * Return data from an archived ping + */ + static archivePingDataFiltering(payload, process) { + return payload.processes[process].keyedScalars; + } + + static renderData(data, div) { + const scalarsHeadings = [ + "about-telemetry-names-header", + "about-telemetry-values-header", + ]; + for (let scalarId in data) { + // Add the name of the scalar. + let container = document.createElement("div"); + container.classList.add("keyed-scalar"); + container.id = scalarId; + let scalarNameSection = document.createElement("p"); + scalarNameSection.classList.add("keyed-title"); + scalarNameSection.appendChild(document.createTextNode(scalarId)); + container.appendChild(scalarNameSection); + // Populate the section with the key-value pairs from the scalar. + const table = GenericTable.render( + explodeObject(data[scalarId]), + scalarsHeadings + ); + container.appendChild(table); + div.appendChild(container); + } + } + + /** + * Render the keyed scalar data - if present - from the payload in a simple key-value table. + * @param aPayload A payload object to render the data from. + */ + static render(aPayload) { + const divName = "keyed-scalars"; + const section = "keyed-scalars-section"; + this.renderSection(divName, section, aPayload); + } +} + +var Events = { + /** + * Render the event data - if present - from the payload in a simple table. + * @param aPayload A payload object to render the data from. + */ + render(aPayload) { + let eventsDiv = document.getElementById("events"); + removeAllChildNodes(eventsDiv); + const headings = [ + "about-telemetry-time-stamp-header", + "about-telemetry-category-header", + "about-telemetry-method-header", + "about-telemetry-object-header", + "about-telemetry-values-header", + "about-telemetry-extra-header", + ]; + let payload = aPayload.processes; + let hasData = false; + if (payload) { + for (const process of Object.keys(aPayload.processes)) { + let data = aPayload.processes[process].events; + hasData = hasData || data !== {}; + if (data && Object.keys(data).length) { + let s = GenericSubsection.renderSubsectionHeader( + process, + true, + "events-section" + ); + let heading = document.createElement("h2"); + heading.textContent = process; + s.appendChild(heading); + const table = GenericTable.render(data, headings); + s.appendChild(table); + eventsDiv.appendChild(s); + let separator = document.createElement("div"); + separator.classList.add("clearfix"); + eventsDiv.appendChild(separator); + } + } + } else { + // handle archived ping + for (const process of Object.keys(aPayload.events)) { + let data = process; + hasData = hasData || data !== {}; + if (data && Object.keys(data).length) { + let s = GenericSubsection.renderSubsectionHeader( + process, + true, + "events-section" + ); + let heading = document.createElement("h2"); + heading.textContent = process; + s.appendChild(heading); + const table = GenericTable.render(data, headings); + eventsDiv.appendChild(table); + let separator = document.createElement("div"); + separator.classList.add("clearfix"); + eventsDiv.appendChild(separator); + } + } + } + setHasData("events-section", hasData); + }, +}; + +var Origins = { + render(aOrigins) { + let originSection = document.getElementById("origins"); + removeAllChildNodes(originSection); + + const headings = [ + "about-telemetry-origin-origin", + "about-telemetry-origin-count", + ]; + + let hasData = false; + for (let [metric, origins] of Object.entries(aOrigins || {})) { + if (!Object.entries(origins).length) { + continue; + } + hasData = true; + const metricHeader = document.createElement("caption"); + metricHeader.appendChild(document.createTextNode(metric)); + + const table = GenericTable.render(Object.entries(origins), headings); + table.appendChild(metricHeader); + originSection.appendChild(table); + } + + setHasData("origin-telemetry-section", hasData); + }, +}; + +/** + * Helper function for showing either the toggle element or "No data collected" message for a section + * + * @param aSectionID ID of the section element that needs to be changed + * @param aHasData true (default) indicates that toggle should be displayed + */ +function setHasData(aSectionID, aHasData) { + let sectionElement = document.getElementById(aSectionID); + sectionElement.classList[aHasData ? "add" : "remove"]("has-data"); + + // Display or Hide the section in the sidebar + let sectionCategory = document.querySelector( + ".category[value=" + aSectionID + "]" + ); + sectionCategory.classList[aHasData ? "add" : "remove"]("has-data"); +} + +/** + * Sets l10n attributes based on the Telemetry Server Owner pref. + */ +function setupServerOwnerBranding() { + let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla"); + const elements = [ + [document.getElementById("page-subtitle"), "about-telemetry-page-subtitle"], + [ + document.getElementById("origins-explanation"), + "about-telemetry-origins-explanation", + ], + ]; + for (const [elt, l10nName] of elements) { + document.l10n.setAttributes(elt, l10nName, { + telemetryServerOwner: serverOwner, + }); + } +} + +/** + * Display the store selector if we are on one + * of the whitelisted sections + */ +function displayStoresSelector(selectedSection) { + let whitelist = [ + "scalars-section", + "keyed-scalars-section", + "histograms-section", + "keyed-histograms-section", + ]; + let stores = document.getElementById("stores"); + stores.hidden = !whitelist.includes(selectedSection); + let storesLabel = document.getElementById("storesLabel"); + storesLabel.hidden = !whitelist.includes(selectedSection); +} + +function refreshSearch() { + removeSearchSectionTitles(); + let selectedSection = document + .querySelector(".category.selected") + .getAttribute("value"); + let search = document.getElementById("search"); + if (!Search.blacklist.includes(selectedSection)) { + Search.search(search.value); + } +} + +function adjustSearchState() { + removeSearchSectionTitles(); + let selectedSection = document + .querySelector(".category.selected") + .getAttribute("value"); + let search = document.getElementById("search"); + search.value = ""; + search.hidden = Search.blacklist.includes(selectedSection); + document.getElementById("no-search-results").classList.add("hidden"); + Search.search(""); // reinitialize search state. +} + +function removeSearchSectionTitles() { + for (let sectionTitleDiv of Array.from( + document.getElementsByClassName("search-section-title") + )) { + sectionTitleDiv.remove(); + } +} + +function adjustSection() { + let selectedCategory = document.querySelector(".category.selected"); + if (!selectedCategory.classList.contains("has-data")) { + PingPicker._showStructuredPingData(); + } +} + +function adjustHeaderState(title = null) { + let selected = document.querySelector(".category.selected .category-name"); + let selectedTitle = selected.textContent.trim(); + let sectionTitle = document.getElementById("sectionTitle"); + if (title !== null) { + document.l10n.setAttributes( + sectionTitle, + "about-telemetry-results-for-search", + { searchTerms: title } + ); + } else { + sectionTitle.textContent = selectedTitle; + } + let search = document.getElementById("search"); + if (selected.parentElement.id === "category-home") { + document.l10n.setAttributes( + search, + "about-telemetry-filter-all-placeholder" + ); + } else { + document.l10n.setAttributes(search, "about-telemetry-filter-placeholder", { + selectedTitle, + }); + } +} + +/** + * Change the url according to the current section displayed + * e.g about:telemetry#general-data + */ +function changeUrlPath(selectedSection, subSection) { + if (subSection) { + let hash = window.location.hash.split("_")[0] + "_" + selectedSection; + window.location.hash = hash; + } else { + window.location.hash = selectedSection.replace("-section", "-tab"); + } +} + +/** + * Change the url according to the current search text + */ +function changeUrlSearch(searchText) { + let currentHash = window.location.hash; + let hashWithoutSearch = currentHash.split(Search.HASH_SEARCH)[0]; + let hash = ""; + + if (!currentHash && !searchText) { + return; + } + if (!currentHash.includes(Search.HASH_SEARCH) && hashWithoutSearch) { + hashWithoutSearch += "_"; + } + if (searchText) { + hash = + hashWithoutSearch + Search.HASH_SEARCH + searchText.replace(/ /g, "+"); + } else if (hashWithoutSearch) { + hash = hashWithoutSearch.slice(0, hashWithoutSearch.length - 1); + } + + window.location.hash = hash; +} + +/** + * Change the section displayed + */ +function show(selected) { + let selectedValue = selected.getAttribute("value"); + if (selectedValue === "raw-json-viewer") { + openJsonInFirefoxJsonViewer(JSON.stringify(gPingData, null, 2)); + return; + } + + let selected_section = document.getElementById(selectedValue); + let subsections = selected_section.querySelectorAll(".sub-section"); + if (selected.classList.contains("has-subsection")) { + for (let subsection of selected.children) { + subsection.classList.remove("selected"); + } + } + if (subsections) { + for (let subsection of subsections) { + subsection.hidden = false; + } + } + + let current_button = document.querySelector(".category.selected"); + if (current_button == selected) { + return; + } + current_button.classList.remove("selected"); + selected.classList.add("selected"); + + document.querySelectorAll("section").forEach(section => { + section.classList.remove("active"); + }); + selected_section.classList.add("active"); + + adjustHeaderState(); + displayStoresSelector(selectedValue); + adjustSearchState(); + changeUrlPath(selectedValue); +} + +function showSubSection(selected) { + if (!selected) { + return; + } + let current_selection = document.querySelector( + ".category-subsection.selected" + ); + if (current_selection) { + current_selection.classList.remove("selected"); + } + selected.classList.add("selected"); + + let section = document.getElementById(selected.getAttribute("value")); + section.parentElement.childNodes.forEach(element => { + element.hidden = true; + }); + section.hidden = false; + + let title = selected.parentElement.querySelector(".category-name") + .textContent; + let subsection = selected.textContent; + document.getElementById("sectionTitle").textContent = + title + " - " + subsection; + changeUrlPath(subsection, true); +} + +/** + * Initializes load/unload, pref change and mouse-click listeners + */ +function setupListeners() { + Settings.attachObservers(); + PingPicker.attachObservers(); + RawPayloadData.attachObservers(); + + let menu = document.getElementById("categories"); + menu.addEventListener("click", e => { + if (e.target && e.target.parentNode == menu) { + show(e.target); + } + }); + + let search = document.getElementById("search"); + search.addEventListener("input", Search.searchHandler); + + // Clean up observers when page is closed + window.addEventListener( + "unload", + function(aEvent) { + Settings.detachObservers(); + }, + { once: true } + ); + + document + .getElementById("captured-stacks-fetch-symbols") + .addEventListener("click", function() { + if (!gPingData) { + return; + } + let capturedStacks = gPingData.payload.processes.parent.capturedStacks; + let req = new SymbolicationRequest( + "captured-stacks", + CapturedStacks.renderCaptureHeader, + capturedStacks.memoryMap, + capturedStacks.stacks, + capturedStacks.captures + ); + req.fetchSymbols(); + }); + + document + .getElementById("captured-stacks-hide-symbols") + .addEventListener("click", function() { + if (gPingData) { + CapturedStacks.render(gPingData.payload); + } + }); + + document + .getElementById("late-writes-fetch-symbols") + .addEventListener("click", function() { + if (!gPingData) { + return; + } + + let lateWrites = gPingData.payload.lateWrites; + let req = new SymbolicationRequest( + "late-writes", + LateWritesSingleton.renderHeader, + lateWrites.memoryMap, + lateWrites.stacks + ); + req.fetchSymbols(); + }); + + document + .getElementById("late-writes-hide-symbols") + .addEventListener("click", function() { + if (!gPingData) { + return; + } + + LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites); + }); +} + +// Restores the sections states +function urlSectionRestore(hash) { + if (hash) { + let section = hash.replace("-tab", "-section"); + let subsection = section.split("_")[1]; + section = section.split("_")[0]; + let category = document.querySelector(".category[value=" + section + "]"); + if (category) { + show(category); + if (subsection) { + let selector = + ".category-subsection[value=" + section + "-" + subsection + "]"; + let subcategory = document.querySelector(selector); + showSubSection(subcategory); + } + } + } +} + +// Restore sections states and search terms +function urlStateRestore() { + let hash = window.location.hash; + let searchQuery = ""; + if (hash) { + hash = hash.slice(1); + if (hash.includes(Search.HASH_SEARCH)) { + searchQuery = hash.split(Search.HASH_SEARCH)[1].replace(/[+]/g, " "); + hash = hash.split(Search.HASH_SEARCH)[0]; + } + urlSectionRestore(hash); + } + if (searchQuery) { + let search = document.getElementById("search"); + search.value = searchQuery; + } +} + +function openJsonInFirefoxJsonViewer(json) { + json = unescape(encodeURIComponent(json)); + try { + window.open("data:application/json;base64," + btoa(json)); + } catch (e) { + show(document.querySelector(".category[value=raw-payload-section]")); + } +} + +function onLoad() { + window.removeEventListener("load", onLoad); + Telemetry.scalarAdd("telemetry.about_telemetry_pageload", 1); + + // Set the text in the page header and elsewhere that needs the server owner. + setupServerOwnerBranding(); + + // Set up event listeners + setupListeners(); + + // Render settings. + Settings.render(); + + adjustHeaderState(); + + urlStateRestore(); + + // Update ping data when async Telemetry init is finished. + Telemetry.asyncFetchTelemetryData(async () => { + await PingPicker.update(); + }); +} + +var LateWritesSingleton = { + renderHeader: function LateWritesSingleton_renderHeader(aIndex) { + StackRenderer.renderHeader("late-writes", [aIndex + 1]); + }, + + renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) { + let hasData = !!( + lateWrites && + lateWrites.stacks && + lateWrites.stacks.length + ); + setHasData("late-writes-section", hasData); + if (!hasData) { + return; + } + + let stacks = lateWrites.stacks; + let memoryMap = lateWrites.memoryMap; + StackRenderer.renderStacks( + "late-writes", + stacks, + memoryMap, + LateWritesSingleton.renderHeader + ); + }, +}; + +class HistogramSection extends Section { + /** + * Return data from the current ping + */ + static dataFiltering(payload, selectedStore, process) { + return payload[selectedStore][process].histograms; + } + + /** + * Return data from an archived ping + */ + static archivePingDataFiltering(payload, process) { + if (process === "parent") { + return payload.histograms; + } + return payload.processes[process].histograms; + } + + static renderData(data, div) { + for (let [hName, hgram] of Object.entries(data)) { + Histogram.render(div, hName, hgram, { unpacked: true }); + } + } + + static render(aPayload) { + const divName = "histograms"; + const section = "histograms-section"; + this.renderSection(divName, section, aPayload); + } +} + +class KeyedHistogramSection extends Section { + /** + * Return data from the current ping + */ + static dataFiltering(payload, selectedStore, process) { + return payload[selectedStore][process].keyedHistograms; + } + + /** + * Return data from an archived ping + */ + static archivePingDataFiltering(payload, process) { + if (process === "parent") { + return payload.keyedHistograms; + } + return payload.processes[process].keyedHistograms; + } + + static renderData(data, div) { + for (let [id, keyed] of Object.entries(data)) { + KeyedHistogram.render(div, id, keyed, { unpacked: true }); + } + } + + static render(aPayload) { + const divName = "keyed-histograms"; + const section = "keyed-histograms-section"; + this.renderSection(divName, section, aPayload); + } +} + +var SessionInformation = { + render(aPayload) { + let infoSection = document.getElementById("session-info"); + removeAllChildNodes(infoSection); + + let hasData = !!Object.keys(aPayload.info).length; + setHasData("session-info-section", hasData); + + if (hasData) { + const table = GenericTable.render(explodeObject(aPayload.info)); + infoSection.appendChild(table); + } + }, +}; + +var SimpleMeasurements = { + render(aPayload) { + let simpleSection = document.getElementById("simple-measurements"); + removeAllChildNodes(simpleSection); + + let simpleMeasurements = this.sortStartupMilestones( + aPayload.simpleMeasurements + ); + let hasData = !!Object.keys(simpleMeasurements).length; + setHasData("simple-measurements-section", hasData); + + if (hasData) { + const table = GenericTable.render(explodeObject(simpleMeasurements)); + simpleSection.appendChild(table); + } + }, + + /** + * Helper function for sorting the startup milestones in the Simple Measurements + * section into temporal order. + * + * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data + * @return Sorted measurements + */ + sortStartupMilestones(aSimpleMeasurements) { + const telemetryTimestamps = TelemetryTimestamps.get(); + let startupEvents = Services.startup.getStartupInfo(); + delete startupEvents.process; + + function keyIsMilestone(k) { + return k in startupEvents || k in telemetryTimestamps; + } + + let sortedKeys = Object.keys(aSimpleMeasurements); + + // Sort the measurements, with startup milestones at the front + ordered by time + sortedKeys.sort(function keyCompare(keyA, keyB) { + let isKeyAMilestone = keyIsMilestone(keyA); + let isKeyBMilestone = keyIsMilestone(keyB); + + // First order by startup vs non-startup measurement + if (isKeyAMilestone && !isKeyBMilestone) { + return -1; + } + if (!isKeyAMilestone && isKeyBMilestone) { + return 1; + } + // Don't change order of non-startup measurements + if (!isKeyAMilestone && !isKeyBMilestone) { + return 0; + } + + // If both keys are startup measurements, order them by value + return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB]; + }); + + // Insert measurements into a result object in sort-order + let result = {}; + for (let key of sortedKeys) { + result[key] = aSimpleMeasurements[key]; + } + + return result; + }, +}; + +/** + * Render stores options + */ +function renderStoreList(payload) { + let storeSelect = document.getElementById("stores"); + let storesLabel = document.getElementById("storesLabel"); + removeAllChildNodes(storeSelect); + + if (!("stores" in payload)) { + storeSelect.classList.add("hidden"); + storesLabel.classList.add("hidden"); + return; + } + + storeSelect.classList.remove("hidden"); + storesLabel.classList.remove("hidden"); + storeSelect.disabled = false; + + for (let store of Object.keys(payload.stores)) { + let option = document.createElement("option"); + option.appendChild(document.createTextNode(store)); + option.setAttribute("value", store); + // Select main store by default + if (store === "main") { + option.selected = true; + } + storeSelect.appendChild(option); + } +} + +/** + * Return the selected store + */ +function getSelectedStore() { + let storeSelect = document.getElementById("stores"); + let storeSelectedOption = storeSelect.selectedOptions.item(0); + let selectedStore = + storeSelectedOption !== null + ? storeSelectedOption.getAttribute("value") + : undefined; + return selectedStore; +} + +function togglePingSections(isMainPing) { + // We always show the sections that are "common" to all pings. + let commonSections = new Set([ + "heading", + "home-section", + "general-data-section", + "environment-data-section", + "raw-json-viewer", + ]); + + let elements = document.querySelectorAll(".category"); + for (let section of elements) { + if (commonSections.has(section.getAttribute("value"))) { + continue; + } + // Only show the raw payload for non main ping. + if (section.getAttribute("value") == "raw-payload-section") { + section.classList.toggle("has-data", !isMainPing); + } else { + section.classList.toggle("has-data", isMainPing); + } + } +} + +function displayPingData(ping, updatePayloadList = false) { + gPingData = ping; + try { + PingPicker.render(); + displayRichPingData(ping, updatePayloadList); + adjustSection(); + refreshSearch(); + } catch (err) { + console.log(err); + PingPicker._showRawPingData(); + } +} + +function displayRichPingData(ping, updatePayloadList) { + // Update the payload list and store lists + if (updatePayloadList) { + renderStoreList(ping.payload); + } + + // Show general data. + GeneralData.render(ping); + + // Show environment data. + EnvironmentData.render(ping); + + RawPayloadData.render(ping); + + // We have special rendering code for the payloads from "main" and "event" pings. + // For any other pings we just render the raw JSON payload. + let isMainPing = ping.type == "main" || ping.type == "saved-session"; + let isEventPing = ping.type == "event"; + togglePingSections(isMainPing); + + if (isEventPing) { + // Copy the payload, so we don't modify the raw representation + // Ensure we always have at least the parent process. + let payload = { processes: { parent: {} } }; + for (let process of Object.keys(ping.payload.events)) { + payload.processes[process] = { + events: ping.payload.events[process], + }; + } + + // We transformed the actual payload, let's reload the store list if necessary. + if (updatePayloadList) { + renderStoreList(payload); + } + + // Show event data. + Events.render(payload); + return; + } + + if (!isMainPing) { + return; + } + + // Show slow SQL stats + SlowSQL.render(ping); + + // Render Addon details. + AddonDetails.render(ping); + + let payload = ping.payload; + // Show basic session info gathered + SessionInformation.render(payload); + + // Show scalar data. + Scalars.render(payload); + KeyedScalars.render(payload); + + // Show histogram data + HistogramSection.render(payload); + + // Show keyed histogram data + KeyedHistogramSection.render(payload); + + // Show event data. + Events.render(payload); + + // Show captured stacks. + CapturedStacks.render(payload); + + LateWritesSingleton.renderLateWrites(payload.lateWrites); + + // Show origin telemetry. + Origins.render(payload.origins); + + // Show simple measurements + SimpleMeasurements.render(payload); +} + +window.addEventListener("load", onLoad); diff --git a/toolkit/content/aboutTelemetry.xhtml b/toolkit/content/aboutTelemetry.xhtml new file mode 100644 index 0000000000..d3d473e87f --- /dev/null +++ b/toolkit/content/aboutTelemetry.xhtml @@ -0,0 +1,223 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- 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/. --> + +<!DOCTYPE html> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome: resource:; object-src 'none'" /> + <title data-l10n-id="about-telemetry-page-title"></title> + <link rel="stylesheet" href="chrome://global/content/aboutTelemetry.css" + type="text/css"/> + + <script src="chrome://global/content/aboutTelemetry.js"/> + <link rel="localization" href="branding/brand.ftl"/> + <link rel="localization" href="toolkit/about/aboutTelemetry.ftl"/> + </head> + + <body id="body"> + + <div id="categories"> + <div class="heading"> + <span id="ping-type" class="change-ping dropdown"></span> + <div id="controls" hidden="true"> + <span id="older-ping" data-l10n-id="about-telemetry-previous-ping"></span> + <span id="ping-date" class="change-ping"></span> + <span id="newer-ping" data-l10n-id="about-telemetry-next-ping"></span> + </div> + </div> + <div id="category-home" class="category category-no-icon has-data selected" value="home-section"> + <span class="category-name" data-l10n-id="about-telemetry-home-section"></span> + </div> + <div class="category category-no-icon" value="general-data-section"> + <span class="category-name" data-l10n-id="about-telemetry-general-data-section"></span> + </div> + <div class="category category-no-icon" value="environment-data-section"> + <span class="category-name" data-l10n-id="about-telemetry-environment-data-section"></span> + </div> + <div class="category category-no-icon" value="session-info-section"> + <span class="category-name" data-l10n-id="about-telemetry-session-info-section"></span> + </div> + <div class="category category-no-icon" value="scalars-section"> + <span class="category-name" data-l10n-id="about-telemetry-scalar-section"></span> + </div> + <div class="category category-no-icon" value="keyed-scalars-section"> + <span class="category-name" data-l10n-id="about-telemetry-keyed-scalar-section"></span> + </div> + <div class="category category-no-icon" value="histograms-section"> + <span class="category-name" data-l10n-id="about-telemetry-histograms-section"></span> + </div> + <div class="category category-no-icon" value="keyed-histograms-section"> + <span class="category-name" data-l10n-id="about-telemetry-keyed-histogram-section"></span> + </div> + <div class="category category-no-icon" value="events-section"> + <span class="category-name" data-l10n-id="about-telemetry-events-section"></span> + </div> + <div class="category category-no-icon" value="simple-measurements-section"> + <span class="category-name" data-l10n-id="about-telemetry-simple-measurements-section"></span> + </div> + <div class="category category-no-icon" value="slow-sql-section"> + <span class="category-name" data-l10n-id="about-telemetry-slow-sql-section"></span> + </div> + <div class="category category-no-icon" value="addon-details-section"> + <span class="category-name" data-l10n-id="about-telemetry-addon-details-section"></span> + </div> + <div class="category category-no-icon" value="captured-stacks-section"> + <span class="category-name" data-l10n-id="about-telemetry-captured-stacks-section"></span> + </div> + <div class="category category-no-icon" value="late-writes-section"> + <span class="category-name" data-l10n-id="about-telemetry-late-writes-section"></span> + </div> + <div class="category category-no-icon" value="origin-telemetry-section"> + <span class="category-name" data-l10n-id="about-telemetry-origin-section"></span> + </div> + <div class="category category-no-icon has-data" value="raw-payload-section"> + <span class="category-name" data-l10n-id="about-telemetry-raw-payload-section"></span> + </div> + <div id="category-raw" class="category category-no-icon has-data" value="raw-json-viewer"> + <span class="category-name" data-l10n-id="about-telemetry-raw"></span> + </div> + </div> + + <div id="main" class="main-content"> + + <div id="ping-picker" class="hidden"> + <div id="ping-source-picker"> + <h4 class="title" data-l10n-id="about-telemetry-ping-data-source"></h4> + <div> + <input type="radio" id="ping-source-current" name="choose-ping-source" value="current" checked="checked" /> + <label for="ping-source-current" data-l10n-id="about-telemetry-show-current-data"></label> + </div> + <div id="ping-source-archive-container"> + <input type="radio" id="ping-source-archive" name="choose-ping-source" value="archive" /> + <label for="ping-source-archive" data-l10n-id="about-telemetry-show-archived-ping-data"></label> + </div> + </div> + <div id="current-ping-picker"> + <input id="show-subsession-data" type="checkbox" checked="checked" /> + <label for="show-subsession-data" data-l10n-id="about-telemetry-show-subsession-data"></label> + </div> + <div id="archived-ping-picker"> + <h4 class="title" data-l10n-id="about-telemetry-choose-ping"></h4> + <div> + <h4 class="title" data-l10n-id="about-telemetry-archive-ping-type"></h4> + <select id="choose-ping-type"></select> + </div> + <div> + <h4 class="title" data-l10n-id="about-telemetry-archive-ping-header"></h4> + <select id="choose-ping-id"> + <optgroup data-l10n-id="about-telemetry-option-group-today"> + </optgroup> + <optgroup data-l10n-id="about-telemetry-option-group-yesterday"> + </optgroup> + <optgroup data-l10n-id="about-telemetry-option-group-older"> + </optgroup> + </select> + </div> + </div> + </div> + + <div class="header"> + <div id="sectionTitle" class="header-name" data-l10n-id="about-telemetry-page-title" /> + <div id="sectionFilters"> + <label id="storesLabel" for="stores" hidden="true" data-l10n-id="about-telemetry-current-store" /> + <select id="stores" hidden="true"></select> + <input type="text" id="search" placeholder="" /> + </div> + </div> + + <div id="no-search-results" hidden="true" class="hidden"> + <span id="no-search-results-text"></span> + <div class="no-search-results-image"></div> + </div> + + <section id="home-section" class="active"> + <p id="page-subtitle"></p> + <p id="settings-explanation"><a id="uploadLink" data-l10n-name="upload-link" class="change-data-choices-link" href="#"></a></p> + <p id="ping-explanation"><a id="pingLink" data-l10n-name="ping-link" href="https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/concepts/pings.html"></a></p> + <p data-l10n-id="about-telemetry-more-information"></p> + <ul> + <li data-l10n-id="about-telemetry-firefox-data-doc"><a id="dataDocLink" data-l10n-name="data-doc-link" href="https://docs.telemetry.mozilla.org/"></a></li> + <li data-l10n-id="about-telemetry-telemetry-client-doc"><a id="clientDocLink" data-l10n-name="client-doc-link" href="https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/index.html"></a></li> + <li data-l10n-id="about-telemetry-telemetry-dashboard"><a id="dashboardLink" data-l10n-name="dashboard-link" href="https://telemetry.mozilla.org/"></a></li> + <li data-l10n-id="about-telemetry-telemetry-probe-dictionary"><a id="probeDictionaryLink" data-l10n-name="probe-dictionary-link" href="https://probes.telemetry.mozilla.org/"></a></li> + </ul> + </section> + + <section id="raw-payload-section"> + <button id="payload-json-viewer" data-l10n-id="about-telemetry-show-in-Firefox-json-viewer"></button> + <pre id="raw-payload-data"></pre> + </section> + + <section id="general-data-section"> + <div id="general-data" class="data"></div> + </section> + + <section id="environment-data-section"> + <div id="environment-data" class="data"></div> + </section> + + <section id="session-info-section"> + <div id="session-info" class="data"></div> + </section> + + <section id="scalars-section"> + <div id="scalars" class="data"></div> + </section> + + <section id="keyed-scalars-section"> + <div id="keyed-scalars" class="data"></div> + </section> + + <section id="histograms-section"> + <div id="histograms" class="data"></div> + </section> + + <section id="keyed-histograms-section"> + <div id="keyed-histograms" class="data"></div> + </section> + + <section id="events-section"> + <div id="events" class="data"></div> + </section> + + <section id="simple-measurements-section"> + <div id="simple-measurements" class="data"></div> + </section> + + <section id="slow-sql-section"> + <p id="sql-warning" data-l10n-id="about-telemetry-full-sql-warning"></p> + <div id="slow-sql-tables" class="data"></div> + </section> + + <section id="late-writes-section"> + <a id="late-writes-fetch-symbols" href="" data-l10n-id="about-telemetry-fetch-stack-symbols"></a> + <a id="late-writes-hide-symbols" href="" data-l10n-id="about-telemetry-hide-stack-symbols"></a> + <div id="late-writes" class="data"></div> + </section> + + <section id="origin-telemetry-section"> + <div id="origins-explanation"> + <a data-l10n-name="origin-doc-link" href="https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/origin.html"></a> + <a data-l10n-name="prio-blog-link" href="https://hacks.mozilla.org/2018/10/testing-privacy-preserving-telemetry-with-prio/"></a> + </div> + <div id="origins" class="data"></div> + </section> + + <section id="addon-details-section"> + <div id="addon-details" class="data"></div> + </section> + + <section id="captured-stacks-section"> + <a id="captured-stacks-fetch-symbols" href="" data-l10n-id="about-telemetry-fetch-stack-symbols"></a> + <a id="captured-stacks-hide-symbols" href="" data-l10n-id="about-telemetry-hide-stack-symbols"></a> + <div id="captured-stacks" class="data"></div> + </section> + </div> + + </body> + +</html> diff --git a/toolkit/content/aboutUrlClassifier.css b/toolkit/content/aboutUrlClassifier.css new file mode 100644 index 0000000000..316ee6a0ea --- /dev/null +++ b/toolkit/content/aboutUrlClassifier.css @@ -0,0 +1,35 @@ +/* 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/. */ + +@import url("chrome://global/skin/in-content/info-pages.css"); + +.major-section { + margin-top: 2em; + margin-bottom: 1em; + font-size: large; + text-align: start; + font-weight: bold; +} + +#provider-table > tbody > tr > td:last-child { + text-align: center; +} + +#cache-table > tbody > tr > td:last-child { + text-align: center; +} + +#debug-table, #cache-table { + margin-top: 20px; +} + +.options > .toggle-container-with-text { + display: inline-flex; +} + +button { + margin-inline-start: 0; + margin-inline-end: 8px; + padding: 3px; +} diff --git a/toolkit/content/aboutUrlClassifier.js b/toolkit/content/aboutUrlClassifier.js new file mode 100644 index 0000000000..0fae8bd41f --- /dev/null +++ b/toolkit/content/aboutUrlClassifier.js @@ -0,0 +1,724 @@ +/* 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/. */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const UPDATE_BEGIN = "safebrowsing-update-begin"; +const UPDATE_FINISH = "safebrowsing-update-finished"; +const JSLOG_PREF = "browser.safebrowsing.debug"; + +window.onunload = function() { + Search.uninit(); + Provider.uninit(); + Cache.uninit(); + Debug.uninit(); +}; + +window.onload = function() { + Search.init(); + Provider.init(); + Cache.init(); + Debug.init(); +}; + +/* + * Search + */ +var Search = { + init() { + let classifier = Cc["@mozilla.org/url-classifier/dbservice;1"].getService( + Ci.nsIURIClassifier + ); + let featureNames = classifier.getFeatureNames(); + + let fragment = document.createDocumentFragment(); + featureNames.forEach(featureName => { + let div = document.createElement("div"); + fragment.appendChild(div); + + let checkbox = document.createElement("input"); + checkbox.id = "feature_" + featureName; + checkbox.type = "checkbox"; + checkbox.checked = true; + div.appendChild(checkbox); + + let label = document.createElement("label"); + label.for = checkbox.id; + div.appendChild(label); + + let text = document.createTextNode(featureName); + label.appendChild(text); + }); + + let list = document.getElementById("search-features"); + list.appendChild(fragment); + + let btn = document.getElementById("search-button"); + btn.addEventListener("click", this.search); + + this.hideError(); + this.hideResults(); + }, + + uninit() { + let list = document.getElementById("search-features"); + while (list.firstChild) { + list.firstChild.remove(); + } + + let btn = document.getElementById("search-button"); + btn.removeEventListener("click", this.search); + }, + + search() { + Search.hideError(); + Search.hideResults(); + + let input = document.getElementById("search-input").value; + + let uri; + try { + uri = Services.io.newURI(input); + if (!uri) { + Search.reportError("url-classifier-search-error-invalid-url"); + return; + } + } catch (ex) { + Search.reportError("url-classifier-search-error-invalid-url"); + return; + } + + let classifier = Cc["@mozilla.org/url-classifier/dbservice;1"].getService( + Ci.nsIURIClassifier + ); + + let featureNames = classifier.getFeatureNames(); + let features = []; + featureNames.forEach(featureName => { + if (document.getElementById("feature_" + featureName).checked) { + let feature = classifier.getFeatureByName(featureName); + if (feature) { + features.push(feature); + } + } + }); + + if (!features.length) { + Search.reportError("url-classifier-search-error-no-features"); + return; + } + + let listType = + document.getElementById("search-listtype").value == 0 + ? Ci.nsIUrlClassifierFeature.blocklist + : Ci.nsIUrlClassifierFeature.entitylist; + classifier.asyncClassifyLocalWithFeatures(uri, features, listType, list => + Search.showResults(list) + ); + + Search.hideError(); + }, + + hideError() { + let errorMessage = document.getElementById("search-error-message"); + errorMessage.style.display = "none"; + }, + + reportError(msg) { + let errorMessage = document.getElementById("search-error-message"); + document.l10n.setAttributes(errorMessage, msg); + errorMessage.style.display = ""; + }, + + hideResults() { + let resultTitle = document.getElementById("result-title"); + resultTitle.style.display = "none"; + + let resultTable = document.getElementById("result-table"); + resultTable.style.display = "none"; + }, + + showResults(results) { + let fragment = document.createDocumentFragment(); + results.forEach(result => { + let tr = document.createElement("tr"); + fragment.appendChild(tr); + + let th = document.createElement("th"); + tr.appendChild(th); + th.appendChild(document.createTextNode(result.feature.name)); + + let td = document.createElement("td"); + tr.appendChild(td); + + let featureName = document.createElement("div"); + document.l10n.setAttributes( + featureName, + "url-classifier-search-result-uri", + { uri: result.uri.spec } + ); + td.appendChild(featureName); + + let list = document.createElement("div"); + document.l10n.setAttributes(list, "url-classifier-search-result-list", { + list: result.list, + }); + td.appendChild(list); + }); + + let resultTable = document.getElementById("result-table"); + while (resultTable.firstChild) { + resultTable.firstChild.remove(); + } + + resultTable.appendChild(fragment); + resultTable.style.display = ""; + + let resultTitle = document.getElementById("result-title"); + resultTitle.style.display = ""; + }, +}; + +/* + * Provider + */ +var Provider = { + providers: null, + + updatingProvider: "", + + init() { + this.providers = new Set(); + let branch = Services.prefs.getBranch("browser.safebrowsing.provider."); + let children = branch.getChildList(""); + for (let child of children) { + let provider = child.split(".")[0]; + if (this.isActiveProvider(provider)) { + this.providers.add(provider); + } + } + + this.register(); + this.render(); + this.refresh(); + }, + + uninit() { + Services.obs.removeObserver(this.onBeginUpdate, UPDATE_BEGIN); + Services.obs.removeObserver(this.onFinishUpdate, UPDATE_FINISH); + }, + + onBeginUpdate(aSubject, aTopic, aData) { + this.updatingProvider = aData; + let p = this.updatingProvider; + + // Disable update button for the provider while we are doing update. + document.getElementById("update-" + p).disabled = true; + + let elem = document.getElementById(p + "-col-lastupdateresult"); + document.l10n.setAttributes(elem, "url-classifier-updating"); + }, + + onFinishUpdate(aSubject, aTopic, aData) { + let p = this.updatingProvider; + this.updatingProvider = ""; + + // It is possible that we get update-finished event only because + // about::url-classifier is opened after update-begin event is fired. + if (p === "") { + this.refresh(); + return; + } + + this.refresh([p]); + + document.getElementById("update-" + p).disabled = false; + + let elem = document.getElementById(p + "-col-lastupdateresult"); + if (aData.startsWith("success")) { + document.l10n.setAttributes(elem, "url-classifier-success"); + } else if (aData.startsWith("update error")) { + document.l10n.setAttributes(elem, "url-classifier-update-error", { + error: aData.split(": ")[1], + }); + } else if (aData.startsWith("download error")) { + document.l10n.setAttributes(elem, "url-classifier-download-error", { + error: aData.split(": ")[1], + }); + } else { + elem.childNodes[0].nodeValue = aData; + } + }, + + register() { + // Handle begin update + this.onBeginUpdate = this.onBeginUpdate.bind(this); + Services.obs.addObserver(this.onBeginUpdate, UPDATE_BEGIN); + + // Handle finish update + this.onFinishUpdate = this.onFinishUpdate.bind(this); + Services.obs.addObserver(this.onFinishUpdate, UPDATE_FINISH); + }, + + // This should only be called once because we assume number of providers + // won't change. + render() { + let tbody = document.getElementById("provider-table-body"); + + for (let provider of this.providers) { + let tr = document.createElement("tr"); + let cols = document.getElementById("provider-head-row").childNodes; + for (let column of cols) { + if (!column.id) { + continue; + } + let td = document.createElement("td"); + td.id = provider + "-" + column.id; + + if (column.id === "col-update") { + let btn = document.createElement("button"); + btn.id = "update-" + provider; + btn.addEventListener("click", () => { + this.update(provider); + }); + + document.l10n.setAttributes(btn, "url-classifier-trigger-update"); + td.appendChild(btn); + } else if (column.id === "col-lastupdateresult") { + document.l10n.setAttributes(td, "url-classifier-not-available"); + } else { + td.appendChild(document.createTextNode("")); + } + tr.appendChild(td); + } + tbody.appendChild(tr); + } + }, + + refresh(listProviders = this.providers) { + for (let provider of listProviders) { + let values = {}; + values["col-provider"] = provider; + + let pref = + "browser.safebrowsing.provider." + provider + ".lastupdatetime"; + let lut = Services.prefs.getCharPref(pref, ""); + values["col-lastupdatetime"] = lut ? new Date(lut * 1) : null; + + pref = "browser.safebrowsing.provider." + provider + ".nextupdatetime"; + let nut = Services.prefs.getCharPref(pref, ""); + values["col-nextupdatetime"] = nut ? new Date(nut * 1) : null; + + let listmanager = Cc[ + "@mozilla.org/url-classifier/listmanager;1" + ].getService(Ci.nsIUrlListManager); + let bot = listmanager.getBackOffTime(provider); + values["col-backofftime"] = bot ? new Date(bot * 1) : null; + + for (let key of Object.keys(values)) { + let elem = document.getElementById(provider + "-" + key); + if (values[key]) { + elem.removeAttribute("data-l10n-id"); + elem.childNodes[0].nodeValue = values[key]; + } else { + document.l10n.setAttributes(elem, "url-classifier-not-available"); + } + } + } + }, + + // Call update for the provider. + update(provider) { + let listmanager = Cc[ + "@mozilla.org/url-classifier/listmanager;1" + ].getService(Ci.nsIUrlListManager); + + let pref = "browser.safebrowsing.provider." + provider + ".lists"; + let tables = Services.prefs.getCharPref(pref, ""); + + if (!listmanager.forceUpdates(tables)) { + // This may because of back-off algorithm. + let elem = document.getElementById(provider + "-col-lastupdateresult"); + document.l10n.setAttributes(elem, "url-classifier-cannot-update"); + } + }, + + // if we can find any table registered an updateURL in the listmanager, + // the provider is active. This is used to filter out google v2 provider + // without changing the preference. + isActiveProvider(provider) { + let listmanager = Cc[ + "@mozilla.org/url-classifier/listmanager;1" + ].getService(Ci.nsIUrlListManager); + + let pref = "browser.safebrowsing.provider." + provider + ".lists"; + let tables = Services.prefs.getCharPref(pref, "").split(","); + + for (let i = 0; i < tables.length; i++) { + let updateUrl = listmanager.getUpdateUrl(tables[i]); + if (updateUrl) { + return true; + } + } + + return false; + }, +}; + +/* + * Cache + */ +var Cache = { + // Tables that show cahe entries. + showCacheEnties: null, + + init() { + this.showCacheEnties = new Set(); + + this.register(); + this.render(); + }, + + uninit() { + Services.obs.removeObserver(this.refresh, UPDATE_FINISH); + }, + + register() { + this.refresh = this.refresh.bind(this); + Services.obs.addObserver(this.refresh, UPDATE_FINISH); + }, + + render() { + this.createCacheEntries(); + + let refreshBtn = document.getElementById("refresh-cache-btn"); + refreshBtn.addEventListener("click", () => { + this.refresh(); + }); + + let clearBtn = document.getElementById("clear-cache-btn"); + clearBtn.addEventListener("click", () => { + let dbservice = Cc["@mozilla.org/url-classifier/dbservice;1"].getService( + Ci.nsIUrlClassifierDBService + ); + dbservice.clearCache(); + // Since clearCache is async call, we just simply assume it will be + // updated in 100 milli-seconds. + setTimeout(() => { + this.refresh(); + }, 100); + }); + }, + + refresh() { + this.clearCacheEntries(); + this.createCacheEntries(); + }, + + clearCacheEntries() { + let ctbody = document.getElementById("cache-table-body"); + while (ctbody.firstChild) { + ctbody.firstChild.remove(); + } + + let cetbody = document.getElementById("cache-entries-table-body"); + while (cetbody.firstChild) { + cetbody.firstChild.remove(); + } + }, + + createCacheEntries() { + function createRow(tds, body, cols) { + let tr = document.createElement("tr"); + tds.forEach(function(v, i, a) { + let td = document.createElement("td"); + if (i == 0 && tds.length != cols) { + td.setAttribute("colspan", cols - tds.length + 1); + } + + if (typeof v === "object") { + if (v.l10n) { + document.l10n.setAttributes(td, v.l10n); + } else { + td.removeAttribute("data-l10n-id"); + td.appendChild(v); + } + } else { + td.removeAttribute("data-l10n-id"); + td.textContent = v; + } + + tr.appendChild(td); + }); + body.appendChild(tr); + } + + let dbservice = Cc["@mozilla.org/url-classifier/dbservice;1"].getService( + Ci.nsIUrlClassifierInfo + ); + + for (let provider of Provider.providers) { + let pref = "browser.safebrowsing.provider." + provider + ".lists"; + let tables = Services.prefs.getCharPref(pref, "").split(","); + + for (let table of tables) { + dbservice.getCacheInfo(table, { + onGetCacheComplete: aCache => { + let entries = aCache.entries; + if (entries.length === 0) { + this.showCacheEnties.delete(table); + return; + } + + let positiveCacheCount = 0; + for (let i = 0; i < entries.length; i++) { + let entry = entries.queryElementAt( + i, + Ci.nsIUrlClassifierCacheEntry + ); + let matches = entry.matches; + positiveCacheCount += matches.length; + + // If we don't have to show cache entries for this table then just + // skip the following code. + if (!this.showCacheEnties.has(table)) { + continue; + } + + let tds = [ + table, + entry.prefix, + new Date(entry.expiry * 1000).toString(), + ]; + let j = 0; + do { + if (matches.length >= 1) { + let match = matches.queryElementAt( + j, + Ci.nsIUrlClassifierPositiveCacheEntry + ); + let list = [ + match.fullhash, + new Date(match.expiry * 1000).toString(), + ]; + tds = tds.concat(list); + } else { + tds = tds.concat([ + { l10n: "url-classifier-not-available" }, + { l10n: "url-classifier-not-available" }, + ]); + } + createRow( + tds, + document.getElementById("cache-entries-table-body"), + 5 + ); + j++; + tds = [""]; + } while (j < matches.length); + } + + // Create cache information entries. + let chk = document.createElement("input"); + chk.type = "checkbox"; + chk.checked = this.showCacheEnties.has(table); + chk.addEventListener("click", () => { + if (chk.checked) { + this.showCacheEnties.add(table); + } else { + this.showCacheEnties.delete(table); + } + this.refresh(); + }); + + let tds = [table, entries.length, positiveCacheCount, chk]; + createRow( + tds, + document.getElementById("cache-table-body"), + tds.length + ); + }, + }); + } + } + + let entries_div = document.getElementById("cache-entries"); + entries_div.style.display = + this.showCacheEnties.size == 0 ? "none" : "block"; + }, +}; + +/* + * Debug + */ +var Debug = { + // url-classifier NSPR Log modules. + modules: [ + "UrlClassifierDbService", + "nsChannelClassifier", + "UrlClassifier", + "UrlClassifierProtocolParser", + "UrlClassifierStreamUpdater", + "UrlClassifierPrefixSet", + "ApplicationReputation", + ], + + init() { + this.register(); + this.render(); + this.refresh(); + }, + + uninit() { + Services.prefs.removeObserver(JSLOG_PREF, this.refreshJSDebug); + }, + + register() { + this.refreshJSDebug = this.refreshJSDebug.bind(this); + Services.prefs.addObserver(JSLOG_PREF, this.refreshJSDebug); + }, + + render() { + // This function update the log module text field if we click + // safebrowsing log module check box. + function logModuleUpdate(module) { + let txt = document.getElementById("log-modules"); + let chk = document.getElementById("chk-" + module); + + let dst = chk.checked ? "," + module + ":5" : ""; + let re = new RegExp(",?" + module + ":[0-9]"); + + let str = txt.value.replace(re, dst); + if (chk.checked) { + str = txt.value === str ? str + dst : str; + } + txt.value = str.replace(/^,/, ""); + } + + let setLog = document.getElementById("set-log-modules"); + setLog.addEventListener("click", this.nsprlog); + + let setLogFile = document.getElementById("set-log-file"); + setLogFile.addEventListener("click", this.logfile); + + let setJSLog = document.getElementById("js-log"); + setJSLog.addEventListener("click", this.jslog); + + let modules = document.getElementById("log-modules"); + let sbModules = document.getElementById("sb-log-modules"); + for (let module of this.modules) { + let container = document.createElement("div"); + container.className = "toggle-container-with-text"; + sbModules.appendChild(container); + + let chk = document.createElement("input"); + chk.id = "chk-" + module; + chk.type = "checkbox"; + chk.checked = true; + chk.addEventListener("click", () => { + logModuleUpdate(module); + }); + container.appendChild(chk, modules); + + let label = document.createElement("label"); + label.for = chk.id; + label.appendChild(document.createTextNode(module)); + container.appendChild(label, modules); + } + + this.modules.map(logModuleUpdate); + + let file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append("safebrowsing.log"); + + let logFile = document.getElementById("log-file"); + logFile.value = file.path; + + let curLog = document.getElementById("cur-log-modules"); + curLog.childNodes[0].nodeValue = ""; + + let curLogFile = document.getElementById("cur-log-file"); + curLogFile.childNodes[0].nodeValue = ""; + }, + + refresh() { + this.refreshJSDebug(); + + // Disable configure log modules if log modules are already set + // by environment variable. + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + let logModules = + env.get("MOZ_LOG") || + env.get("MOZ_LOG_MODULES") || + env.get("NSPR_LOG_MODULES"); + + if (logModules.length) { + document.getElementById("set-log-modules").disabled = true; + for (let module of this.modules) { + document.getElementById("chk-" + module).disabled = true; + } + + let curLogModules = document.getElementById("cur-log-modules"); + curLogModules.childNodes[0].nodeValue = logModules; + } + + // Disable set log file if log file is already set + // by environment variable. + let logFile = env.get("MOZ_LOG_FILE") || env.get("NSPR_LOG_FILE"); + if (logFile.length) { + document.getElementById("set-log-file").disabled = true; + document.getElementById("log-file").value = logFile; + } + }, + + refreshJSDebug() { + let enabled = Services.prefs.getBoolPref(JSLOG_PREF, false); + + let jsChk = document.getElementById("js-log"); + jsChk.checked = enabled; + + let curJSLog = document.getElementById("cur-js-log"); + if (enabled) { + document.l10n.setAttributes(curJSLog, "url-classifier-enabled"); + } else { + document.l10n.setAttributes(curJSLog, "url-classifier-disabled"); + } + }, + + jslog() { + let enabled = Services.prefs.getBoolPref(JSLOG_PREF, false); + Services.prefs.setBoolPref(JSLOG_PREF, !enabled); + }, + + nsprlog() { + // Turn off debugging for all the modules. + let children = Services.prefs.getBranch("logging.").getChildList(""); + for (let pref of children) { + if (!pref.startsWith("config.")) { + Services.prefs.clearUserPref(`logging.${pref}`); + } + } + + let value = document.getElementById("log-modules").value; + let logModules = value.split(","); + for (let module of logModules) { + let [key, value] = module.split(":"); + Services.prefs.setIntPref(`logging.${key}`, parseInt(value, 10)); + } + + let curLogModules = document.getElementById("cur-log-modules"); + curLogModules.childNodes[0].nodeValue = value; + }, + + logfile() { + let logFile = document.getElementById("log-file").value.trim(); + Services.prefs.setCharPref("logging.config.LOG_FILE", logFile); + + let curLogFile = document.getElementById("cur-log-file"); + curLogFile.childNodes[0].nodeValue = logFile; + }, +}; diff --git a/toolkit/content/aboutUrlClassifier.xhtml b/toolkit/content/aboutUrlClassifier.xhtml new file mode 100644 index 0000000000..39b15e66ba --- /dev/null +++ b/toolkit/content/aboutUrlClassifier.xhtml @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- 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/. --> + +<!DOCTYPE html [ +<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> %htmlDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" /> + <title data-l10n-id="url-classifier-title"></title> + <link rel="stylesheet" href="chrome://global/content/aboutUrlClassifier.css" type="text/css"/> + <link rel="localization" href="toolkit/about/url-classifier.ftl"/> + <script src="chrome://global/content/aboutUrlClassifier.js"></script> +</head> + +<body class="wide-container"> + <h1 data-l10n-id="url-classifier-title"></h1> + <div id="search"> + <h2 class="major-section" data-l10n-id="url-classifier-search-title"></h2> + <div class="options"> + <table id="search-table"> + <tbody> + <tr> + <th class="column" data-l10n-id="url-classifier-search-input"></th> + <td> + <input id="search-input" type="text" value=""/> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="url-classifier-search-listType"></th> + <td> + <select id="search-listtype"> + <option value="0">Blocklist</option> + <option value="1">Entitylist</option> + </select> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="url-classifier-search-features"></th> + <td id="search-features"></td> + </tr> + <tr> + <th></th> + <td> + <button id="search-button" data-l10n-id="url-classifier-search-btn"></button> + </td> + </tr> + </tbody> + </table> + <p id="search-error-message"></p> + <h2 class="major-section" id="result-title" data-l10n-id="url-classifier-search-result-title"></h2> + <table id="result-table"> + <tr> + <td> + <input id="search-input" type="text" value=""/> + </td> + </tr> + </table> + </div> + </div> + <div id="provider"> + <h2 class="major-section" data-l10n-id="url-classifier-provider-title"></h2> + <table id="provider-table"> + <thead> + <tr id="provider-head-row"> + <th id="col-provider" data-l10n-id="url-classifier-provider"></th> + <th id="col-lastupdatetime" data-l10n-id="url-classifier-provider-last-update-time"></th> + <th id="col-nextupdatetime" data-l10n-id="url-classifier-provider-next-update-time"></th> + <th id="col-backofftime" data-l10n-id="url-classifier-provider-back-off-time"></th> + <th id="col-lastupdateresult" data-l10n-id="url-classifier-provider-last-update-status"></th> + <th id="col-update" data-l10n-id="url-classifier-provider-update-btn"></th> + </tr> + </thead> + <tbody id="provider-table-body"> + <!-- data is generated in javascript --> + </tbody> + </table> + </div> + <div id="cache"> + <h2 class="major-section" data-l10n-id="url-classifier-cache-title"></h2> + <div id="cache-modules" class="options"> + <button id="refresh-cache-btn" data-l10n-id="url-classifier-cache-refresh-btn"></button> + <button id="clear-cache-btn" data-l10n-id="url-classifier-cache-clear-btn"></button> + <br></br> + </div> + <table id="cache-table"> + <thead> + <tr id="cache-head-row"> + <th id="col-tablename" data-l10n-id="url-classifier-cache-table-name"></th> + <th id="col-negativeentries" data-l10n-id="url-classifier-cache-ncache-entries"></th> + <th id="col-positiveentries" data-l10n-id="url-classifier-cache-pcache-entries"></th> + <th id="col-showentries" data-l10n-id="url-classifier-cache-show-entries"></th> + </tr> + </thead> + <tbody id="cache-table-body"> + <!-- data is generated in javascript --> + </tbody> + </table> + <br></br> + </div> + <div id="cache-entries"> + <h2 class="major-section" data-l10n-id="url-classifier-cache-entries"></h2> + <table id="cache-entries-table"> + <thead> + <tr id="cache-entries-row"> + <th id="col-table" data-l10n-id="url-classifier-cache-table-name"></th> + <th id="col-prefix" data-l10n-id="url-classifier-cache-prefix"></th> + <th id="col-n-expire" data-l10n-id="url-classifier-cache-ncache-expiry"></th> + <th id="col-fullhash" data-l10n-id="url-classifier-cache-fullhash"></th> + <th id="col-p-expire" data-l10n-id="url-classifier-cache-pcache-expiry"></th> + </tr> + </thead> + <tbody id="cache-entries-table-body"> + <!-- data is generated in javascript --> + </tbody> + </table> + </div> + <div id="debug"> + <h2 class="major-section" data-l10n-id="url-classifier-debug-title"></h2> + <div id="debug-modules" class="options"> + <input id="log-modules" type="text" value=""/> + <button id="set-log-modules" data-l10n-id="url-classifier-debug-module-btn"></button> + <br></br> + <input id="log-file" type="text" value=""/> + <button id="set-log-file" data-l10n-id="url-classifier-debug-file-btn"></button> + <br></br> + <div class="toggle-container-with-text"> + <input id="js-log" type="checkbox"/> + <label for="js-log" data-l10n-id="url-classifier-debug-js-log-chk"></label> + </div> + </div> + <table id="debug-table"> + <tbody> + <tr> + <th class="column" data-l10n-id="url-classifier-debug-sb-modules"></th> + <td id="sb-log-modules"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="url-classifier-debug-modules"></th> + <td id="cur-log-modules"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="url-classifier-debug-sbjs-modules"></th> + <td id="cur-js-log"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="url-classifier-debug-file"></th> + <td id="cur-log-file"> + </td> + </tr> + </tbody> + </table> + </div> +</body> +</html> diff --git a/toolkit/content/aboutwebrtc/aboutWebrtc.css b/toolkit/content/aboutwebrtc/aboutWebrtc.css new file mode 100644 index 0000000000..9836f3295e --- /dev/null +++ b/toolkit/content/aboutwebrtc/aboutWebrtc.css @@ -0,0 +1,154 @@ +/* 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/. */ + +html { + background-color: #edeceb; +} + +table { + font-family: monospace; +} + +.controls { + font-size: 1.1em; + display: inline-block; + margin: 0 0.5em; +} + +.control { + margin: 0.5em 0; +} + +.control > button { + margin: 0 0.25em; +} + +.message > p { + margin: 0.5em 0.5em; +} + +.log p, .prefs p { + font-family: monospace; + padding-left: 2em; + text-indent: -2em; + margin-top: 2px; + margin-bottom: 2px; +} + +#content > div { + padding: 1em 2em; + margin: 1em 0; + border: 1px solid #afaca9; + border-radius: 10px; + background: none repeat scroll 0% 0% #fff; +} + +.section-heading * +{ + display: inline-block; +} + +.section-heading > button { + margin-left: 1em; + margin-right: 1em; +} + +.peer-connection > h3 +{ + background-color: #ddd; +} + +.peer-connection table { + width: 100%; + text-align: center; + border: none; +} + +.peer-connection table th { + font-weight: bold; +} + +.peer-connection table th, +.peer-connection table td { + padding: 0.4em; +} + +.peer-connection table tr:nth-child(even) { + background-color: #ddd; +} + +.peer-connection table caption { + text-align: left; +} + +.peer-connection table.raw-candidate { + text-align: left; +} + +.bottom-border td { + border-bottom: thin solid; + border-color: black; +} + +.peer-connection-config div { + margin-inline: 1em; +} + +.peer-connection-config div:nth-of-type(odd) { + background-color: #ddd; +} + +/* This is the pale colour scheme from: + https://personal.sron.nl/~pault/#sec:qualitative */ +.ice-trickled { background-color: #cceeff; } +.ice-succeeded { background-color: #ccddaa; } +.ice-failed { background-color: #ffcccc; } +.ice-cancelled { background-color: #eeeebb; } + +.info-label { + font-weight: bold; +} + +.section-ctrl { + margin: 1em 1.5em; +} + +div.fold-trigger { + color: blue; + cursor: pointer; +} + +@media screen { + .fold-closed { + display: none !important; + } +} + +@media print { + .no-print { + display: none !important; + } +} + +.sdp-history { + display: flex; + height: 400px; +} + +.sdp-history-link { + text-decoration: underline; +} + +.sdp-history h5 { + background-color: #ddd; +} + +.sdp-history div { + border: thin solid; + border-color: black; + flex: 1; + padding: 1em; + width: 50%; + overflow: scroll; +} diff --git a/toolkit/content/aboutwebrtc/aboutWebrtc.html b/toolkit/content/aboutwebrtc/aboutWebrtc.html new file mode 100644 index 0000000000..c73427f56b --- /dev/null +++ b/toolkit/content/aboutwebrtc/aboutWebrtc.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> + +<!-- 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/. --> + +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; object-src 'none'" /> + <meta charset="utf-8" /> + <title>about:webrtc</title> + <link rel="stylesheet" type="text/css" media="all" + href="chrome://global/content/aboutwebrtc/aboutWebrtc.css"/> + <script src="chrome://global/content/aboutwebrtc/aboutWebrtc.js" + defer="defer"></script> +</head> +<body id="body"> + <div id="controls" class="no-print"></div> + <div id="content"></div> +</body> +</html> diff --git a/toolkit/content/aboutwebrtc/aboutWebrtc.js b/toolkit/content/aboutwebrtc/aboutWebrtc.js new file mode 100644 index 0000000000..8e69c5b35a --- /dev/null +++ b/toolkit/content/aboutwebrtc/aboutWebrtc.js @@ -0,0 +1,1074 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "FileUtils", + "resource://gre/modules/FileUtils.jsm" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "FilePicker", + "@mozilla.org/filepicker;1", + "nsIFilePicker" +); +XPCOMUtils.defineLazyGetter(this, "strings", () => + Services.strings.createBundle("chrome://global/locale/aboutWebrtc.properties") +); + +const string = strings.GetStringFromName; +const format = strings.formatStringFromName; +const WGI = WebrtcGlobalInformation; + +const LOGFILE_NAME_DEFAULT = "aboutWebrtc.html"; +const WEBRTC_TRACE_ALL = 65535; + +async function getStats() { + const { reports } = await new Promise(r => WGI.getAllStats(r)); + return [...reports].sort((a, b) => b.timestamp - a.timestamp); +} + +const getLog = () => new Promise(r => WGI.getLogging("", r)); + +const renderElement = (name, options) => + Object.assign(document.createElement(name), options); + +const renderText = (name, textContent, options) => + renderElement(name, Object.assign({ textContent }, options)); + +const renderElements = (name, options, list) => { + const element = renderElement(name, options); + element.append(...list); + return element; +}; + +// Button control classes + +class Control { + label = null; + message = null; + messageHeader = null; + + render() { + this.ctrl = renderElement("button", { onclick: () => this.onClick() }); + this.msg = renderElement("p"); + this.update(); + return [this.ctrl, this.msg]; + } + + update() { + this.ctrl.textContent = this.label; + this.msg.textContent = ""; + if (this.message) { + this.msg.append( + renderText("span", `${this.messageHeader}: `, { + className: "info-label", + }), + this.message + ); + } + } +} + +class SavePage extends Control { + constructor() { + super(); + this.messageHeader = string("save_page_label"); + this.label = string("save_page_label"); + } + + async onClick() { + FoldEffect.expandAll(); + FilePicker.init( + window, + string("save_page_dialog_title"), + FilePicker.modeSave + ); + FilePicker.defaultString = LOGFILE_NAME_DEFAULT; + const rv = await new Promise(r => FilePicker.open(r)); + if (rv != FilePicker.returnOK && rv != FilePicker.returnReplace) { + return; + } + const fout = FileUtils.openAtomicFileOutputStream( + FilePicker.file, + FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE + ); + const content = document.querySelector("#content"); + const noPrintList = [...content.querySelectorAll(".no-print")]; + for (const node of noPrintList) { + node.style.setProperty("display", "none"); + } + try { + fout.write(content.outerHTML, content.outerHTML.length); + } finally { + FileUtils.closeAtomicFileOutputStream(fout); + for (const node of noPrintList) { + node.style.removeProperty("display"); + } + } + this.message = format("save_page_msg", [FilePicker.file.path]); + this.update(); + } +} + +class DebugMode extends Control { + constructor() { + super(); + this.messageHeader = string("debug_mode_msg_label"); + + if (WGI.debugLevel > 0) { + this.setState(true); + } else { + this.label = string("debug_mode_off_state_label"); + } + } + + setState(state) { + const stateString = state ? "on" : "off"; + this.label = string(`debug_mode_${stateString}_state_label`); + try { + const file = Services.prefs.getCharPref("media.webrtc.debug.log_file"); + this.message = format(`debug_mode_${stateString}_state_msg`, [file]); + } catch (e) { + this.message = null; + } + return state; + } + + onClick() { + this.setState((WGI.debugLevel = WGI.debugLevel ? 0 : WEBRTC_TRACE_ALL)); + this.update(); + } +} + +class AecLogging extends Control { + constructor() { + super(); + this.messageHeader = string("aec_logging_msg_label"); + + if (WGI.aecDebug) { + this.setState(true); + } else { + this.label = string("aec_logging_off_state_label"); + this.message = null; + } + } + + setState(state) { + this.label = string(`aec_logging_${state ? "on" : "off"}_state_label`); + try { + if (!state) { + const file = WGI.aecDebugLogDir; + this.message = format("aec_logging_off_state_msg", [file]); + } else { + this.message = string("aec_logging_on_state_msg"); + } + } catch (e) { + this.message = null; + } + } + + onClick() { + this.setState((WGI.aecDebug = !WGI.aecDebug)); + this.update(); + } +} + +class ShowTab extends Control { + constructor(browserId) { + super(); + this.label = string("show_tab_label"); + this.message = null; + this.browserId = browserId; + } + + onClick() { + const gBrowser = + window.ownerGlobal.browsingContext.topChromeWindow.gBrowser; + for (const tab of gBrowser.visibleTabs) { + if (tab.linkedBrowser && tab.linkedBrowser.browserId == this.browserId) { + gBrowser.selectedTab = tab; + return; + } + } + this.ctrl.disabled = true; + } +} + +(async () => { + // Setup. Retrieve reports & log while page loads. + const haveReports = getStats(); + const haveLog = getLog(); + await new Promise(r => (window.onload = r)); + + document.title = string("document_title"); + { + const ctrl = renderElement("div", { className: "control" }); + const msg = renderElement("div", { className: "message" }); + const add = ([control, message]) => { + ctrl.appendChild(control); + msg.appendChild(message); + }; + add(new SavePage().render()); + add(new DebugMode().render()); + add(new AecLogging().render()); + + const ctrls = document.querySelector("#controls"); + ctrls.append(renderElements("div", { className: "controls" }, [ctrl, msg])); + } + + // Render pcs and log + let reports = await haveReports; + let log = await haveLog; + + reports.sort((a, b) => a.browserId - b.browserId); + + let peerConnections = renderElement("div"); + let connectionLog = renderElement("div"); + let userPrefs = renderElement("div"); + + const content = document.querySelector("#content"); + content.append(peerConnections, connectionLog, userPrefs); + + function refresh() { + const pcDiv = renderElements("div", { className: "stats" }, [ + renderElements("span", { className: "section-heading" }, [ + renderText("h3", string("stats_heading")), + renderText("button", string("stats_clear"), { + className: "no-print", + onclick: async () => { + WGI.clearAllStats(); + reports = await getStats(); + refresh(); + }, + }), + ]), + ...reports.map(renderPeerConnection), + ]); + const logDiv = renderElements("div", { className: "log" }, [ + renderElements("span", { className: "section-heading" }, [ + renderText("h3", string("log_heading")), + renderElement("button", { + textContent: string("log_clear"), + className: "no-print", + onclick: async () => { + WGI.clearLogging(); + log = await getLog(); + refresh(); + }, + }), + ]), + ]); + if (log.length) { + const div = renderFoldableSection(logDiv, { + showMsg: string("log_show_msg"), + hideMsg: string("log_hide_msg"), + }); + div.append(...log.map(line => renderText("p", line))); + logDiv.append(div); + } + // Replace previous info + peerConnections.replaceWith(pcDiv); + connectionLog.replaceWith(logDiv); + userPrefs.replaceWith((userPrefs = renderUserPrefs())); + + peerConnections = pcDiv; + connectionLog = logDiv; + } + refresh(); + + window.setInterval( + async history => { + userPrefs.replaceWith((userPrefs = renderUserPrefs())); + const reports = await getStats(); + reports.forEach(report => { + const replace = (id, renderFunc) => { + const elem = document.getElementById(`${id}: ${report.pcid}`); + if (elem) { + elem.replaceWith(renderFunc(report, history)); + } + }; + replace("ice-stats", renderICEStats); + replace("rtp-stats", renderRTPStats); + replace("bandwidth-stats", renderBandwidthStats); + replace("frame-stats", renderFrameRateStats); + }); + }, + 500, + {} + ); +})(); + +function renderPeerConnection(report) { + const { pcid, browserId, closed, timestamp, configuration } = report; + + const pcDiv = renderElement("div", { className: "peer-connection" }); + { + const id = pcid.match(/id=(\S+)/)[1]; + const url = pcid.match(/url=([^)]+)/)[1]; + const closedStr = closed ? `(${string("connection_closed")})` : ""; + const now = new Date(timestamp).toString(); + + pcDiv.append( + renderText("h3", `[ ${browserId} | ${id} ] ${url} ${closedStr} ${now}`) + ); + pcDiv.append(new ShowTab(browserId).render()[0]); + } + { + const section = renderFoldableSection(pcDiv); + section.append( + renderElements("div", {}, [ + renderText("span", `${string("peer_connection_id_label")}: `, { + className: "info-label", + }), + renderText("span", pcid, { className: "info-body" }), + ]), + renderConfiguration(configuration), + renderICEStats(report), + renderSDPStats(report), + renderBandwidthStats(report), + renderFrameRateStats(report), + renderRTPStats(report) + ); + pcDiv.append(section); + } + return pcDiv; +} + +function renderSDPStats({ offerer, localSdp, remoteSdp, sdpHistory }) { + const trimNewlines = sdp => sdp.replaceAll("\r\n", "\n"); + + const statsDiv = renderElements("div", {}, [ + renderText("h4", string("sdp_heading")), + renderText( + "h5", + `${string("local_sdp_heading")} (${string(offerer ? "offer" : "answer")})` + ), + renderText("pre", trimNewlines(localSdp)), + renderText( + "h5", + `${string("remote_sdp_heading")} (${string( + offerer ? "answer" : "offer" + )})` + ), + renderText("pre", trimNewlines(remoteSdp)), + renderText("h4", string("sdp_history_heading")), + ]); + + // All SDP in sequential order. Add onclick handler to scroll the associated + // SDP into view below. + for (const { isLocal, timestamp } of sdpHistory) { + const histDiv = renderElement("div", {}); + const text = renderText( + "h5", + format("sdp_set_at_timestamp", [ + string(`${isLocal ? "local" : "remote"}_sdp_heading`), + timestamp, + ]), + { className: "sdp-history-link" } + ); + text.onclick = () => { + const elem = document.getElementById("sdp-history: " + timestamp); + if (elem) { + elem.scrollIntoView(); + } + }; + histDiv.append(text); + statsDiv.append(histDiv); + } + + // Render the SDP into separate columns for local and remote. + const section = renderElement("div", { className: "sdp-history" }); + const localDiv = renderElements("div", {}, [ + renderText("h4", `${string("local_sdp_heading")}`), + ]); + const remoteDiv = renderElements("div", {}, [ + renderText("h4", `${string("remote_sdp_heading")}`), + ]); + + let first = NaN; + for (const { isLocal, timestamp, sdp, errors } of sdpHistory) { + if (isNaN(first)) { + first = timestamp; + } + const histDiv = isLocal ? localDiv : remoteDiv; + histDiv.append( + renderText( + "h5", + format("sdp_set_timestamp", [timestamp, timestamp - first]), + { id: "sdp-history: " + timestamp } + ) + ); + if (errors.length) { + histDiv.append(renderElement("h5", string("sdp_parsing_errors_heading"))); + } + for (const { lineNumber, error } of errors) { + histDiv.append(renderElement("br"), `${lineNumber}: ${error}`); + } + histDiv.append(renderText("pre", trimNewlines(sdp))); + } + section.append(localDiv, remoteDiv); + statsDiv.append(section); + return statsDiv; +} + +function renderBandwidthStats(report) { + const statsDiv = renderElement("div", { + id: "bandwidth-stats: " + report.pcid, + }); + const table = renderSimpleTable( + "", + [ + "track_identifier", + "send_bandwidth_bytes_sec", + "receive_bandwidth_bytes_sec", + "max_padding_bytes_sec", + "pacer_delay_ms", + "round_trip_time_ms", + ].map(columnName => string(columnName)), + report.bandwidthEstimations.map(stat => [ + stat.trackIdentifier, + stat.sendBandwidthBps, + stat.receiveBandwidthBps, + stat.maxPaddingBps, + stat.pacerDelayMs, + stat.rttMs, + ]) + ); + statsDiv.append(renderText("h4", string("bandwidth_stats_heading")), table); + return statsDiv; +} + +function renderFrameRateStats(report) { + const statsDiv = renderElement("div", { id: "frame-stats: " + report.pcid }); + report.videoFrameHistories.forEach(history => { + const stats = history.entries.map(stat => { + stat.elapsed = stat.lastFrameTimestamp - stat.firstFrameTimestamp; + if (stat.elapsed < 1) { + stat.elapsed = 0; + } + stat.elapsed = (stat.elapsed / 1_000).toFixed(3); + if (stat.elapsed && stat.consecutiveFrames) { + stat.avgFramerate = (stat.consecutiveFrames / stat.elapsed).toFixed(2); + } else { + stat.avgFramerate = string("n_a"); + } + return stat; + }); + + const table = renderSimpleTable( + "", + [ + "width_px", + "height_px", + "consecutive_frames", + "time_elapsed", + "estimated_framerate", + "rotation_degrees", + "first_frame_timestamp", + "last_frame_timestamp", + "local_receive_ssrc", + "remote_send_ssrc", + ].map(columnName => string(columnName)), + stats.map(stat => + [ + stat.width, + stat.height, + stat.consecutiveFrames, + stat.elapsed, + stat.avgFramerate, + stat.rotationAngle, + stat.firstFrameTimestamp, + stat.lastFrameTimestamp, + stat.localSsrc, + stat.remoteSsrc || "?", + ].map(entry => (Object.is(entry, undefined) ? "<<undefined>>" : entry)) + ) + ); + + statsDiv.append( + renderText( + "h4", + `${string("frame_stats_heading")} - MediaStreamTrack Id: ${ + history.trackIdentifier + }` + ), + table + ); + }); + + return statsDiv; +} + +function renderRTPStats(report, history) { + const rtpStats = [ + ...(report.inboundRtpStreamStats || []), + ...(report.outboundRtpStreamStats || []), + ]; + const remoteRtpStats = [ + ...(report.remoteInboundRtpStreamStats || []), + ...(report.remoteOutboundRtpStreamStats || []), + ]; + + // Generate an id-to-streamStat index for each remote streamStat. This will + // be used next to link the remote to its local side. + const remoteRtpStatsMap = {}; + for (const stat of remoteRtpStats) { + remoteRtpStatsMap[stat.id] = stat; + } + + // If a streamStat has a remoteId attribute, create a remoteRtpStats + // attribute that references the remote streamStat entry directly. + // That is, the index generated above is merged into the returned list. + for (const stat of rtpStats.filter(s => "remoteId" in s)) { + stat.remoteRtpStats = remoteRtpStatsMap[stat.remoteId]; + } + const stats = [...rtpStats, ...remoteRtpStats]; + + // Render stats set + return renderElements("div", { id: "rtp-stats: " + report.pcid }, [ + renderText("h4", string("rtp_stats_heading")), + ...stats.map(stat => { + const { id, remoteId, remoteRtpStats } = stat; + const div = renderElements("div", {}, [ + renderText("h5", id), + renderCoderStats(stat), + renderTransportStats(stat, true, history), + ]); + if (remoteId && remoteRtpStats) { + div.append(renderTransportStats(remoteRtpStats, false)); + } + return div; + }), + ]); +} + +function renderCoderStats({ + bitrateMean, + bitrateStdDev, + framerateMean, + framerateStdDev, + droppedFrames, + discardedPackets, + packetsReceived, +}) { + let s = ""; + + if (bitrateMean) { + s += ` ${string("avg_bitrate_label")}: ${(bitrateMean / 1000000).toFixed( + 2 + )} Mbps`; + if (bitrateStdDev) { + s += ` (${(bitrateStdDev / 1000000).toFixed(2)} SD)`; + } + } + if (framerateMean) { + s += ` ${string("avg_framerate_label")}: ${framerateMean.toFixed(2)} fps`; + if (framerateStdDev) { + s += ` (${framerateStdDev.toFixed(2)} SD)`; + } + } + if (droppedFrames) { + s += ` ${string("dropped_frames_label")}: ${droppedFrames}`; + } + if (discardedPackets) { + s += ` ${string("discarded_packets_label")}: ${discardedPackets}`; + } + if (s.length) { + s = ` ${string(`${packetsReceived ? "de" : "en"}coder_label`)}:${s}`; + } + return renderText("p", s); +} + +function renderTransportStats( + { + id, + timestamp, + type, + ssrc, + packetsReceived, + bytesReceived, + packetsLost, + jitter, + roundTripTime, + packetsSent, + bytesSent, + }, + local, + history +) { + const typeLabel = local ? string("typeLocal") : string("typeRemote"); + + if (history) { + if (history[id] === undefined) { + history[id] = {}; + } + } + + const estimateKbps = (timestamp, lastTimestamp, bytes, lastBytes) => { + if (!timestamp || !lastTimestamp || !bytes || !lastBytes) { + return string("n_a"); + } + const elapsedTime = timestamp - lastTimestamp; + if (elapsedTime <= 0) { + return string("n_a"); + } + return ((bytes - lastBytes) / elapsedTime).toFixed(1); + }; + + const time = new Date(timestamp).toTimeString(); + let s = `${typeLabel}: ${time} ${type} SSRC: ${ssrc}`; + + const packets = string("packets"); + if (packetsReceived) { + s += ` ${string("received_label")}: ${packetsReceived} ${packets}`; + + if (bytesReceived) { + s += ` (${(bytesReceived / 1024).toFixed(2)} Kb`; + if (local && history) { + s += ` , ~${estimateKbps( + timestamp, + history[id].lastTimestamp, + bytesReceived, + history[id].lastBytesReceived + )} Kbps`; + } + s += ")"; + } + + s += ` ${string("lost_label")}: ${packetsLost} ${string( + "jitter_label" + )}: ${jitter}`; + + if (roundTripTime) { + s += ` RTT: ${roundTripTime * 1000} ms`; + } + } else if (packetsSent) { + s += ` ${string("sent_label")}: ${packetsSent} ${packets}`; + if (bytesSent) { + s += ` (${(bytesSent / 1024).toFixed(2)} Kb`; + if (local && history) { + s += `, ~${estimateKbps( + timestamp, + history[id].lastTimestamp, + bytesSent, + history[id].lastBytesSent + )} Kbps`; + } + s += ")"; + } + } + + // Update history + if (history) { + history[id].lastBytesReceived = bytesReceived; + history[id].lastBytesSent = bytesSent; + history[id].lastTimestamp = timestamp; + } + + return renderText("p", s); +} + +function renderRawIceTable(caption, candidates) { + const table = renderSimpleTable( + "", + [string(caption)], + [...new Set(candidates.sort())].filter(i => i).map(i => [i]) + ); + table.className = "raw-candidate"; + return table; +} + +function renderConfiguration(c) { + const provided = string("configuration_element_provided"); + const notProvided = string("configuration_element_not_provided"); + + // Create the text for a configuration field + const cfg = (obj, key) => [ + renderElement("br"), + `${key}: `, + key in obj ? obj[key] : renderText("i", notProvided), + ]; + + // Create the text for a fooProvided configuration field + const pro = (obj, key) => [ + renderElement("br"), + `${key}(`, + renderText("i", provided), + `/`, + renderText("i", notProvided), + `): `, + renderText("i", obj[`${key}Provided`] ? provided : notProvided), + ]; + + return renderElements("div", { classList: "peer-connection-config" }, [ + "RTCConfiguration", + ...cfg(c, "bundlePolicy"), + ...cfg(c, "iceTransportPolicy"), + ...pro(c, "peerIdentity"), + ...cfg(c, "sdpSemantics"), + renderElement("br"), + "iceServers: ", + ...(!c.iceServers + ? [renderText("i", notProvided)] + : c.iceServers.map(i => + renderElements("div", {}, [ + `urls: ${JSON.stringify(i.urls)}`, + ...pro(i, "credential"), + ...pro(i, "userName"), + ]) + )), + ]); +} + +function renderICEStats(report) { + const iceDiv = renderElements("div", { id: "ice-stats: " + report.pcid }, [ + renderText("h4", string("ice_stats_heading")), + ]); + + // Render ICECandidate table + { + const caption = renderElement("caption", { className: "no-print" }); + + // This takes the caption message with the replacement token, breaks + // it around the token, and builds the spans for each portion of the + // caption. This is to allow localization to put the color name for + // the highlight wherever it is appropriate in the translated string + // while avoiding innerHTML warnings from eslint. + const [start, end] = string("trickle_caption_msg2").split(/%(?:1\$)?S/); + + // only append span if non-whitespace chars present + if (/\S/.test(start)) { + caption.append(renderText("span", start)); + } + caption.append( + renderText("span", string("trickle_highlight_color_name2"), { + className: "ice-trickled", + }) + ); + // only append span if non-whitespace chars present + if (/\S/.test(end)) { + caption.append(renderText("span", end)); + } + + // Generate ICE stats + const stats = []; + { + // Create an index based on candidate ID for each element in the + // iceCandidateStats array. + const candidates = {}; + for (const candidate of report.iceCandidateStats) { + candidates[candidate.id] = candidate; + } + + // a method to see if a given candidate id is in the array of tickled + // candidates. + const isTrickled = candidateId => + report.trickledIceCandidateStats.some(({ id }) => id == candidateId); + + // A component may have a remote or local candidate address or both. + // Combine those with both; these will be the peer candidates. + const matched = {}; + + for (const { + localCandidateId, + remoteCandidateId, + componentId, + state, + priority, + nominated, + selected, + bytesSent, + bytesReceived, + } of report.iceCandidatePairStats) { + const local = candidates[localCandidateId]; + if (local) { + const stat = { + ["local-candidate"]: candidateToString(local), + componentId, + state, + priority, + nominated, + selected, + bytesSent, + bytesReceived, + }; + matched[local.id] = true; + if (isTrickled(local.id)) { + stat["local-trickled"] = true; + } + + const remote = candidates[remoteCandidateId]; + if (remote) { + stat["remote-candidate"] = candidateToString(remote); + matched[remote.id] = true; + if (isTrickled(remote.id)) { + stat["remote-trickled"] = true; + } + } + stats.push(stat); + } + } + + // sort (group by) componentId first, then bytesSent if available, else by + // priority + stats.sort((a, b) => { + if (a.componentId != b.componentId) { + return a.componentId - b.componentId; + } + return b.bytesSent + ? b.bytesSent - (a.bytesSent || 0) + : (b.priority || 0) - (a.priority || 0); + }); + } + // Render ICE stats + // don't use |stat.x || ""| here because it hides 0 values + const statsTable = renderSimpleTable( + caption, + [ + "ice_state", + "nominated", + "selected", + "local_candidate", + "remote_candidate", + "ice_component_id", + "priority", + "ice_pair_bytes_sent", + "ice_pair_bytes_received", + ].map(columnName => string(columnName)), + stats.map(stat => + [ + stat.state, + stat.nominated, + stat.selected, + stat["local-candidate"], + stat["remote-candidate"], + stat.componentId, + stat.priority, + stat.bytesSent, + stat.bytesReceived, + ].map(entry => (Object.is(entry, undefined) ? "" : entry)) + ) + ); + + // after rendering the table, we need to change the class name for each + // candidate pair's local or remote candidate if it was trickled. + let index = 0; + for (const { + state, + nominated, + selected, + "local-trickled": localTrickled, + "remote-trickled": remoteTrickled, + } of stats) { + // look at statsTable row index + 1 to skip column headers + const { cells } = statsTable.rows[++index]; + cells[0].className = `ice-${state}`; + if (nominated) { + cells[1].className = "ice-succeeded"; + } + if (selected) { + cells[2].className = "ice-succeeded"; + } + if (localTrickled) { + cells[3].className = "ice-trickled"; + } + if (remoteTrickled) { + cells[4].className = "ice-trickled"; + } + } + + // if the current row's component id changes, mark the bottom of the + // previous row with a thin, black border to differentiate the + // component id grouping. + let previousRow; + for (const row of statsTable.rows) { + if (previousRow) { + if (previousRow.cells[5].innerHTML != row.cells[5].innerHTML) { + previousRow.className = "bottom-border"; + } + } + previousRow = row; + } + iceDiv.append(statsTable); + } + // add just a bit of vertical space between the restart/rollback + // counts and the ICE candidate pair table above. + iceDiv.append( + renderElement("br"), + renderIceMetric("ice_restart_count_label", report.iceRestarts), + renderIceMetric("ice_rollback_count_label", report.iceRollbacks) + ); + + // Render raw ICECandidate section + { + const section = renderElements("div", {}, [ + renderText("h4", string("raw_candidates_heading")), + ]); + const foldSection = renderFoldableSection(section, { + showMsg: string("raw_cand_show_msg"), + hideMsg: string("raw_cand_hide_msg"), + }); + + // render raw candidates + foldSection.append( + renderElements("div", {}, [ + renderRawIceTable("raw_local_candidate", report.rawLocalCandidates), + renderRawIceTable("raw_remote_candidate", report.rawRemoteCandidates), + ]) + ); + section.append(foldSection); + iceDiv.append(section); + } + return iceDiv; +} + +function renderIceMetric(label, value) { + return renderElements("div", {}, [ + renderText("span", `${string(label)}: `, { className: "info-label" }), + renderText("span", value, { className: "info-body" }), + ]); +} + +function candidateToString({ + type, + address, + port, + protocol, + candidateType, + relayProtocol, + proxied, +} = {}) { + if (!type) { + return "*"; + } + if (relayProtocol) { + candidateType = `${candidateType}-${relayProtocol}`; + } + proxied = type == "local-candidate" ? ` [${proxied}]` : ""; + return `${address}:${port}/${protocol}(${candidateType})${proxied}`; +} + +function renderUserPrefs() { + const getPref = key => { + switch (Services.prefs.getPrefType(key)) { + case Services.prefs.PREF_BOOL: + return Services.prefs.getBoolPref(key); + case Services.prefs.PREF_INT: + return Services.prefs.getIntPref(key); + case Services.prefs.PREF_STRING: + return Services.prefs.getStringPref(key); + } + return ""; + }; + const prefs = [ + "media.peerconnection", + "media.navigator", + "media.getusermedia", + ]; + const renderPref = p => renderText("p", `${p}: ${getPref(p)}`); + const display = prefs + .flatMap(Services.prefs.getChildList) + .filter(Services.prefs.prefHasUserValue) + .map(renderPref); + return renderElements( + "div", + { + id: "prefs", + className: "prefs", + style: display.length ? "" : "visibility:hidden", + }, + [ + renderElements("span", { className: "section-heading" }, [ + renderText("h3", string("custom_webrtc_configuration_heading")), + ]), + ...display, + ] + ); +} + +function renderFoldableSection(parent, options = {}) { + const section = renderElement("div"); + if (parent) { + const ctrl = renderElements("div", { className: "section-ctrl no-print" }, [ + new FoldEffect(section, options).render(), + ]); + parent.append(ctrl); + } + return section; +} + +function renderSimpleTable(caption, headings, data) { + const heads = headings.map(text => renderText("th", text)); + const renderCell = text => renderText("td", text); + + return renderElements("table", {}, [ + caption, + renderElements("tr", {}, heads), + ...data.map(line => renderElements("tr", {}, line.map(renderCell))), + ]); +} + +class FoldEffect { + static allSections = []; + + constructor( + target, + { + showMsg = string("fold_show_msg"), + showHint = string("fold_show_hint"), + hideMsg = string("fold_hide_msg"), + hideHint = string("fold_hide_hint"), + } = {} + ) { + showMsg = `\u25BC ${showMsg}`; + hideMsg = `\u25B2 ${hideMsg}`; + Object.assign(this, { target, showMsg, showHint, hideMsg, hideHint }); + } + + render() { + this.target.classList.add("fold-target"); + this.trigger = renderElement("div", { className: "fold-trigger" }); + this.collapse(); + this.trigger.onclick = () => { + if (this.target.classList.contains("fold-closed")) { + this.expand(); + } else { + this.collapse(); + } + }; + FoldEffect.allSections.push(this); + return this.trigger; + } + + expand() { + this.target.classList.remove("fold-closed"); + this.trigger.setAttribute("title", this.hideHint); + this.trigger.textContent = this.hideMsg; + } + + collapse() { + this.target.classList.add("fold-closed"); + this.trigger.setAttribute("title", this.showHint); + this.trigger.textContent = this.showMsg; + } + + static expandAll() { + for (const section of FoldEffect.allSections) { + section.expand(); + } + } + + static collapseAll() { + for (const section of FoldEffect.allSections) { + section.collapse(); + } + } +} diff --git a/toolkit/content/autocomplete.css b/toolkit/content/autocomplete.css new file mode 100644 index 0000000000..93a7fe3425 --- /dev/null +++ b/toolkit/content/autocomplete.css @@ -0,0 +1,25 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace html url("http://www.w3.org/1999/xhtml"); + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + .ac-site-icon { + image-rendering: -moz-crisp-edges; + } +} + +.autocomplete-richlistbox > richlistitem { + -moz-box-orient: horizontal; + overflow: hidden; +} + +.ac-title-text, +.ac-url-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/toolkit/content/browser-child.js b/toolkit/content/browser-child.js new file mode 100644 index 0000000000..7f9620fc19 --- /dev/null +++ b/toolkit/content/browser-child.js @@ -0,0 +1,50 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm" +); + +try { + docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIBrowserChild) + .beginSendingWebProgressEventsToParent(); +} catch (e) { + // In responsive design mode, we do not have a BrowserChild for the in-parent + // document. +} + +// This message is used to measure content process startup performance in Talos +// tests. +sendAsyncMessage("Content:BrowserChildReady", { + time: Services.telemetry.msSystemNow(), +}); + +// This is here for now until we find a better way of forcing an about:blank load +// with a particular principal that doesn't involve the message manager. We can't +// do this with JS Window Actors for now because JS Window Actors are tied to the +// document principals themselves, so forcing the load with a new principal is +// self-destructive in that case. +addMessageListener("BrowserElement:CreateAboutBlank", message => { + if (!content.document || content.document.documentURI != "about:blank") { + throw new Error("Can't create a content viewer unless on about:blank"); + } + let { principal, partitionedPrincipal } = message.data; + principal = BrowserUtils.principalWithMatchingOA( + principal, + content.document.nodePrincipal + ); + partitionedPrincipal = BrowserUtils.principalWithMatchingOA( + partitionedPrincipal, + content.document.partitionedPrincipal + ); + docShell.createAboutBlankContentViewer(principal, partitionedPrincipal); +}); diff --git a/toolkit/content/buildconfig.css b/toolkit/content/buildconfig.css new file mode 100644 index 0000000000..43064488e6 --- /dev/null +++ b/toolkit/content/buildconfig.css @@ -0,0 +1,7 @@ +/* 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/. */ + +th { text-align: start; } +h2 { margin-top: 1.5em; } +th, td { vertical-align: top; } diff --git a/toolkit/content/buildconfig.html b/toolkit/content/buildconfig.html new file mode 100644 index 0000000000..5e7e77bc61 --- /dev/null +++ b/toolkit/content/buildconfig.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +# 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/. +# +#filter substitution +#include @TOPOBJDIR@/source-repo.h +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src chrome:; object-src 'none'" /> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width; user-scalable=false;"> + <title>Build Configuration</title> + <link rel="stylesheet" href="chrome://global/skin/about.css" type="text/css"> + <link rel="stylesheet" href="chrome://global/content/buildconfig.css" type="text/css"> +</head> +<body class="aboutPageWideContainer"> +<h1>Build Configuration</h1> +#ifdef MOZ_SOURCE_URL +<h2>Source</h2> +<p>Built from <a href="@MOZ_SOURCE_URL@">@MOZ_SOURCE_URL@</a></p> +#endif +<h2>Build platform</h2> +<table> + <tbody> + <tr> + <th>target</th> + </tr> + <tr> + <td>@target@</td> + </tr> + </tbody> +</table> +#if defined(CC) && defined(CXX) && defined(RUSTC) +<h2>Build tools</h2> +<table> + <tbody> + <tr> + <th>Compiler</th> + <th>Version</th> + <th>Compiler flags</th> + </tr> + <tr> + <td>@CC@</td> + <td>@CC_VERSION@</td> + <td>@CFLAGS@</td> + </tr> + <tr> + <td>@CXX@</td> + <td>@CC_VERSION@</td> + <td>@CXXFLAGS@</td> + </tr> + <tr> + <td>@RUSTC@</td> + <td>@RUSTC_VERSION@</td> + <td>@RUSTFLAGS@</td> + </tr> + </tbody> +</table> +#endif +<h2>Configure options</h2> +<p>@MOZ_CONFIGURE_OPTIONS@</p> +#ifdef ANDROID +<h2>Package name</h2> +<p>@ANDROID_PACKAGE_NAME@</p> +#endif +</body> +</html> diff --git a/toolkit/content/contentAreaUtils.js b/toolkit/content/contentAreaUtils.js new file mode 100644 index 0000000000..71b44f84e0 --- /dev/null +++ b/toolkit/content/contentAreaUtils.js @@ -0,0 +1,1334 @@ +/* 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 { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + BrowserUtils: "resource://gre/modules/BrowserUtils.jsm", + Downloads: "resource://gre/modules/Downloads.jsm", + DownloadPaths: "resource://gre/modules/DownloadPaths.jsm", + DownloadLastDir: "resource://gre/modules/DownloadLastDir.jsm", + FileUtils: "resource://gre/modules/FileUtils.jsm", + OS: "resource://gre/modules/osfile.jsm", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", + Deprecated: "resource://gre/modules/Deprecated.jsm", + NetUtil: "resource://gre/modules/NetUtil.jsm", +}); + +var ContentAreaUtils = { + get stringBundle() { + delete this.stringBundle; + return (this.stringBundle = Services.strings.createBundle( + "chrome://global/locale/contentAreaCommands.properties" + )); + }, +}; + +function urlSecurityCheck(aURL, aPrincipal, aFlags) { + return BrowserUtils.urlSecurityCheck(aURL, aPrincipal, aFlags); +} + +// Clientele: (Make sure you don't break any of these) +// - File -> Save Page/Frame As... +// - Context -> Save Page/Frame As... +// - Context -> Save Link As... +// - Alt-Click links in web pages +// - Alt-Click links in the UI +// +// Try saving each of these types: +// - A complete webpage using File->Save Page As, and Context->Save Page As +// - A webpage as HTML only using the above methods +// - A webpage as Text only using the above methods +// - An image with an extension (e.g. .jpg) in its file name, using +// Context->Save Image As... +// - An image without an extension (e.g. a banner ad on cnn.com) using +// the above method. +// - A linked document using Save Link As... +// - A linked document using Alt-click Save Link As... +// +function saveURL( + aURL, + aFileName, + aFilePickerTitleKey, + aShouldBypassCache, + aSkipPrompt, + aReferrerInfo, + aCookieJarSettings, + aSourceDocument, + aIsContentWindowPrivate, + aPrincipal +) { + internalSave( + aURL, + null, + aFileName, + null, + null, + aShouldBypassCache, + aFilePickerTitleKey, + null, + aReferrerInfo, + aCookieJarSettings, + aSourceDocument, + aSkipPrompt, + null, + aIsContentWindowPrivate, + aPrincipal + ); +} + +// Save the current document inside any browser/frame-like element, +// whether in-process or out-of-process. +function saveBrowser(aBrowser, aSkipPrompt, aBrowsingContext = null) { + if (!aBrowser) { + throw new Error("Must have a browser when calling saveBrowser"); + } + let persistable = aBrowser.frameLoader; + // PDF.js has its own way to handle saving PDFs since it may need to + // generate a new PDF to save modified form data. + if (aBrowser.contentPrincipal.spec == "resource://pdf.js/web/viewer.html") { + aBrowser.sendMessageToActor("PDFJS:Save", {}, "Pdfjs"); + return; + } + let stack = Components.stack.caller; + persistable.startPersistence(aBrowsingContext, { + onDocumentReady(document) { + if (!document || !(document instanceof Ci.nsIWebBrowserPersistDocument)) { + throw new Error("Must have an nsIWebBrowserPersistDocument!"); + } + + internalSave( + document.documentURI, + document, + null, // file name + document.contentDisposition, + document.contentType, + false, // bypass cache + null, // file picker title key + null, // chosen file data + document.referrerInfo, + document.cookieJarSettings, + document, + aSkipPrompt, + document.cacheKey + ); + }, + onError(status) { + throw new Components.Exception( + "saveBrowser failed asynchronously in startPersistence", + status, + stack + ); + }, + }); +} + +function DownloadListener(win, transfer) { + function makeClosure(name) { + return function() { + transfer[name].apply(transfer, arguments); + }; + } + + this.window = win; + + // Now... we need to forward all calls to our transfer + for (var i in transfer) { + if (i != "QueryInterface") { + this[i] = makeClosure(i); + } + } +} + +DownloadListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIWebProgressListener", + "nsIWebProgressListener2", + ]), + + getInterface: function dl_gi(aIID) { + if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) { + var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService( + Ci.nsIPromptFactory + ); + return ww.getPrompt(this.window, aIID); + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, +}; + +const kSaveAsType_Complete = 0; // Save document with attached objects. +XPCOMUtils.defineConstant(this, "kSaveAsType_Complete", 0); +// const kSaveAsType_URL = 1; // Save document or URL by itself. +const kSaveAsType_Text = 2; // Save document, converting to plain text. +XPCOMUtils.defineConstant(this, "kSaveAsType_Text", kSaveAsType_Text); + +/** + * internalSave: Used when saving a document or URL. + * + * If aChosenData is null, this method: + * - Determines a local target filename to use + * - Prompts the user to confirm the destination filename and save mode + * (aContentType affects this) + * - [Note] This process involves the parameters aURL, aReferrerInfo, + * aDocument, aDefaultFileName, aFilePickerTitleKey, and aSkipPrompt. + * + * If aChosenData is non-null, this method: + * - Uses the provided source URI and save file name + * - Saves the document as complete DOM if possible (aDocument present and + * right aContentType) + * - [Note] The parameters aURL, aDefaultFileName, aFilePickerTitleKey, and + * aSkipPrompt are ignored. + * + * In any case, this method: + * - Creates a 'Persist' object (which will perform the saving in the + * background) and then starts it. + * - [Note] This part of the process only involves the parameters aDocument, + * aShouldBypassCache and aReferrerInfo. The source, the save name and the + * save mode are the ones determined previously. + * + * @param aURL + * The String representation of the URL of the document being saved + * @param aDocument + * The document to be saved + * @param aDefaultFileName + * The caller-provided suggested filename if we don't + * find a better one + * @param aContentDisposition + * The caller-provided content-disposition header to use. + * @param aContentType + * The caller-provided content-type to use + * @param aShouldBypassCache + * If true, the document will always be refetched from the server + * @param aFilePickerTitleKey + * Alternate title for the file picker + * @param aChosenData + * If non-null this contains an instance of object AutoChosen (see below) + * which holds pre-determined data so that the user does not need to be + * prompted for a target filename. + * @param aReferrerInfo + * the referrerInfo object to use, or null if no referrer should be sent. + * @param aCookieJarSettings + * the cookieJarSettings object to use. This will be used for the channel + * used to save. + * @param aInitiatingDocument [optional] + * The document from which the save was initiated. + * If this is omitted then aIsContentWindowPrivate has to be provided. + * @param aSkipPrompt [optional] + * If set to true, we will attempt to save the file to the + * default downloads folder without prompting. + * @param aCacheKey [optional] + * If set will be passed to saveURI. See nsIWebBrowserPersist for + * allowed values. + * @param aIsContentWindowPrivate [optional] + * This parameter is provided when the aInitiatingDocument is not a + * real document object. Stores whether aInitiatingDocument.defaultView + * was private or not. + * @param aPrincipal [optional] + * This parameter is provided when neither aDocument nor + * aInitiatingDocument is provided. Used to determine what level of + * privilege to load the URI with. + */ +function internalSave( + aURL, + aDocument, + aDefaultFileName, + aContentDisposition, + aContentType, + aShouldBypassCache, + aFilePickerTitleKey, + aChosenData, + aReferrerInfo, + aCookieJarSettings, + aInitiatingDocument, + aSkipPrompt, + aCacheKey, + aIsContentWindowPrivate, + aPrincipal +) { + if (aSkipPrompt == undefined) { + aSkipPrompt = false; + } + + if (aCacheKey == undefined) { + aCacheKey = 0; + } + + // Note: aDocument == null when this code is used by save-link-as... + var saveMode = GetSaveModeForContentType(aContentType, aDocument); + + var file, sourceURI, saveAsType; + let contentPolicyType = Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD; + // Find the URI object for aURL and the FileName/Extension to use when saving. + // FileName/Extension will be ignored if aChosenData supplied. + if (aChosenData) { + file = aChosenData.file; + sourceURI = aChosenData.uri; + saveAsType = kSaveAsType_Complete; + + continueSave(); + } else { + var charset = null; + if (aDocument) { + charset = aDocument.characterSet; + } + var fileInfo = new FileInfo(aDefaultFileName); + initFileInfo( + fileInfo, + aURL, + charset, + aDocument, + aContentType, + aContentDisposition + ); + sourceURI = fileInfo.uri; + + if (aContentType && aContentType.startsWith("image/")) { + contentPolicyType = Ci.nsIContentPolicy.TYPE_IMAGE; + } + var fpParams = { + fpTitleKey: aFilePickerTitleKey, + fileInfo, + contentType: aContentType, + saveMode, + saveAsType: kSaveAsType_Complete, + file, + }; + + // Find a URI to use for determining last-downloaded-to directory + let relatedURI = aReferrerInfo ? aReferrerInfo.originalReferrer : sourceURI; + + promiseTargetFile(fpParams, aSkipPrompt, relatedURI) + .then(aDialogAccepted => { + if (!aDialogAccepted) { + return; + } + + saveAsType = fpParams.saveAsType; + file = fpParams.file; + + continueSave(); + }) + .catch(Cu.reportError); + } + + function continueSave() { + // XXX We depend on the following holding true in appendFiltersForContentType(): + // If we should save as a complete page, the saveAsType is kSaveAsType_Complete. + // If we should save as text, the saveAsType is kSaveAsType_Text. + var useSaveDocument = + aDocument && + ((saveMode & SAVEMODE_COMPLETE_DOM && + saveAsType == kSaveAsType_Complete) || + (saveMode & SAVEMODE_COMPLETE_TEXT && saveAsType == kSaveAsType_Text)); + // If we're saving a document, and are saving either in complete mode or + // as converted text, pass the document to the web browser persist component. + // If we're just saving the HTML (second option in the list), send only the URI. + + let isPrivate = aIsContentWindowPrivate; + if (isPrivate === undefined) { + isPrivate = + aInitiatingDocument.nodeType == 9 /* DOCUMENT_NODE */ + ? PrivateBrowsingUtils.isContentWindowPrivate( + aInitiatingDocument.defaultView + ) + : aInitiatingDocument.isPrivate; + } + + // We have to cover the cases here where we were either passed an explicit + // principal, or a 'real' document (with a nodePrincipal property), or an + // nsIWebBrowserPersistDocument which has a principal property. + let sourcePrincipal = + aPrincipal || + (aDocument && (aDocument.nodePrincipal || aDocument.principal)) || + (aInitiatingDocument && aInitiatingDocument.nodePrincipal); + + var persistArgs = { + sourceURI, + sourcePrincipal, + sourceReferrerInfo: aReferrerInfo, + sourceDocument: useSaveDocument ? aDocument : null, + targetContentType: saveAsType == kSaveAsType_Text ? "text/plain" : null, + targetFile: file, + sourceCacheKey: aCacheKey, + sourcePostData: aDocument ? getPostData(aDocument) : null, + bypassCache: aShouldBypassCache, + contentPolicyType, + cookieJarSettings: aCookieJarSettings, + isPrivate, + }; + + // Start the actual save process + internalPersist(persistArgs); + } +} + +/** + * internalPersist: Creates a 'Persist' object (which will perform the saving + * in the background) and then starts it. + * + * @param persistArgs.sourceURI + * The nsIURI of the document being saved + * @param persistArgs.sourceCacheKey [optional] + * If set will be passed to savePrivacyAwareURI + * @param persistArgs.sourceDocument [optional] + * The document to be saved, or null if not saving a complete document + * @param persistArgs.sourceReferrerInfo + * Required and used only when persistArgs.sourceDocument is NOT present, + * the nsIReferrerInfo of the referrer info to use, or null if no + * referrer should be sent. + * @param persistArgs.sourcePostData + * Required and used only when persistArgs.sourceDocument is NOT present, + * represents the POST data to be sent along with the HTTP request, and + * must be null if no POST data should be sent. + * @param persistArgs.targetFile + * The nsIFile of the file to create + * @param persistArgs.contentPolicyType + * The type of content we're saving. Will be used to determine what + * content is accepted, enforce sniffing restrictions, etc. + * @param persistArgs.cookieJarSettings [optional] + * The nsICookieJarSettings that will be used for the saving channel, or + * null that savePrivacyAwareURI will create one based on the current + * state of the prefs/permissions + * @param persistArgs.targetContentType + * Required and used only when persistArgs.sourceDocument is present, + * determines the final content type of the saved file, or null to use + * the same content type as the source document. Currently only + * "text/plain" is meaningful. + * @param persistArgs.bypassCache + * If true, the document will always be refetched from the server + * @param persistArgs.isPrivate + * Indicates whether this is taking place in a private browsing context. + */ +function internalPersist(persistArgs) { + var persist = makeWebBrowserPersist(); + + // Calculate persist flags. + const nsIWBP = Ci.nsIWebBrowserPersist; + const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES; + if (persistArgs.bypassCache) { + persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_BYPASS_CACHE; + } else { + persist.persistFlags = flags | nsIWBP.PERSIST_FLAGS_FROM_CACHE; + } + + // Leave it to WebBrowserPersist to discover the encoding type (or lack thereof): + persist.persistFlags |= nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; + + // Find the URI associated with the target file + var targetFileURL = makeFileURI(persistArgs.targetFile); + + // Create download and initiate it (below) + var tr = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer); + tr.init( + persistArgs.sourceURI, + targetFileURL, + "", + null, + null, + null, + persist, + persistArgs.isPrivate, + Ci.nsITransfer.DOWNLOAD_ACCEPTABLE + ); + persist.progressListener = new DownloadListener(window, tr); + + if (persistArgs.sourceDocument) { + // Saving a Document, not a URI: + var filesFolder = null; + if (persistArgs.targetContentType != "text/plain") { + // Create the local directory into which to save associated files. + filesFolder = persistArgs.targetFile.clone(); + + var nameWithoutExtension = getFileBaseName(filesFolder.leafName); + var filesFolderLeafName = ContentAreaUtils.stringBundle.formatStringFromName( + "filesFolder", + [nameWithoutExtension] + ); + + filesFolder.leafName = filesFolderLeafName; + } + + var encodingFlags = 0; + if (persistArgs.targetContentType == "text/plain") { + encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED; + encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS; + encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT; + } else { + encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES; + } + + const kWrapColumn = 80; + persist.saveDocument( + persistArgs.sourceDocument, + targetFileURL, + filesFolder, + persistArgs.targetContentType, + encodingFlags, + kWrapColumn + ); + } else { + persist.savePrivacyAwareURI( + persistArgs.sourceURI, + persistArgs.sourcePrincipal, + persistArgs.sourceCacheKey, + persistArgs.sourceReferrerInfo, + persistArgs.cookieJarSettings, + persistArgs.sourcePostData, + null, + targetFileURL, + persistArgs.contentPolicyType || Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD, + persistArgs.isPrivate + ); + } +} + +/** + * Structure for holding info about automatically supplied parameters for + * internalSave(...). This allows parameters to be supplied so the user does not + * need to be prompted for file info. + * @param aFileAutoChosen This is an nsIFile object that has been + * pre-determined as the filename for the target to save to + * @param aUriAutoChosen This is the nsIURI object for the target + */ +function AutoChosen(aFileAutoChosen, aUriAutoChosen) { + this.file = aFileAutoChosen; + this.uri = aUriAutoChosen; +} + +/** + * Structure for holding info about a URL and the target filename it should be + * saved to. This structure is populated by initFileInfo(...). + * @param aSuggestedFileName This is used by initFileInfo(...) when it + * cannot 'discover' the filename from the url + * @param aFileName The target filename + * @param aFileBaseName The filename without the file extension + * @param aFileExt The extension of the filename + * @param aUri An nsIURI object for the url that is being saved + */ +function FileInfo( + aSuggestedFileName, + aFileName, + aFileBaseName, + aFileExt, + aUri +) { + this.suggestedFileName = aSuggestedFileName; + this.fileName = aFileName; + this.fileBaseName = aFileBaseName; + this.fileExt = aFileExt; + this.uri = aUri; +} + +/** + * Determine what the 'default' filename string is, its file extension and the + * filename without the extension. This filename is used when prompting the user + * for confirmation in the file picker dialog. + * @param aFI A FileInfo structure into which we'll put the results of this method. + * @param aURL The String representation of the URL of the document being saved + * @param aURLCharset The charset of aURL. + * @param aDocument The document to be saved + * @param aContentType The content type we're saving, if it could be + * determined by the caller. + * @param aContentDisposition The content-disposition header for the object + * we're saving, if it could be determined by the caller. + */ +function initFileInfo( + aFI, + aURL, + aURLCharset, + aDocument, + aContentType, + aContentDisposition +) { + try { + // Get an nsIURI object from aURL if possible: + try { + aFI.uri = makeURI(aURL, aURLCharset); + // Assuming nsiUri is valid, calling QueryInterface(...) on it will + // populate extra object fields (eg filename and file extension). + var url = aFI.uri.QueryInterface(Ci.nsIURL); + aFI.fileExt = url.fileExtension; + } catch (e) {} + + // Get the default filename: + aFI.fileName = getDefaultFileName( + aFI.suggestedFileName || aFI.fileName, + aFI.uri, + aDocument, + aContentDisposition + ); + // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied + // if saveURL(...) was the original caller (hence both aContentType and + // aDocument are blank). If they were saving a link to a website then make + // the extension .htm . + if ( + !aFI.fileExt && + !aDocument && + !aContentType && + /^http(s?):\/\//i.test(aURL) + ) { + aFI.fileExt = "htm"; + aFI.fileBaseName = aFI.fileName; + } else { + aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType); + aFI.fileBaseName = getFileBaseName(aFI.fileName); + } + } catch (e) {} +} + +/** + * Given the Filepicker Parameters (aFpP), show the file picker dialog, + * prompting the user to confirm (or change) the fileName. + * @param aFpP + * A structure (see definition in internalSave(...) method) + * containing all the data used within this method. + * @param aSkipPrompt + * If true, attempt to save the file automatically to the user's default + * download directory, thus skipping the explicit prompt for a file name, + * but only if the associated preference is set. + * If false, don't save the file automatically to the user's + * default download directory, even if the associated preference + * is set, but ask for the target explicitly. + * @param aRelatedURI + * An nsIURI associated with the download. The last used + * directory of the picker is retrieved from/stored in the + * Content Pref Service using this URI. + * @return Promise + * @resolve a boolean. When true, it indicates that the file picker dialog + * is accepted. + */ +function promiseTargetFile( + aFpP, + /* optional */ aSkipPrompt, + /* optional */ aRelatedURI +) { + return (async function() { + let downloadLastDir = new DownloadLastDir(window); + let prefBranch = Services.prefs.getBranch("browser.download."); + let useDownloadDir = prefBranch.getBoolPref("useDownloadDir"); + + if (!aSkipPrompt) { + useDownloadDir = false; + } + + // Default to the user's default downloads directory configured + // through download prefs. + let dirPath = await Downloads.getPreferredDownloadsDirectory(); + let dirExists = await OS.File.exists(dirPath); + let dir = new FileUtils.File(dirPath); + + if (useDownloadDir && dirExists) { + dir.append( + getNormalizedLeafName(aFpP.fileInfo.fileName, aFpP.fileInfo.fileExt) + ); + aFpP.file = uniqueFile(dir); + return true; + } + + // We must prompt for the file name explicitly. + // If we must prompt because we were asked to... + let file = await new Promise(resolve => { + if (useDownloadDir) { + // Keep async behavior in both branches + Services.tm.dispatchToMainThread(function() { + resolve(null); + }); + } else { + downloadLastDir.getFileAsync(aRelatedURI, function getFileAsyncCB( + aFile + ) { + resolve(aFile); + }); + } + }); + if (file && (await OS.File.exists(file.path))) { + dir = file; + dirExists = true; + } + + if (!dirExists) { + // Default to desktop. + dir = Services.dirsvc.get("Desk", Ci.nsIFile); + } + + let fp = makeFilePicker(); + let titleKey = aFpP.fpTitleKey || "SaveLinkTitle"; + fp.init( + window, + ContentAreaUtils.stringBundle.GetStringFromName(titleKey), + Ci.nsIFilePicker.modeSave + ); + + fp.displayDirectory = dir; + fp.defaultExtension = aFpP.fileInfo.fileExt; + fp.defaultString = getNormalizedLeafName( + aFpP.fileInfo.fileName, + aFpP.fileInfo.fileExt + ); + appendFiltersForContentType( + fp, + aFpP.contentType, + aFpP.fileInfo.fileExt, + aFpP.saveMode + ); + + // The index of the selected filter is only preserved and restored if there's + // more than one filter in addition to "All Files". + if (aFpP.saveMode != SAVEMODE_FILEONLY) { + // eslint-disable-next-line mozilla/use-default-preference-values + try { + fp.filterIndex = prefBranch.getIntPref("save_converter_index"); + } catch (e) {} + } + + let result = await new Promise(resolve => { + fp.open(function(aResult) { + resolve(aResult); + }); + }); + if (result == Ci.nsIFilePicker.returnCancel || !fp.file) { + return false; + } + + if (aFpP.saveMode != SAVEMODE_FILEONLY) { + prefBranch.setIntPref("save_converter_index", fp.filterIndex); + } + + // Do not store the last save directory as a pref inside the private browsing mode + downloadLastDir.setFile(aRelatedURI, fp.file.parent); + + fp.file.leafName = validateFileName(fp.file.leafName); + + aFpP.saveAsType = fp.filterIndex; + aFpP.file = fp.file; + aFpP.fileURL = fp.fileURL; + + return true; + })(); +} + +// Since we're automatically downloading, we don't get the file picker's +// logic to check for existing files, so we need to do that here. +// +// Note - this code is identical to that in +// mozilla/toolkit/mozapps/downloads/src/nsHelperAppDlg.js.in +// If you are updating this code, update that code too! We can't share code +// here since that code is called in a js component. +function uniqueFile(aLocalFile) { + var collisionCount = 0; + while (aLocalFile.exists()) { + collisionCount++; + if (collisionCount == 1) { + // Append "(2)" before the last dot in (or at the end of) the filename + // special case .ext.gz etc files so we don't wind up with .tar(2).gz + if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) { + aLocalFile.leafName = aLocalFile.leafName.replace( + /\.[^\.]{1,3}\.(gz|bz2|Z)$/i, + "(2)$&" + ); + } else { + aLocalFile.leafName = aLocalFile.leafName.replace( + /(\.[^\.]*)?$/, + "(2)$&" + ); + } + } else { + // replace the last (n) in the filename with (n+1) + aLocalFile.leafName = aLocalFile.leafName.replace( + /^(.*\()\d+\)/, + "$1" + (collisionCount + 1) + ")" + ); + } + } + return aLocalFile; +} + +/** + * Download a URL using the Downloads API. + * + * @param aURL + * the url to download + * @param [optional] aFileName + * the destination file name, if omitted will be obtained from the url. + * @param aInitiatingDocument + * The document from which the download was initiated. + */ +function DownloadURL(aURL, aFileName, aInitiatingDocument) { + // For private browsing, try to get document out of the most recent browser + // window, or provide our own if there's no browser window. + let isPrivate = aInitiatingDocument.defaultView.docShell.QueryInterface( + Ci.nsILoadContext + ).usePrivateBrowsing; + + let fileInfo = new FileInfo(aFileName); + initFileInfo(fileInfo, aURL, null, null, null, null); + + let filepickerParams = { + fileInfo, + saveMode: SAVEMODE_FILEONLY, + }; + + (async function() { + let accepted = await promiseTargetFile( + filepickerParams, + true, + fileInfo.uri + ); + if (!accepted) { + return; + } + + let file = filepickerParams.file; + let download = await Downloads.createDownload({ + source: { url: aURL, isPrivate }, + target: { path: file.path, partFilePath: file.path + ".part" }, + }); + download.tryToKeepPartialData = true; + + // Ignore errors because failures are reported through the download list. + download.start().catch(() => {}); + + // Add the download to the list, allowing it to be managed. + let list = await Downloads.getList(Downloads.ALL); + list.add(download); + })().catch(Cu.reportError); +} + +// We have no DOM, and can only save the URL as is. +const SAVEMODE_FILEONLY = 0x00; +XPCOMUtils.defineConstant(this, "SAVEMODE_FILEONLY", SAVEMODE_FILEONLY); +// We have a DOM and can save as complete. +const SAVEMODE_COMPLETE_DOM = 0x01; +XPCOMUtils.defineConstant(this, "SAVEMODE_COMPLETE_DOM", SAVEMODE_COMPLETE_DOM); +// We have a DOM which we can serialize as text. +const SAVEMODE_COMPLETE_TEXT = 0x02; +XPCOMUtils.defineConstant( + this, + "SAVEMODE_COMPLETE_TEXT", + SAVEMODE_COMPLETE_TEXT +); + +// If we are able to save a complete DOM, the 'save as complete' filter +// must be the first filter appended. The 'save page only' counterpart +// must be the second filter appended. And the 'save as complete text' +// filter must be the third filter appended. +function appendFiltersForContentType( + aFilePicker, + aContentType, + aFileExtension, + aSaveMode +) { + // The bundle name for saving only a specific content type. + var bundleName; + // The corresponding filter string for a specific content type. + var filterString; + + // Every case where GetSaveModeForContentType can return non-FILEONLY + // modes must be handled here. + if (aSaveMode != SAVEMODE_FILEONLY) { + switch (aContentType) { + case "text/html": + bundleName = "WebPageHTMLOnlyFilter"; + filterString = "*.htm; *.html"; + break; + + case "application/xhtml+xml": + bundleName = "WebPageXHTMLOnlyFilter"; + filterString = "*.xht; *.xhtml"; + break; + + case "image/svg+xml": + bundleName = "WebPageSVGOnlyFilter"; + filterString = "*.svg; *.svgz"; + break; + + case "text/xml": + case "application/xml": + bundleName = "WebPageXMLOnlyFilter"; + filterString = "*.xml"; + break; + } + } + + if (!bundleName) { + if (aSaveMode != SAVEMODE_FILEONLY) { + throw new Error(`Invalid save mode for type '${aContentType}'`); + } + + var mimeInfo = getMIMEInfoForType(aContentType, aFileExtension); + if (mimeInfo) { + var extString = ""; + for (var extension of mimeInfo.getFileExtensions()) { + if (extString) { + extString += "; "; + } // If adding more than one extension, + // separate by semi-colon + extString += "*." + extension; + } + + if (extString) { + aFilePicker.appendFilter(mimeInfo.description, extString); + } + } + } + + if (aSaveMode & SAVEMODE_COMPLETE_DOM) { + aFilePicker.appendFilter( + ContentAreaUtils.stringBundle.GetStringFromName("WebPageCompleteFilter"), + filterString + ); + // We should always offer a choice to save document only if + // we allow saving as complete. + aFilePicker.appendFilter( + ContentAreaUtils.stringBundle.GetStringFromName(bundleName), + filterString + ); + } + + if (aSaveMode & SAVEMODE_COMPLETE_TEXT) { + aFilePicker.appendFilters(Ci.nsIFilePicker.filterText); + } + + // Always append the all files (*) filter + aFilePicker.appendFilters(Ci.nsIFilePicker.filterAll); +} + +function getPostData(aDocument) { + if (aDocument instanceof Ci.nsIWebBrowserPersistDocument) { + return aDocument.postData; + } + try { + // Find the session history entry corresponding to the given document. In + // the current implementation, nsIWebPageDescriptor.currentDescriptor always + // returns a session history entry. + let sessionHistoryEntry = aDocument.defaultView.docShell + .QueryInterface(Ci.nsIWebPageDescriptor) + .currentDescriptor.QueryInterface(Ci.nsISHEntry); + return sessionHistoryEntry.postData; + } catch (e) {} + return null; +} + +function makeWebBrowserPersist() { + const persistContractID = + "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"; + const persistIID = Ci.nsIWebBrowserPersist; + return Cc[persistContractID].createInstance(persistIID); +} + +function makeURI(aURL, aOriginCharset, aBaseURI) { + return Services.io.newURI(aURL, aOriginCharset, aBaseURI); +} + +function makeFileURI(aFile) { + return Services.io.newFileURI(aFile); +} + +function makeFilePicker() { + const fpContractID = "@mozilla.org/filepicker;1"; + const fpIID = Ci.nsIFilePicker; + return Cc[fpContractID].createInstance(fpIID); +} + +function getMIMEService() { + const mimeSvcContractID = "@mozilla.org/mime;1"; + const mimeSvcIID = Ci.nsIMIMEService; + const mimeSvc = Cc[mimeSvcContractID].getService(mimeSvcIID); + return mimeSvc; +} + +// Given aFileName, find the fileName without the extension on the end. +function getFileBaseName(aFileName) { + // Remove the file extension from aFileName: + return aFileName.replace(/\.[^.]*$/, ""); +} + +function getMIMETypeForURI(aURI) { + try { + return getMIMEService().getTypeFromURI(aURI); + } catch (e) {} + return null; +} + +function getMIMEInfoForType(aMIMEType, aExtension) { + if (aMIMEType || aExtension) { + try { + return getMIMEService().getFromTypeAndExtension(aMIMEType, aExtension); + } catch (e) {} + } + return null; +} + +function getDefaultFileName( + aDefaultFileName, + aURI, + aDocument, + aContentDisposition +) { + // 1) look for a filename in the content-disposition header, if any + if (aContentDisposition) { + const mhpContractID = "@mozilla.org/network/mime-hdrparam;1"; + const mhpIID = Ci.nsIMIMEHeaderParam; + const mhp = Cc[mhpContractID].getService(mhpIID); + var dummy = { value: null }; // Need an out param... + var charset = getCharsetforSave(aDocument); + + var fileName = null; + try { + fileName = mhp.getParameter( + aContentDisposition, + "filename", + charset, + true, + dummy + ); + } catch (e) { + try { + fileName = mhp.getParameter( + aContentDisposition, + "name", + charset, + true, + dummy + ); + } catch (e) {} + } + if (fileName) { + return validateFileName(Services.textToSubURI.unEscapeURIForUI(fileName)); + } + } + + let docTitle; + if (aDocument && aDocument.title && aDocument.title.trim()) { + // If the document looks like HTML or XML, try to use its original title. + docTitle = validateFileName(aDocument.title); + let contentType = aDocument.contentType; + if ( + contentType == "application/xhtml+xml" || + contentType == "application/xml" || + contentType == "image/svg+xml" || + contentType == "text/html" || + contentType == "text/xml" + ) { + // 2) Use the document title + return docTitle; + } + } + + try { + var url = aURI.QueryInterface(Ci.nsIURL); + if (url.fileName != "") { + // 3) Use the actual file name, if present + return validateFileName( + Services.textToSubURI.unEscapeURIForUI(url.fileName) + ); + } + } catch (e) { + // This is something like a data: and so forth URI... no filename here. + } + + if (docTitle) { + // 4) Use the document title + return docTitle; + } + + if (aDefaultFileName) { + // 5) Use the caller-provided name, if any + return validateFileName(aDefaultFileName); + } + + // 6) If this is a directory, use the last directory name + var path = aURI.pathQueryRef.match(/\/([^\/]+)\/$/); + if (path && path.length > 1) { + return validateFileName(path[1]); + } + + try { + if (aURI.host) { + // 7) Use the host. + return validateFileName(aURI.host); + } + } catch (e) { + // Some files have no information at all, like Javascript generated pages + } + try { + // 8) Use the default file name + return ContentAreaUtils.stringBundle.GetStringFromName( + "DefaultSaveFileName" + ); + } catch (e) { + // in case localized string cannot be found + } + // 9) If all else fails, use "index" + return "index"; +} + +function validateFileName(aFileName) { + let processed = DownloadPaths.sanitize(aFileName) || "_"; + if (AppConstants.platform == "android") { + // If a large part of the filename has been sanitized, then we + // will use a default filename instead + if (processed.replace(/_/g, "").length <= processed.length / 2) { + // We purposefully do not use a localized default filename, + // which we could have done using + // ContentAreaUtils.stringBundle.GetStringFromName("DefaultSaveFileName") + // since it may contain invalid characters. + var original = processed; + processed = "download"; + + // Preserve a suffix, if there is one + if (original.includes(".")) { + var suffix = original.split(".").slice(-1)[0]; + if (suffix && !suffix.includes("_")) { + processed += "." + suffix; + } + } + } + } + return processed; +} + +// This is the set of image extensions supported by extraMimeEntries in +// nsExternalHelperAppService. +const kImageExtensions = new Set([ + "art", + "bmp", + "gif", + "ico", + "cur", + "jpeg", + "jpg", + "jfif", + "pjpeg", + "pjp", + "png", + "apng", + "tiff", + "tif", + "xbm", + "svg", + "webp", + "avif", +]); + +function getNormalizedLeafName(aFile, aDefaultExtension) { + if (!aDefaultExtension) { + return aFile; + } + + if (AppConstants.platform == "win") { + // Remove trailing dots and spaces on windows + aFile = aFile.replace(/[\s.]+$/, ""); + } + + // Remove leading dots + aFile = aFile.replace(/^\.+/, ""); + + // Include the default extension in the file name to which we're saving. + var i = aFile.lastIndexOf("."); + let previousExtension = aFile.substr(i + 1).toLowerCase(); + if (previousExtension != aDefaultExtension.toLowerCase()) { + // Suffixing the extension is the safe bet - it preserves the previous + // extension in case that's meaningful (e.g. various text files served + // with text/plain will end up as `foo.cpp.txt`, in the worst case). + // However, for images, the extension derived from the URL should be + // replaced if the content type indicates a different filetype - this makes + // sure that we treat e.g. feature-tested webp/avif images correctly. + if (kImageExtensions.has(previousExtension)) { + return aFile.substr(0, i + 1) + aDefaultExtension; + } + return aFile + "." + aDefaultExtension; + } + + return aFile; +} + +function getDefaultExtension(aFilename, aURI, aContentType) { + if ( + aContentType == "text/plain" || + aContentType == "application/octet-stream" || + aURI.scheme == "ftp" + ) { + return ""; + } // temporary fix for bug 120327 + + // First try the extension from the filename + var url = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("http://example.com") // construct the URL + .setFilePath(aFilename) + .finalize() + .QueryInterface(Ci.nsIURL); + + var ext = url.fileExtension; + + // This mirrors some code in nsExternalHelperAppService::DoContent + // Use the filename first and then the URI if that fails + + // For images, rely solely on the mime type if known. + // All the extension is going to do is lie to us. + var lookupExt = ext; + if (aContentType?.startsWith("image/")) { + lookupExt = ""; + } + var mimeInfo = getMIMEInfoForType(aContentType, lookupExt); + + if (ext && mimeInfo && mimeInfo.extensionExists(ext)) { + return ext; + } + + // Well, that failed. Now try the extension from the URI + var urlext; + try { + url = aURI.QueryInterface(Ci.nsIURL); + urlext = url.fileExtension; + } catch (e) {} + + if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) { + return urlext; + } + + // That failed as well. If we could lookup the MIME use the primary + // extension for that type. + try { + if (mimeInfo) { + return mimeInfo.primaryExtension; + } + } catch (e) {} + + // Fall back on the extensions in the filename and URI for lack + // of anything better. + return ext || urlext; +} + +function GetSaveModeForContentType(aContentType, aDocument) { + // We can only save a complete page if we have a loaded document, + if (!aDocument) { + return SAVEMODE_FILEONLY; + } + + // Find the possible save modes using the provided content type + var saveMode = SAVEMODE_FILEONLY; + switch (aContentType) { + case "text/html": + case "application/xhtml+xml": + case "image/svg+xml": + saveMode |= SAVEMODE_COMPLETE_TEXT; + // Fall through + case "text/xml": + case "application/xml": + saveMode |= SAVEMODE_COMPLETE_DOM; + break; + } + + return saveMode; +} + +function getCharsetforSave(aDocument) { + if (aDocument) { + return aDocument.characterSet; + } + + if (document.commandDispatcher.focusedWindow) { + return document.commandDispatcher.focusedWindow.document.characterSet; + } + + return window.content.document.characterSet; +} + +/** + * Open a URL from chrome, determining if we can handle it internally or need to + * launch an external application to handle it. + * @param aURL The URL to be opened + * + * WARNING: Please note that openURL() does not perform any content security checks!!! + */ +function openURL(aURL) { + var uri = aURL instanceof Ci.nsIURI ? aURL : makeURI(aURL); + + var protocolSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + + let recentWindow = Services.wm.getMostRecentWindow("navigator:browser"); + + if (!protocolSvc.isExposedProtocol(uri.scheme)) { + // If we're not a browser, use the external protocol service to load the URI. + protocolSvc.loadURI(uri, recentWindow?.document.contentPrincipal); + } else { + if (recentWindow) { + recentWindow.openWebLinkIn(uri.spec, "tab", { + triggeringPrincipal: recentWindow.document.contentPrincipal, + }); + return; + } + + var loadgroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + var appstartup = Services.startup; + + var loadListener = { + onStartRequest: function ll_start(aRequest) { + appstartup.enterLastWindowClosingSurvivalArea(); + }, + onStopRequest: function ll_stop(aRequest, aStatusCode) { + appstartup.exitLastWindowClosingSurvivalArea(); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsISupportsWeakReference", + ]), + }; + loadgroup.groupObserver = loadListener; + + var uriListener = { + doContent(ctype, preferred, request, handler) { + return false; + }, + isPreferred(ctype, desired) { + return false; + }, + canHandleContent(ctype, preferred, desired) { + return false; + }, + loadCookie: null, + parentContentListener: null, + getInterface(iid) { + if (iid.equals(Ci.nsIURIContentListener)) { + return this; + } + if (iid.equals(Ci.nsILoadGroup)) { + return loadgroup; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + }; + + var channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + + if (channel) { + channel.channelIsForDownload = true; + } + + var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader); + uriLoader.openURI( + channel, + Ci.nsIURILoader.IS_CONTENT_PREFERRED, + uriListener + ); + } +} diff --git a/toolkit/content/customElements.js b/toolkit/content/customElements.js new file mode 100644 index 0000000000..4ac3148c6e --- /dev/null +++ b/toolkit/content/customElements.js @@ -0,0 +1,866 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file defines these globals on the window object. +// Define them here so that ESLint can find them: +/* globals MozXULElement, MozHTMLElement, MozElements */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +(() => { + // Handle customElements.js being loaded as a script in addition to the subscriptLoader + // from MainProcessSingleton, to handle pages that can open both before and after + // MainProcessSingleton starts. See Bug 1501845. + if (window.MozXULElement) { + return; + } + + const MozElements = {}; + window.MozElements = MozElements; + + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + const env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + const instrumentClasses = env.get("MOZ_INSTRUMENT_CUSTOM_ELEMENTS"); + const instrumentedClasses = instrumentClasses ? new Set() : null; + const instrumentedBaseClasses = instrumentClasses ? new WeakSet() : null; + + // If requested, wrap the normal customElements.define to give us a chance + // to modify the class so we can instrument function calls in local development: + if (instrumentClasses) { + let define = window.customElements.define; + window.customElements.define = function(name, c, opts) { + instrumentCustomElementClass(c); + return define.call(this, name, c, opts); + }; + window.addEventListener( + "load", + () => { + MozElements.printInstrumentation(true); + }, + { once: true, capture: true } + ); + } + + MozElements.printInstrumentation = function(collapsed) { + let summaries = []; + let totalCalls = 0; + let totalTime = 0; + for (let c of instrumentedClasses) { + // Allow passing in something like MOZ_INSTRUMENT_CUSTOM_ELEMENTS=MozXULElement,Button to filter + let includeClass = + instrumentClasses == 1 || + instrumentClasses + .split(",") + .some(n => c.name.toLowerCase().includes(n.toLowerCase())); + let summary = c.__instrumentation_summary; + if (includeClass && summary) { + summaries.push(summary); + totalCalls += summary.totalCalls; + totalTime += summary.totalTime; + } + } + if (summaries.length) { + let groupName = `Instrumentation data for custom elements in ${document.documentURI}`; + console[collapsed ? "groupCollapsed" : "group"](groupName); + console.log( + `Total function calls ${totalCalls} and total time spent inside ${totalTime.toFixed( + 2 + )}` + ); + for (let summary of summaries) { + console.log(`${summary.name} (# instances: ${summary.instances})`); + if (Object.keys(summary.data).length > 1) { + console.table(summary.data); + } + } + console.groupEnd(groupName); + } + }; + + function instrumentCustomElementClass(c) { + // Climb up prototype chain to see if we inherit from a MozElement. + // Keep track of classes to instrument, for example: + // MozMenuCaption->MozMenuBase->BaseText->BaseControl->MozXULElement + let inheritsFromBase = instrumentedBaseClasses.has(c); + let classesToInstrument = [c]; + let proto = Object.getPrototypeOf(c); + while (proto) { + classesToInstrument.push(proto); + if (instrumentedBaseClasses.has(proto)) { + inheritsFromBase = true; + break; + } + proto = Object.getPrototypeOf(proto); + } + + if (inheritsFromBase) { + for (let c of classesToInstrument.reverse()) { + instrumentIndividualClass(c); + } + } + } + + function instrumentIndividualClass(c) { + if (instrumentedClasses.has(c)) { + return; + } + + instrumentedClasses.add(c); + let data = { instances: 0 }; + + function wrapFunction(name, fn) { + return function() { + if (!data[name]) { + data[name] = { time: 0, calls: 0 }; + } + data[name].calls++; + let n = performance.now(); + let r = fn.apply(this, arguments); + data[name].time += performance.now() - n; + return r; + }; + } + function wrapPropertyDescriptor(obj, name) { + if (name == "constructor") { + return; + } + let prop = Object.getOwnPropertyDescriptor(obj, name); + if (prop.get) { + prop.get = wrapFunction(`<get> ${name}`, prop.get); + } + if (prop.set) { + prop.set = wrapFunction(`<set> ${name}`, prop.set); + } + if (prop.writable && prop.value && prop.value.apply) { + prop.value = wrapFunction(name, prop.value); + } + Object.defineProperty(obj, name, prop); + } + + // Handle static properties + for (let name of Object.getOwnPropertyNames(c)) { + wrapPropertyDescriptor(c, name); + } + + // Handle instance properties + for (let name of Object.getOwnPropertyNames(c.prototype)) { + wrapPropertyDescriptor(c.prototype, name); + } + + c.__instrumentation_data = data; + Object.defineProperty(c, "__instrumentation_summary", { + enumerable: false, + configurable: false, + get() { + if (data.instances == 0) { + return null; + } + + let clonedData = JSON.parse(JSON.stringify(data)); + delete clonedData.instances; + let totalCalls = 0; + let totalTime = 0; + for (let d in clonedData) { + let { time, calls } = clonedData[d]; + time = parseFloat(time.toFixed(2)); + totalCalls += calls; + totalTime += time; + clonedData[d]["time (ms)"] = time; + delete clonedData[d].time; + clonedData[d].timePerCall = parseFloat((time / calls).toFixed(4)); + } + + let timePerCall = parseFloat((totalTime / totalCalls).toFixed(4)); + totalTime = parseFloat(totalTime.toFixed(2)); + + // Add a spaced-out final row with summed up totals + clonedData["\ntotals"] = { + "time (ms)": `\n${totalTime}`, + calls: `\n${totalCalls}`, + timePerCall: `\n${timePerCall}`, + }; + return { + instances: data.instances, + data: clonedData, + name: c.name, + totalCalls, + totalTime, + }; + }, + }); + } + + // The listener of DOMContentLoaded must be set on window, rather than + // document, because the window can go away before the event is fired. + // In that case, we don't want to initialize anything, otherwise we + // may be leaking things because they will never be destroyed after. + let gIsDOMContentLoaded = false; + const gElementsPendingConnection = new Set(); + window.addEventListener( + "DOMContentLoaded", + () => { + gIsDOMContentLoaded = true; + for (let element of gElementsPendingConnection) { + try { + if (element.isConnected) { + element.isRunningDelayedConnectedCallback = true; + element.connectedCallback(); + } + } catch (ex) { + console.error(ex); + } + element.isRunningDelayedConnectedCallback = false; + } + gElementsPendingConnection.clear(); + }, + { once: true, capture: true } + ); + + const gXULDOMParser = new DOMParser(); + gXULDOMParser.forceEnableXULXBL(); + + MozElements.MozElementMixin = Base => { + let MozElementBase = class extends Base { + constructor() { + super(); + + if (instrumentClasses) { + let proto = this.constructor; + while (proto && proto != Base) { + proto.__instrumentation_data.instances++; + proto = Object.getPrototypeOf(proto); + } + } + } + /* + * A declarative way to wire up attribute inheritance and automatically generate + * the `observedAttributes` getter. For example, if you returned: + * { + * ".foo": "bar,baz=bat" + * } + * + * Then the base class will automatically return ["bar", "bat"] from `observedAttributes`, + * and set up an `attributeChangedCallback` to pass those attributes down onto an element + * matching the ".foo" selector. + * + * See the `inheritAttribute` function for more details on the attribute string format. + * + * @return {Object<string selector, string attributes>} + */ + static get inheritedAttributes() { + return null; + } + + static get flippedInheritedAttributes() { + // Have to be careful here, if a subclass overrides inheritedAttributes + // and its parent class is instantiated first, then reading + // this._flippedInheritedAttributes on the child class will return the + // computed value from the parent. We store it separately on each class + // to ensure everything works correctly when inheritedAttributes is + // overridden. + if (!this.hasOwnProperty("_flippedInheritedAttributes")) { + let { inheritedAttributes } = this; + if (!inheritedAttributes) { + this._flippedInheritedAttributes = null; + } else { + this._flippedInheritedAttributes = {}; + for (let selector in inheritedAttributes) { + let attrRules = inheritedAttributes[selector].split(","); + for (let attrRule of attrRules) { + let attrName = attrRule; + let attrNewName = attrRule; + let split = attrName.split("="); + if (split.length == 2) { + attrName = split[1]; + attrNewName = split[0]; + } + + if (!this._flippedInheritedAttributes[attrName]) { + this._flippedInheritedAttributes[attrName] = []; + } + this._flippedInheritedAttributes[attrName].push([ + selector, + attrNewName, + ]); + } + } + } + } + + return this._flippedInheritedAttributes; + } + /* + * Generate this array based on `inheritedAttributes`, if any. A class is free to override + * this if it needs to do something more complex or wants to opt out of this behavior. + */ + static get observedAttributes() { + return Object.keys(this.flippedInheritedAttributes || {}); + } + + /* + * Provide default lifecycle callback for attribute changes that will inherit attributes + * based on the static `inheritedAttributes` Object. This can be overridden by callers. + */ + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue === newValue || !this.initializedAttributeInheritance) { + return; + } + + let list = this.constructor.flippedInheritedAttributes[name]; + if (list) { + this.inheritAttribute(list, name); + } + } + + /* + * After setting content, calling this will cache the elements from selectors in the + * static `inheritedAttributes` Object. It'll also do an initial call to `this.inheritAttributes()`, + * so in the simple case, this is the only function you need to call. + * + * This should be called any time the children that are inheriting attributes changes. For instance, + * it's common in a connectedCallback to do something like: + * + * this.textContent = ""; + * this.append(MozXULElement.parseXULToFragment(`<label />`)) + * this.initializeAttributeInheritance(); + * + */ + initializeAttributeInheritance() { + let { flippedInheritedAttributes } = this.constructor; + if (!flippedInheritedAttributes) { + return; + } + + // Clear out any existing cached elements: + this._inheritedElements = null; + + this.initializedAttributeInheritance = true; + for (let attr in flippedInheritedAttributes) { + if (this.hasAttribute(attr)) { + this.inheritAttribute(flippedInheritedAttributes[attr], attr); + } + } + } + + /* + * Implements attribute value inheritance by child elements. + * + * @param {array} list + * An array of (to-element-selector, to-attr) pairs. + * @param {string} attr + * An attribute to propagate. + */ + inheritAttribute(list, attr) { + if (!this._inheritedElements) { + this._inheritedElements = {}; + } + + let hasAttr = this.hasAttribute(attr); + let attrValue = this.getAttribute(attr); + + for (let [selector, newAttr] of list) { + if (!(selector in this._inheritedElements)) { + this._inheritedElements[ + selector + ] = this.getElementForAttrInheritance(selector); + } + let el = this._inheritedElements[selector]; + if (el) { + if (newAttr == "text") { + el.textContent = hasAttr ? attrValue : ""; + } else if (hasAttr) { + el.setAttribute(newAttr, attrValue); + } else { + el.removeAttribute(newAttr); + } + } + } + } + + /** + * Used in setting up attribute inheritance. Takes a selector and returns + * an element for that selector from shadow DOM if there is a shadowRoot, + * or from the light DOM if not. + * + * Here's one problem this solves. ElementB extends ElementA which extends + * MozXULElement. ElementA has a shadowRoot. ElementB tries to inherit + * attributes in light DOM by calling `initializeAttributeInheritance` + * but that fails because it defaults to inheriting from the shadow DOM + * and not the light DOM. (See bug 1545824.) + * + * To solve this, ElementB can override `getElementForAttrInheritance` so + * it queries the light DOM for some selectors as needed. For example: + * + * class ElementA extends MozXULElement { + * static get inheritedAttributes() { + * return { ".one": "attr" }; + * } + * } + * + * class ElementB extends customElements.get("elementa") { + * static get inheritedAttributes() { + * return Object.assign({}, super.inheritedAttributes(), { + * ".two": "attr", + * }); + * } + * getElementForAttrInheritance(selector) { + * if (selector == ".two") { + * return this.querySelector(selector) + * } else { + * return super.getElementForAttrInheritance(selector); + * } + * } + * } + * + * @param {string} selector + * A selector used to query an element. + * + * @return {Element} The element found by the selector. + */ + getElementForAttrInheritance(selector) { + let parent = this.shadowRoot || this; + return parent.querySelector(selector); + } + + /** + * Sometimes an element may not want to run connectedCallback logic during + * parse. This could be because we don't want to initialize the element before + * the element's contents have been fully parsed, or for performance reasons. + * If you'd like to opt-in to this, then add this to the beginning of your + * `connectedCallback` and `disconnectedCallback`: + * + * if (this.delayConnectedCallback()) { return } + * + * And this at the beginning of your `attributeChangedCallback` + * + * if (!this.isConnectedAndReady) { return; } + */ + delayConnectedCallback() { + if (gIsDOMContentLoaded) { + return false; + } + gElementsPendingConnection.add(this); + return true; + } + + get isConnectedAndReady() { + return gIsDOMContentLoaded && this.isConnected; + } + + /** + * Passes DOM events to the on_<event type> methods. + */ + handleEvent(event) { + let methodName = "on_" + event.type; + if (methodName in this) { + this[methodName](event); + } else { + throw new Error("Unrecognized event: " + event.type); + } + } + + /** + * Used by custom elements for caching fragments. We now would be + * caching once per class while also supporting subclasses. + * + * If available, returns the cached fragment. + * Otherwise, creates it. + * + * Example: + * + * class ElementA extends MozXULElement { + * static get markup() { + * return `<hbox class="example"`; + * } + * + * static get entities() { + * // Optional field for parseXULToFragment + * return `["chrome://global/locale/notification.dtd"]`; + * } + * + * connectedCallback() { + * this.appendChild(this.constructor.fragment); + * } + * } + * + * @return {importedNode} The imported node that has not been + * inserted into document tree. + */ + static get fragment() { + if (!this.hasOwnProperty("_fragment")) { + let markup = this.markup; + if (markup) { + this._fragment = MozXULElement.parseXULToFragment( + markup, + this.entities + ); + } else { + throw new Error("Markup is null"); + } + } + return document.importNode(this._fragment, true); + } + + /** + * Allows eager deterministic construction of XUL elements with XBL attached, by + * parsing an element tree and returning a DOM fragment to be inserted in the + * document before any of the inner elements is referenced by JavaScript. + * + * This process is required instead of calling the createElement method directly + * because bindings get attached when: + * + * 1. the node gets a layout frame constructed, or + * 2. the node gets its JavaScript reflector created, if it's in the document, + * + * whichever happens first. The createElement method would return a JavaScript + * reflector, but the element wouldn't be in the document, so the node wouldn't + * get XBL attached. After that point, even if the node is inserted into a + * document, it won't get XBL attached until either the frame is constructed or + * the reflector is garbage collected and the element is touched again. + * + * @param {string} str + * String with the XML representation of XUL elements. + * @param {string[]} [entities] + * An array of DTD URLs containing entity definitions. + * + * @return {DocumentFragment} `DocumentFragment` instance containing + * the corresponding element tree, including element nodes + * but excluding any text node. + */ + static parseXULToFragment(str, entities = []) { + let doc = gXULDOMParser.parseFromSafeString( + ` + ${ + entities.length + ? `<!DOCTYPE bindings [ + ${entities.reduce((preamble, url, index) => { + return ( + preamble + + `<!ENTITY % _dtd-${index} SYSTEM "${url}"> + %_dtd-${index}; + ` + ); + }, "")} + ]>` + : "" + } + <box xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + ${str} + </box> + `, + "application/xml" + ); + + if (doc.documentElement.localName === "parsererror") { + throw new Error("not well-formed XML"); + } + + // The XUL/XBL parser is set to ignore all-whitespace nodes, whereas (X)HTML + // does not do this. Most XUL code assumes that the whitespace has been + // stripped out, so we simply remove all text nodes after using the parser. + let nodeIterator = doc.createNodeIterator(doc, NodeFilter.SHOW_TEXT); + let currentNode = nodeIterator.nextNode(); + while (currentNode) { + // Remove whitespace-only nodes. Regex is taken from: + // https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace_in_the_DOM + if (!/[^\t\n\r ]/.test(currentNode.textContent)) { + currentNode.remove(); + } + + currentNode = nodeIterator.nextNode(); + } + // We use a range here so that we don't access the inner DOM elements from + // JavaScript before they are imported and inserted into a document. + let range = doc.createRange(); + range.selectNodeContents(doc.querySelector("box")); + return range.extractContents(); + } + + /** + * Insert a localization link to an FTL file. This is used so that + * a Custom Element can wait to inject the link until it's connected, + * and so that consuming documents don't require the correct <link> + * present in the markup. + * + * @param path + * The path to the FTL file + */ + static insertFTLIfNeeded(path) { + let container = document.head || document.querySelector("linkset"); + if (!container) { + if ( + document.documentElement.namespaceURI === + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + ) { + container = document.createXULElement("linkset"); + document.documentElement.appendChild(container); + } else if (document.documentURI == AppConstants.BROWSER_CHROME_URL) { + // Special case for browser.xhtml. Here `document.head` is null, so + // just insert the link at the end of the window. + container = document.documentElement; + } else { + throw new Error( + "Attempt to inject localization link before document.head is available" + ); + } + } + + for (let link of container.querySelectorAll("link")) { + if (link.getAttribute("href") == path) { + return; + } + } + + let link = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "link" + ); + link.setAttribute("rel", "localization"); + link.setAttribute("href", path); + + container.appendChild(link); + } + + /** + * Indicate that a class defining a XUL element implements one or more + * XPCOM interfaces by adding a getCustomInterface implementation to it, + * as well as an implementation of QueryInterface. + * + * The supplied class should implement the properties and methods of + * all of the interfaces that are specified. + * + * @param cls + * The class that implements the interface. + * @param names + * Array of interface names. + */ + static implementCustomInterface(cls, ifaces) { + if (cls.prototype.customInterfaces) { + ifaces.push(...cls.prototype.customInterfaces); + } + cls.prototype.customInterfaces = ifaces; + + cls.prototype.QueryInterface = ChromeUtils.generateQI(ifaces); + cls.prototype.getCustomInterfaceCallback = function getCustomInterfaceCallback( + ifaceToCheck + ) { + if ( + cls.prototype.customInterfaces.some(iface => + iface.equals(ifaceToCheck) + ) + ) { + return getInterfaceProxy(this); + } + return null; + }; + } + }; + + // Rename the class so we can distinguish between MozXULElement and MozXULPopupElement, for example. + Object.defineProperty(MozElementBase, "name", { value: `Moz${Base.name}` }); + if (instrumentedBaseClasses) { + instrumentedBaseClasses.add(MozElementBase); + } + return MozElementBase; + }; + + const MozXULElement = MozElements.MozElementMixin(XULElement); + const MozHTMLElement = MozElements.MozElementMixin(HTMLElement); + + /** + * Given an object, add a proxy that reflects interface implementations + * onto the object itself. + */ + function getInterfaceProxy(obj) { + /* globals MozQueryInterface */ + if (!obj._customInterfaceProxy) { + obj._customInterfaceProxy = new Proxy(obj, { + get(target, prop, receiver) { + let propOrMethod = target[prop]; + if (typeof propOrMethod == "function") { + if (propOrMethod instanceof MozQueryInterface) { + return Reflect.get(target, prop, receiver); + } + return function(...args) { + return propOrMethod.apply(target, args); + }; + } + return propOrMethod; + }, + }); + } + + return obj._customInterfaceProxy; + } + + MozElements.BaseControlMixin = Base => { + class BaseControl extends Base { + get disabled() { + return this.getAttribute("disabled") == "true"; + } + + set disabled(val) { + if (val) { + this.setAttribute("disabled", "true"); + } else { + this.removeAttribute("disabled"); + } + } + + get tabIndex() { + return parseInt(this.getAttribute("tabindex")) || 0; + } + + set tabIndex(val) { + if (val) { + this.setAttribute("tabindex", val); + } else { + this.removeAttribute("tabindex"); + } + } + } + + MozXULElement.implementCustomInterface(BaseControl, [ + Ci.nsIDOMXULControlElement, + ]); + return BaseControl; + }; + MozElements.BaseControl = MozElements.BaseControlMixin(MozXULElement); + + const BaseTextMixin = Base => + class BaseText extends MozElements.BaseControlMixin(Base) { + set label(val) { + this.setAttribute("label", val); + return val; + } + + get label() { + return this.getAttribute("label"); + } + + set crop(val) { + this.setAttribute("crop", val); + return val; + } + + get crop() { + return this.getAttribute("crop"); + } + + set image(val) { + this.setAttribute("image", val); + return val; + } + + get image() { + return this.getAttribute("image"); + } + + set command(val) { + this.setAttribute("command", val); + return val; + } + + get command() { + return this.getAttribute("command"); + } + + set accessKey(val) { + // Always store on the control + this.setAttribute("accesskey", val); + // If there is a label, change the accesskey on the labelElement + // if it's also set there + if (this.labelElement) { + this.labelElement.accessKey = val; + } + return val; + } + + get accessKey() { + return this.labelElement + ? this.labelElement.accessKey + : this.getAttribute("accesskey"); + } + }; + MozElements.BaseTextMixin = BaseTextMixin; + MozElements.BaseText = BaseTextMixin(MozXULElement); + + // Attach the base class to the window so other scripts can use it: + window.MozXULElement = MozXULElement; + window.MozHTMLElement = MozHTMLElement; + + customElements.setElementCreationCallback("browser", () => { + Services.scriptloader.loadSubScript( + "chrome://global/content/elements/browser-custom-element.js", + window + ); + }); + + // Skip loading any extra custom elements in the extension dummy document + // and GeckoView windows. + const loadExtraCustomElements = !( + document.documentURI == "chrome://extensions/content/dummy.xhtml" || + document.documentURI == "chrome://geckoview/content/geckoview.xhtml" + ); + if (loadExtraCustomElements) { + for (let script of [ + "chrome://global/content/elements/arrowscrollbox.js", + "chrome://global/content/elements/dialog.js", + "chrome://global/content/elements/general.js", + "chrome://global/content/elements/button.js", + "chrome://global/content/elements/checkbox.js", + "chrome://global/content/elements/menu.js", + "chrome://global/content/elements/menupopup.js", + "chrome://global/content/elements/moz-input-box.js", + "chrome://global/content/elements/notificationbox.js", + "chrome://global/content/elements/panel.js", + "chrome://global/content/elements/popupnotification.js", + "chrome://global/content/elements/radio.js", + "chrome://global/content/elements/richlistbox.js", + "chrome://global/content/elements/autocomplete-popup.js", + "chrome://global/content/elements/autocomplete-richlistitem.js", + "chrome://global/content/elements/tabbox.js", + "chrome://global/content/elements/text.js", + "chrome://global/content/elements/toolbarbutton.js", + "chrome://global/content/elements/tree.js", + "chrome://global/content/elements/wizard.js", + ]) { + Services.scriptloader.loadSubScript(script, window); + } + + for (let [tag, script] of [ + ["findbar", "chrome://global/content/elements/findbar.js"], + ["menulist", "chrome://global/content/elements/menulist.js"], + ["search-textbox", "chrome://global/content/elements/search-textbox.js"], + ["stringbundle", "chrome://global/content/elements/stringbundle.js"], + [ + "printpreview-toolbar", + "chrome://global/content/printPreviewToolbar.js", + ], + [ + "printpreview-pagination", + "chrome://global/content/printPreviewPagination.js", + ], + [ + "autocomplete-input", + "chrome://global/content/elements/autocomplete-input.js", + ], + ["editor", "chrome://global/content/elements/editor.js"], + ]) { + customElements.setElementCreationCallback(tag, () => { + Services.scriptloader.loadSubScript(script, window); + }); + } + } +})(); diff --git a/toolkit/content/datepicker.xhtml b/toolkit/content/datepicker.xhtml new file mode 100644 index 0000000000..d72551e559 --- /dev/null +++ b/toolkit/content/datepicker.xhtml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> + <title>Date Picker</title> + <link rel="stylesheet" href="chrome://global/skin/datetimeinputpickers.css"/> + <script src="chrome://global/content/bindings/datekeeper.js"></script> + <script src="chrome://global/content/bindings/spinner.js"></script> + <script src="chrome://global/content/bindings/calendar.js"></script> + <script src="chrome://global/content/bindings/datepicker.js"></script> +</head> +<body> + <div id="date-picker"> + <div class="calendar-container"> + <div class="nav"> + <button class="prev"/> + <button class="next"/> + </div> + <div class="week-header"></div> + <div class="days-viewport"> + <div class="days-view"></div> + </div> + </div> + <div class="month-year-container"> + <button class="month-year"/> + </div> + <div class="month-year-view"></div> + </div> + <template id="spinner-template"> + <div class="spinner-container"> + <button class="up"/> + <div class="spinner"></div> + <button class="down"/> + </div> + </template> + <script> + /* import-globals-from widgets/datepicker.js */ + // Create a DatePicker instance and prepare to be + // initialized by the "DatePickerInit" event from datetimepopup.xml + const root = document.getElementById("date-picker"); + new DatePicker({ + monthYear: root.querySelector(".month-year"), + monthYearView: root.querySelector(".month-year-view"), + buttonPrev: root.querySelector(".prev"), + buttonNext: root.querySelector(".next"), + weekHeader: root.querySelector(".week-header"), + daysView: root.querySelector(".days-view"), + }); + </script> +</body> +</html> diff --git a/toolkit/content/editMenuKeys.inc.xhtml b/toolkit/content/editMenuKeys.inc.xhtml new file mode 100644 index 0000000000..0def1c270d --- /dev/null +++ b/toolkit/content/editMenuKeys.inc.xhtml @@ -0,0 +1,29 @@ +# 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/. + + <!-- These key nodes are here only for show. The real bindings come from + XBL, in platformHTMLBindings.xml. See bugs 57078 and 71779. --> + + <keyset id="editMenuKeys"> + <key id="key_undo" data-l10n-id="text-action-undo-shortcut" modifiers="accel" command="cmd_undo"/> + <key id="key_redo" +#ifdef XP_UNIX + data-l10n-id="text-action-undo-shortcut" + modifiers="accel,shift" +#else + data-l10n-id="text-action-redo-shortcut" + modifiers="accel" +#endif + command="cmd_redo"/> + <key id="key_cut" data-l10n-id="text-action-cut-shortcut" modifiers="accel" command="cmd_cut"/> + <key id="key_copy" data-l10n-id="text-action-copy-shortcut" modifiers="accel" command="cmd_copy"/> + <key id="key_paste" data-l10n-id="text-action-paste-shortcut" modifiers="accel" command="cmd_paste"/> + <key id="key_delete" keycode="VK_DELETE" command="cmd_delete"/> + <key id="key_selectAll" data-l10n-id="text-action-select-all-shortcut" modifiers="accel" command="cmd_selectAll"/> + <key id="key_find" key="&findCmd.key;" modifiers="accel" command="cmd_find"/> + <key id="key_findAgain" key="&findAgainCmd.key;" modifiers="accel" command="cmd_findAgain"/> + <key id="key_findPrevious" key="&findAgainCmd.key;" modifiers="shift,accel" command="cmd_findPrevious"/> + <key id="key_findAgain2" keycode="&findAgainCmd.key2;" command="cmd_findAgain"/> + <key id="key_findPrevious2" keycode="&findAgainCmd.key2;" modifiers="shift" command="cmd_findPrevious"/> + </keyset> diff --git a/toolkit/content/editMenuOverlay.js b/toolkit/content/editMenuOverlay.js new file mode 100644 index 0000000000..49e0da9543 --- /dev/null +++ b/toolkit/content/editMenuOverlay.js @@ -0,0 +1,135 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* 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/. */ + +// update menu items that rely on focus or on the current selection +function goUpdateGlobalEditMenuItems(force) { + // Don't bother updating the edit commands if they aren't visible in any way + // (i.e. the Edit menu isn't open, nor is the context menu open, nor have the + // cut, copy, and paste buttons been added to the toolbars) for performance. + // This only works in applications/on platforms that set the gEditUIVisible + // flag, so we check to see if that flag is defined before using it. + if (!force && typeof gEditUIVisible != "undefined" && !gEditUIVisible) { + return; + } + + goUpdateCommand("cmd_undo"); + goUpdateCommand("cmd_redo"); + goUpdateCommand("cmd_cut"); + goUpdateCommand("cmd_copy"); + goUpdateCommand("cmd_paste"); + goUpdateCommand("cmd_selectAll"); + goUpdateCommand("cmd_delete"); + goUpdateCommand("cmd_switchTextDirection"); +} + +// update menu items that relate to undo/redo +function goUpdateUndoEditMenuItems() { + goUpdateCommand("cmd_undo"); + goUpdateCommand("cmd_redo"); +} + +// update menu items that depend on clipboard contents +function goUpdatePasteMenuItems() { + goUpdateCommand("cmd_paste"); +} + +// Inject the commandset here instead of relying on preprocessor to share this across documents. +window.addEventListener( + "DOMContentLoaded", + () => { + // Bug 371900: Remove useless oncommand attribute once bug 371900 is fixed + // If you remove/update the oncommand attribute for any of the cmd_*, please + // also remove/update the sha512 hash in the CSP within about:downloads + let container = + document.querySelector("commandset") || document.documentElement; + let fragment = MozXULElement.parseXULToFragment(` + <commandset id="editMenuCommands"> + <commandset id="editMenuCommandSetAll" commandupdater="true" events="focus,select" /> + <commandset id="editMenuCommandSetUndo" commandupdater="true" events="undo" /> + <commandset id="editMenuCommandSetPaste" commandupdater="true" events="clipboard" /> + <command id="cmd_undo" oncommand=";" /> + <command id="cmd_redo" oncommand=";" /> + <command id="cmd_cut" oncommand=";" /> + <command id="cmd_copy" oncommand=";" /> + <command id="cmd_paste" oncommand=";" /> + <command id="cmd_delete" oncommand=";" /> + <command id="cmd_selectAll" oncommand=";" /> + <command id="cmd_switchTextDirection" oncommand=";" /> + </commandset> + `); + + let editMenuCommandSetAll = fragment.querySelector( + "#editMenuCommandSetAll" + ); + editMenuCommandSetAll.addEventListener("commandupdate", function() { + goUpdateGlobalEditMenuItems(); + }); + + let editMenuCommandSetUndo = fragment.querySelector( + "#editMenuCommandSetUndo" + ); + editMenuCommandSetUndo.addEventListener("commandupdate", function() { + goUpdateUndoEditMenuItems(); + }); + + let editMenuCommandSetPaste = fragment.querySelector( + "#editMenuCommandSetPaste" + ); + editMenuCommandSetPaste.addEventListener("commandupdate", function() { + goUpdatePasteMenuItems(); + }); + + fragment.firstElementChild.addEventListener("command", event => { + let commandID = event.target.id; + goDoCommand(commandID); + }); + + container.appendChild(fragment); + }, + { once: true } +); + +// Support context menus on html textareas in the parent process: +window.addEventListener("contextmenu", e => { + const HTML_NS = "http://www.w3.org/1999/xhtml"; + let needsContextMenu = + e.composedTarget.ownerDocument == document && + !e.defaultPrevented && + e.composedTarget.parentNode.nodeName != "moz-input-box" && + ((["textarea", "input"].includes(e.composedTarget.localName) && + e.composedTarget.namespaceURI == HTML_NS) || + e.composedTarget.closest("search-textbox")); + + if (!needsContextMenu) { + return; + } + + let popup = document.getElementById("textbox-contextmenu"); + if (!popup) { + MozXULElement.insertFTLIfNeeded("toolkit/global/textActions.ftl"); + document.documentElement.appendChild( + MozXULElement.parseXULToFragment(` + <menupopup id="textbox-contextmenu" class="textbox-contextmenu"> + <menuitem data-l10n-id="text-action-undo" command="cmd_undo"></menuitem> + <menuseparator></menuseparator> + <menuitem data-l10n-id="text-action-cut" command="cmd_cut"></menuitem> + <menuitem data-l10n-id="text-action-copy" command="cmd_copy"></menuitem> + <menuitem data-l10n-id="text-action-paste" command="cmd_paste"></menuitem> + <menuitem data-l10n-id="text-action-delete" command="cmd_delete"></menuitem> + <menuseparator></menuseparator> + <menuitem data-l10n-id="text-action-select-all" command="cmd_selectAll"></menuitem> + </menupopup> + `) + ); + popup = document.documentElement.lastElementChild; + } + + goUpdateGlobalEditMenuItems(true); + popup.openPopupAtScreen(e.screenX, e.screenY, true); + // Don't show any other context menu at the same time. There can be a + // context menu from an ancestor too but we only want to show this one. + e.preventDefault(); +}); diff --git a/toolkit/content/filepicker.properties b/toolkit/content/filepicker.properties new file mode 100644 index 0000000000..27b8f4a8fc --- /dev/null +++ b/toolkit/content/filepicker.properties @@ -0,0 +1,12 @@ +# 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/. + +allFilter=* +htmlFilter=*.html; *.htm; *.shtml; *.xhtml +textFilter=*.txt; *.text +imageFilter=*.jpe; *.jpg; *.jpeg; *.gif; *.png; *.bmp; *.ico; *.svg; *.svgz; *.tif; *.tiff; *.ai; *.drw; *.pct; *.psp; *.xcf; *.psd; *.raw; *.webp +xmlFilter=*.xml +xulFilter=*.xul +audioFilter=*.aac; *.aif; *.flac; *.iff; *.m4a; *.m4b; *.mid; *.midi; *.mp3; *.mpa; *.mpc; *.oga; *.ogg; *.opus; *.ra; *.ram; *.snd; *.wav; *.wma +videoFilter=*.avi; *.divx; *.flv; *.m4v; *.mkv; *.mov; *.mp4; *.mpeg; *.mpg; *.ogm; *.ogv; *.ogx; *.rm; *.rmvb; *.smil; *.webm; *.wmv; *.xvid diff --git a/toolkit/content/globalOverlay.js b/toolkit/content/globalOverlay.js new file mode 100644 index 0000000000..2cc0804e7b --- /dev/null +++ b/toolkit/content/globalOverlay.js @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function closeWindow(aClose, aPromptFunction) { + let { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + // Closing the last window doesn't quit the application on OS X. + if (AppConstants.platform != "macosx") { + var windowCount = 0; + for (let w of Services.wm.getEnumerator(null)) { + if (w.closed) { + continue; + } + if (++windowCount == 2) { + break; + } + } + + // If we're down to the last window and someone tries to shut down, check to make sure we can! + if (windowCount == 1 && !canQuitApplication("lastwindow")) { + return false; + } + if ( + windowCount != 1 && + typeof aPromptFunction == "function" && + !aPromptFunction() + ) { + return false; + } + } else if (typeof aPromptFunction == "function" && !aPromptFunction()) { + return false; + } + + if (aClose) { + window.close(); + return window.closed; + } + + return true; +} + +function canQuitApplication(aData) { + try { + var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + aData || null + ); + + // Something aborted the quit process. + if (cancelQuit.data) { + return false; + } + } catch (ex) {} + return true; +} + +function goQuitApplication() { + if (!canQuitApplication()) { + return false; + } + + Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit); + return true; +} + +// +// Command Updater functions +// +function goUpdateCommand(aCommand) { + try { + var controller = top.document.commandDispatcher.getControllerForCommand( + aCommand + ); + + var enabled = false; + if (controller) { + enabled = controller.isCommandEnabled(aCommand); + } + + goSetCommandEnabled(aCommand, enabled); + } catch (e) { + Cu.reportError( + "An error occurred updating the " + aCommand + " command: " + e + ); + } +} + +function goDoCommand(aCommand) { + try { + var controller = top.document.commandDispatcher.getControllerForCommand( + aCommand + ); + if (controller && controller.isCommandEnabled(aCommand)) { + controller.doCommand(aCommand); + } + } catch (e) { + Cu.reportError( + "An error occurred executing the " + aCommand + " command: " + e + ); + } +} + +function goSetCommandEnabled(aID, aEnabled) { + var node = document.getElementById(aID); + + if (node) { + if (aEnabled) { + node.removeAttribute("disabled"); + } else { + node.setAttribute("disabled", "true"); + } + } +} diff --git a/toolkit/content/gmp-sources/openh264.json b/toolkit/content/gmp-sources/openh264.json new file mode 100644 index 0000000000..caf3379e58 --- /dev/null +++ b/toolkit/content/gmp-sources/openh264.json @@ -0,0 +1,71 @@ +{ + "vendors": { + "gmp-gmpopenh264": { + "platforms": { + "Android_aarch64-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-android-aarch64-42954cf0fe8a2bdc97fdc180462a3eaefceb035f.zip", + "filesize": 557884, + "hashValue": "307d188876f3612a9168c0b4ed191db2132f2e3193bdd3024ce50adcb9c1e085ab43008531a25e93d570a377283336cda9bcd7609ee6b702c5292f12d20b616b" + }, + "Android_arm-eabi-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-android-arm-42954cf0fe8a2bdc97fdc180462a3eaefceb035f.zip", + "filesize": 539311, + "hashValue": "f4f0bfe333b7e0cd0453e787dc3c15bebe9cc771cb3e57540d53f0ac9a37eee4ea8559a45a51824ee4d706ee0b3d80b2d331468a0aa533cd958081f23ee0aaae" + }, + "Android_x86-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-android-x86-42954cf0fe8a2bdc97fdc180462a3eaefceb035f.zip", + "filesize": 589947, + "hashValue": "eb7a1c9c2d29a2fd12dfe82d0f575f1d855478640816a7fb9402ce82c65878ffc5aa3d5f8bb46cd01231005c37d86984d7a631cfe45c7d56a6d4dabc427b15a0" + }, + "Darwin_x86_64-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-macosx64-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", + "filesize": 466258, + "hashValue": "fc1ddb4b7cff2f27a0f10d033850211d9186ae7576194e1387974e8f7a8c350ef50e12238694003f16ed5917ffcd22ae0d54cfb738b9724440d65f8afdf7a49f" + }, + "Linux_x86-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-linux32-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", + "filesize": 527704, + "hashValue": "903aecd631624db3047fc477363ac076794517bbc72b33a88a73627066b5997d9c1194975729ef2acbabba19e93574333b54e32763a5a834b8d9431b99181fd1" + }, + "Linux_x86_64-gcc3": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-linux64-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", + "filesize": 511815, + "hashValue": "94531e267314de661b2205c606283fb066d781e5c11027578f2a3c3aa353437c2289544074a28101b6b6f0179f0fe6bd890a0ae2bb6e1cf9053650472576366c" + }, + "WINNT_aarch64-msvc-aarch64": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-win64-aarch64-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", + "filesize": 558607, + "hashValue": "8d936bca08dcf3538c5c118c0f468d672c556ac2ac828a4b9d1fcbb4339885d17ebcc748a918457abbea87d21c5cab2c007ca5b4ef87f04a52d44f42ee5fdbb9" + }, + "WINNT_x86-msvc": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-win32-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", + "filesize": 491261, + "hashValue": "9ed5b4c27c2c159b83a1b887a1215d0472171cff422d2bc1962312f90e62d1b212955fe68bc88f826d613c9fb58b86f6fa16ebc1533e863f6a5648dcb1319bcb" + }, + "WINNT_x86-msvc-x64": { + "alias": "WINNT_x86-msvc" + }, + "WINNT_x86-msvc-x86": { + "alias": "WINNT_x86-msvc" + }, + "WINNT_x86_64-msvc": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-win64-2e1774ab6dc6c43debb0b5b628bdf122a391d521.zip", + "filesize": 453023, + "hashValue": "06511f1f6c6d44d076b3c593528c26a602348d9c41689dbf5ff716b671c3ca5756b12cb2e5869f836dedce27b1a5cfe79b93c707fd01f8e84b620923bb61b5f1" + }, + "WINNT_x86_64-msvc-x64": { + "alias": "WINNT_x86_64-msvc" + }, + "android-x86_64": { + "fileUrl": "http://ciscobinary.openh264.org/openh264-android-x86_64-42954cf0fe8a2bdc97fdc180462a3eaefceb035f.zip", + "filesize": 539311, + "hashValue": "2c80df83c84841477cf5489e4109a0913cf3ca801063d788e100a511c9226d46059e4d28ea76496c3208c046cc44c5ce0b5263b1bfda5b731f8461ce8ce7d1b7" + } + }, + "version": "1.8.1.1" + } + }, + "hashFunction": "sha512", + "name": "OpenH264-1.8.1.1", + "schema_version": 1000 +} diff --git a/toolkit/content/gmp-sources/widevinecdm.json b/toolkit/content/gmp-sources/widevinecdm.json new file mode 100644 index 0000000000..a870f1f7af --- /dev/null +++ b/toolkit/content/gmp-sources/widevinecdm.json @@ -0,0 +1,55 @@ +{ + "vendors": { + "gmp-widevinecdm": { + "platforms": { + "WINNT_x86-msvc-x64": { + "alias": "WINNT_x86-msvc" + }, + "WINNT_x86-msvc": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/4.10.1582.2-win-ia32.zip", + "hashValue": "d448fa194996c3154b7a47a4447331125fab9c3d6d52a08a640ab21677535102117a0262798bc95ba75310060fe0517d7b4593245ad48da683956455417502cb", + "filesize": 5197739 + }, + "WINNT_x86-msvc-x86": { + "alias": "WINNT_x86-msvc" + }, + "Linux_x86_64-gcc3": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/4.10.1582.2-linux-x64.zip", + "hashValue": "e750e055ec563f2411ef84bf359037ff523f8db3c5143b42af2478a44e9b1ca9b135707d6dcacab178df355927c68c8eb43bdd63456e22c6d787fc86dd668aa2", + "filesize": 4000517 + }, + "Linux_x86_64-gcc3-asan": { + "alias": "Linux_x86_64-gcc3" + }, + "Darwin_x86_64-gcc3-u-i386-x86_64": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/4.10.1582.2-mac-x64.zip", + "hashValue": "9ad5288a8c6488dc46ff4232beb440ad533c4c8e52d943673959d7a949f9cd68c09c45b1d23a1cf6e43241429e398d46873e94ef55b55c36e3d1da3942433837", + "filesize": 4270381 + }, + "Darwin_x86_64-gcc3": { + "alias": "Darwin_x86_64-gcc3-u-i386-x86_64" + }, + "Linux_x86-gcc3": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/4.10.1582.2-linux-ia32.zip", + "hashValue": "feb9cfddf2b805122e450a3ee8c8843e2d46f868f5d10169ad5e4ce49b1e6736e0d11f11ffe5a74f3184eab8317608a80c7128e59bdfa0a14319f89ab830b39e", + "filesize": 4289036 + }, + "WINNT_aarch64-msvc-aarch64": { + "alias": "WINNT_x86-msvc" + }, + "WINNT_x86_64-msvc-x64": { + "alias": "WINNT_x86_64-msvc" + }, + "WINNT_x86_64-msvc": { + "fileUrl": "https://redirector.gvt1.com/edgedl/widevine-cdm/4.10.1582.2-win-x64.zip", + "hashValue": "8054294ad328f0b32e7474448e77fb4c56dbe4e7100920461508dd18cab94f69b13dbc1e9776800f49993d478827b4a65732a9bc39a3940251d9cb96ffffddd9", + "filesize": 5097580 + } + }, + "version": "4.10.1582.2" + } + }, + "hashFunction": "sha512", + "name": "Widevine-4.10.1582.2", + "schema_version": 1000 +}
\ No newline at end of file diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn new file mode 100644 index 0000000000..c83b3e2382 --- /dev/null +++ b/toolkit/content/jar.mn @@ -0,0 +1,114 @@ +toolkit.jar: +% content global %content/global/ contentaccessible=yes +* content/global/license.html + content/global/minimal-xul.css +* content/global/xul.css + content/global/autocomplete.css + content/global/aboutAbout.js + content/global/aboutAbout.html +#ifdef MOZILLA_OFFICIAL + content/global/aboutRights.xhtml +#else + content/global/aboutRights.xhtml (aboutRights-unbranded.xhtml) +#endif + content/global/aboutNetworking.js + content/global/aboutNetworking.html +#ifndef ANDROID + content/global/aboutProfiles.js + content/global/aboutProfiles.xhtml +#endif + content/global/aboutRights.js + content/global/aboutServiceWorkers.js + content/global/aboutServiceWorkers.xhtml + content/global/aboutwebrtc/aboutWebrtc.css (aboutwebrtc/aboutWebrtc.css) + content/global/aboutwebrtc/aboutWebrtc.js (aboutwebrtc/aboutWebrtc.js) + content/global/aboutwebrtc/aboutWebrtc.html (aboutwebrtc/aboutWebrtc.html) + content/global/aboutSupport.js +* content/global/aboutSupport.xhtml +#ifdef MOZ_GLEAN + content/global/aboutGlean.js + content/global/aboutGlean.html + content/global/aboutGlean.css +#endif + content/global/aboutTelemetry.js + content/global/aboutTelemetry.xhtml + content/global/aboutTelemetry.css + content/global/aboutUrlClassifier.js + content/global/aboutUrlClassifier.xhtml + content/global/aboutUrlClassifier.css + content/global/plugins.html + content/global/plugins.css + content/global/plugins.js + content/global/browser-child.js +* content/global/buildconfig.html + content/global/buildconfig.css + content/global/contentAreaUtils.js + content/global/datepicker.xhtml +#ifndef MOZ_FENNEC + content/global/editMenuOverlay.js +#endif + content/global/filepicker.properties + content/global/customElements.js + content/global/globalOverlay.js + content/global/mozilla.html + content/global/aboutMozilla.css + content/global/preferencesBindings.js + content/global/process-content.js + content/global/resetProfile.css + content/global/resetProfile.js + content/global/resetProfile.xhtml + content/global/resetProfileProgress.xhtml + content/global/TopLevelVideoDocument.js + content/global/timepicker.xhtml + content/global/treeUtils.js +#ifndef MOZ_FENNEC + content/global/viewZoomOverlay.js +#endif + content/global/widgets.css + content/global/bindings/calendar.js (widgets/calendar.js) + content/global/bindings/datekeeper.js (widgets/datekeeper.js) + content/global/bindings/datepicker.js (widgets/datepicker.js) + content/global/bindings/datetimebox.css (widgets/datetimebox.css) + content/global/bindings/spinner.js (widgets/spinner.js) + content/global/bindings/timekeeper.js (widgets/timekeeper.js) + content/global/bindings/timepicker.js (widgets/timepicker.js) + content/global/elements/autocomplete-input.js (widgets/autocomplete-input.js) + content/global/elements/autocomplete-popup.js (widgets/autocomplete-popup.js) + content/global/elements/autocomplete-richlistitem.js (widgets/autocomplete-richlistitem.js) + content/global/elements/browser-custom-element.js (widgets/browser-custom-element.js) + content/global/elements/button.js (widgets/button.js) + content/global/elements/checkbox.js (widgets/checkbox.js) + content/global/elements/datetimebox.js (widgets/datetimebox.js) + content/global/elements/dialog.js (widgets/dialog.js) + content/global/elements/findbar.js (widgets/findbar.js) + content/global/elements/editor.js (widgets/editor.js) + content/global/elements/general.js (widgets/general.js) + content/global/elements/menu.js (widgets/menu.js) + content/global/elements/menupopup.js (widgets/menupopup.js) + content/global/elements/moz-input-box.js (widgets/moz-input-box.js) + content/global/elements/notificationbox.js (widgets/notificationbox.js) + content/global/elements/panel.js (widgets/panel.js) + content/global/elements/pluginProblem.js (widgets/pluginProblem.js) + content/global/elements/radio.js (widgets/radio.js) + content/global/elements/richlistbox.js (widgets/richlistbox.js) + content/global/elements/marquee.css (widgets/marquee.css) + content/global/elements/marquee.js (widgets/marquee.js) + content/global/elements/menulist.js (widgets/menulist.js) + content/global/elements/popupnotification.js (widgets/popupnotification.js) + content/global/elements/arrowscrollbox.js (widgets/arrowscrollbox.js) + content/global/elements/search-textbox.js (widgets/search-textbox.js) + content/global/elements/stringbundle.js (widgets/stringbundle.js) + content/global/elements/tabbox.js (widgets/tabbox.js) + content/global/elements/text.js (widgets/text.js) + content/global/elements/toolbarbutton.js (widgets/toolbarbutton.js) + content/global/elements/videocontrols.js (widgets/videocontrols.js) + content/global/elements/tree.js (widgets/tree.js) + content/global/elements/wizard.js (widgets/wizard.js) +#ifdef XP_MACOSX + content/global/macWindowMenu.js +#endif + content/global/gmp-sources/openh264.json (gmp-sources/openh264.json) + content/global/gmp-sources/widevinecdm.json (gmp-sources/widevinecdm.json) + +# Third party files + content/global/third_party/d3/d3.js (/third_party/js/d3/d3.js) diff --git a/toolkit/content/license.html b/toolkit/content/license.html new file mode 100644 index 0000000000..1913fbbd88 --- /dev/null +++ b/toolkit/content/license.html @@ -0,0 +1,7624 @@ +<!DOCTYPE HTML> +<!-- 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/. --> + +<html lang="en"> + <head> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src chrome:; img-src chrome:; object-src 'none'" /> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> + <title>Licenses</title> + <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css"> + <link rel="stylesheet" href="chrome://global/skin/aboutLicense.css" type="text/css"/> + </head> + + <body id="lic-info"> + <div class="license-header"> + <div> + <h1><a id="top"></a>Licenses</h1> +#ifdef APP_LICENSE_BLOCK +#includesubst @APP_LICENSE_BLOCK@ +#endif + </div> + </div> + <div> + <p>All of the <b>source code</b> to this product is + available under licenses which are both + <a href="https://www.gnu.org/philosophy/free-sw.html">free</a> and + <a href="https://www.opensource.org/docs/definition.php">open source</a>. + A URL identifying the specific source code used to create this copy can be found + on the <a href="about:buildconfig">build configuration page</a>, and you can read + <a href="https://developer.mozilla.org/en/Mozilla_Source_Code_%28Mercurial%29">instructions + on how to download and build the code for yourself</a>. + </p> + + <p>More specifically, most of the source code is available under the + <a href="about:license#mpl">Mozilla Public License 2.0</a> (MPL). + The MPL has a + <a href="https://www.mozilla.org/MPL/2.0/FAQ/">FAQ</a> to help + you understand it. The remainder of the software which is not + under the MPL is available under one of a variety of other + free and open source licenses. Those that require reproduction + of the license text in the distribution are given below. + (Note: your copy of this product may not contain code covered by one + or more of the licenses listed here, depending on the exact product + and version you choose.) + </p> + + <ul> + <li><a href="about:license#mpl">Mozilla Public License 2.0</a> + <br><br> + </li> + <li><a href="about:license#lgpl">GNU Lesser General Public License 2.1</a> + <br><br> + </li> + <li><a href="about:license#lgpl-3.0">GNU Lesser General Public License 3.0</a> + <br><br> + </li> + <li><a href="about:license#gpl-3.0">GNU General Public License 3.0</a> + <br><br> + </li> + <li><a href="about:license#ACE">ACE License</a></li> + <li><a href="about:license#acorn">acorn License</a></li> +#ifdef MOZ_INSTALL_TRACKING + <li><a href="about:license#adjust">Adjust SDK License</a></li> +#endif + <li><a href="about:license#android">Android Open Source License</a></li> + <li><a href="about:license#angle">ANGLE License</a></li> + <li><a href="about:license#apache">Apache License 2.0</a></li> + <li><a href="about:license#apache-llvm">Apache License 2.0 with LLVM exception</a></li> + <li><a href="about:license#apple">Apple License</a></li> + <li><a href="about:license#apple-mozilla">Apple/Mozilla NPRuntime License</a></li> + <li><a href="about:license#arm">ARM License</a></li> + <li><a href="about:license#babel">Babel License</a></li> + <li><a href="about:license#babylon">Babylon License</a></li> + <li><a href="about:license#bincode">bincode License</a></li> + <li><a href="about:license#bsd2clause">BSD 2-Clause License</a></li> + <li><a href="about:license#bsd3clause">BSD 3-Clause License</a></li> + <li><a href="about:license#bspatch">bspatch License</a></li> + <li><a href="about:license#byteorder">byteorder License</a></li> + <li><a href="about:license#cairo">Cairo Component Licenses</a></li> + <li><a href="about:license#chromium">Chromium License</a></li> + <li><a href="about:license#codemirror">CodeMirror License</a></li> + <li><a href="about:license#cryptogams">CRYPTOGAMS License</a></li> + <li><a href="about:license#cubic-bezier">cubic-bezier License</a></li> + <li><a href="about:license#d3">D3 License</a></li> + <li><a href="about:license#dagre-d3">Dagre-D3 License</a></li> + <li><a href="about:license#diff">diff License</a></li> + <li><a href="about:license#dtoa">dtoa License</a></li> + <li><a href="about:license#ECCKiila">ECCKiila License</a></li> + <li><a href="about:license#Fiat-Crypto">Fiat-Crypto License</a></li> + <li><a href="about:license#fuzz-aldrin">fuzz-aldrin License</a></li> + <li><a href="about:license#hunspell-nl">Dutch Spellchecking Dictionary License</a></li> +#if defined(XP_WIN) || defined(XP_LINUX) + <li><a href="about:license#twemoji">Twemoji License</a></li> +#endif + <li><a href="about:license#hunspell-ee">Estonian Spellchecking Dictionary License</a></li> + <li><a href="about:license#emitter">Emitter License</a></li> + <li><a href="about:license#expat">Expat License</a></li> + <li><a href="about:license#firebug">Firebug License</a></li> + <li><a href="about:license#gdi32-sys">gdi32-sys License</a></li> + <li><a href="about:license#gfx-font-list">gfxFontList License</a></li> + <li><a href="about:license#google-bsd">Google BSD License</a></li> + <li><a href="about:license#gears">Google Gears License</a></li> + <li><a href="about:license#gears-istumbler">Google Gears/iStumbler License</a></li> + <li><a href="about:license#vp8">Google VP8 License</a></li> + <li><a href="about:license#gsl">GSL License</a></li> + <li><a href="about:license#gyp">gyp License</a></li> + <li><a href="about:license#halloc">halloc License</a></li> + <li><a href="about:license#harfbuzz">HarfBuzz License</a></li> + <li><a href="about:license#icu">ICU License</a></li> + <li><a href="about:license#immutable">Immutable.js License</a></li> + <li><a href="about:license#jpnic">Japan Network Information Center License</a></li> + <li><a href="about:license#jszip">JSZip License</a></li> + <li><a href="about:license#jemalloc">jemalloc License</a></li> + <li><a href="about:license#jquery">jQuery License</a></li> + <li><a href="about:license#k_exp">k_exp License</a></li> + <li><a href="about:license#kernel32-sys">kernel32-sys License</a></li> + <li><a href="about:license#khronos">Khronos group License</a></li> + <li><a href="about:license#kiss_fft">Kiss FFT License</a></li> + <li><a href="about:license#lazy_static">lazy_static License</a></li> +#ifdef MOZ_USE_LIBCXX + <li><a href="about:license#libc++">libc++ License</a></li> +#endif + <li><a href="about:license#libcubeb">libcubeb License</a></li> + <li><a href="about:license#libevent">libevent License</a></li> + <li><a href="about:license#libffi">libffi License</a></li> + <li><a href="about:license#libjingle">libjingle License</a></li> + <li><a href="about:license#libnestegg">libnestegg License</a></li> + <li><a href="about:license#libsoundtouch">libsoundtouch License</a></li> + <li><a href="about:license#libyuv">libyuv License</a></li> + <li><a href="about:license#hunspell-lt">Lithuanian Spellchecking Dictionary License</a></li> + <li><a href="about:license#lodash">lodash License</a></li> + <li><a href="about:license#matches">matches License</a></li> + <li><a href="about:license#myspell">MySpell License</a></li> + <li><a href="about:license#mqtt-packet">mqtt-packet License</a></li> + <li><a href="about:license#naturalSort">naturalSort License</a></li> + <li><a href="about:license#nicer">nICEr License</a></li> + <li><a href="about:license#node-md5">node-md5 License</a></li> + <li><a href="about:license#node-properties">node-properties License</a></li> + <li><a href="about:license#nom">nom License</a></li> + <li><a href="about:license#nrappkit">nrappkit License</a></li> + <li><a href="about:license#openldap">OpenLDAP Public License</a></li> + <li><a href="about:license#openvision">OpenVision License</a></li> +#if defined(XP_WIN) || defined(XP_MACOSX) || defined(XP_LINUX) + <li><a href="about:license#openvr">OpenVR License</a></li> +#endif + <li><a href="about:license#ordered-float">ordered-float License</a></li> + <li><a href="about:license#owning_ref">owning_ref License</a></li> + <li><a href="about:license#pbkdf2-sha256">pbkdf2_sha256 License</a></li> + <li><a href="about:license#phf">phf, phf_codegen, phf_generator, phf_shared License</a></li> + <li><a href="about:license#polymer">Polymer License</a></li> + <li><a href="about:license#praton">praton License</a></li> + <li><a href="about:license#praton1">praton and inet_ntop License</a></li> + <li><a href="about:license#precomputed-hash">precomputed-hash License</a></li> + <li><a href="about:license#prop-types">prop-types License</a></li> + <li><a href="about:license#qcms">qcms License</a></li> + <li><a href="about:license#qrcode-generator">QR Code Generator License</a></li> + <li><a href="about:license#raven-js">Raven.js License</a></li> + <li><a href="about:license#react">React License</a></li> + <li><a href="about:license#react-mit">React MIT License</a></li> + <li><a href="about:license#react-redux">React-Redux License</a></li> + <li><a href="about:license#react-router">React Router License</a></li> + <li><a href="about:license#xdg">Red Hat xdg_user_dir_lookup License</a></li> + <li><a href="about:license#redox_syscall">redox_syscall License</a></li> + <li><a href="about:license#redux">Redux License</a></li> + <li><a href="about:license#reselect">Reselect License</a></li> + <li><a href="about:license#ring">Ring License</a></li> + <li><a href="about:license#hunspell-ru">Russian Spellchecking Dictionary License</a></li> + <li><a href="about:license#sctp">SCTP Licenses</a></li> + <li><a href="about:license#skia">Skia License</a></li> + <li><a href="about:license#snappy">Snappy License</a></li> + <li><a href="about:license#socket.io-parser">Socket.IO Parser License</a></li> + <li><a href="about:license#sockjs-client">SockJS-Client License</a></li> + <li><a href="about:license#sprintf.js">sprintf.js License</a></li> + <li><a href="about:license#stompjs">stompjs License</a></li> + <li><a href="about:license#sunsoft">SunSoft License</a></li> + <li><a href="about:license#superfasthash">SuperFastHash License</a></li> + <li><a href="about:license#synstructure">synstructure License</a></li> + <li><a href="about:license#unicase">unicase License</a></li> + <li><a href="about:license#unicode">Unicode License</a></li> + <li><a href="about:license#ucal">University of California License</a></li> + <li><a href="about:license#unreachable">unreachable License</a></li> + <li><a href="about:license#hunspell-en">English Spellchecking Dictionary Licenses</a></li> + <li><a href="about:license#utf8-ranges">utf8-ranges License</a></li> + <li><a href="about:license#v8">V8 License</a></li> + <li><a href="about:license#validator">Validator License</a></li> + <li><a href="about:license#void">void License</a></li> + <li><a href="about:license#vtune">VTune License</a></li> + <li><a href="about:license#webrtc">WebRTC License</a></li> +#ifdef MOZ_DEFAULT_BROWSER_AGENT + <li><a href="about:license#wintoast">WinToast License</a></li> +#endif + <li><a href="about:license#x264">x264 License</a></li> + <li><a href="about:license#xiph">Xiph.org Foundation License</a></li> + <li><a href="about:license#zydis">Zydis License</a></li> + </ul> + +<br> + + <ul> + <li><a href="about:license#other-notices">Other Required Notices</a> + <li><a href="about:license#optional-notices">Optional Notices</a> +#ifdef XP_WIN + <li><a href="about:license#proprietary-notices">Proprietary Operating System Components</a> +#endif + </ul> + +#ifdef APP_LICENSE_LIST_BLOCK +#ifndef APP_LICENSE_BODY_BLOCK +#error +#endif +#ifndef APP_LICENSE_PRODUCT_NAME +#error +#endif +The following licenses are specific to code used by the +#includesubst @APP_LICENSE_PRODUCT_NAME@ +product. + +<!-- Index of product-specific licenses for non-Firefox apps. --> +#includesubst @APP_LICENSE_LIST_BLOCK@ +#endif + + </div> + + <hr> + + <h1 id="mpl">Mozilla Public License 2.0</h1> + + <h2 id="definitions">1. Definitions</h2> + + <dl> + <dt>1.1. "Contributor"</dt> + + <dd> + <p>means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software.</p> + </dd> + + <dt>1.2. "Contributor Version"</dt> + + <dd> + <p>means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution.</p> + </dd> + + <dt>1.3. "Contribution"</dt> + + <dd> + <p>means Covered Software of a particular Contributor.</p> + </dd> + + <dt>1.4. "Covered Software"</dt> + + <dd> + <p>means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code Form, + and Modifications of such Source Code Form, in each case including + portions thereof.</p> + </dd> + + <dt>1.5. "Incompatible With Secondary Licenses"</dt> + + <dd> + <p>means</p> + + <ol type="a"> + <li> + <p>that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or</p> + </li> + + <li> + <p>that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms + of a Secondary License.</p> + </li> + </ol> + </dd> + + <dt>1.6. "Executable Form"</dt> + + <dd> + <p>means any form of the work other than Source Code Form.</p> + </dd> + + <dt>1.7. "Larger Work"</dt> + + <dd> + <p>means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software.</p> + </dd> + + <dt>1.8. "License"</dt> + + <dd> + <p>means this document.</p> + </dd> + + <dt>1.9. "Licensable"</dt> + + <dd> + <p>means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and all + of the rights conveyed by this License.</p> + </dd> + + <dt>1.10. "Modifications"</dt> + + <dd> + <p>means any of the following:</p> + + <ol type="a"> + <li> + <p>any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; + or</p> + </li> + + <li> + <p>any new file in Source Code Form that contains any Covered + Software.</p> + </li> + </ol> + </dd> + + <dt>1.11. "Patent Claims" of a Contributor</dt> + + <dd> + <p>means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version.</p> + </dd> + + <dt>1.12. "Secondary License"</dt> + + <dd> + <p>means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses.</p> + </dd> + + <dt>1.13. "Source Code Form"</dt> + + <dd> + <p>means the form of the work preferred for making modifications.</p> + </dd> + + <dt>1.14. "You" (or "Your")</dt> + + <dd> + <p>means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, + is controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, to + cause the direction or management of such entity, whether by contract + or otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity.</p> + </dd> + </dl> + + <h2 id="license-grants-and-conditions">2. License Grants and + Conditions</h2> + + <h3 id="grants">2.1. Grants</h3> + + <p>Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license:</p> + + <ol type="a"> + <li> + <p>under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and</p> + </li> + + <li> + <p>under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version.</p> + </li> + </ol> + + <h3 id="effective-date">2.2. Effective Date</h3> + + <p>The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution.</p> + + <h3 id="limitations-on-grant-scope">2.3. Limitations on Grant Scope</h3> + + <p>The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor:</p> + + <ol type="a"> + <li> + <p>for any code that a Contributor has removed from Covered Software; + or</p> + </li> + + <li> + <p>for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or</p> + </li> + + <li> + <p>under Patent Claims infringed by Covered Software in the absence of + its Contributions.</p> + </li> + </ol> + + <p>This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4).</p> + + <h3 id="subsequent-licenses">2.4. Subsequent Licenses</h3> + + <p>No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3).</p> + + <h3 id="representation">2.5. Representation</h3> + + <p>Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License.</p> + + <h3 id="fair-use">2.6. Fair Use</h3> + + <p>This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents.</p> + + <h3 id="conditions">2.7. Conditions</h3> + + <p>Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted + in Section 2.1.</p> + + <h2 id="responsibilities">3. Responsibilities</h2> + + <h3 id="distribution-of-source-form">3.1. Distribution of Source Form</h3> + + <p>All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients' rights in the Source Code Form.</p> + + <h3 id="distribution-of-executable-form">3.2. Distribution of Executable + Form</h3> + + <p>If You distribute Covered Software in Executable Form then:</p> + + <ol type="a"> + <li> + <p>such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code Form + by reasonable means in a timely manner, at a charge no more than the + cost of distribution to the recipient; and</p> + </li> + + <li> + <p>You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License.</p> + </li> + </ol> + + <h3 id="distribution-of-a-larger-work">3.3. Distribution of a Larger + Work</h3> + + <p>You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s).</p> + + <h3 id="notices">3.4. Notices</h3> + + <p>You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies.</p> + + <h3 id="application-of-additional-terms">3.5. Application of Additional + Terms</h3> + + <p>You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction.</p> + + <h2 id="inability-to-comply-due-to-statute-or-regulation">4. Inability to + Comply Due to Statute or Regulation</h2> + + <p>If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it.</p> + + <h2 id="termination">5. Termination</h2> + + <h3>5.1.</h3> + + <p>The rights granted under this License will terminate automatically + if You fail to comply with any of its terms. However, if You become + compliant, then the rights granted under this License from a particular + Contributor are reinstated (a) provisionally, unless and until such + Contributor explicitly and finally terminates Your grants, and (b) on an + ongoing basis, if such Contributor fails to notify You of the + non-compliance by some reasonable means prior to 60 days after You have + come back into compliance. Moreover, Your grants from a particular + Contributor are reinstated on an ongoing basis if such Contributor notifies + You of the non-compliance by some reasonable means, this is the first time + You have received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice.</p> + + <h3>5.2.</h3> + + <p>If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate.</p> + + <h3>5.3.</h3> + + <p>In the event of termination under Sections 5.1 or 5.2 above, all + end user license agreements (excluding distributors and resellers) which + have been validly granted by You or Your distributors under this License + prior to termination shall survive termination.</p> + + <h2 id="disclaimer-of-warranty">6. Disclaimer of Warranty</h2> + + <p><em>Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer.</em></p> + + <h2 id="limitation-of-liability">7. Limitation of Liability</h2> + + <p><em>Under no circumstances and under no legal theory, whether tort + (including negligence), contract, or otherwise, shall any Contributor, or + anyone who distributes Covered Software as permitted above, be liable to + You for any direct, indirect, special, incidental, or consequential damages + of any character including, without limitation, damages for lost profits, + loss of goodwill, work stoppage, computer failure or malfunction, or any + and all other commercial damages or losses, even if such party shall have + been informed of the possibility of such damages. This limitation of + liability shall not apply to liability for death or personal injury + resulting from such party's negligence to the extent applicable law + prohibits such limitation. Some jurisdictions do not allow the exclusion or + limitation of incidental or consequential damages, so this exclusion and + limitation may not apply to You.</em></p> + + <h2 id="litigation">8. Litigation</h2> + + <p>Any litigation relating to this License may be brought only in the + courts of a jurisdiction where the defendant maintains its principal place + of business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims.</p> + + <h2 id="miscellaneous">9. Miscellaneous</h2> + + <p>This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor.</p> + + <h2 id="versions-of-the-license">10. Versions of the License</h2> + + <h3 id="new-versions">10.1. New Versions</h3> + + <p>Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number.</p> + + <h3 id="effect-of-new-versions">10.2. Effect of New Versions</h3> + + <p>You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward.</p> + + <h3 id="modified-versions">10.3. Modified Versions</h3> + + <p>If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any references + to the name of the license steward (except to note that such modified + license differs from this License).</p> + + <h3 id= + "distributing-source-code-form-that-is-incompatible-with-secondary-licenses"> + 10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses</h3> + + <p>If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached.</p> + + <h2 id="exhibit-a---source-code-form-license-notice">Exhibit A - Source + Code Form License Notice</h2> + + <blockquote> + <p>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 https://mozilla.org/MPL/2.0/.</p> + </blockquote> + + <p>If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE file + in a relevant directory) where a recipient would be likely to look for such + a notice.</p> + + <p>You may add additional accurate notices of copyright ownership.</p> + + <h2 id="exhibit-b---incompatible-with-secondary-licenses-notice">Exhibit B + - "Incompatible With Secondary Licenses" Notice</h2> + + <blockquote> + <p>This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0.</p> + </blockquote> + + + <hr> + + <h1 id="lgpl">GNU Lesser General Public License 2.1</h1> + +<p>This product contains code from the following LGPLed libraries:</p> + +<ul> +<li><a href="https://www.surina.net/soundtouch/">libsoundtouch</a> +<li><a href="https://libav.org/">Libav</a> +<li><a href="https://ffmpeg.org/">FFmpeg</a> +</ul> + +<pre> +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] +</pre> + +<h3><a id="SEC2">Preamble</a></h3> + +<p> + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. +</p> +<p> + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. +</p> +<p> + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. +</p> +<p> + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. +</p> +<p> + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. +</p> +<p> + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. +</p> +<p> + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. +</p> +<p> + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. +</p> +<p> + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. +</p> +<p> + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. +</p> +<p> + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. +</p> +<p> + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. +</p> +<p> + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. +</p> +<p> + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. +</p> +<p> + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. +</p> + +<h3><a id="SEC3">TERMS AND CONDITIONS FOR COPYING, +DISTRIBUTION AND MODIFICATION</a></h3> + + +<p> +<strong>0.</strong> +This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". +</p> +<p> + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. +</p> +<p> + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) +</p> +<p> + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. +</p> +<p> + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. +</p> +<p> +<strong>1.</strong> +You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. +</p> +<p> + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. +</p> +<p> +<strong>2.</strong> +You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: +</p> + +<ul> + <li><strong>a)</strong> + The modified work must itself be a software library.</li> + <li><strong>b)</strong> + You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change.</li> + + <li><strong>c)</strong> + You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License.</li> + + <li><strong>d)</strong> + If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + <p> + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.)</p></li> +</ul> + +<p> +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be +reasonably considered independent and separate works in themselves, then +this License, and its terms, do not apply to those sections when you +distribute them as separate works. But when you distribute the same +sections as part of a whole which is a work based on the Library, the +distribution of the whole must be on the terms of this License, whose +permissions for other licensees extend to the entire whole, and thus to +each and every part regardless of who wrote it. +</p> +<p> +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works +based on the Library. +</p> +<p> +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of +this License. +</p> +<p> +<strong>3.</strong> +You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. +</p> +<p> + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. +</p> +<p> + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. +</p> +<p> +<strong>4.</strong> +You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. +</p> +<p> + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. +</p> +<p> +<strong>5.</strong> +A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. +</p> +<p> + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. +</p> +<p> + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. +</p> +<p> + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) +</p> +<p> + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. +</p> +<p> +<strong>6.</strong> +As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. +</p> +<p> + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: +</p> + +<ul> + <li><strong>a)</strong> Accompany the work with the complete + corresponding machine-readable source code for the Library + including whatever changes were used in the work (which must be + distributed under Sections 1 and 2 above); and, if the work is an + executable linked with the Library, with the complete + machine-readable "work that uses the Library", as object code + and/or source code, so that the user can modify the Library and + then relink to produce a modified executable containing the + modified Library. (It is understood that the user who changes the + contents of definitions files in the Library will not necessarily + be able to recompile the application to use the modified + definitions.)</li> + + <li><strong>b)</strong> Use a suitable shared library mechanism + for linking with the Library. A suitable mechanism is one that + (1) uses at run time a copy of the library already present on the + user's computer system, rather than copying library functions into + the executable, and (2) will operate properly with a modified + version of the library, if the user installs one, as long as the + modified version is interface-compatible with the version that the + work was made with.</li> + + <li><strong>c)</strong> Accompany the work with a written offer, + valid for at least three years, to give the same user the + materials specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution.</li> + + <li><strong>d)</strong> If distribution of the work is made by + offering access to copy from a designated place, offer equivalent + access to copy the above specified materials from the same + place.</li> + + <li><strong>e)</strong> Verify that the user has already received + a copy of these materials or that you have already sent this user + a copy.</li> +</ul> + +<p> + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. +</p> +<p> + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. +</p> +<p> +<strong>7.</strong> You may place library facilities that are a work +based on the Library side-by-side in a single library together with +other library facilities not covered by this License, and distribute +such a combined library, provided that the separate distribution of +the work based on the Library and of the other library facilities is +otherwise permitted, and provided that you do these two things: +</p> + +<ul> + <li><strong>a)</strong> Accompany the combined library with a copy + of the same work based on the Library, uncombined with any other + library facilities. This must be distributed under the terms of + the Sections above.</li> + + <li><strong>b)</strong> Give prominent notice with the combined + library of the fact that part of it is a work based on the + Library, and explaining where to find the accompanying uncombined + form of the same work.</li> +</ul> + +<p> +<strong>8.</strong> You may not copy, modify, sublicense, link with, +or distribute the Library except as expressly provided under this +License. Any attempt otherwise to copy, modify, sublicense, link +with, or distribute the Library is void, and will automatically +terminate your rights under this License. However, parties who have +received copies, or rights, from you under this License will not have +their licenses terminated so long as such parties remain in full +compliance. +</p> +<p> +<strong>9.</strong> +You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. +</p> +<p> +<strong>10.</strong> +Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. +</p> +<p> +<strong>11.</strong> +If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. +</p> +<p> +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. +</p> +<p> +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. +</p> +<p> +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. +</p> +<p> +<strong>12.</strong> +If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. +</p> +<p> +<strong>13.</strong> +The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. +</p> +<p> +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. +</p> +<p> +<strong>14.</strong> +If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. +</p> +<p> +<strong>NO WARRANTY</strong> +</p> +<p> +<strong>15.</strong> +BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. +</p> +<p> +<strong>16.</strong> +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. +</p> + + <hr> + + <h1 id="lgpl-3.0">GNU Lesser General Public License 3.0</h1> + +<p>Some versions of this product contains code from the following LGPLed libraries:</p> + +<ul> +<li><a +href="https://addons.mozilla.org/en-US/firefox/addon/görans-hemmasnickrade-ordli/">Swedish dictionary</a> +</ul> + +<pre>Copyright © 2007 Free Software Foundation, Inc. + <<a href="https://www.fsf.org/">https://www.fsf.org/</a>> + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed.</pre> + +<p>This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below.</p> + +<h3><a id="section0">0. Additional Definitions</a></h3> + +<p>As used herein, “this License” refers to version 3 of the GNU Lesser +General Public License, and the “GNU GPL” refers to version 3 of the GNU +General Public License.</p> + +<p>“The Library” refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below.</p> + +<p>An “Application” is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library.</p> + +<p>A “Combined Work” is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the “Linked +Version”.</p> + +<p>The “Minimal Corresponding Source” for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version.</p> + +<p>The “Corresponding Application Code” for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work.</p> + +<h3><a id="section1">1. Exception to Section 3 of the GNU GPL.</a></h3> + +<p>You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL.</p> + +<h3><a id="section2">2. Conveying Modified Versions.</a></h3> + +<p>If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version:</p> + +<ul> +<li>a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or</li> + +<li>b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy.</li> +</ul> + +<h3><a id="section3">3. Object Code Incorporating Material from Library Header Files.</a></h3> + +<p>The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following:</p> + +<ul> +<li>a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License.</li> + +<li>b) Accompany the object code with a copy of the GNU GPL and this license + document.</li> +</ul> + +<h3><a id="section4">4. Combined Works.</a></h3> + +<p>You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following:</p> + +<ul> +<li>a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License.</li> + +<li>b) Accompany the Combined Work with a copy of the GNU GPL and this license + document.</li> + +<li>c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document.</li> + +<li>d) Do one of the following: + +<ul> +<li>0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source.</li> + +<li>1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version.</li> +</ul></li> + +<li>e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.)</li> +</ul> + +<h3><a id="section5">5. Combined Libraries.</a></h3> + +<p>You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following:</p> + +<ul> +<li>a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License.</li> + +<li>b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work.</li> +</ul> + +<h3><a id="section6">6. Revised Versions of the GNU Lesser General Public License.</a></h3> + +<p>The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns.</p> + +<p>Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License “or any later version” +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation.</p> + +<p>If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library.</p> + + + <hr> + + + <h1 id="gpl-3.0">GNU General Public License 3.0</h1> + + <p>This license does not apply to any of the code shipped with + Firefox, but may apply to blocklists downloaded after installation + for use with the tracking protection feature. Firefox and such + blocklists are separate and independent works as described in + Sections 5 and 6 of this license. Our blocklist is based on one + originally written by Disconnect.me.</p> + +<pre>Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. +<<a href="https://www.fsf.org/">https://www.fsf.org/</a>> + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed.</pre> + +<h2><a name="preamble"></a>Preamble</h2> + +<p>The GNU General Public License is a free, copyleft license for +software and other kinds of works.</p> + +<p>The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too.</p> + +<p>When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things.</p> + +<p>To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others.</p> + +<p>For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights.</p> + +<p>Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it.</p> + +<p>For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions.</p> + +<p>Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users.</p> + +<p>Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free.</p> + +<p>The precise terms and conditions for copying, distribution and +modification follow.</p> + +<h2><a name="terms"></a>TERMS AND CONDITIONS</h2> + +<h3><a name="section0"></a>0. Definitions.</h3> + +<p>“This License” refers to version 3 of the GNU General Public License.</p> + +<p>“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks.</p> + +<p>“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations.</p> + +<p>To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work.</p> + +<p>A “covered work” means either the unmodified Program or a work based +on the Program.</p> + +<p>To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well.</p> + +<p>To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying.</p> + +<p>An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion.</p> + +<h3><a name="section1"></a>1. Source Code.</h3> + +<p>The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work.</p> + +<p>A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language.</p> + +<p>The “System Libraries” of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it.</p> + +<p>The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work.</p> + +<p>The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source.</p> + +<p>The Corresponding Source for a work in source code form is that +same work.</p> + +<h3><a name="section2"></a>2. Basic Permissions.</h3> + +<p>All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law.</p> + +<p>You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you.</p> + +<p>Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary.</p> + +<h3><a name="section3"></a>3. Protecting Users' Legal Rights From Anti-Circumvention Law.</h3> + +<p>No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures.</p> + +<p>When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures.</p> + +<h3><a name="section4"></a>4. Conveying Verbatim Copies.</h3> + +<p>You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program.</p> + +<p>You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee.</p> + +<h3><a name="section5"></a>5. Conveying Modified Source Versions.</h3> + +<p>You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions:</p> + +<ul> +<li>a) The work must carry prominent notices stating that you modified + it, and giving a relevant date.</li> + +<li>b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + “keep intact all notices”.</li> + +<li>c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it.</li> + +<li>d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so.</li> +</ul> + +<p>A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate.</p> + +<h3><a name="section6"></a>6. Conveying Non-Source Forms.</h3> + +<p>You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways:</p> + +<ul> +<li>a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange.</li> + +<li>b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge.</li> + +<li>c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b.</li> + +<li>d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements.</li> + +<li>e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d.</li> +</ul> + +<p>A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work.</p> + +<p>A “User Product” is either (1) a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product.</p> + +<p>“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made.</p> + +<p>If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM).</p> + +<p>The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network.</p> + +<p>Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying.</p> + +<h3><a name="section7"></a>7. Additional Terms.</h3> + +<p>“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions.</p> + +<p>When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission.</p> + +<p>Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms:</p> + +<ul> +<li>a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or</li> + +<li>b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or</li> + +<li>c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or</li> + +<li>d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or</li> + +<li>e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or</li> + +<li>f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors.</li> +</ul> + +<p>All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying.</p> + +<p>If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms.</p> + +<p>Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way.</p> + +<h3><a name="section8"></a>8. Termination.</h3> + +<p>You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11).</p> + +<p>However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation.</p> + +<p>Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice.</p> + +<p>Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10.</p> + +<h3><a name="section9"></a>9. Acceptance Not Required for Having Copies.</h3> + +<p>You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so.</p> + +<h3><a name="section10"></a>10. Automatic Licensing of Downstream Recipients.</h3> + +<p>Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License.</p> + +<p>An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts.</p> + +<p>You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it.</p> + +<h3><a name="section11"></a>11. Patents.</h3> + +<p>A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”.</p> + +<p>A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License.</p> + +<p>Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version.</p> + +<p>In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party.</p> + +<p>If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid.</p> + +<p>If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it.</p> + +<p>A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007.</p> + +<p>Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law.</p> + +<h3><a name="section12"></a>12. No Surrender of Others' Freedom.</h3> + +<p>If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program.</p> + +<h3><a name="section13"></a>13. Use with the GNU Affero General Public License.</h3> + +<p>Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such.</p> + +<h3><a name="section14"></a>14. Revised Versions of this License.</h3> + +<p>The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns.</p> + +<p>Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation.</p> + +<p>If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program.</p> + +<p>Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version.</p> + +<h3><a name="section15"></a>15. Disclaimer of Warranty.</h3> + +<p>THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION.</p> + +<h3><a name="section16"></a>16. Limitation of Liability.</h3> + +<p>IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES.</p> + +<h3><a name="section17"></a>17. Interpretation of Sections 15 and 16.</h3> + +<p>If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee.</p> + +<p>END OF TERMS AND CONDITIONS</p> + +<h2><a name="howto"></a>How to Apply These Terms to Your New Programs</h2> + +<p>If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms.</p> + +<p>To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found.</p> + +<pre> <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +</pre> + +<p>Also add information on how to contact you by electronic and paper mail.</p> + +<p>If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode:</p> + +<pre> <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. +</pre> + +<p>The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an “about box”.</p> + +<p>You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<<a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>>.</p> + +<p>The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<<a href="https://www.gnu.org/philosophy/why-not-lgpl.html">https://www.gnu.org/philosophy/why-not-lgpl.html</a>>.</p> + +<hr> + + <h1><a id="ACE"></a>ACE License</h1> + + <p>This license applies to the file + <code>media/webrtc/trunk/webrtc/system_wrappers/source/condition_variable_event_win.cc</code>.</p> + +<pre> +ACE(TM), TAO(TM), CIAO(TM), DAnCE(TM), and CoSMIC(TM) +(henceforth referred to as "DOC software") are copyrighted by +Douglas C. Schmidt and his research group at Washington University, +University of California, Irvine, and Vanderbilt University, +Copyright (c) 1993-2009, all rights reserved. +Since DOC software is open-source, freely available software, +you are free to use, modify, copy, and distribute--perpetually and +irrevocably--the DOC software source code and object code produced +from the source, as well as copy and distribute modified versions of +this software. You must, however, include this copyright statement +along with any code built using DOC software that you release. No +copyright statement needs to be provided if you just ship binary +executables of your software products. +</pre> + + <hr> + + <h1><a id="android"></a>Android Open Source License</h1> + + <p>This license applies to various files in the Mozilla codebase, + including those in the directory <code>gfx/skia/</code>.</p> +<!-- This is the wrong directory, what was intended? --> + +<pre> + Copyright 2009, The Android Open Source Project + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="angle"></a>ANGLE License</h1> + + <p>This license applies to files in the directory <code>gfx/angle/</code>.</p> + +<pre> +Copyright (C) 2002-2010 The ANGLE Project Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + Neither the name of TransGaming Inc., Google Inc., 3DLabs Inc. + Ltd., nor the names of their contributors may be used to endorse + or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="acorn"></a>acorn License</h1> + + <p>This license applies to all files in + <code>devtools/shared/acorn</code>. + </p> +<pre> +Copyright (C) 2012 by Marijn Haverbeke <marijnh@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Please note that some subdirectories of the CodeMirror distribution +include their own LICENSE files, and are released under different +licences. +</pre> + + + <hr> + + <h1><a id="apache"></a>Apache License 2.0</h1> + + <p>This license applies to various files in the Mozilla codebase including, but not limited to:<br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/signalr/HandshakeProtocol.js</code><br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/signalr/IHubProtocol.js</code><br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/signalr/JSONHubProtocol.js</code><br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/signalr/TextMessageFormat.js</code><br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/signalr/Utils.js</code><br/> + <code>third_party/cups/include</code><br/> + </p> + +<pre> + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +</pre> + + + + <hr> + + <h1><a id="apache-llvm"></a>Apache License 2.0 with LLVM exception</h1> + + <p>This license applies to files in the directories:</p> + <ul> + <li><code>third_party/rust/cranelift-codegen</code></li> + <li><code>third_party/rust/cranelift-entity</code></li> + <li><code>third_party/rust/cranelift-frontend</code></li> + <li><code>third_party/rust/cranelift-wasm</code></li> + <li><code>third_party/rust/target-lexicon</code></li> + </ul> + +<pre> + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. +</pre> + + + + <hr> + + <h1><a id="apple"></a>Apple License</h1> + + <p>This license applies to certain files in the directories <code>dom/media/webaudio/blink</code>, and <code>widget/cocoa</code>.</p> + +<pre> +Copyright (C) 2008, 2009 Apple Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="apple-mozilla"></a>Apple/Mozilla NPRuntime License</h1> + + <p>This license applies to the file + <code>dom/plugins/base/npruntime.h</code>.</p> + +<pre> +Copyright © 2004, Apple Computer, Inc. and The Mozilla Foundation. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. Neither the names of Apple Computer, Inc. ("Apple") or The Mozilla +Foundation ("Mozilla") nor the names of their contributors may be used +to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY APPLE, MOZILLA AND THEIR CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE, MOZILLA OR +THEIR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="arm"></a>ARM License</h1> + + <p>This license applies to files in the directory <code>js/src/jit/arm64/vixl/</code>.</p> + +<pre> +Copyright 2013, ARM Limited +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of ARM Limited nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="babel"></a>Babel License</h1> + + <p>This license applies to this file in the directory + <code>devtools/client/debugger/debugger.js/</code>. + </p> + +<pre> +Copyright (c) 2014-2017 Sebastian McKenzie <sebmck@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + </hr> + + <h1><a id="babylon"></a>Babylon License</h1> + + <p>This license applies to this file in the directory + <code>devtools/client/debugger/debugger.js/</code>. + </p> + +<pre> +Copyright (C) 2012-2014 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="bincode"></a>bincode License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/bincode</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2014 Ty Overby + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + + <hr> + + <h1><a id="bsd2clause"></a>BSD 2-Clause License</h1> + + <p>This license applies to files in the following directories: + <ul> + <li><code>third_party/rust/arrayref</code></li> + <li><code>third_party/rust/cloudabi</code></li> + <li><code>third_party/rust/Inflector</code></li> + <li><code>third_party/rust/mach</code></li> + <li><code>third_party/rust/qlog</code></li> + </ul> + See the individual LICENSE files for copyright owners.</p> + +<pre> +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + +<hr> + +<h1><a id="bsd3clause"></a>BSD 3-Clause License</h1> + +<p>This license applies to files in the following directories: +<ul> + <li><code>browser/components/newtab/vendor/react-transition-group.js</code></li> + <li><code>third_party/rust/bindgen/</code></li> + <li><code>third_party/rust/instant/</code></li> +</ul> +See the individual LICENSE files for copyright owners.</p> + +<pre> +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="bspatch"></a>bspatch License</h1> + + <p>This license applies to the files + <code>toolkit/mozapps/update/updater/bspatch/bspatch.cpp</code> and + <code>toolkit/mozapps/update/updater/bspatch/bspatch.h</code>. + </p> + +<pre> +Copyright 2003,2004 Colin Percival +All rights reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted providing that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="byteorder"></a>byteorder License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/byteorder</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="cairo"></a>Cairo Component Licenses</h1> + + <p>This license, with different copyright holders, applies to certain files + in the directory <code>gfx/cairo/</code>. The copyright + holders and the applicable ranges of dates are as follows: + + <ul> +<li>2004 Richard D. Worth +<li>2004, 2005 Red Hat, Inc. +<li>2003 USC, Information Sciences Institute +<li>2004 David Reveman +<li>2005 Novell, Inc. +<li>2004 David Reveman, Peter Nilsson +<li>2000 Keith Packard, member of The XFree86 Project, Inc. +<li>2005 Lars Knoll & Zack Rusin, Trolltech +<li>1998, 2000, 2002, 2004 Keith Packard +<li>2004 Nicholas Miell +<li>2005 Trolltech AS +<li>2000 SuSE, Inc. +<li>2003 Carl Worth +<li>1987, 1988, 1989, 1998 The Open Group +<li>1987, 1988, 1989 Digital Equipment Corporation, Maynard, Massachusetts. +<li>1998 Keith Packard +<li>2003 Richard Henderson + </ul> + +<pre> +Copyright © <date> <copyright holder> + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without +fee, provided that the above copyright notice appear in all copies +and that both that copyright notice and this permission notice +appear in supporting documentation, and that the name of +<copyright holder> not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior permission. +<copyright holder> makes no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +<COPYRIGHT HOLDER> DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + + <hr> + + <h1><a id="chromium"></a>Chromium License</h1> + + <p>This license applies to parts of the code in:</p> + <ul> + <li><code>browser/extensions/formautofill/content/heuristicsRegexp.js</code></li> + <li><code>browser/extensions/formautofill/FormAutofillHeuristics.jsm</code></li> + <li><code>browser/extensions/formautofill/FormAutofillNameUtils.jsm</code></li> + <li><code>editor/libeditor/EditorEventListener.cpp</code></li> + <li><code>mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/StrictModeContext.java</code></li> + <li><code>security/sandbox/</code></li> + <li><code>toolkit/components/passwordmgr/PasswordGenerator.jsm</code></li> + <li><code>widget/cocoa/GfxInfo.mm</code></li> + <li><code>widget/windows/nsWindow.cpp</code></li> + </ul> + <p>and also some files in these directories:</p> + <ul> + <li><code>dom/media/webspeech/recognition/</code></li> + <li><code>dom/plugins/</code></li> + <li><code>gfx/ots/</code></li> + <li><code>gfx/ycbcr/</code></li> + <li><code>ipc/chromium/</code></li> + <li><code>media/openmax_dl/</code></li> + <li><code>toolkit/components/downloads/chromium/</code></li> + <li><code>toolkit/components/url-classifier/chromium/</code></li> + <li><code>tools/profiler/</code></li> + </ul> + +<pre> +Copyright (c) 2006-2018 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="codemirror"></a>CodeMirror License</h1> + + <p>This license applies to all files in + <code>devtools/client/shared/sourceeditor/codemirror</code> and + to specified files in the <code>devtools/client/shared/sourceeditor/test/</code>: + </p> + <ul> + <li><code>cm_comment_test.js</code></li> + <li><code>cm_driver.js</code></li> + <li><code>cm_mode_javascript_test.js</code></li> + <li><code>cm_mode_test.css</code></li> + <li><code>cm_mode_test.js</code></li> + <li><code>cm_test.js</code></li> + </ul> +<pre> +Copyright (C) 2013 by Marijn Haverbeke <marijnh@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Please note that some subdirectories of the CodeMirror distribution +include their own LICENSE files, and are released under different +licences. +</pre> + + + + <hr> + + <h1><a id="cryptogams"></a>CRYPTOGAMS License</h1> + + <p>This license applies all files in + <code>security/nss/lib/freebl/scripts/</code> and to the file + <code>security/nss/lib/freebl/sha512-p8.s</code>. + </p> +<pre> +Copyright (c) 2006, CRYPTOGAMS by <appro@openssl.org> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain copyright notices, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + + * Neither the name of the CRYPTOGAMS nor the names of its + copyright holder and contributors may be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="cubic-bezier"></a>cubic-bezier License</h1> + + <p>This license applies to the file + <code>devtools/client/shared/widgets/CubicBezierWidget.js + </code>.</p> +<pre> +Copyright (c) 2013 Lea Verou. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="d3"></a>D3 License</h1> + + <p>This license applies to the file + <code>third_party/js/d3/d3.js</code>. + </p> +<pre> +Copyright (c) 2010-2016, Michael Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="dagre-d3"></a>Dagre-D3 License</h1> + + <p>This license applies to the file + <code>devtools/client/shared/vendor/dagre-d3.js</code>. + </p> +<pre> +Copyright (c) 2013 Chris Pettitt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + +<hr> + +<h1><a id="diff"></a>diff License</h1> + +<p>This license applies to the file +<code>devtools/client/inspector/markup/test/helper_diff.js</code>.</p> + +<pre> +Copyright (c) 2014 Slava + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + +<hr> + +<h1><a id="dtoa"></a>dtoa License</h1> + +<p>This license applies to the file +<code>nsprpub/pr/src/misc/dtoa.c</code>.</p> + +<pre> +The author of this software is David M. Gay. + +Copyright (c) 1991, 2000, 2001 by Lucent Technologies. + +Permission to use, copy, modify, and distribute this software for any +purpose without fee is hereby granted, provided that this entire notice +is included in all copies of any software which is or includes a copy +or modification of this software and in all copies of the supporting +documentation for such software. + +THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED +WARRANTY. IN PARTICULAR, NEITHER THE AUTHOR NOR LUCENT MAKES ANY +REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY +OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. +</pre> + + +<hr> + +<h1><a id="ECCKiila"></a>ECCKiila License</h1> + +<p>This license applies to the files + <code>security/nss/lib/freebl/ecl/ecp_secp384r1.c</code> + and <code>security/nss/lib/freebl/ecl/ecp_secp521r1.c</code> +</p> +<pre> +The MIT License (MIT) + +Copyright (c) 2020 Luis Rivera-Zamarripa, Jesús-Javier Chi-Domínguez, Billy Bob Brumley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + +<hr> + +<h1><a id="Fiat-Crypto"></a>Fiat-Crypto License</h1> + +<p>This license applies to the files + <code>security/nss/lib/freebl/ecl/curve25519_32.c</code> + , <code>security/nss/lib/freebl/ecl/ecp_secp384r1.c</code> + , and <code>security/nss/lib/freebl/ecl/ecp_secp521r1.c</code> +</p> +<pre> +The MIT License (MIT) + +Copyright (c) 2015-2019 the fiat-crypto authors (see +https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + + <hr> + + <h1><a id="fuzz-aldrin"></a>fuzz-aldrin License</h1> + + <p>This license applies to some of the code in + <code>devtools/client/debugger/debugger.js</code>.</p> + +<pre> +Copyright (c) 2015 Jean Christophe Roy + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +</pre> + + + <hr> + + <h1><a id="hunspell-nl"></a>Dutch Spellchecking Dictionary License</h1> + + <p>This license applies to the Dutch Spellchecking Dictionary. (This + code only ships in some localized versions of this product.)</p> + +<pre> +Copyright (c) 2006, 2007 OpenTaal +Copyright (c) 2001, 2002, 2003, 2005 Simon Brouwer e.a. +Copyright (c) 1996 Nederlandstalige Tex Gebruikersgroep + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. +* Neither the name of the OpenTaal, Simon Brouwer e.a., or Nederlandstalige Tex +Gebruikersgroep nor the names of its contributors may be used to endorse or +promote products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + +#if defined(XP_WIN) || defined(XP_LINUX) + <h1><a id="twemoji"></a>Twemoji License</h1> + + <p>This license applies to the emoji art contained within the bundled +emoji font file.</p> + +<pre> +Copyright (c) 2018 Twitter, Inc and other contributors. + +Creative Commons Attribution 4.0 International (CC BY 4.0) + +See https://creativecommons.org/licenses/by/4.0/legalcode or +for the human readable summary: https://creativecommons.org/licenses/by/4.0/ + +You are free to: + +Share — copy and redistribute the material in any medium or format + +Adapt — remix, transform, and build upon the material for any purpose, even commercially. + +The licensor cannot revoke these freedoms as long as you follow the license terms. + +Under the following terms: + +Attribution — You must give appropriate credit, provide a link to the license, +and indicate if changes were made. You may do so in any reasonable manner, +but not in any way that suggests the licensor endorses you or your use. + +No additional restrictions — You may not apply legal terms or technological +measures that legally restrict others from doing anything the license permits. + +Notices: + +You do not have to comply with the license for elements of the material in +the public domain or where your use is permitted by an applicable exception or +limitation. No warranties are given. The license may not give you all of the +permissions necessary for your intended use. For example, other rights such as +publicity, privacy, or moral rights may limit how you use the material. +</pre> + + + <hr> + +#endif + <h1><a id="hunspell-ee"></a>Estonian Spellchecking Dictionary License</h1> + + <p>This license applies to precursor works to certain files which are + part of the Estonian Spellchecking Dictionary. The + shipped versions are under the GNU Lesser General Public License. (This + code only ships in some localized versions of this product.)</p> + +<pre> +Copyright © Institute of the Estonian Language + +E-mail: litsents@eki.ee +URL: https://www.eki.ee/tarkvara/ + +The present Licence Agreement gives the user of this Software Product +(hereinafter: Product) the right to use the Product for whatever purpose +(incl. distribution, copying, altering, inclusion in other software, and +selling) on the following conditions: + +1. The present Licence Agreement should belong unaltered to each copy ever + made of this Product; +2. Neither the Institute of the Estonian Language (hereinafter: IEL) nor the + author(s) of the Product will take responsibility for any detriment, direct + or indirect, possibly ensuing from the application of the Product; +3. The IEL is ready to share the Product with other users as we wish to + advance research on the Estonian language and to promote the use of + Estonian in rapidly developing infotechnology, yet we refuse to bind + ourselves to any further obligation, which means that the IEL is not + obliged either to warrant the suitability of the Product for a specific + purpose, to improve the software, or to provide a more detailed description + of the underlying algorithms. (Which does not mean, though, that we may not + do it.) + +Notification Request: + +As a courtesy, we would appreciate being informed whenever our linguistic +products are used to create derivative works. If you modify our software or +include it in other products, please inform us by sending e-mail to +litsents@eki.ee or by letter to + +Institute of the Estonian Language +Roosikrantsi 6 +10119 Tallinn +ESTONIA + +Phone & Fax: +372 6411443 +</pre> + + <hr> + + <h1><a id="emitter"></a>Emitter License</h1> + + <p>This license applies to the file + <code>devtools/client/netmonitor/src/components/messages/parsers/socket-io/component-emitter.js</code>.</p> + +<pre> +(The MIT License) + +Copyright (c) 2014 Component contributors <dev@component.io> + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="expat"></a>Expat License</h1> + + <p>This license applies to certain files in the directory + <code>parser/expat/</code>.</p> + +<pre> +Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd + and Clark Cooper +Copyright (c) 2001, 2002, 2003 Expat maintainers. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + + + <hr> + + + <h1><a id="firebug"></a>Firebug License</h1> + + <p>This license applies to the code + <code>devtools/shared/webconsole/network-helper.js</code>.</p> + +<pre> +Copyright (c) 2007, Parakey Inc. +All rights reserved. + +Redistribution and use of this software in source and binary forms, with or +without modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of Parakey Inc. nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of Parakey Inc. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="gdi32-sys"></a>gdi32-sys License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/gdi32-sys</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2015 Peter Atashian + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="gfx-font-list"></a>gfxFontList License</h1> + + <p>This license applies to the files + <code>gfx/thebes/gfxMacPlatformFontList.mm</code> and + <code>gfx/thebes/gfxPlatformFontList.cpp</code>. + </p> + +<pre> +Copyright (C) 2006 Apple Computer, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + + <hr> + + <h1><a id="google-bsd"></a>Google BSD License</h1> + + <p>This license applies to files in the directories + <code>toolkit/crashreporter/google-breakpad/</code>, + <code>toolkit/components/protobuf/</code> and + <code>devtools/client/netmonitor/src/utils/filter-text-utils.js.</code></p> + +<pre> +Copyright (c) 2006, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="vp8"></a>Google VP8 License</h1> + + <p>This license applies to certain files in the directory + <code>media/libvpx</code>.</p> +<pre> +Copyright (c) 2010, Google, Inc. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +- Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Subject to the terms and conditions of the above License, Google +hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this +section) patent license to make, have made, use, offer to sell, sell, +import, and otherwise transfer this implementation of VP8, where such +license applies only to those patent claims, both currently owned by +Google and acquired in the future, licensable by Google that are +necessarily infringed by this implementation of VP8. If You or your +agent or exclusive licensee institute or order or agree to the +institution of patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that this +implementation of VP8 or any code incorporated within this +implementation of VP8 constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any rights +granted to You under this License for this implementation of VP8 +shall terminate as of the date such litigation is filed. +</pre> + + <hr> + + <h1><a id="gears-istumbler"></a>Google Gears/iStumbler License</h1> + + <p>This license applies to the file + <code>netwerk/wifi/osx_wifi.h</code>.</p> + +<pre> +Copyright 2008, Google Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of Google Inc. nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The contents of this file are taken from Apple80211.h from the iStumbler +project (https://istumbler.net/). This project is released under the BSD +license with the following restrictions. + +Copyright (c) 02006, Alf Watt (alf@istumbler.net). All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of iStumbler nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="gsl"></a>GSL License</h1> + + <p>This license applies to <code>mfbt/Span.h</code> and + <code>mfbt/tests/gtest/TestSpan.cpp</code>.</p> + <!-- https://github.com/Microsoft/GSL/blob/3819df6e378ffccf0e29465afe99c3b324c2aa70/LICENSE --> +<pre> +Copyright (c) 2015 Microsoft Corporation. All rights reserved. + +This code is licensed under the MIT License (MIT). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="gyp"></a>gyp License</h1> + + <p>This license applies to certain files in the directory + <code>media/webrtc/trunk/tools/gyp</code>.</p> +<pre> +Copyright (c) 2009 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="halloc"></a>halloc License</h1> + + <p>This license applies to certain files in the directory + <code>media/libnestegg/src</code>.</p> +<pre> +Copyright (c) 2004-2010 Alex Pankratov. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the project nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="harfbuzz"></a>HarfBuzz License</h1> + + <p>This license, with different copyright holders, applies to the files in + the directory <code>gfx/harfbuzz/</code>. + The copyright holders and the applicable ranges of dates are as follows:</p> + + <ul> + <li>1998-2004 David Turner and Werner Lemberg</li> + <li>2004, 2007, 2008, 2009, 2010 Red Hat, Inc.</li> + <li>2006 Behdad Esfahbod</li> + <li>2007 Chris Wilson</li> + <li>2009 Keith Stribley <devel@thanlwinsoft.org></li> + <li>2010 Mozilla Foundation</li> + </ul> + +<pre> +Copyright (C) <date> <copyright holder> + + This is part of HarfBuzz, an OpenType Layout engine library. + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +</pre> + + + <hr> + + <h1><a id="icu"></a>ICU License</h1> + + <p>This license applies to some code in the + <code>gfx/thebes</code> directory.</p> + +<pre> +ICU License - ICU 1.8.1 and later + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1995-2012 International Business Machines Corporation and +others + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, provided that the above copyright notice(s) and this +permission notice appear in all copies of the Software and that both the +above copyright notice(s) and this permission notice appear in supporting +documentation. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization of the +copyright holder. +All trademarks and registered trademarks mentioned herein are the property +of their respective owners. +</pre> + <hr> + <h1><a id="immutable"></a>Immutable.js License</h1> + +<pre> +BSD License + +For Immutable JS software + +Copyright (c) 2014-2015, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="jpnic"></a>Japan Network Information Center License</h1> + <p>This license applies to certain files in the + directory <code>netwerk/dns/</code>.</p> +<pre> +Copyright (c) 2001,2002 Japan Network Information Center. +All rights reserved. + +By using this file, you agree to the terms and conditions set forth below. + + LICENSE TERMS AND CONDITIONS + +The following License Terms and Conditions apply, unless a different +license is obtained from Japan Network Information Center ("JPNIC"), +a Japanese association, Kokusai-Kougyou-Kanda Bldg 6F, 2-3-4 Uchi-Kanda, +Chiyoda-ku, Tokyo 101-0047, Japan. + +1. Use, Modification and Redistribution (including distribution of any + modified or derived work) in source and/or binary forms is permitted + under this License Terms and Conditions. + +2. Redistribution of source code must retain the copyright notices as they + appear in each source code file, this License Terms and Conditions. + +3. Redistribution in binary form must reproduce the Copyright Notice, + this License Terms and Conditions, in the documentation and/or other + materials provided with the distribution. For the purposes of binary + distribution the "Copyright Notice" refers to the following language: + "Copyright (c) 2000-2002 Japan Network Information Center. All rights + reserved." + +4. The name of JPNIC may not be used to endorse or promote products + derived from this Software without specific prior written approval of + JPNIC. + +5. Disclaimer/Limitation of Liability: THIS SOFTWARE IS PROVIDED BY JPNIC + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JPNIC BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +</pre> + + <hr> + + <h1><a id="jszip"></a>JSZip License</h1> + + <p>This license applies to the file + <code>devtools/client/shared/vendor/jszip.js</code>.</p> + +<pre> +Copyright (c) 2009-2016 Stuart Knightley, David Duponchel, Franz Buchinger, António Afonso + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + <hr> + + <h1><a id="jemalloc"></a>jemalloc License</h1> + + <p>This license applies to portions of the files in the directory + <code>memory/build/</code>. + </p> + +<pre> +Copyright (C) 2006-2008 Jason Evans <jasone@canonware.com>. +All rights reserved. +Copyright (C) 2007-2017 Mozilla Foundation. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice(s), this list of conditions and the following disclaimer as + the first lines of this file unmodified other than the possible + addition of one or more copyright notices. +2. Redistributions in binary form must reproduce the above copyright + notice(s), this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="jquery"></a>jQuery License</h1> + + <p>This license applies to all copies of jQuery in the code.</p> + +<pre> +Copyright (c) 2010 John Resig, https://jquery.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + <hr> + + <h1><a id="k_exp"></a>k_exp License</h1> + + <p>This license applies to the file + <code>modules/fdlibm/src/k_exp.cpp</code>. + </p> + +<pre> +Copyright (c) 2011 David Schultz <das@FreeBSD.ORG> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="kernel32-sys"></a>kernel32-sys License</h1> + + <p>This license applies to files in the directories + <code>third_party/rust/kernel32-sys</code> and + <code>third_party/rust/kernel32-sys-0.1.4</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2015 Peter Atashian + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="khronos"></a>Khronos group License</h1> + + <p>This license applies to the following files:</p> + + <ul> + <li><code>openmax_dl/dl/api/omxtypes.h</code></li> + <li><code>openmax_dl/dl/sp/api/omxSP.h</code></li> + </ul> + +<pre> +Copyright 2005-2008 The Khronos Group Inc. All Rights Reserved. + +These materials are protected by copyright laws and contain material +proprietary to the Khronos Group, Inc. You may use these materials +for implementing Khronos specifications, without altering or removing +any trademark, copyright or other notice from the specification. + +Khronos Group makes no, and expressly disclaims any, representations +or warranties, express or implied, regarding these materials, including, +without limitation, any implied warranties of merchantability or fitness +for a particular purpose or non-infringement of any intellectual property. +Khronos Group makes no, and expressly disclaims any, warranties, express +or implied, regarding the correctness, accuracy, completeness, timeliness, +and reliability of these materials. + +Under no circumstances will the Khronos Group, or any of its Promoters, +Contributors or Members or their respective partners, officers, directors, +employees, agents or representatives be liable for any damages, whether +direct, indirect, special or consequential damages for lost revenues, +lost profits, or otherwise, arising from or in connection with these +materials. + +Khronos and OpenMAX are trademarks of the Khronos Group Inc. +</pre> + + <hr> + + <h1><a id="kiss_fft"></a>Kiss FFT License</h1> + + <p>This license applies to files in the directory + <code>media/kiss_fft/</code>.</p> + +<pre> +Copyright (c) 2003-2010 Mark Borgerding + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the author nor the names of any contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="lazy_static"></a>lazy_static License</h1> + + <p>This license applies to files in the directories + <code>third_party/rust/lazy_static</code> and + <code>third_party/rust/lazy_static-0.1.6</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2014 Marvin Löbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + + <hr> + +#ifdef MOZ_USE_LIBCXX + <h1><a id="libc++"></a>libc++ License</h1> + + <p class="correctme">This license applies to the copy of libc++ obtained + from the Android NDK.</p> + +<pre> +Copyright (c) 2009-2014 by the contributors listed in the libc++ CREDITS.TXT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + <hr> + +#endif + + <h1><a id="libcubeb"></a>libcubeb License</h1> + + <p class="correctme">This license applies to files in the directory + <code>media/libcubeb</code>. + </p> + +<pre> +Copyright © 2011 Mozilla Foundation + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + <hr> + + <h1><a id="libevent"></a>libevent License</h1> + + <p>This license applies to files in the directory + <code>ipc/chromium/src/third_party/libevent/</code>. + </p> + +<pre> +Copyright (c) 2000-2007 Niels Provos <provos@citi.umich.edu> +Copyright (c) 2007-2012 Niels Provos and Nick Mathewson + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +============================== + +Portions of Libevent are based on works by others, also made available by +them under the three-clause BSD license above. The copyright notices are +available in the corresponding source files; the license is as above. Here's +a list: + +log.c: + Copyright (c) 2000 Dug Song <dugsong@monkey.org> + Copyright (c) 1993 The Regents of the University of California. + +strlcpy.c: + Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com> + +evport.c: + Copyright (c) 2007 Sun Microsystems + +ht-internal.h: + Copyright (c) 2002 Christopher Clark + +minheap-internal.h: + Copyright (c) 2006 Maxim Yegorushkin <maxim.yegorushkin@gmail.com> + +============================== + +The arc4module is available under the following, sometimes called the +"OpenBSD" license: + + Copyright (c) 1996, David Mazieres <dm@uun.org> + Copyright (c) 2008, Damien Miller <djm@openbsd.org> + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +</pre> + + + <hr> + + <h1><a id="libffi"></a>libffi License</h1> + + <p>This license applies to files in the directory + <code>js/src/ctypes/libffi/</code>. + </p> + +<pre> +libffi - Copyright (c) 1996-2008 Red Hat, Inc and others. +See source files for details. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +``Software''), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="libjingle"></a>libjingle License</h1> + + <p>This license applies to the following files:</p> + <ul> + <li><code>dom/media/webrtc/transport/sigslot.h</code></li> + <li><code>dom/media/webrtc/transport/test/gtest_utils.h</code></li> + </ul> + +<pre> +Copyright (c) 2004--2005, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="libnestegg"></a>libnestegg License</h1> + + <p>This license applies to certain files in the directory + <code>media/libnestegg</code>. + </p> + +<pre> +Copyright © 2010 Mozilla Foundation + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + <hr> + + <h1><a id="libsoundtouch"></a>libsoundtouch License</h1> + + <p>This license applies to certain files in the directory + <code>media/libsoundtouch/src/</code>. + </p> + +<pre> +The SoundTouch Library Copyright © Olli Parviainen 2001-2012 + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +</pre> + + <hr> + + <h1><a id="libyuv"></a>libyuv License</h1> + + <p>This license applies to files in the directory + <code>media/libyuv</code> except + for the file <code>media/libyuv/source/x86inc.asm</code>. + </p> + +<pre> +Copyright (c) 2011, The LibYuv project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="hunspell-lt"></a>Lithuanian Spellchecking Dictionary License</h1> + + <p>This license applies to the Lithuanian Spellchecking Dictionary. (This + code only ships in some localized versions of this product.)</p> + +<pre> +Copyright (c) 2000-2013, Albertas Agejevas and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holders nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL ALBERTAS AGEJEVAS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="lodash"></a>License - lodash</h1> + + <p>This license applies to some of the code in + <code>devtools/client/shared/vendor/lodash.js</code>.</p> + +<pre> +Copyright JS Foundation and other contributors <https://js.foundation/> + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors <https://underscorejs.org/> + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: https://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + +</pre> + + + <hr> + + <h1><a id="matches"></a>matches License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/matches</code>.</p> + +<pre> +Copyright (c) 2014-2016 Simon Sapin + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="myspell"></a>MySpell License</h1> + + <p>This license applies to some files in the directory + <code>extensions/spellcheck/hunspell</code>.</p> + +<pre> +Copyright 2002 Kevin B. Hendricks, Stratford, Ontario, Canada +And Contributors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. All modifications to the source code must be clearly marked as + such. Binary redistributions based on modified source code + must be clearly marked as modified versions in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY KEVIN B. HENDRICKS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +KEVIN B. HENDRICKS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="mqtt-packet"></a>mqtt-packet License</h1> + + <p>This license applies to the file + <code>devtools/client/netmonitor/src/components/messages/parsers/mqtt/index.js</code>.</p> + +<pre> +The MIT License (MIT) +===================== + +Copyright (c) 2014-2017 mqtt-packet contributors +--------------------------------------- + +*mqtt-packet contributors listed at <https://github.com/mqttjs/mqtt-packet#contributors>* + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + +<hr> + +<h1><a id="naturalSort"></a>naturalSort License</h1> + +<p>This license applies to <code>devtools/client/shared/natural-sort.js</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2014 Gabriel Llamas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + <hr> + + <h1><a id="nicer"></a>nICEr License</h1> + + <p>This license applies to certain files in the directory + <code>dom/media/webrtc/transport/third_party/nICEr</code>.</p> + +<pre> + Copyright (C) 2007, Adobe Systems Inc. + Copyright (C) 2007-2008, Network Resonance, Inc. + +Each source file bears an individual copyright notice. + +The following license applies to this distribution as a whole. + + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of Adobe Systems, Network Resonance nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="openldap"></a>OpenLDAP Public License</h1> + + <p>This license applies to certain files in the directory + <code>third_party/rust/lmdb-rkv-sys/lmdb/libraries/liblmdb</code>. + </p> + +<pre> +The OpenLDAP Public License + Version 2.8, 17 August 2003 + +Redistribution and use of this software and associated documentation +("Software"), with or without modification, are permitted provided +that the following conditions are met: + +1. Redistributions in source form must retain copyright statements + and notices, + +2. Redistributions in binary form must reproduce applicable copyright + statements and notices, this list of conditions, and the following + disclaimer in the documentation and/or other materials provided + with the distribution, and + +3. Redistributions must contain a verbatim copy of this document. + +The OpenLDAP Foundation may revise this license from time to time. +Each revision is distinguished by a version number. You may use +this Software under terms of this license revision or under the +terms of any subsequent revision of the license. + +THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS +CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S) +OR OWNER(S) OF THE SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The names of the authors and copyright holders must not be used in +advertising or otherwise to promote the sale, use or other dealing +in this Software without specific, written prior permission. Title +to copyright in this Software shall at all times remain with copyright +holders. + +OpenLDAP is a registered trademark of the OpenLDAP Foundation. + +Copyright 1999-2003 The OpenLDAP Foundation, Redwood City, +California, USA. All Rights Reserved. Permission to copy and +distribute verbatim copies of this document is granted. +</pre> + + + <hr> + + <h1><a id="openvision"></a>OpenVision License</h1> + + <p>This license applies to the file + <code>extensions/auth/gssapi.h</code>.</p> + +<pre> +Copyright 1993 by OpenVision Technologies, Inc. + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appears in all copies and +that both that copyright notice and this permission notice appear in +supporting documentation, and that the name of OpenVision not be used +in advertising or publicity pertaining to distribution of the software +without specific, written prior permission. OpenVision makes no +representations about the suitability of this software for any +purpose. It is provided "as is" without express or implied warranty. + +OPENVISION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL OPENVISION BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +</pre> + +#if defined(XP_WIN) || defined(XP_MACOSX) || defined(XP_LINUX) + + <hr> + + <h1><a id="openvr"></a>OpenVR License</h1> + + <p>This license applies to certain files in the directory + <code>gfx/vr/service/openvr</code>.</p> +<pre> +Copyright (c) 2015, Valve Corporation +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + +#endif + +<hr> + + <h1><a id="node-md5"></a>node-md5 License</h1> + + <p>This license applies to some of the code in + <code>devtools/client/debugger/debugger.js</code>.</p> + +<pre> +Copyright © 2011-2012, Paul Vorbach. +Copyright © 2009, Jeff Mott. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither the name Crypto-JS nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="node-properties"></a>node-properties License</h1> + + <p>This license applies to + <code>devtools/shared/node-properties/node-properties.js</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2014 Gabriel Llamas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="nom"></a>nom License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/nom</code>.</p> + +<pre> +Copyright (c) 2015 Geoffroy Couprie + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="nrappkit"></a>nrappkit License</h1> + + <p>This license applies to certain files in the directory + <code>dom/media/webrtc/transport/third_party/nrappkit</code>.</p> + +<pre> +Copyright (C) 2001-2007, Network Resonance, Inc. +All Rights Reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Network Resonance, Inc. nor the name of any + contributors to this software may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +</pre> + + <p>This license applies to certain files in the directory + <code>dom/media/webrtc/transport/third_party/nrappkit</code>.</p> + +<pre> +Copyright (C) 1999-2003 RTFM, Inc. +All Rights Reserved + +This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla +<ekr@rtfm.com> and licensed by RTFM, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + + This product includes software developed by Eric Rescorla for + RTFM, Inc. + +4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be + used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE ERIC RESCORLA AND RTFM ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +oDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + <p>Note that RTFM, Inc. has waived clause (3) above as of June 20, 2012 + for files appearing in this distribution. This waiver applies only to + files included in this distribution. it does not apply to any other + part of ssldump not included in this distribution.</p> + + <p>This license applies to the file <code>dom/media/webrtc/transport/third_party/nrappkit/src/port/generic/include/sys/queue.h</code>.</p> + +<pre> +Copyright (c) 1991, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + + <p>This license applies to the file: + <code>dom/media/webrtc/transport/third_party/nrappkit/src/util/util.c</code>.</p> + +<pre> +Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="praton"></a>praton License</h1> + + <p>This license applies to the file + <code>nsprpub/pr/src/misc/praton.c</code>.</p> + +<pre> +Copyright (c) 1983, 1990, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + + +Portions Copyright (c) 1993 by Digital Equipment Corporation. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies, and that +the name of Digital Equipment Corporation not be used in advertising or +publicity pertaining to distribution of the document or software without +specific, written prior permission. + +THE SOFTWARE IS PROVIDED "AS IS" AND DIGITAL EQUIPMENT CORP. DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DIGITAL EQUIPMENT +CORPORATION BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. +</pre> + + + <hr> + + <h1><a id="praton1"></a>praton and inet_ntop License</h1> + + <p>This license applies to the files + <code>nsprpub/pr/src/misc/praton.c</code> and + <code>dom/media/webrtc/transport/third_party/nrappkit/src/util/util.c</code>.</p> + +<pre> +Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC") +Portions Copyright (c) 1996-1999 by Internet Software Consortium. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + + <hr> + + <h1><a id="ordered-float"></a>ordered-float License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/ordered-float</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2015 Jonathan Reem + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="owning_ref"></a>owning_ref License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/owning_ref</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2015 Marvin Löbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + + <hr> + + <h1><a id="pbkdf2-sha256"></a>pbkdf2_sha256 License</h1> + + <p>This license applies to the code + <code>mozglue/android/pbkdf2_sha256.c</code> and + <code>mozglue/android/pbkdf2_sha256.h</code>. + </p> + +<pre> +Copyright 2005,2007,2009 Colin Percival +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="phf"></a>phf, phf_codegen, phf_generator, phf_shared License</h1> + + <p>This license applies to files in the directories + <code>third_party/rust/phf</code>, + <code>third_party/rust/phf_codegen</code>, + <code>third_party/rust/phf_generator</code>, and + <code>third_party/rust/phf_shared</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2014 Steven Fackler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + + <hr> + + <h1><a id="polymer"></a>Polymer License</h1> + + <p>This license applies to the file + <code>browser/components/payments/res/vendor/custom-elements.min.js</code>.</p> + +<pre> +Copyright (c) 2014 The Polymer Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="precomputed-hash"></a>precomputed-hash License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/precomputed-hash</code>.</p> + +<pre> +MIT License + +Copyright (c) 2017 Emilio Cobos Álvarez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + + <hr> + + <h1><a id="prop-types"></a>prop-types License</h1> + + <p>This license applies to the files + <code>browser/components/newtab/vendor/prop-types*</code>.</p> + +<pre> +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + + <hr> + + <h1><a id="qcms"></a>qcms License</h1> + + <p>This license applies to certain files in the directory + <code>gfx/qcms/</code>.</p> +<pre> +Copyright (C) 2009 Mozilla Corporation +Copyright (C) 1998-2007 Marti Maria + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject +to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="qrcode-generator"></a>QR Code Generator License</h1> + + <p>This license applies to certain files in the directory + <code>devtools/shared/qrcode/encoder/</code>.</p> +<pre> +Copyright (c) 2009 Kazuhiko Arase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + <hr> + + <h1><a id="raven-js"></a>Raven.js License</h1> + + <p>This license applies to the file + <code>browser/extensions/screenshots/webextension/build/raven.js</code>.</p> +<pre> +Copyright (c) 2014 Matt Robenolt and other contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="react"></a>React License</h1> + + <p>This license applies to various files in the Mozilla codebase.</p> + +<pre> +Copyright (c) 2013-2015, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1> + <a id="react-mit"></a>React MIT License</h1> + + <p>This license applies to various files in the Mozilla codebase.</p> + + <pre> +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + </pre> + + <hr> + + <h1><a id="react-redux"></a>React-Redux License</h1> + + <p>This license applies to the files + <code>devtools/client/shared/vendor/react-redux.js</code> and + <code>browser/components/newtab/vendor/react-redux.js</code>.</p> +<pre> +Copyright (c) 2015 Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + <hr> + + <h1><a id="react-router"></a>React Router License</h1> + + <p>This license applies to the file + <code>devtools/client/shared/vendor/react-router-dom.js</code>.</p> +<pre> +MIT License + +Copyright (c) React Training 2016-2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + <hr> + + <h1><a id="xdg"></a>Red Hat xdg_user_dir_lookup License</h1> + + <p>This license applies to the + <code>xdg_user_dir_lookup</code> function in + <code>xpcom/io/SpecialSystemDirectory.cpp</code>.</p> + +<pre> +Copyright (c) 2007 Red Hat, Inc. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + + <hr> + + <h1><a id="redox_syscall"></a>redox_syscall License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/redox_syscall</code>.</p> + +<pre> +MIT License + +Copyright (c) 2016 Jeremy Soller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + + <hr> + + <h1><a id="redux"></a>Redux License</h1> + + <p>This license applies to the file + <code>devtools/client/shared/vendor/redux.js</code> and + <code>browser/components/newtab/vendor/Redux.jsm</code>.</p> +<pre> +Copyright (c) 2015 Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + +<hr> + +<h1><a id="relative-time"></a>relative-time License</h1> + +<p>This license applies to the file +<code>toolkit/components/mozintl/mozIntl.js +</code>.</p> +<pre> +The MIT License (MIT) + +Copyright (c) 2016 Rafael Xavier de Souza http://rafael.xavier.blog.br + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +</pre> + +<hr> + +<h1><a id="reselect"></a>Reselect License</h1> + +<p>This license applies to the files + <code>devtools/client/shared/vendor/reselect.js</code> and + <code>browser/components/newtab/data/content/activity-stream.bundle.js</code>. +</p> +<pre> +The MIT License (MIT) + +Copyright (c) 2015-2018 Reselect Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + +<hr> + +<h1><a id="ring"></a>Ring License</h1> + +<p>This license applies to certain files in the directory + <code>third_party/rust/rc_crypto/</code> +</p> +<pre> +Copyright 2015-2017 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + <hr> + + <h1><a id="hunspell-ru"></a>Russian Spellchecking Dictionary License</h1> + + <p>This license applies to the Russian Spellchecking Dictionary. (This + code only ships in some localized versions of this product.)</p> + +<pre> +* Copyright (c) 1997-2008, Alexander I. Lebedev + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Modified versions must be clearly marked as such. +* The name of Alexander I. Lebedev may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="sctp"></a>SCTP Licenses</h1> + + <p>These licenses apply to certain files in the directory + <code>netwerk/sctp/src/</code>.</p> + +<pre> +Copyright (c) 2009-2010 Brad Penoff +Copyright (c) 2009-2010 Humaira Kamal +Copyright (c) 2011-2012 Irene Ruengeler +Copyright (c) 2010-2012, by Michael Tuexen. All rights reserved. +Copyright (c) 2010-2012, by Randall Stewart. All rights reserved. +Copyright (c) 2010-2012, by Robin Seggelmann. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +Copyright (c) 2001-2008, by Cisco Systems, Inc. All rights reserved. +Copyright (c) 2008-2012, by Randall Stewart. All rights reserved. +Copyright (c) 2008-2012, by Michael Tuexen. All rights reserved. +Copyright (c) 2008-2012, by Brad Penoff. All rights reserved. +Copyright (c) 1980, 1982, 1986, 1987, 1988, 1990, 1993 + The Regents of the University of California. +Copyright (c) 2005 Robert N. M. Watson All rights reserved. +Copyright (C) 1995, 1996, 1997, and 1998 WIDE Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +a) Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +b) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. +c) Neither the name of Cisco Systems, Inc, the name of the university, + the WIDE project, nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="skia"></a>Skia License</h1> + + <p>This license applies to certain files in the directory + <code>gfx/skia/</code>.</p> + +<pre> +Copyright (c) 2011 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. +* Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="snappy"></a>Snappy License</h1> + + <p>This license applies to certain files in the directory + <code>other-licenses/snappy/</code>.</p> + +<pre> +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="socket.io-parser"></a>Socket.IO Parser License</h1> + + <p>This license applies to the files<br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/socket-io/binary.js</code><br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/socket-io/index.js</code><br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/socket-io/is-buffer.js</code><br/> + </p> + +<pre> +(The MIT License) + +Copyright (c) 2014 Guillermo Rauch <guillermo@learnboost.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + <hr> + + <h1><a id="sockjs-client"></a>SockJS-Client License</h1> + + <p>This license applies to specific file + <code>devtools/client/netmonitor/src/components/messages/parsers/sockjs/index.js</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2011-2018 The sockjs-client Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + <hr> + + <h1><a id="sprintf.js"></a>sprintf.js License</h1> + + <p>This license applies to + <code>devtools/shared/sprintfjs/sprintf.js</code>.</p> + +<pre> +Copyright (c) 2007-2016, Alexandru Marasteanu <hello [at) alexei (dot] ro> +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of this software nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + <hr> + + <h1><a id="stompjs"></a>stompjs License</h1> + + <p>This license applies to the files<br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/stomp/byte.js</code><br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/stomp/frame.js</code><br/> + <code>devtools/client/netmonitor/src/components/messages/parsers/stomp/parser.js</code><br/> + +<pre> +Copyright (c) 2018 Deepak Kumar + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + <hr> + + <h1><a id="sunsoft"></a>SunSoft License</h1> + + <p>This license applies to the + <code>ICC_H</code> block in + <code>gfx/qcms/qcms.h</code>.</p> + +<pre> +Copyright (c) 1994-1996 SunSoft, Inc. + + Rights Reserved + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restrict- +ion, including without limitation the rights to use, copy, modify, +merge, publish distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON- +INFRINGEMENT. IN NO EVENT SHALL SUNSOFT, INC. OR ITS PARENT +COMPANY BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of SunSoft, Inc. +shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without written +authorization from SunSoft Inc. +</pre> + + + <hr> + + <h1><a id="superfasthash"></a>SuperFastHash License</h1> + + <p>This license applies to files in the directory + <code>security/sandbox/chromium/base/third_party/superfasthash/</code>.</p> + +<pre> +Copyright (c) 2010, Paul Hsieh +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither my name, Paul Hsieh, nor the names of any other contributors to the + code use may not be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="synstructure"></a>synstructure License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/synstructure</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2016 Nika Layzell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="unicase"></a>unicase License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/unicase</code>.</p> + +<pre> +Copyright (c) 2014-2015 Sean McArthur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="unicode"></a>Unicode License</h1> + + <p>This license applies to files in the <code>intl/icu</code> + and <code>intl/tzdata</code> directories and certain files in + the <code>js/src/util</code> directory.</p> + </p> + +<pre> +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2016 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in https://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +--------------------- + +Third-Party Software Licenses + +This section contains third-party software notices and/or additional +terms for licensed third-party software components included within ICU +libraries. + +1. ICU License - ICU 1.8.1 to ICU 57.1 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1995-2016 International Business Machines Corporation and others +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, provided that the above +copyright notice(s) and this permission notice appear in all copies of +the Software and that both the above copyright notice(s) and this +permission notice appear in supporting documentation. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY +SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, use +or other dealings in this Software without prior written authorization +of the copyright holder. + +All trademarks and registered trademarks mentioned herein are the +property of their respective owners. + +2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt) + + # The Google Chrome software developed by Google is licensed under + # the BSD license. Other software included in this distribution is + # provided under other licenses, as set forth below. + # + # The BSD License + # https://opensource.org/licenses/bsd-license.php + # Copyright (C) 2006-2008, Google Inc. + # + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions are met: + # + # Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # Redistributions in binary form must reproduce the above + # copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided with + # the distribution. + # Neither the name of Google Inc. nor the names of its + # contributors may be used to endorse or promote products derived from + # this software without specific prior written permission. + # + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + # + # + # The word list in cjdict.txt are generated by combining three word lists + # listed below with further processing for compound word breaking. The + # frequency is generated with an iterative training against Google web + # corpora. + # + # * Libtabe (Chinese) + # - https://sourceforge.net/project/?group_id=1519 + # - Its license terms and conditions are shown below. + # + # * IPADIC (Japanese) + # - http://chasen.aist-nara.ac.jp/chasen/distribution.html + # - Its license terms and conditions are shown below. + # + # ---------COPYING.libtabe ---- BEGIN-------------------- + # + # /* + # * Copyrighy (c) 1999 TaBE Project. + # * Copyright (c) 1999 Pai-Hsiang Hsiao. + # * All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the TaBE Project nor the names of its + # * contributors may be used to endorse or promote products derived + # * from this software without specific prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # /* + # * Copyright (c) 1999 Computer Systems and Communication Lab, + # * Institute of Information Science, Academia + # * Sinica. All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the Computer Systems and Communication Lab + # * nor the names of its contributors may be used to endorse or + # * promote products derived from this software without specific + # * prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # Copyright 1996 Chih-Hao Tsai @ Beckman Institute, + # University of Illinois + # c-tsai4@uiuc.edu http://casper.beckman.uiuc.edu/~c-tsai4 + # + # ---------------COPYING.libtabe-----END-------------------------------- + # + # + # ---------------COPYING.ipadic-----BEGIN------------------------------- + # + # Copyright 2000, 2001, 2002, 2003 Nara Institute of Science + # and Technology. All Rights Reserved. + # + # Use, reproduction, and distribution of this software is permitted. + # Any copy of this software, whether in its original form or modified, + # must include both the above copyright notice and the following + # paragraphs. + # + # Nara Institute of Science and Technology (NAIST), + # the copyright holders, disclaims all warranties with regard to this + # software, including all implied warranties of merchantability and + # fitness, in no event shall NAIST be liable for + # any special, indirect or consequential damages or any damages + # whatsoever resulting from loss of use, data or profits, whether in an + # action of contract, negligence or other tortuous action, arising out + # of or in connection with the use or performance of this software. + # + # A large portion of the dictionary entries + # originate from ICOT Free Software. The following conditions for ICOT + # Free Software applies to the current dictionary as well. + # + # Each User may also freely distribute the Program, whether in its + # original form or modified, to any third party or parties, PROVIDED + # that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear + # on, or be attached to, the Program, which is distributed substantially + # in the same form as set out herein and that such intended + # distribution, if actually made, will neither violate or otherwise + # contravene any of the laws and regulations of the countries having + # jurisdiction over the User or the intended distribution itself. + # + # NO WARRANTY + # + # The program was produced on an experimental basis in the course of the + # research and development conducted during the project and is provided + # to users as so produced on an experimental basis. Accordingly, the + # program is provided without any warranty whatsoever, whether express, + # implied, statutory or otherwise. The term "warranty" used herein + # includes, but is not limited to, any warranty of the quality, + # performance, merchantability and fitness for a particular purpose of + # the program and the nonexistence of any infringement or violation of + # any right of any third party. + # + # Each user of the program will agree and understand, and be deemed to + # have agreed and understood, that there is no warranty whatsoever for + # the program and, accordingly, the entire risk arising from or + # otherwise connected with the program is assumed by the user. + # + # Therefore, neither ICOT, the copyright holder, or any other + # organization that participated in or was otherwise related to the + # development of the program and their respective officials, directors, + # officers and other employees shall be held liable for any and all + # damages, including, without limitation, general, special, incidental + # and consequential damages, arising out of or otherwise in connection + # with the use or inability to use the program or any product, material + # or result produced or otherwise obtained by using the program, + # regardless of whether they have been advised of, or otherwise had + # knowledge of, the possibility of such damages at any time during the + # project or thereafter. Each user will be deemed to have agreed to the + # foregoing by his or her commencement of use of the program. The term + # "use" as used herein includes, but is not limited to, the use, + # modification, copying and distribution of the program and the + # production of secondary products from the program. + # + # In the case where the program, whether in its original form or + # modified, was distributed or delivered to or received by a user from + # any person, organization or entity other than ICOT, unless it makes or + # grants independently of ICOT any specific warranty to the user in + # writing, such person, organization or entity, will also be exempted + # from and not be held liable to the user for any such damages as noted + # above as far as the program is concerned. + # + # ---------------COPYING.ipadic-----END---------------------------------- + +3. Lao Word Break Dictionary Data (laodict.txt) + + # Copyright (c) 2013 International Business Machines Corporation + # and others. All Rights Reserved. + # + # Project: https://code.google.com/p/lao-dictionary/ + # Dictionary: https://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt + # License: https://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt + # (copied below) + # + # This file is derived from the above dictionary, with slight + # modifications. + # ---------------------------------------------------------------------- + # Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, + # are permitted provided that the following conditions are met: + # + # + # Redistributions of source code must retain the above copyright notice, this + # list of conditions and the following disclaimer. Redistributions in + # binary form must reproduce the above copyright notice, this list of + # conditions and the following disclaimer in the documentation and/or + # other materials provided with the distribution. + # + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # OF THE POSSIBILITY OF SUCH DAMAGE. + # -------------------------------------------------------------------------- + +4. Burmese Word Break Dictionary Data (burmesedict.txt) + + # Copyright (c) 2014 International Business Machines Corporation + # and others. All Rights Reserved. + # + # This list is part of a project hosted at: + # github.com/kanyawtech/myanmar-karen-word-lists + # + # -------------------------------------------------------------------------- + # Copyright (c) 2013, LeRoy Benjamin Sharon + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: Redistributions of source code must retain the above + # copyright notice, this list of conditions and the following + # disclaimer. Redistributions in binary form must reproduce the + # above copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided + # with the distribution. + # + # Neither the name Myanmar Karen Word Lists, nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS + # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + # SUCH DAMAGE. + # -------------------------------------------------------------------------- + +5. Time Zone Database + + ICU uses the public domain data and code derived from Time Zone +Database for its time zone support. The ownership of the TZ database +is explained in BCP 175: Procedure for Maintaining the Time Zone +Database section 7. + + # 7. Database Ownership + # + # The TZ database itself is not an IETF Contribution or an IETF + # document. Rather it is a pre-existing and regularly updated work + # that is in the public domain, and is intended to remain in the + # public domain. Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do + # not apply to the TZ Database or contributions that individuals make + # to it. Should any claims be made and substantiated against the TZ + # Database, the organization that is providing the IANA + # Considerations defined in this RFC, under the memorandum of + # understanding with the IETF, currently ICANN, may act in accordance + # with all competent court orders. No ownership claims will be made + # by ICANN or the IETF Trust on the database or the code. Any person + # making a contribution to the database or code waives all rights to + # future claims in that contribution or in the TZ Database.</pre> + + + <hr> + + <h1><a id="ucal"></a>University of California License</h1> + + <p>This license applies to the following files or, in the case of + directories, certain files in those directories:</p> + + <ul> + <li><code>dbm/</code></li> + <li><code>db/mork/src/morkQuickSort.cpp</code></li> + <li><code>xpcom/ds/nsQuickSort.cpp</code></li> + <li><code>nsprpub/pr/src/misc/praton.c</code></li> + <li><code>dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.c</code></li> + </ul> + +<pre> +Copyright (c) 1990, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +[3 Deleted as of 22nd July 1999; see + <a href="ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change">ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change</a> + for details] +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="unreachable"></a>unreachable License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/unreachable</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2015 Jonathan Reem + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="hunspell-en"></a>English Spellchecking Dictionary Licenses</h1> + + <p>These licenses apply to certain files in the directory + <code>extensions/spellcheck/locales/en-US/hunspell/</code>, and the + Canadian English dictionary. (This code only ships in some localized + versions of this product.)</p> + +<pre> +Different parts of the SCOWL English dictionaries are subject to the +following licenses as shown below. For additional details, sources, +credits, and public domain references, see <a href="https://searchfox.org/mozilla-central/source/extensions/spellcheck/locales/en-US/hunspell/README_en_US.txt">README.txt</a>. + +The collective work of the Spell Checking Oriented Word Lists (SCOWL) is under +the following copyright: + +Copyright 2000-2007 by Kevin Atkinson +Permission to use, copy, modify, distribute and sell these word lists, the +associated scripts, the output created from the scripts, and its documentation +for any purpose is hereby granted without fee, provided that the above +copyright notice appears in all copies and that both that copyright notice and +this permission notice appear in supporting documentation. Kevin Atkinson makes +no representations about the suitability of this array for any purpose. It is +provided "as is" without express or implied warranty. + +The WordNet database is under the following copyright: + +This software and database is being provided to you, the LICENSEE, by Princeton +University under the following license. By obtaining, using and/or copying +this software and database, you agree that you have read, understood, and will +comply with these terms and conditions: +Permission to use, copy, modify and distribute this software and database and +its documentation for any purpose and without fee or royalty is hereby granted, +provided that you agree to comply with the following copyright notice and +statements, including the disclaimer, and that the same appear on ALL copies of +the software, database and documentation, including modifications that you make +for internal use or for distribution. +WordNet 1.6 Copyright 1997 by Princeton University. All rights reserved. +THIS SOFTWARE AND DATABASE IS PROVIDED "AS IS" AND PRINCETON UNIVERSITY +MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF +EXAMPLE, BUT NOT LIMITATION, PRINCETON UNIVERSITY MAKES NO +REPRESENTATIONS OR WARRANTIES OF MERCHANT- ABILITY OR FITNESS FOR ANY +PARTICULAR PURPOSE OR THAT THE USE OF THE LICENSED SOFTWARE, DATABASE OR +DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, +TRADEMARKS OR OTHER RIGHTS. +The name of Princeton University or Princeton may not be used in advertising or +publicity pertaining to distribution of the software and/or database. Title to +copyright in this software, database and any associated documentation shall at +all times remain with Princeton University and LICENSEE agrees to preserve same. + +The "UK Advanced Cryptics Dictionary" is under the following copyright: + +Copyright (c) J Ross Beresford 1993-1999. All Rights Reserved. +The following restriction is placed on the use of this publication: if The UK +Advanced Cryptics Dictionary is used in a software package or redistributed in +any form, the copyright notice must be prominently displayed and the text of +this document must be included verbatim. There are no other restrictions: I +would like to see the list distributed as widely as possible. + +Various parts are under the Ispell copyright: + +Copyright 1993, Geoff Kuenning, Granada Hills, CA +All rights reserved. Redistribution and use in source and binary forms, with +or without modification, are permitted provided that the following conditions +are met: + 1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + 3. All modifications to the source code must be clearly marked as such. +Binary redistributions based on modified source code must be clearly marked as +modified versions in the documentation and/or other materials provided with +the distribution. + (clause 4 removed with permission from Geoff Kuenning) + 5. The name of Geoff Kuenning may not be used to endorse or promote products +derived from this software without specific prior written permission. + THIS SOFTWARE IS PROVIDED BY GEOFF KUENNING AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GEOFF KUENNING OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +Additional Contributors: + + Alan Beale <biljir@pobox.com> + M Cooper <thegrendel@theriver.com> +</pre> + + + <hr> + + <h1><a id="utf8-ranges"></a>utf8-ranges License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/utf8-ranges</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="void"></a>void License</h1> + + <p>This license applies to files in the directory + <code>third_party/rust/void</code>.</p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2015 Jonathan Reem + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +</pre> + + + <hr> + + <h1><a id="v8"></a>V8 License</h1> + + <p>This license applies to certain files in the directories + <code>js/src/irregexp</code>, + <code>js/src/builtin</code>, + <code>js/src/jit/arm</code> and + <code>js/src/jit/mips</code>. + </p> +<pre> +Copyright 2006-2012 the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + +<hr> + +<h1><a id="zydis"></a>zydis License</h1> + +<p>This license applies to all files in the directory + <code>js/src/zydis</code> unless otherwise specified. </p> + +<pre> +The MIT License (MIT) + +Copyright (c) 2014-2019 Florian Bernd +Copyright (c) 2014-2019 Joel Höner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + +<hr> + +<h1><a id="validator"></a>Validator License</h1> + +<p>This license applies to certain files in the directory + <code>devtools/shared/stringvalidator/</code>, +</p> +<pre> + +Copyright (c) 2016 Chris O"Hara <cohara87@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +</pre> + + <hr> + + + <h1><a id="vtune"></a>VTune License</h1> + + <p>This license applies to certain files in the directory + <code>js/src/vtune</code> and <code>tools/profiler/core/vtune</code>.</p> +<pre> +Copyright (c) 2011 Intel Corporation. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Intel Corporation nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="webrtc"></a>WebRTC License</h1> + + <p>This license applies to certain files in the directory + <code>media/webrtc/trunk</code>.</p> +<pre> +Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + +#ifdef MOZ_DEFAULT_BROWSER_AGENT + <h1><a id="WinToast"></a>WinToast License</h1> + <p>This license applies to all files in the directory + <code>third_party/WinToast</code> + unless otherwise specified.</p> + +<pre> +MIT License + +Copyright (c) 2016 Mohammed Boujemaoui Boulaghmoudi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</pre> + + <hr> +#endif + + <h1><a id="x264"></a>x264 License</h1> + + <p>This license applies to the file <code> + media/webrtc/trunk/third_party/libyuv/source/x86inc.asm</code>. + </p> + +<pre> +Copyright (C) 2005-2012 x264 project + +Authors: Loren Merritt <lorenm@u.washington.edu> + Anton Mitrofanov <BugMaster@narod.ru> + Jason Garrett-Glaser <darkshikari@gmail.com> + Henrik Gramner <hengar-6@student.ltu.se> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +</pre> + + <hr> + + <h1><a id="xiph"></a>Xiph.org Foundation License</h1> + + <p>This license applies to files in the following directories + with the specified copyright year ranges:</p> + <ul> + <li><code>media/libogg/</code>, 2002</li> + <li><code>media/libtheora/</code>, 2002-2007</li> + <li><code>media/libvorbis/</code>, 2002-2004</li> + <li><code>media/libtremor/</code>, 2002-2010</li> + <li><code>media/libspeex_resampler/</code>, 2002-2008</li> + </ul> + +<pre> +Copyright (c) <year>, Xiph.org Foundation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +- Neither the name of the Xiph.org Foundation nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +</pre> + + + <hr> + + <h1><a id="other-notices"></a>Other Required Notices</h1> + + <ul> + <li>This software is based in part on the work of the Independent + JPEG Group.</li> + <li>Portions of the OS/2 and Android versions + of this software are copyright ©1996-2012 + <a href="https://www.freetype.org/">The FreeType Project</a>. + All rights reserved.</li> + </ul> + + + <hr> + + <h1><a id="optional-notices"></a>Optional Notices</h1> + + <p>Some permissive software licenses request but do not require an + acknowledgement of the use of their software. We are very grateful + to the following people and projects for their contributions to + this product:</p> + + <ul> + <li>The <a href="https://www.zlib.net/">zlib</a> compression library + (Jean-loup Gailly, Mark Adler and team)</li> + <li>The <a href="http://www.libpng.org/pub/png/">libpng</a> graphics library + (Glenn Randers-Pehrson and team)</li> + <li>The <a href="https://www.sqlite.org/">sqlite</a> database engine + (D. Richard Hipp and team)</li> + <li>The <a href="http://nsis.sourceforge.net/">Nullsoft Scriptable Install System</a> + (Amir Szekely and team)</li> + <li>The <a href="https://mattmccutchen.net/bigint/">C++ Big Integer Library</a> + (Matt McCutchen)</li> + </ul> + + + +#ifdef XP_WIN + + <hr> + + <h1><a id="proprietary-notices"></a>Proprietary Operating System Components</h1> + + <p>Under some circumstances, under our + <a href="https://www.mozilla.org/foundation/licensing/binary-components/">binary components policy</a>, + Mozilla may decide to include additional + operating system vendor code with the installer of our products designed + for that vendor's proprietary platform, to make our products work well on + that specific operating system. The following license statements + apply to such inclusions.</p> + + <h2><a id="directx"></a>Microsoft Windows: Terms for 'Microsoft Distributable Code'</h2> + + <p>These terms apply to the following files; + they are referred to below as "Distributable Code": + <ul> + <li><code>d3d*.dll</code> (Direct3D libraries)</li> + <li><code>msvc*.dll</code> (C and C++ runtime libraries)</li> + </ul> + </p> + +<pre> +Copyright (c) Microsoft Corporation. + +The Distributable Code may be used and distributed only if you comply with the +following terms: + +(i) You may use, copy, and distribute the Distributable Code only as part of + this product; +(ii) You may not use the Distributable Code on a platform other than Windows; +(iii) You may not alter any copyright, trademark or patent notice in the + Distributable Code; +(iv) You may not modify or distribute the source code of any Distributable + Code so that any part of the source code becomes subject to the MPL or + any other copyleft license; +(v) You must comply with any technical limitations in the Distributable Code + that only allow you to use it in certain ways; and +(vi) You must comply with all domestic and international export laws and + regulations that apply to the Distributable Code. +</pre> + +#endif + +#ifdef APP_LICENSE_BODY_BLOCK +#ifndef APP_LICENSE_LIST_BLOCK +#error +#endif +<!-- List of product-specific licenses for non-Firefox apps. --> +#includesubst @APP_LICENSE_BODY_BLOCK@ +#endif + + <hr> + + <p><a href="about:license#top">Return to top</a>.</p> + </div> + </body> +</html> diff --git a/toolkit/content/macWindowMenu.js b/toolkit/content/macWindowMenu.js new file mode 100644 index 0000000000..6f479287b2 --- /dev/null +++ b/toolkit/content/macWindowMenu.js @@ -0,0 +1,43 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function macWindowMenuDidShow() { + let frag = document.createDocumentFragment(); + for (let win of Services.wm.getEnumerator("")) { + if (win.document.documentElement.getAttribute("inwindowmenu") == "false") { + continue; + } + let item = document.createXULElement("menuitem"); + item.setAttribute("label", win.document.title); + if (win == window) { + item.setAttribute("checked", "true"); + } + item.addEventListener("command", () => { + if (win.windowState == window.STATE_MINIMIZED) { + win.restore(); + } + win.focus(); + }); + frag.appendChild(item); + } + document.getElementById("windowPopup").appendChild(frag); +} + +function macWindowMenuDidHide() { + let sep = document.getElementById("sep-window-list"); + // Clear old items + while (sep.nextElementSibling) { + sep.nextElementSibling.remove(); + } +} + +function zoomWindow() { + if (window.windowState == window.STATE_NORMAL) { + window.maximize(); + } else { + window.restore(); + } +} diff --git a/toolkit/content/minimal-xul.css b/toolkit/content/minimal-xul.css new file mode 100644 index 0000000000..e9db5d92a7 --- /dev/null +++ b/toolkit/content/minimal-xul.css @@ -0,0 +1,231 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file should only contain a minimal set of rules for the XUL elements + * that may be implicitly created as part of HTML/SVG documents (e.g. + * scrollbars). Rules for everything else related to XUL can be found in + * xul.css. (This split of the XUL rules is to minimize memory use and improve + * performance in HTML/SVG documents.) + * + * ANYTHING ADDED TO THIS FILE WILL APPLY TO ALL DOCUMENTS, INCLUDING WEB CONTENT. + * IF UA RULES ARE ONLY NEEDED IN CHROME, THEY SHOULD BE ADDED TO xul.css. + */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ + +* { + -moz-user-focus: ignore; + user-select: none; + display: -moz-box; + box-sizing: border-box; +} + +/* hide the content and destroy the frame */ +[hidden="true"] { + display: none; +} + +/* hide the content, but don't destroy the frames */ +[collapsed="true"] { + visibility: collapse; +} + +/* Rules required for style caching of anonymous content scrollbar parts */ +@supports -moz-bool-pref("layout.css.cached-scrollbar-styles.enabled") { + :is(scrollcorner, resizer, scrollbar, scrollbarbutton, slider):where(:-moz-native-anonymous) { + /* All scrollbar parts must not inherit any properties from the scrollable + * element (except for visibility and pointer-events), for the anonymous + * content style caching system to work. + */ + all: initial; + visibility: inherit; + pointer-events: inherit; + + /* These properties are not included in 'all'. */ + -moz-list-reversed: initial; + -moz-font-smoothing-background-color: initial; + -moz-min-font-size-ratio: initial; + math-depth: initial; + /* As long as inert implies pointer-events: none as it does now, we're + * good. */ + -moz-inert: initial; + + /* Using initial is not sufficient for direction, since its initial value can + * depend on the document's language. + * + * LTR is what we want for all scrollbar parts anyway, so that e.g. we don't + * reverse the rendering of a horizontal scrollbar. + */ + direction: ltr; + + /* Similarly for font properties, whose initial values depend on the + * document's language. Scrollbar parts don't have any text or rely on + * font metrics. + */ + font: 16px sans-serif; + + /* The initial value of justify-items is `legacy`, which makes it depend on + * the parent style. + * + * Reset it to something else. + */ + justify-items: start; + + /* Avoid `object > *` rule in html.css from setting a useless, non-initial + * value of vertical-align. + */ + vertical-align: initial !important; + + /* Duplicate the rules from the '*' rule above, which were clobbered by the + * 'all: initial' declaration. + * + * The other zero specificity rules above are on :root, and scrollbar parts + * cannot match :root, so no need to duplicate them. + */ + -moz-user-focus: ignore; + user-select: none; + display: -moz-box; + box-sizing: border-box; + } + + /* There are other rules that set direction and cursor on scrollbar, + * expecting them to inherit into its children. Explicitly inherit those, + * overriding the 'all: initial; direction: ltr;' declarations above. + */ + :is(scrollbarbutton, slider, thumb):where(:-moz-native-anonymous) { + direction: inherit; + cursor: inherit; + } +} + +scrollbar[orient="vertical"], +slider[orient="vertical"], +thumb[orient="vertical"] { + -moz-box-orient: vertical; +} + +thumb { + /* Prevent -moz-user-modify declaration from designmode.css having an + * effect. */ + -moz-user-modify: initial; + + -moz-box-align: center; + -moz-box-pack: center; +} + +/********** resizer **********/ + +resizer { + position: relative; + z-index: 2147483647; + /* Widget gets decide on its own whether or not the native theme should apply, + based on the context/OS theme. If it does not, SVG background will kick in. */ + appearance: auto; + -moz-default-appearance: resizer; + + /* native resizer should never flip on its own; + we will flip it (or the SVG background) with CSS transform below. */ + direction: ltr; + writing-mode: initial; + + background: url("chrome://global/skin/icons/resizer.svg") no-repeat; + background-size: 100% 100%; + cursor: se-resize; + width: 15px; + height: 15px; +} + +/* bottomstart/bottomend is supported in XUL window only */ +resizer[dir="bottom"][flip], +resizer[dir="bottomleft"], +resizer[dir="bottomstart"]:-moz-locale-dir(ltr), +resizer[dir="bottomend"]:-moz-locale-dir(rtl) { + transform: scaleX(-1); +} + +resizer[dir="bottomleft"], +resizer[dir="bottomstart"]:-moz-locale-dir(ltr), +resizer[dir="bottomend"]:-moz-locale-dir(rtl) { + cursor: sw-resize; +} + +resizer[dir="top"], +resizer[dir="bottom"] { + cursor: ns-resize; +} + +resizer[dir="left"] { + transform: scaleX(-1); +} + +resizer[dir="left"], +resizer[dir="right"] { + cursor: ew-resize; +} + +resizer[dir="topleft"] { + cursor: nw-resize; +} + +resizer[dir="topright"] { + cursor: ne-resize; +} + +/********** scrollbar **********/ + +thumb { + display: -moz-box !important; +} + +/* Don't collapse thumb when scrollbar is disabled. */ +thumb[collapsed="true"] { + visibility: hidden; +} + +scrollbar, scrollbarbutton, scrollcorner, slider, thumb { + user-select: none; +} + +scrollcorner { + display: -moz-box !important; +} + +scrollcorner[hidden="true"] { + display: none !important; +} + +scrollbar[value="hidden"] { + visibility: hidden; +} + +@media (-moz-scrollbar-start-backward: 0) { + scrollbarbutton[sbattr="scrollbar-up-top"] { + display: none; + } +} + +@media (-moz-scrollbar-start-forward: 0) { + scrollbarbutton[sbattr="scrollbar-down-top"] { + display: none; + } +} + +@media (-moz-scrollbar-end-backward: 0) { + scrollbarbutton[sbattr="scrollbar-up-bottom"] { + display: none; + } +} + +@media (-moz-scrollbar-end-forward: 0) { + scrollbarbutton[sbattr="scrollbar-down-bottom"] { + display: none; + } +} + +@media (-moz-scrollbar-thumb-proportional) { + thumb { + -moz-box-flex: 1; + } +} diff --git a/toolkit/content/moz.build b/toolkit/content/moz.build new file mode 100644 index 0000000000..dd5705bd18 --- /dev/null +++ b/toolkit/content/moz.build @@ -0,0 +1,275 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +TEST_DIRS += ["tests"] + +for var in ("CC", "CC_VERSION", "CXX", "RUSTC", "RUSTC_VERSION"): + if CONFIG[var]: + DEFINES[var] = CONFIG[var] + +for var in ("MOZ_CONFIGURE_OPTIONS",): + DEFINES[var] = CONFIG[var] + +if CONFIG["MOZ_ANDROID_FAT_AAR_ARCHITECTURES"]: + DEFINES["target"] = "</td><td>".join( + sorted(CONFIG["MOZ_ANDROID_FAT_AAR_ARCHITECTURES"]) + ) +else: + DEFINES["target"] = CONFIG["target"] + +DEFINES["CFLAGS"] = " ".join(CONFIG["OS_CFLAGS"]) + +rustflags = CONFIG["RUSTFLAGS"] +if not rustflags: + rustflags = [] +DEFINES["RUSTFLAGS"] = " ".join(rustflags) + +cxx_flags = [] +for var in ("OS_CPPFLAGS", "OS_CXXFLAGS", "DEBUG", "OPTIMIZE", "FRAMEPTR"): + cxx_flags += COMPILE_FLAGS[var] or [] + +DEFINES["CXXFLAGS"] = " ".join(cxx_flags) + +if CONFIG["OS_TARGET"] == "Android": + DEFINES["ANDROID_PACKAGE_NAME"] = CONFIG["ANDROID_PACKAGE_NAME"] + DEFINES["MOZ_USE_LIBCXX"] = True + +if CONFIG["MOZ_INSTALL_TRACKING"]: + DEFINES["MOZ_INSTALL_TRACKING"] = 1 + +if CONFIG["MOZ_BUILD_APP"] == "mobile/android": + DEFINES["MOZ_FENNEC"] = True + +if CONFIG["OS_ARCH"] == "WINNT" and CONFIG["MOZ_DEFAULT_BROWSER_AGENT"]: + DEFINES["MOZ_DEFAULT_BROWSER_AGENT"] = True + +JAR_MANIFESTS += ["jar.mn"] + +SPHINX_TREES["toolkit_widgets"] = "widgets/docs" + +DEFINES["TOPOBJDIR"] = TOPOBJDIR + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("aboutwebrtc/*"): + BUG_COMPONENT = ("Core", "WebRTC") + +with Files("gmp-sources/*"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("tests/browser/browser_*autoplay*"): + BUG_COMPONENT = ("Core", "Audio/Video: Playback") + +with Files("tests/browser/*silent*"): + BUG_COMPONENT = ("Core", "Audio/Video: Playback") + +with Files("tests/browser/*1170531*"): + BUG_COMPONENT = ("Firefox", "Menus") + +with Files("tests/browser/*1198465*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/browser/*451286*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/browser/*594509*"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("tests/browser/*982298*"): + BUG_COMPONENT = ("Core", "Layout") + +with Files("tests/browser/browser_content_url_annotation.js"): + BUG_COMPONENT = ("Toolkit", "Crash Reporting") + +with Files("tests/browser/browser_default_image_filename.js"): + BUG_COMPONENT = ("Firefox", "File Handling") + +with Files("tests/browser/*caret*"): + BUG_COMPONENT = ("Firefox", "Keyboard Navigation") + +with Files("tests/browser/*find*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/browser/browser_isSynthetic.js"): + BUG_COMPONENT = ("Firefox", "Tabbed Browser") + +with Files("tests/browser/*mediaPlayback*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("tests/browser/*mute*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("tests/browser/*save*"): + BUG_COMPONENT = ("Firefox", "File Handling") + +with Files("tests/browser/*scroll*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("tests/chrome/**"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("tests/chrome/*networking*"): + BUG_COMPONENT = ("Core", "Networking") + +with Files("tests/chrome/*autocomplete*"): + BUG_COMPONENT = ("Toolkit", "Autocomplete") + +with Files("tests/chrome/*drop*"): + BUG_COMPONENT = ("Core", "DOM: Drag & Drop") + +with Files("tests/chrome/*1048178*"): + BUG_COMPONENT = ("Core", "XUL") + +with Files("tests/chrome/*263683*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/chrome/*304188*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/chrome/*331215*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/chrome/*360220*"): + BUG_COMPONENT = ("Core", "XUL") + +with Files("tests/chrome/*360437*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/chrome/*409624*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/chrome/*418874*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/chrome/*429723*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/chrome/*437844*"): + BUG_COMPONENT = ("Toolkit", "General") +with Files("tests/chrome/rtlchrome/**"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("tests/chrome/*451540*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/chrome/*557987*"): + BUG_COMPONENT = ("Firefox", "Menus") +with Files("tests/chrome/*562554*"): + BUG_COMPONENT = ("Firefox", "Menus") + +with Files("tests/chrome/*findbar*"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +with Files("tests/chrome/test_preferences*"): + BUG_COMPONENT = ("Toolkit", "Preferences") + +with Files("tests/mochitest/*autocomplete*"): + BUG_COMPONENT = ("Toolkit", "Autocomplete") + +with Files("tests/mochitest/*mousecapture*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("tests/reftests/*videocontrols*"): + BUG_COMPONENT = ("Toolkit", "Video/Audio Controls") + +with Files("tests/unit/**"): + BUG_COMPONENT = ("Toolkit", "General") + + +with Files("tests/widgets/*audiocontrols*"): + BUG_COMPONENT = ("Toolkit", "Video/Audio Controls") +with Files("tests/widgets/*898940*"): + BUG_COMPONENT = ("Toolkit", "Video/Audio Controls") + +with Files("tests/widgets/*contextmenu*"): + BUG_COMPONENT = ("Firefox", "Menus") + +with Files("tests/widgets/*editor*"): + BUG_COMPONENT = ("Core", "XUL") + +with Files("tests/widgets/*menubar*"): + BUG_COMPONENT = ("Core", "XUL") + +with Files("tests/widgets/*capture*"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("tests/widgets/*popup*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") +with Files("tests/widgets/*tree*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("tests/widgets/*videocontrols*"): + BUG_COMPONENT = ("Toolkit", "Video/Audio Controls") + +with Files("widgets/*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("TopLevelVideoDocument.js"): + BUG_COMPONENT = ("Toolkit", "Video/Audio Controls") + +with Files("about*"): + BUG_COMPONENT = ("Firefox", "General") + +with Files("aboutGlean.*"): + BUG_COMPONENT = ("Toolkit", "Telemetry") + +with Files("aboutNetworking*"): + BUG_COMPONENT = ("Core", "Networking") + +with Files("aboutProfile*"): + BUG_COMPONENT = ("Toolkit", "Startup and Profile System") + +with Files("aboutRights*"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("aboutService*"): + BUG_COMPONENT = ("Core", "DOM: Workers") + +with Files("aboutSupport*"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("aboutTelemetry*"): + BUG_COMPONENT = ("Toolkit", "Telemetry") + +with Files("autocomplete.css"): + BUG_COMPONENT = ("Firefox", "Search") + +with Files("browser-*.js"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("buildconfig.html"): + BUG_COMPONENT = ("Firefox Build System", "General") + +with Files("contentAreaUtils.js"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("*picker*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("edit*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("globalOverlay.*"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("minimal-xul.css"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("plugins*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("resetProfile*"): + BUG_COMPONENT = ("Firefox", "Migration") + +with Files("timepicker*"): + BUG_COMPONENT = ("Toolkit", "XUL Widgets") + +with Files("treeUtils.js"): + BUG_COMPONENT = ("Toolkit", "General") + +with Files("viewZoomOverlay.js"): + BUG_COMPONENT = ("Toolkit", "General") diff --git a/toolkit/content/mozilla.html b/toolkit/content/mozilla.html new file mode 100644 index 0000000000..9acb311324 --- /dev/null +++ b/toolkit/content/mozilla.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> + +<!-- 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/. --> + +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src chrome:; object-src 'none'" > + <meta charset='utf-8'> + <title data-l10n-id="about-mozilla-title-6-27"></title> + <link rel="stylesheet" href="chrome://global/content/aboutMozilla.css" type="text/css"> + <link rel="localization" href="toolkit/about/aboutMozilla.ftl"> +</head> + +<body> + <section> + <p id="moztext" data-l10n-id="about-mozilla-quote-6-27"></p> + <p id="from" data-l10n-id="about-mozilla-from-6-27"></p> + </section> +</body> +</html> diff --git a/toolkit/content/plugins.css b/toolkit/content/plugins.css new file mode 100644 index 0000000000..2ceb1b8297 --- /dev/null +++ b/toolkit/content/plugins.css @@ -0,0 +1,86 @@ +/* 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/. */ + +/* ===== plugins.css ===================================================== + == Styles used by the about:plugins page. + ======================================================================= */ + +body { + background-color: Field; + color: FieldText; + font: message-box; +} + +div#outside { + text-align: justify; + width: 90%; + margin-inline: 5%; +} + +#plugs { + text-align: center; + font-size: xx-large; + font-weight: bold; +} + +#noplugs { + font-size: x-large; + font-weight: bold; +} + +.plugname { + margin-block: 2em 1em; + font-size: large; + text-align: start; + font-weight: bold; +} + +dl { + margin: 0 0 3px; +} + +table { + background-color: -moz-Dialog; + color: -moz-DialogText; + font: message-box; + text-align: start; + width: 100%; + border: 1px solid ThreeDShadow; + border-spacing: 0; +} + +th, td { + border: none; + padding: 3px; +} + +th { + text-align: center; + background-color: Highlight; + color: HighlightText; +} + +th + th, +td + td { + border-inline-start: 1px dotted ThreeDShadow; +} + +td { + text-align: start; + border-top: 1px dotted ThreeDShadow; +} + +th.type, th.suff { + width: 25%; +} + +th.desc { + width: 50%; +} + +.notice { + background: -moz-cellhighlight; + border: 1px solid ThreeDShadow; + padding: 10px; +} diff --git a/toolkit/content/plugins.html b/toolkit/content/plugins.html new file mode 100644 index 0000000000..e9e4276f75 --- /dev/null +++ b/toolkit/content/plugins.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> + +<!-- 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/. --> + +<html> +<head> +<meta http-equiv="Content-Security-Policy" content="default-src chrome: resource:; object-src 'none'" /> +<title data-l10n-id="title-label"></title> +<link rel="stylesheet" type="text/css" href="chrome://global/content/plugins.css"> +<link rel="stylesheet" type="text/css" href="chrome://global/skin/plugins.css"> +<link rel="localization" href="toolkit/about/aboutPlugins.ftl"/> +</head> +<body> +<div id="outside"> +<script type="application/javascript" src="chrome://global/content/plugins.js"></script> +</div> +</body> +</html> diff --git a/toolkit/content/plugins.js b/toolkit/content/plugins.js new file mode 100644 index 0000000000..3395cd6955 --- /dev/null +++ b/toolkit/content/plugins.js @@ -0,0 +1,197 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +/* JavaScript to enumerate and display all installed plug-ins + + * First, refresh plugins in case anything has been changed recently in + * prefs: (The "false" argument tells refresh not to reload or activate + * any plug-ins that would be active otherwise. In contrast, one would + * use "true" in the case of ASD instead of restarting) + */ +navigator.plugins.refresh(false); + +RPMSendQuery("RequestPlugins", {}).then(aPlugins => { + var fragment = document.createDocumentFragment(); + + // "Installed plugins" + var id, label; + if (aPlugins.length) { + id = "plugs"; + label = "installed-plugins-label"; + } else { + id = "noplugs"; + label = "no-plugins-are-installed-label"; + } + var enabledplugins = document.createElement("h1"); + enabledplugins.setAttribute("id", id); + document.l10n.setAttributes(enabledplugins, label); + fragment.appendChild(enabledplugins); + + var deprecation = document.createElement("p"); + var deprecationLink = document.createElement("a"); + let deprecationLink_href = + Services.urlFormatter.formatURLPref("app.support.baseURL") + "npapi"; + deprecationLink.setAttribute("data-l10n-name", "deprecation-link"); + deprecationLink.setAttribute("href", deprecationLink_href); + deprecation.appendChild(deprecationLink); + deprecation.setAttribute("class", "notice"); + document.l10n.setAttributes(deprecation, "deprecation-description"); + fragment.appendChild(deprecation); + + var stateNames = {}; + [ + "STATE_SOFTBLOCKED", + "STATE_BLOCKED", + "STATE_OUTDATED", + "STATE_VULNERABLE_UPDATE_AVAILABLE", + "STATE_VULNERABLE_NO_UPDATE", + ].forEach(function(label) { + stateNames[Ci.nsIBlocklistService[label]] = label; + }); + + for (var i = 0; i < aPlugins.length; i++) { + var plugin = aPlugins[i]; + if (plugin) { + // "Shockwave Flash" + var plugname = document.createElement("h2"); + plugname.setAttribute("class", "plugname"); + plugname.appendChild(document.createTextNode(plugin.name)); + fragment.appendChild(plugname); + + var dl = document.createElement("dl"); + fragment.appendChild(dl); + + // "File: Flash Player.plugin" + var fileDd = document.createElement("dd"); + var file = document.createElement("span"); + file.setAttribute("data-l10n-name", "file"); + file.setAttribute("class", "label"); + fileDd.appendChild(file); + document.l10n.setAttributes(fileDd, "file-dd", { + pluginLibraries: plugin.pluginLibraries[0], + }); + dl.appendChild(fileDd); + + // "Path: /usr/lib/mozilla/plugins/libtotem-cone-plugin.so" + var pathDd = document.createElement("dd"); + var path = document.createElement("span"); + path.setAttribute("data-l10n-name", "path"); + path.setAttribute("class", "label"); + pathDd.appendChild(path); + document.l10n.setAttributes(pathDd, "path-dd", { + pluginFullPath: plugin.pluginFullpath[0], + }); + dl.appendChild(pathDd); + + // "Version: " + var versionDd = document.createElement("dd"); + var version = document.createElement("span"); + version.setAttribute("data-l10n-name", "version"); + version.setAttribute("class", "label"); + versionDd.appendChild(version); + document.l10n.setAttributes(versionDd, "version-dd", { + version: plugin.version, + }); + dl.appendChild(versionDd); + + // "State: " + var stateDd = document.createElement("dd"); + var state = document.createElement("span"); + state.setAttribute("data-l10n-name", "state"); + state.setAttribute("label", "state"); + stateDd.appendChild(state); + if (plugin.isActive) { + if (plugin.blocklistState in stateNames) { + document.l10n.setAttributes( + stateDd, + "state-dd-enabled-block-list-state", + { blockListState: stateNames[plugin.blocklistState] } + ); + } else { + document.l10n.setAttributes(stateDd, "state-dd-enabled"); + } + } else if (plugin.blocklistState in stateNames) { + document.l10n.setAttributes( + stateDd, + "state-dd-disabled-block-list-state", + { blockListState: stateNames[plugin.blocklistState] } + ); + } else { + document.l10n.setAttributes(stateDd, "state-dd-disabled"); + } + dl.appendChild(stateDd); + + // Plugin Description + var descDd = document.createElement("dd"); + descDd.appendChild(document.createTextNode(plugin.description)); + dl.appendChild(descDd); + + // MIME Type table + var mimetypeTable = document.createElement("table"); + mimetypeTable.setAttribute("border", "1"); + mimetypeTable.setAttribute("class", "contenttable"); + fragment.appendChild(mimetypeTable); + + var thead = document.createElement("thead"); + mimetypeTable.appendChild(thead); + var tr = document.createElement("tr"); + thead.appendChild(tr); + + // "MIME Type" column header + var typeTh = document.createElement("th"); + typeTh.setAttribute("class", "type"); + document.l10n.setAttributes(typeTh, "mime-type-label"); + tr.appendChild(typeTh); + + // "Description" column header + var descTh = document.createElement("th"); + descTh.setAttribute("class", "desc"); + document.l10n.setAttributes(descTh, "description-label"); + tr.appendChild(descTh); + + // "Suffixes" column header + var suffixesTh = document.createElement("th"); + suffixesTh.setAttribute("class", "suff"); + document.l10n.setAttributes(suffixesTh, "suffixes-label"); + tr.appendChild(suffixesTh); + + var tbody = document.createElement("tbody"); + mimetypeTable.appendChild(tbody); + + var mimeTypes = plugin.pluginMimeTypes; + for (var j = 0; j < mimeTypes.length; j++) { + var mimetype = mimeTypes[j]; + if (mimetype) { + var mimetypeRow = document.createElement("tr"); + tbody.appendChild(mimetypeRow); + + // "application/x-shockwave-flash" + var typename = document.createElement("td"); + typename.appendChild(document.createTextNode(mimetype.type)); + mimetypeRow.appendChild(typename); + + // "Shockwave Flash" + var description = document.createElement("td"); + description.appendChild( + document.createTextNode(mimetype.description) + ); + mimetypeRow.appendChild(description); + + // "swf" + var suffixes = document.createElement("td"); + suffixes.appendChild(document.createTextNode(mimetype.suffixes)); + mimetypeRow.appendChild(suffixes); + } + } + } + } + + document.getElementById("outside").appendChild(fragment); +}); diff --git a/toolkit/content/preferencesBindings.js b/toolkit/content/preferencesBindings.js new file mode 100644 index 0000000000..274d726443 --- /dev/null +++ b/toolkit/content/preferencesBindings.js @@ -0,0 +1,665 @@ +/* - This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// We attach Preferences to the window object so other contexts (tests, JSMs) +// have access to it. +const Preferences = (window.Preferences = (function() { + const { EventEmitter } = ChromeUtils.import( + "resource://gre/modules/EventEmitter.jsm" + ); + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + + const lazy = {}; + ChromeUtils.defineModuleGetter( + lazy, + "DeferredTask", + "resource://gre/modules/DeferredTask.jsm" + ); + + function getElementsByAttribute(name, value) { + // If we needed to defend against arbitrary values, we would escape + // double quotes (") and escape characters (\) in them, i.e.: + // ${value.replace(/["\\]/g, '\\$&')} + return value + ? document.querySelectorAll(`[${name}="${value}"]`) + : document.querySelectorAll(`[${name}]`); + } + + const domContentLoadedPromise = new Promise(resolve => { + window.addEventListener("DOMContentLoaded", resolve, { + capture: true, + once: true, + }); + }); + + const Preferences = { + _all: {}, + + _add(prefInfo) { + if (this._all[prefInfo.id]) { + throw new Error(`preference with id '${prefInfo.id}' already added`); + } + const pref = new Preference(prefInfo); + this._all[pref.id] = pref; + domContentLoadedPromise.then(() => { + pref.updateElements(); + }); + return pref; + }, + + add(prefInfo) { + const pref = this._add(prefInfo); + return pref; + }, + + addAll(prefInfos) { + prefInfos.map(prefInfo => this._add(prefInfo)); + }, + + get(id) { + return this._all[id] || null; + }, + + getAll() { + return Object.values(this._all); + }, + + defaultBranch: Services.prefs.getDefaultBranch(""), + + get type() { + return document.documentElement.getAttribute("type") || ""; + }, + + get instantApply() { + // The about:preferences page forces instantApply. + if (this._instantApplyForceEnabled) { + return true; + } + + // Dialogs of type="child" are never instantApply. + if (this.type === "child") { + return false; + } + + // All other pref windows observe the value of the instantApply + // preference. Note that, as of this writing, the only such windows + // are in tests, so it should be possible to remove the pref + // (and forceEnableInstantApply) in favor of always applying in a parent + // and never applying in a child. + return Services.prefs.getBoolPref("browser.preferences.instantApply"); + }, + + _instantApplyForceEnabled: false, + + // Override the computed value of instantApply for this window. + forceEnableInstantApply() { + this._instantApplyForceEnabled = true; + }, + + observe(subject, topic, data) { + const pref = this._all[data]; + if (pref) { + pref.value = pref.valueFromPreferences; + } + }, + + onDOMContentLoaded() { + // Iterate elements with a "preference" attribute and log an error + // if there isn't a corresponding Preference object in order to catch + // any cases of elements referencing preferences that haven't (yet?) + // been registered. + // + // TODO: remove this code once we determine that there are no such + // elements (or resolve any bugs that cause this behavior). + // + const elements = getElementsByAttribute("preference"); + for (const element of elements) { + const id = element.getAttribute("preference"); + const pref = this.get(id); + if (!pref) { + console.error(`Missing preference for ID ${id}`); + } + } + }, + + updateQueued: false, + + updateAllElements() { + if (this.updateQueued) { + return; + } + + this.updateQueued = true; + + Promise.resolve().then(() => { + const preferences = Preferences.getAll(); + for (const preference of preferences) { + preference.updateElements(); + } + this.updateQueued = false; + }); + }, + + onUnload() { + Services.prefs.removeObserver("", this); + }, + + QueryInterface: ChromeUtils.generateQI(["nsITimerCallback", "nsIObserver"]), + + _deferredValueUpdateElements: new Set(), + + writePreferences(aFlushToDisk) { + // Write all values to preferences. + if (this._deferredValueUpdateElements.size) { + this._finalizeDeferredElements(); + } + + const preferences = Preferences.getAll(); + for (const preference of preferences) { + preference.batching = true; + preference.valueFromPreferences = preference.value; + preference.batching = false; + } + if (aFlushToDisk) { + Services.prefs.savePrefFile(null); + } + }, + + getPreferenceElement(aStartElement) { + let temp = aStartElement; + while ( + temp && + temp.nodeType == Node.ELEMENT_NODE && + !temp.hasAttribute("preference") + ) { + temp = temp.parentNode; + } + return temp && temp.nodeType == Node.ELEMENT_NODE ? temp : aStartElement; + }, + + _deferredValueUpdate(aElement) { + delete aElement._deferredValueUpdateTask; + const prefID = aElement.getAttribute("preference"); + const preference = Preferences.get(prefID); + const prefVal = preference.getElementValue(aElement); + preference.value = prefVal; + this._deferredValueUpdateElements.delete(aElement); + }, + + _finalizeDeferredElements() { + for (const el of this._deferredValueUpdateElements) { + if (el._deferredValueUpdateTask) { + el._deferredValueUpdateTask.finalize(); + } + } + }, + + userChangedValue(aElement) { + const element = this.getPreferenceElement(aElement); + if (element.hasAttribute("preference")) { + if (element.getAttribute("delayprefsave") != "true") { + const preference = Preferences.get( + element.getAttribute("preference") + ); + const prefVal = preference.getElementValue(element); + preference.value = prefVal; + } else { + if (!element._deferredValueUpdateTask) { + element._deferredValueUpdateTask = new lazy.DeferredTask( + this._deferredValueUpdate.bind(this, element), + 1000 + ); + this._deferredValueUpdateElements.add(element); + } else { + // Each time the preference is changed, restart the delay. + element._deferredValueUpdateTask.disarm(); + } + element._deferredValueUpdateTask.arm(); + } + } + }, + + onCommand(event) { + // This "command" event handler tracks changes made to preferences by + // the user in this window. + if (event.sourceEvent) { + event = event.sourceEvent; + } + this.userChangedValue(event.target); + }, + + onChange(event) { + // This "change" event handler tracks changes made to preferences by + // the user in this window. + this.userChangedValue(event.target); + }, + + onInput(event) { + // This "input" event handler tracks changes made to preferences by + // the user in this window. + this.userChangedValue(event.target); + }, + + _fireEvent(aEventName, aTarget) { + try { + const event = new CustomEvent(aEventName, { + bubbles: true, + cancelable: true, + }); + return aTarget.dispatchEvent(event); + } catch (e) { + Cu.reportError(e); + } + return false; + }, + + onDialogAccept(event) { + let dialog = document.querySelector("dialog"); + if (!this._fireEvent("beforeaccept", dialog)) { + event.preventDefault(); + return false; + } + this.writePreferences(true); + return true; + }, + + close(event) { + if (Preferences.instantApply) { + window.close(); + } + event.stopPropagation(); + event.preventDefault(); + }, + + handleEvent(event) { + switch (event.type) { + case "change": + return this.onChange(event); + case "command": + return this.onCommand(event); + case "dialogaccept": + return this.onDialogAccept(event); + case "input": + return this.onInput(event); + case "unload": + return this.onUnload(event); + default: + return undefined; + } + }, + + _syncFromPrefListeners: new WeakMap(), + _syncToPrefListeners: new WeakMap(), + + addSyncFromPrefListener(aElement, callback) { + this._syncFromPrefListeners.set(aElement, callback); + // Make sure elements are updated correctly with the listener attached. + let elementPref = aElement.getAttribute("preference"); + if (elementPref) { + let pref = this.get(elementPref); + if (pref) { + pref.updateElements(); + } + } + }, + + addSyncToPrefListener(aElement, callback) { + this._syncToPrefListeners.set(aElement, callback); + // Make sure elements are updated correctly with the listener attached. + let elementPref = aElement.getAttribute("preference"); + if (elementPref) { + let pref = this.get(elementPref); + if (pref) { + pref.updateElements(); + } + } + }, + + removeSyncFromPrefListener(aElement) { + this._syncFromPrefListeners.delete(aElement); + }, + + removeSyncToPrefListener(aElement) { + this._syncToPrefListeners.delete(aElement); + }, + }; + + Services.prefs.addObserver("", Preferences); + domContentLoadedPromise.then(result => + Preferences.onDOMContentLoaded(result) + ); + window.addEventListener("change", Preferences); + window.addEventListener("command", Preferences); + window.addEventListener("dialogaccept", Preferences); + window.addEventListener("input", Preferences); + window.addEventListener("select", Preferences); + window.addEventListener("unload", Preferences, { once: true }); + + class Preference extends EventEmitter { + constructor({ id, type, inverted }) { + super(); + this.on("change", this.onChange.bind(this)); + + this._value = null; + this.readonly = false; + this._useDefault = false; + this.batching = false; + + this.id = id; + this.type = type; + this.inverted = !!inverted; + + // In non-instant apply mode, we must try and use the last saved state + // from any previous opens of a child dialog instead of the value from + // preferences, to pick up any edits a user may have made. + + if ( + Preferences.type == "child" && + window.opener && + window.opener.Preferences && + window.opener.document.nodePrincipal.isSystemPrincipal + ) { + // Try to find the preference in the parent window. + const preference = window.opener.Preferences.get(this.id); + + // Don't use the value setter here, we don't want updateElements to be + // prematurely fired. + this._value = preference ? preference.value : this.valueFromPreferences; + } else { + this._value = this.valueFromPreferences; + } + } + + reset() { + // defer reset until preference update + this.value = undefined; + } + + _reportUnknownType() { + const msg = `Preference with id=${this.id} has unknown type ${this.type}.`; + Services.console.logStringMessage(msg); + } + + setElementValue(aElement) { + if (this.locked) { + aElement.disabled = true; + } + + if (!this.isElementEditable(aElement)) { + return; + } + + let rv = undefined; + + if (Preferences._syncFromPrefListeners.has(aElement)) { + rv = Preferences._syncFromPrefListeners.get(aElement)(aElement); + } + let val = rv; + if (val === undefined) { + val = Preferences.instantApply ? this.valueFromPreferences : this.value; + } + // if the preference is marked for reset, show default value in UI + if (val === undefined) { + val = this.defaultValue; + } + + /** + * Initialize a UI element property with a value. Handles the case + * where an element has not yet had a XBL binding attached for it and + * the property setter does not yet exist by setting the same attribute + * on the XUL element using DOM apis and assuming the element's + * constructor or property getters appropriately handle this state. + */ + function setValue(element, attribute, value) { + if (attribute in element) { + element[attribute] = value; + } else if (attribute === "checked") { + // The "checked" attribute can't simply be set to the specified value; + // it has to be set if the value is true and removed if the value + // is false in order to be interpreted correctly by the element. + if (value) { + // In theory we can set it to anything; however xbl implementation + // of `checkbox` only works with "true". + element.setAttribute(attribute, "true"); + } else { + element.removeAttribute(attribute); + } + } else { + element.setAttribute(attribute, value); + } + } + if (aElement.localName == "checkbox") { + setValue(aElement, "checked", val); + } else { + setValue(aElement, "value", val); + } + } + + getElementValue(aElement) { + if (Preferences._syncToPrefListeners.has(aElement)) { + try { + const rv = Preferences._syncToPrefListeners.get(aElement)(aElement); + if (rv !== undefined) { + return rv; + } + } catch (e) { + Cu.reportError(e); + } + } + + /** + * Read the value of an attribute from an element, assuming the + * attribute is a property on the element's node API. If the property + * is not present in the API, then assume its value is contained in + * an attribute, as is the case before a binding has been attached. + */ + function getValue(element, attribute) { + if (attribute in element) { + return element[attribute]; + } + return element.getAttribute(attribute); + } + let value; + if (aElement.localName == "checkbox") { + value = getValue(aElement, "checked"); + } else { + value = getValue(aElement, "value"); + } + + switch (this.type) { + case "int": + return parseInt(value, 10) || 0; + case "bool": + return typeof value == "boolean" ? value : value == "true"; + } + return value; + } + + isElementEditable(aElement) { + switch (aElement.localName) { + case "checkbox": + case "input": + case "radiogroup": + case "textarea": + case "menulist": + return true; + } + return false; + } + + updateElements() { + if (!this.id) { + return; + } + + // This "change" event handler tracks changes made to preferences by + // sources other than the user in this window. + const elements = getElementsByAttribute("preference", this.id); + for (const element of elements) { + this.setElementValue(element); + } + } + + onChange() { + this.updateElements(); + } + + get value() { + return this._value; + } + + set value(val) { + if (this.value !== val) { + this._value = val; + if (Preferences.instantApply) { + this.valueFromPreferences = val; + } + this.emit("change"); + } + return val; + } + + get locked() { + return Services.prefs.prefIsLocked(this.id); + } + + updateControlDisabledState(val) { + if (!this.id) { + return; + } + + val = val || this.locked; + + const elements = getElementsByAttribute("preference", this.id); + for (const element of elements) { + element.disabled = val; + + const labels = getElementsByAttribute("control", element.id); + for (const label of labels) { + label.disabled = val; + } + } + } + + get hasUserValue() { + return ( + Services.prefs.prefHasUserValue(this.id) && this.value !== undefined + ); + } + + get defaultValue() { + this._useDefault = true; + const val = this.valueFromPreferences; + this._useDefault = false; + return val; + } + + get _branch() { + return this._useDefault ? Preferences.defaultBranch : Services.prefs; + } + + get valueFromPreferences() { + try { + // Force a resync of value with preferences. + switch (this.type) { + case "int": + return this._branch.getIntPref(this.id); + case "bool": { + const val = this._branch.getBoolPref(this.id); + return this.inverted ? !val : val; + } + case "wstring": + return this._branch.getComplexValue( + this.id, + Ci.nsIPrefLocalizedString + ).data; + case "string": + case "unichar": + return this._branch.getStringPref(this.id); + case "fontname": { + const family = this._branch.getStringPref(this.id); + const fontEnumerator = Cc[ + "@mozilla.org/gfx/fontenumerator;1" + ].createInstance(Ci.nsIFontEnumerator); + return fontEnumerator.getStandardFamilyName(family); + } + case "file": { + const f = this._branch.getComplexValue(this.id, Ci.nsIFile); + return f; + } + default: + this._reportUnknownType(); + } + } catch (e) {} + return null; + } + + set valueFromPreferences(val) { + // Exit early if nothing to do. + if (this.readonly || this.valueFromPreferences == val) { + return val; + } + + // The special value undefined means 'reset preference to default'. + if (val === undefined) { + Services.prefs.clearUserPref(this.id); + return val; + } + + // Force a resync of preferences with value. + switch (this.type) { + case "int": + Services.prefs.setIntPref(this.id, val); + break; + case "bool": + Services.prefs.setBoolPref(this.id, this.inverted ? !val : val); + break; + case "wstring": { + const pls = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ); + pls.data = val; + Services.prefs.setComplexValue( + this.id, + Ci.nsIPrefLocalizedString, + pls + ); + break; + } + case "string": + case "unichar": + case "fontname": + Services.prefs.setStringPref(this.id, val); + break; + case "file": { + let lf; + if (typeof val == "string") { + lf = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + lf.persistentDescriptor = val; + if (!lf.exists()) { + lf.initWithPath(val); + } + } else { + lf = val.QueryInterface(Ci.nsIFile); + } + Services.prefs.setComplexValue(this.id, Ci.nsIFile, lf); + break; + } + default: + this._reportUnknownType(); + } + if (!this.batching) { + Services.prefs.savePrefFile(null); + } + return val; + } + } + + return Preferences; +})()); diff --git a/toolkit/content/process-content.js b/toolkit/content/process-content.js new file mode 100644 index 0000000000..65cf64df1f --- /dev/null +++ b/toolkit/content/process-content.js @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Creates a new PageListener for this process. This will listen for page loads +// and for those that match URLs provided by the parent process will set up +// a dedicated message port and notify the parent process. +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +Services.cpmm.addMessageListener("gmp-plugin-crash", ({ data }) => { + Cc["@mozilla.org/gecko-media-plugin-service;1"] + .getService(Ci.mozIGeckoMediaPluginService) + .RunPluginCrashCallbacks(data.pluginID, data.pluginName); +}); diff --git a/toolkit/content/resetProfile.css b/toolkit/content/resetProfile.css new file mode 100644 index 0000000000..a83171ff56 --- /dev/null +++ b/toolkit/content/resetProfile.css @@ -0,0 +1,15 @@ +/* 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/. */ + +#migratedItems { + margin-inline-start: 1.5em; +} + +#resetProfileFooter { + font-weight: bold; +} + +#resetProfileProgressDialog { + padding: 10px; +} diff --git a/toolkit/content/resetProfile.js b/toolkit/content/resetProfile.js new file mode 100644 index 0000000000..9c5558705c --- /dev/null +++ b/toolkit/content/resetProfile.js @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +document.addEventListener("dialogaccept", onResetProfileAccepted); + +function onResetProfileAccepted() { + let retVals = window.arguments[0]; + retVals.reset = true; +} diff --git a/toolkit/content/resetProfile.xhtml b/toolkit/content/resetProfile.xhtml new file mode 100644 index 0000000000..7ddafefef0 --- /dev/null +++ b/toolkit/content/resetProfile.xhtml @@ -0,0 +1,34 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://global/content/resetProfile.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="refresh-profile-dialog"> +<dialog id="resetProfileDialog" + buttons="accept,cancel" + defaultButton="cancel" + buttonidaccept="refresh-profile-dialog-button"> + <!-- The empty onload event handler is a hack to get the accept button text applied by Fluent. --> + + <linkset> + <html:link rel="localization" href="branding/brand.ftl"/> + <html:link rel="localization" href="toolkit/global/resetProfile.ftl"/> + </linkset> + + <script src="chrome://global/content/resetProfile.js"/> + + <description data-l10n-id="refresh-profile-description"/> + <label data-l10n-id="refresh-profile-description-details"/> + + <vbox id="migratedItems"> + <label class="migratedLabel" data-l10n-id="refresh-profile-remove"/> + <label class="migratedLabel" data-l10n-id="refresh-profile-restore"/> + </vbox> +</dialog> +</window> diff --git a/toolkit/content/resetProfileProgress.xhtml b/toolkit/content/resetProfileProgress.xhtml new file mode 100644 index 0000000000..f1886c441b --- /dev/null +++ b/toolkit/content/resetProfileProgress.xhtml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % resetProfileDTD SYSTEM "chrome://global/locale/resetProfile.dtd" > +%resetProfileDTD; +]> + +<?xml-stylesheet href="chrome://global/content/resetProfile.css"?> +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<window id="resetProfileProgressDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="&refreshProfile.dialog.title;" + style="min-width: 30em;"> + <vbox> + <description>&refreshProfile.cleaning.description;</description> + <html:progress/> + </vbox> +</window> diff --git a/toolkit/content/tests/browser/audio.ogg b/toolkit/content/tests/browser/audio.ogg Binary files differnew file mode 100644 index 0000000000..7f1833508a --- /dev/null +++ b/toolkit/content/tests/browser/audio.ogg diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini new file mode 100644 index 0000000000..927178c3a9 --- /dev/null +++ b/toolkit/content/tests/browser/browser.ini @@ -0,0 +1,118 @@ +[DEFAULT] +prefs = + plugin.load_flash_only=false + media.cubeb.sandbox=false # BMO 1610640 +support-files = + audio.ogg + empty.png + file_contentTitle.html + file_empty.html + file_findinframe.html + file_mediaPlayback2.html + file_multipleAudio.html + file_multiplePlayingAudio.html + file_nonAutoplayAudio.html + file_redirect.html + file_redirect_to.html + file_silentAudioTrack.html + file_webAudio.html + head.js + image.jpg + image_page.html + silentAudioTrack.webm + +[browser_autoscroll_disabled.js] +skip-if = true # Bug 1312652 +[browser_delay_autoplay_media.js] +tags = audiochannel +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +[browser_delay_autoplay_media_pausedAfterPlay.js] +tags = audiochannel +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +[browser_delay_autoplay_playAfterTabVisible.js] +tags = audiochannel +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +[browser_delay_autoplay_multipleMedia.js] +tags = audiochannel +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +[browser_delay_autoplay_notInTreeAudio.js] +tags = audiochannel +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +[browser_delay_autoplay_playMediaInMuteTab.js] +tags = audiochannel +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +[browser_delay_autoplay_silentAudioTrack_media.js] +tags = audiochannel +skip-if = (os == "win" && processor == "aarch64") || (os == "mac") || (os == "linux" && !debug) # aarch64 due to 1536573 #Bug 1524746 +[browser_delay_autoplay_webAudio.js] +tags = audiochannel +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +[browser_bug1572798.js] +tags = audiochannel +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +support-files = file_document_open_audio.html +[browser_bug1170531.js] +skip-if = + os == "linux" && !debug && !ccov # Bug 1647973 +[browser_bug1198465.js] +[browser_bug295977_autoscroll_overflow.js] +skip-if = ((debug || asan) && os == "win" && bits == 64) +[browser_bug451286.js] +skip-if = true # bug 1399845 tracks re-enabling this test. +[browser_bug594509.js] +[browser_bug982298.js] +[browser_charsetMenu_swapBrowsers.js] +[browser_content_url_annotation.js] +skip-if = !e10s || !crashreporter +[browser_contentTitle.js] +[browser_crash_previous_frameloader.js] +run-if = e10s && crashreporter +[browser_datetime_datepicker.js] +skip-if = tsan # Frequently times out on TSan +[browser_default_image_filename.js] +[browser_default_image_filename_redirect.js] +support-files = + doggy.png + firebird.png + firebird.png^headers^ +[browser_f7_caret_browsing.js] +[browser_findbar.js] +skip-if = os == "linux" && bits == 64 && os_version = "18.04" # Bug 1614739 +[browser_findbar_disabled_manual.js] +[browser_isSynthetic.js] +[browser_keyevents_during_autoscrolling.js] +[browser_label_textlink.js] +[browser_remoteness_change_listeners.js] +[browser_suspend_videos_outside_viewport.js] +support-files = + file_outside_viewport_videos.html + gizmo.mp4 +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +[browser_license_links.js] +[browser_media_wakelock.js] +support-files = + browser_mediaStreamPlayback.html + browser_mediaStreamPlaybackWithoutAudio.html + file_video.html + file_videoWithAudioOnly.html + file_videoWithoutAudioTrack.html + gizmo.mp4 + gizmo-noaudio.webm +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573 +[browser_media_wakelock_PIP.js] +support-files = + file_video.html + gizmo.mp4 +[browser_media_wakelock_webaudio.js] +[browser_quickfind_editable.js] +skip-if = (verify && debug && (os == 'linux')) +[browser_save_resend_postdata.js] +support-files = + common/mockTransfer.js + data/post_form_inner.sjs + data/post_form_outer.sjs +skip-if = e10s # Bug ?????? - test directly manipulates content (gBrowser.contentDocument.getElementById("postForm").submit();) +[browser_saveImageURL.js] +[browser_resume_bkg_video_on_tab_hover.js] +skip-if = (os == "win" && processor == "aarch64") || debug # aarch64 due to 1536573, Bug 1388959 + diff --git a/toolkit/content/tests/browser/browser_autoscroll_disabled.js b/toolkit/content/tests/browser/browser_autoscroll_disabled.js new file mode 100644 index 0000000000..d2f6efbd13 --- /dev/null +++ b/toolkit/content/tests/browser/browser_autoscroll_disabled.js @@ -0,0 +1,82 @@ +add_task(async function() { + const kPrefName_AutoScroll = "general.autoScroll"; + Services.prefs.setBoolPref(kPrefName_AutoScroll, false); + + let dataUri = + 'data:text/html,<html><body id="i" style="overflow-y: scroll"><div style="height: 2000px"></div>\ + <iframe id="iframe" style="display: none;"></iframe>\ +</body></html>'; + + let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.loadURI(gBrowser, dataUri); + await loadedPromise; + + await BrowserTestUtils.synthesizeMouse( + "#i", + 50, + 50, + { button: 1 }, + gBrowser.selectedBrowser + ); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + var iframe = content.document.getElementById("iframe"); + + if (iframe) { + var e = new iframe.contentWindow.PageTransitionEvent("pagehide", { + bubbles: true, + cancelable: true, + persisted: false, + }); + iframe.contentDocument.dispatchEvent(e); + iframe.contentDocument.documentElement.dispatchEvent(e); + } + }); + + await BrowserTestUtils.synthesizeMouse( + "#i", + 100, + 100, + { type: "mousemove", clickCount: "0" }, + gBrowser.selectedBrowser + ); + + // If scrolling didn't work, we wouldn't do any redraws and thus time out, so + // request and force redraws to get the chance to check for scrolling at all. + await new Promise(resolve => window.requestAnimationFrame(resolve)); + + let msg = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function() { + // Skip the first animation frame callback as it's the same callback that + // the browser uses to kick off the scrolling. + return new Promise(resolve => { + function checkScroll() { + let msg = ""; + let elem = content.document.getElementById("i"); + if (elem.scrollTop != 0) { + msg += "element should not have scrolled vertically"; + } + if (elem.scrollLeft != 0) { + msg += "element should not have scrolled horizontally"; + } + + resolve(msg); + } + + content.requestAnimationFrame(checkScroll); + }); + } + ); + + ok(!msg, "element scroll " + msg); + + // restore the changed prefs + if (Services.prefs.prefHasUserValue(kPrefName_AutoScroll)) { + Services.prefs.clearUserPref(kPrefName_AutoScroll); + } + + // wait for focus to fix a failure in the next test if the latter runs too soon. + await SimpleTest.promiseFocus(); +}); diff --git a/toolkit/content/tests/browser/browser_bug1170531.js b/toolkit/content/tests/browser/browser_bug1170531.js new file mode 100644 index 0000000000..d5a6501dcd --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug1170531.js @@ -0,0 +1,112 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +// Test for bug 1170531 +// https://bugzilla.mozilla.org/show_bug.cgi?id=1170531 + +add_task(async function() { + // Get a bunch of DOM nodes + let editMenu = document.getElementById("edit-menu"); + let menuPopup = editMenu.menupopup; + + let closeMenu = function(aCallback) { + if (OS.Constants.Sys.Name == "Darwin") { + executeSoon(aCallback); + return; + } + + menuPopup.addEventListener( + "popuphidden", + function() { + executeSoon(aCallback); + }, + { once: true } + ); + + executeSoon(function() { + editMenu.open = false; + }); + }; + + let openMenu = function(aCallback) { + if (OS.Constants.Sys.Name == "Darwin") { + goUpdateGlobalEditMenuItems(); + // On OSX, we have a native menu, so it has to be updated. In single process browsers, + // this happens synchronously, but in e10s, we have to wait for the main thread + // to deal with it for us. 1 second should be plenty of time. + setTimeout(aCallback, 1000); + return; + } + + menuPopup.addEventListener( + "popupshown", + function() { + executeSoon(aCallback); + }, + { once: true } + ); + + executeSoon(function() { + editMenu.open = true; + }); + }; + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function(browser) { + let menu_cut_disabled, menu_copy_disabled; + + BrowserTestUtils.loadURI(browser, "data:text/html,<div>hello!</div>"); + await BrowserTestUtils.browserLoaded(browser); + browser.focus(); + await new Promise(resolve => waitForFocus(resolve, window)); + await new Promise(resolve => + window.requestAnimationFrame(() => executeSoon(resolve)) + ); + await new Promise(openMenu); + menu_cut_disabled = + menuPopup.querySelector("#menu_cut").getAttribute("disabled") == "true"; + is(menu_cut_disabled, false, "menu_cut should be enabled"); + menu_copy_disabled = + menuPopup.querySelector("#menu_copy").getAttribute("disabled") == + "true"; + is(menu_copy_disabled, false, "menu_copy should be enabled"); + await new Promise(closeMenu); + + BrowserTestUtils.loadURI( + browser, + "data:text/html,<div contentEditable='true'>hello!</div>" + ); + await BrowserTestUtils.browserLoaded(browser); + browser.focus(); + await new Promise(resolve => waitForFocus(resolve, window)); + await new Promise(resolve => + window.requestAnimationFrame(() => executeSoon(resolve)) + ); + await new Promise(openMenu); + menu_cut_disabled = + menuPopup.querySelector("#menu_cut").getAttribute("disabled") == "true"; + is(menu_cut_disabled, false, "menu_cut should be enabled"); + menu_copy_disabled = + menuPopup.querySelector("#menu_copy").getAttribute("disabled") == + "true"; + is(menu_copy_disabled, false, "menu_copy should be enabled"); + await new Promise(closeMenu); + + BrowserTestUtils.loadURI(browser, "about:preferences"); + await BrowserTestUtils.browserLoaded(browser); + browser.focus(); + await new Promise(resolve => waitForFocus(resolve, window)); + await new Promise(resolve => + window.requestAnimationFrame(() => executeSoon(resolve)) + ); + await new Promise(openMenu); + menu_cut_disabled = + menuPopup.querySelector("#menu_cut").getAttribute("disabled") == "true"; + is(menu_cut_disabled, true, "menu_cut should be disabled"); + menu_copy_disabled = + menuPopup.querySelector("#menu_copy").getAttribute("disabled") == + "true"; + is(menu_copy_disabled, true, "menu_copy should be disabled"); + await new Promise(closeMenu); + } + ); +}); diff --git a/toolkit/content/tests/browser/browser_bug1198465.js b/toolkit/content/tests/browser/browser_bug1198465.js new file mode 100644 index 0000000000..7f578b3197 --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug1198465.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var kPrefName = "accessibility.typeaheadfind.prefillwithselection"; +var kEmptyURI = "data:text/html,"; + +// This pref is false by default in OSX; ensure the test still works there. +Services.prefs.setBoolPref(kPrefName, true); + +registerCleanupFunction(function() { + Services.prefs.clearUserPref(kPrefName); +}); + +add_task(async function() { + let aTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kEmptyURI); + ok(!gFindBarInitialized, "findbar isn't initialized yet"); + + // Note: the use case here is when the user types directly in the findbar + // _before_ it's prefilled with a text selection in the page. + + // So `yield BrowserTestUtils.sendChar()` can't be used here: + // - synthesizing a key in the browser won't actually send it to the + // findbar; the findbar isn't part of the browser content. + // - we need to _not_ wait for _startFindDeferred to be resolved; yielding + // a synthesized keypress on the browser implicitely happens after the + // browser has dispatched its return message with the prefill value for + // the findbar, which essentially nulls these tests. + + // The parent-side of the sidebar initialization is also async, so we do + // need to wait for that. We verify a bit further down that _startFindDeferred + // hasn't been resolved yet. + await gFindBarPromise; + + let findBar = gFindBar; + is(findBar._findField.value, "", "findbar is empty"); + + // Test 1 + // Any input in the findbar should erase a previous search. + + findBar._findField.value = "xy"; + findBar.startFind(); + is(findBar._findField.value, "xy", "findbar should have xy initial query"); + is(findBar._findField, document.activeElement, "findbar is now focused"); + + EventUtils.sendChar("z", window); + is(findBar._findField.value, "z", "z erases xy"); + + findBar._findField.value = ""; + ok(!findBar._findField.value, "erase findbar after first test"); + + // Test 2 + // Prefilling the findbar should be ignored if a search has been run. + + findBar.startFind(); + ok(findBar._startFindDeferred, "prefilled value hasn't been fetched yet"); + is(findBar._findField, document.activeElement, "findbar is still focused"); + + EventUtils.sendChar("a", window); + EventUtils.sendChar("b", window); + is(findBar._findField.value, "ab", "initial ab typed in the findbar"); + + // This resolves _startFindDeferred if it's still pending; let's just skip + // over waiting for the browser's return message that should do this as it + // doesn't really matter. + findBar.onCurrentSelection("foo", true); + ok(!findBar._startFindDeferred, "prefilled value fetched"); + is(findBar._findField.value, "ab", "ab kept instead of prefill value"); + + EventUtils.sendChar("c", window); + is(findBar._findField.value, "abc", "c is appended after ab"); + + // Clear the findField value to make the test run successfully + // for multiple runs in the same browser session. + findBar._findField.value = ""; + BrowserTestUtils.removeTab(aTab); +}); diff --git a/toolkit/content/tests/browser/browser_bug1572798.js b/toolkit/content/tests/browser/browser_bug1572798.js new file mode 100644 index 0000000000..8b5dffd432 --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug1572798.js @@ -0,0 +1,29 @@ +add_task(async function test_bug_1572798() { + let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank"); + BrowserTestUtils.loadURI( + tab.linkedBrowser, + "https://example.com/browser/toolkit/content/tests/browser/file_document_open_audio.html" + ); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let windowLoaded = BrowserTestUtils.waitForNewWindow(); + info("- clicking button to spawn a new window -"); + await ContentTask.spawn(tab.linkedBrowser, null, function() { + content.document.querySelector("button").click(); + }); + info("- waiting for the new window -"); + let newWin = await windowLoaded; + info("- checking that the new window plays the audio -"); + let documentOpenedBrowser = newWin.gBrowser.selectedBrowser; + await ContentTask.spawn(documentOpenedBrowser, null, async function() { + try { + await content.document.querySelector("audio").play(); + ok(true, "Could play the audio"); + } catch (e) { + ok(false, "Rejected audio promise" + e); + } + }); + + info("- Cleaning up -"); + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js b/toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js new file mode 100644 index 0000000000..5cc6de19da --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug295977_autoscroll_overflow.js @@ -0,0 +1,381 @@ +requestLongerTimeout(2); +add_task(async function() { + function pushPrefs(prefs) { + return SpecialPowers.pushPrefEnv({ set: prefs }); + } + + await pushPrefs([ + ["general.autoScroll", true], + ["test.events.async.enabled", true], + ]); + + const expectScrollNone = 0; + const expectScrollVert = 1; + const expectScrollHori = 2; + const expectScrollBoth = 3; + + var allTests = [ + { + dataUri: + 'data:text/html,<html><head><meta charset="utf-8"></head><body><style type="text/css">div { display: inline-block; }</style>\ + <div id="a" style="width: 100px; height: 100px; overflow: hidden;"><div style="width: 200px; height: 200px;"></div></div>\ + <div id="b" style="width: 100px; height: 100px; overflow: auto;"><div style="width: 200px; height: 200px;"></div></div>\ + <div id="c" style="width: 100px; height: 100px; overflow-x: auto; overflow-y: hidden;"><div style="width: 200px; height: 200px;"></div></div>\ + <div id="d" style="width: 100px; height: 100px; overflow-y: auto; overflow-x: hidden;"><div style="width: 200px; height: 200px;"></div></div>\ + <select id="e" style="width: 100px; height: 100px;" multiple="multiple"><option>aaaaaaaaaaaaaaaaaaaaaaaa</option><option>a</option><option>a</option>\ + <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option>\ + <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option></select>\ + <select id="f" style="width: 100px; height: 100px;"><option>a</option><option>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</option><option>a</option>\ + <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option>\ + <option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option><option>a</option></select>\ + <div id="g" style="width: 99px; height: 99px; border: 10px solid black; margin: 10px; overflow: auto;"><div style="width: 100px; height: 100px;"></div></div>\ + <div id="h" style="width: 100px; height: 100px; overflow: clip;"><div style="width: 200px; height: 200px;"></div></div>\ + <iframe id="iframe" style="display: none;"></iframe>\ + </body></html>', + }, + { elem: "a", expected: expectScrollNone }, + { elem: "b", expected: expectScrollBoth }, + { elem: "c", expected: expectScrollHori }, + { elem: "d", expected: expectScrollVert }, + { elem: "e", expected: expectScrollVert }, + { elem: "f", expected: expectScrollNone }, + { elem: "g", expected: expectScrollBoth }, + { elem: "h", expected: expectScrollNone }, + { + dataUri: + 'data:text/html,<html><head><meta charset="utf-8"></head><body id="i" style="overflow-y: scroll"><div style="height: 2000px"></div>\ + <iframe id="iframe" style="display: none;"></iframe>\ + </body></html>', + }, + { elem: "i", expected: expectScrollVert }, // bug 695121 + { + dataUri: + 'data:text/html,<html><head><meta charset="utf-8"></head><style>html, body { width: 100%; height: 100%; overflow-x: hidden; overflow-y: scroll; }</style>\ + <body id="j"><div style="height: 2000px"></div>\ + <iframe id="iframe" style="display: none;"></iframe>\ + </body></html>', + }, + { elem: "j", expected: expectScrollVert }, // bug 914251 + { + dataUri: + 'data:text/html,<html><head><meta charset="utf-8">\ +<style>\ +body > div {scroll-behavior: smooth;width: 300px;height: 300px;overflow: scroll;}\ +body > div > div {width: 1000px;height: 1000px;}\ +</style>\ +</head><body><div id="t"><div></div></div></body></html>', + }, + { elem: "t", expected: expectScrollBoth }, // bug 1308775 + { + dataUri: + 'data:text/html,<html><head><meta charset="utf-8"></head><body>\ +<div id="k" style="height: 150px; width: 200px; overflow: scroll; border: 1px solid black;">\ +<iframe style="height: 200px; width: 300px;"></iframe>\ +</div>\ +<div id="l" style="height: 150px; width: 300px; overflow: scroll; border: 1px dashed black;">\ +<iframe style="height: 200px; width: 200px;" src="data:text/html,<div style=\'border: 5px solid blue; height: 200%; width: 200%;\'></div>"></iframe>\ +</div>\ +<iframe id="m"></iframe>\ +<div style="height: 200%; border: 5px dashed black;">filler to make document overflow: scroll;</div>\ +</body></html>', + }, + { elem: "k", expected: expectScrollBoth }, + { elem: "k", expected: expectScrollNone, testwindow: true }, + { elem: "l", expected: expectScrollNone }, + { elem: "m", expected: expectScrollVert, testwindow: true }, + { + dataUri: + 'data:text/html,<html><head><meta charset="utf-8"></head><body>\ +<img width="100" height="100" alt="image map" usemap="%23planetmap">\ +<map name="planetmap">\ + <area id="n" shape="rect" coords="0,0,100,100" href="javascript:void(null)">\ +</map>\ +<a href="javascript:void(null)" id="o" style="width: 100px; height: 100px; border: 1px solid black; display: inline-block; vertical-align: top;">link</a>\ +<input id="p" style="width: 100px; height: 100px; vertical-align: top;">\ +<textarea id="q" style="width: 100px; height: 100px; vertical-align: top;"></textarea>\ +<div style="height: 200%; border: 1px solid black;"></div>\ +</body></html>', + }, + { elem: "n", expected: expectScrollNone, testwindow: true }, + { elem: "o", expected: expectScrollNone, testwindow: true }, + { + elem: "p", + expected: expectScrollVert, + testwindow: true, + middlemousepastepref: false, + }, + { + elem: "q", + expected: expectScrollVert, + testwindow: true, + middlemousepastepref: false, + }, + { + dataUri: + 'data:text/html,<html><head><meta charset="utf-8"></head><body>\ +<input id="r" style="width: 100px; height: 100px; vertical-align: top;">\ +<textarea id="s" style="width: 100px; height: 100px; vertical-align: top;"></textarea>\ +<div style="height: 200%; border: 1px solid black;"></div>\ +</body></html>', + }, + { + elem: "r", + expected: expectScrollNone, + testwindow: true, + middlemousepastepref: true, + }, + { + elem: "s", + expected: expectScrollNone, + testwindow: true, + middlemousepastepref: true, + }, + { + dataUri: + "data:text/html," + + encodeURIComponent(` +<!doctype html> +<iframe id=i height=100 width=100 scrolling="no" srcdoc="<div style='height: 200px'>Auto-scrolling should never make me disappear"></iframe> +<div style="height: 100vh"></div> + `), + }, + { + elem: "i", + // We expect the outer window to scroll vertically, not the iframe's window. + expected: expectScrollVert, + testwindow: true, + }, + { + dataUri: + "data:text/html," + + encodeURIComponent(` +<!doctype html> +<iframe id=i height=100 width=100 srcdoc="<div style='height: 200px'>Auto-scrolling should make me disappear"></iframe> +<div style="height: 100vh"></div> + `), + }, + { + elem: "i", + // We expect the iframe's window to scroll vertically, so the outer window should not scroll. + expected: expectScrollNone, + testwindow: true, + }, + { + // Test: scroll is initiated in out of process iframe having no scrollable area + dataUri: + "data:text/html," + + encodeURIComponent(` +<!doctype html> +<head><meta content="text/html;charset=utf-8"></head><body> +<div id="scroller" style="width: 300px; height: 300px; overflow-y: scroll; overflow-x: hidden; border: solid 1px blue;"> + <iframe id="noscroll-outofprocess-iframe" + src="https://example.com/document-builder.sjs?html=<html><body>Hey!</body></html>" + style="border: solid 1px green; margin: 2px;"></iframe> + <div style="width: 100%; height: 200px;"></div> +</div></body> + `), + }, + { + elem: "noscroll-outofprocess-iframe", + // We expect the div to scroll vertically, not the iframe's window. + expected: expectScrollVert, + scrollable: "scroller", + }, + ]; + + for (let test of allTests) { + if (test.dataUri) { + let loadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser + ); + BrowserTestUtils.loadURI(gBrowser, test.dataUri); + await loadedPromise; + continue; + } + + let prefsChanged = "middlemousepastepref" in test; + if (prefsChanged) { + await pushPrefs([["middlemouse.paste", test.middlemousepastepref]]); + } + + await BrowserTestUtils.synthesizeMouse( + "#" + test.elem, + 50, + 80, + { button: 1 }, + gBrowser.selectedBrowser + ); + + // This ensures bug 605127 is fixed: pagehide in an unrelated document + // should not cancel the autoscroll. + await ContentTask.spawn( + gBrowser.selectedBrowser, + { waitForAutoScrollStart: test.expected != expectScrollNone }, + async ({ waitForAutoScrollStart }) => { + var iframe = content.document.getElementById("iframe"); + + if (iframe) { + var e = new iframe.contentWindow.PageTransitionEvent("pagehide", { + bubbles: true, + cancelable: true, + persisted: false, + }); + iframe.contentDocument.dispatchEvent(e); + iframe.contentDocument.documentElement.dispatchEvent(e); + } + if (waitForAutoScrollStart) { + await new Promise(resolve => + Services.obs.addObserver(resolve, "autoscroll-start") + ); + } + } + ); + + is( + document.activeElement, + gBrowser.selectedBrowser, + "Browser still focused after autoscroll started" + ); + + await BrowserTestUtils.synthesizeMouse( + "#" + test.elem, + 100, + 100, + { type: "mousemove", clickCount: "0" }, + gBrowser.selectedBrowser + ); + + if (prefsChanged) { + await SpecialPowers.popPrefEnv(); + } + + // Start checking for the scroll. + let firstTimestamp = undefined; + let timeCompensation; + do { + let timestamp = await new Promise(resolve => + window.requestAnimationFrame(resolve) + ); + if (firstTimestamp === undefined) { + firstTimestamp = timestamp; + } + + // This value is calculated similarly to the value of the same name in + // ClickEventHandler.autoscrollLoop, except here it's cumulative across + // all frames after the first one instead of being based only on the + // current frame. + timeCompensation = (timestamp - firstTimestamp) / 20; + info( + "timestamp=" + + timestamp + + " firstTimestamp=" + + firstTimestamp + + " timeCompensation=" + + timeCompensation + ); + + // Try to wait until enough time has passed to allow the scroll to happen. + // autoscrollLoop incrementally scrolls during each animation frame, but + // due to how its calculations work, when a frame is very close to the + // previous frame, no scrolling may actually occur during that frame. + // After 100ms's worth of frames, timeCompensation will be 1, making it + // more likely that the accumulated scroll in autoscrollLoop will be >= 1, + // although it also depends on acceleration, which here in this test + // should be > 1 due to how it synthesizes mouse events below. + } while (timeCompensation < 5); + + // Close the autoscroll popup by synthesizing Esc. + EventUtils.synthesizeKey("KEY_Escape"); + let scrollVert = test.expected & expectScrollVert; + let scrollHori = test.expected & expectScrollHori; + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [ + { + scrollVert, + scrollHori, + elemid: test.scrollable || test.elem, + checkWindow: test.testwindow, + }, + ], + async function(args) { + let msg = ""; + if (args.checkWindow) { + if ( + !( + (args.scrollVert && content.scrollY > 0) || + (!args.scrollVert && content.scrollY == 0) + ) + ) { + msg += "Failed: "; + } + msg += + "Window for " + + args.elemid + + " should" + + (args.scrollVert ? "" : " not") + + " have scrolled vertically\n"; + + if ( + !( + (args.scrollHori && content.scrollX > 0) || + (!args.scrollHori && content.scrollX == 0) + ) + ) { + msg += "Failed: "; + } + msg += + " Window for " + + args.elemid + + " should" + + (args.scrollHori ? "" : " not") + + " have scrolled horizontally\n"; + } else { + let elem = content.document.getElementById(args.elemid); + if ( + !( + (args.scrollVert && elem.scrollTop > 0) || + (!args.scrollVert && elem.scrollTop == 0) + ) + ) { + msg += "Failed: "; + } + msg += + " " + + args.elemid + + " should" + + (args.scrollVert ? "" : " not") + + " have scrolled vertically\n"; + if ( + !( + (args.scrollHori && elem.scrollLeft > 0) || + (!args.scrollHori && elem.scrollLeft == 0) + ) + ) { + msg += "Failed: "; + } + msg += + args.elemid + + " should" + + (args.scrollHori ? "" : " not") + + " have scrolled horizontally"; + } + + Assert.ok(!msg.includes("Failed"), msg); + } + ); + + // Before continuing the test, we need to ensure that the IPC + // message that stops autoscrolling has had time to arrive. + await new Promise(resolve => executeSoon(resolve)); + } + + // remove 2 tabs that were opened by middle-click on links + while (gBrowser.visibleTabs.length > 1) { + gBrowser.removeTab(gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1]); + } + + // wait for focus to fix a failure in the next test if the latter runs too soon. + await SimpleTest.promiseFocus(); +}); diff --git a/toolkit/content/tests/browser/browser_bug451286.js b/toolkit/content/tests/browser/browser_bug451286.js new file mode 100644 index 0000000000..7856356608 --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug451286.js @@ -0,0 +1,166 @@ +Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js", + this +); + +add_task(async function() { + const SEARCH_TEXT = "text"; + const DATAURI = "data:text/html," + SEARCH_TEXT; + + // Bug 451286. An iframe that should be highlighted + let visible = "<iframe id='visible' src='" + DATAURI + "'></iframe>"; + + // Bug 493658. An invisible iframe that shouldn't interfere with + // highlighting matches lying after it in the document + let invisible = + "<iframe id='invisible' style='display: none;' " + + "src='" + + DATAURI + + "'></iframe>"; + + let uri = DATAURI + invisible + SEARCH_TEXT + visible + SEARCH_TEXT; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri); + let contentRect = tab.linkedBrowser.getBoundingClientRect(); + let noHighlightSnapshot = snapshotRect(window, contentRect); + ok(noHighlightSnapshot, "Got noHighlightSnapshot"); + + await openFindBarAndWait(); + gFindBar._findField.value = SEARCH_TEXT; + await findAgainAndWait(); + var matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + matchCase.doCommand(); + } + + // Turn on highlighting + await toggleHighlightAndWait(true); + await closeFindBarAndWait(); + + // Take snapshot of highlighting + let findSnapshot = snapshotRect(window, contentRect); + ok(findSnapshot, "Got findSnapshot"); + + // Now, remove the highlighting, and take a snapshot to compare + // to our original state + await openFindBarAndWait(); + await toggleHighlightAndWait(false); + await closeFindBarAndWait(); + + let unhighlightSnapshot = snapshotRect(window, contentRect); + ok(unhighlightSnapshot, "Got unhighlightSnapshot"); + + // Select the matches that should have been highlighted manually + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let doc = content.document; + let win = doc.defaultView; + + // Create a manual highlight in the visible iframe to test bug 451286 + let iframe = doc.getElementById("visible"); + let ifBody = iframe.contentDocument.body; + let range = iframe.contentDocument.createRange(); + range.selectNodeContents(ifBody.childNodes[0]); + let ifWindow = iframe.contentWindow; + let ifDocShell = ifWindow.docShell; + + let ifController = ifDocShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + + let frameFindSelection = ifController.getSelection( + ifController.SELECTION_FIND + ); + frameFindSelection.addRange(range); + + // Create manual highlights in the main document (the matches that lie + // before/after the iframes + let docShell = win.docShell; + + let controller = docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + + let docFindSelection = controller.getSelection(ifController.SELECTION_FIND); + + range = doc.createRange(); + range.selectNodeContents(doc.body.childNodes[0]); + docFindSelection.addRange(range); + range = doc.createRange(); + range.selectNodeContents(doc.body.childNodes[2]); + docFindSelection.addRange(range); + range = doc.createRange(); + range.selectNodeContents(doc.body.childNodes[4]); + docFindSelection.addRange(range); + }); + + // Take snapshot of manual highlighting + let manualSnapshot = snapshotRect(window, contentRect); + ok(manualSnapshot, "Got manualSnapshot"); + + // Test 1: Were the matches in iframe correctly highlighted? + let res = compareSnapshots(findSnapshot, manualSnapshot, true); + ok(res[0], "Matches found in iframe correctly highlighted"); + + // Test 2: Were the matches in iframe correctly unhighlighted? + res = compareSnapshots(noHighlightSnapshot, unhighlightSnapshot, true); + ok(res[0], "Highlighting in iframe correctly removed"); + + BrowserTestUtils.removeTab(tab); +}); + +function toggleHighlightAndWait(shouldHighlight) { + return new Promise(resolve => { + let listener = { + onFindResult() {}, + onHighlightFinished() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + }, + onMatchesCountResult() {}, + }; + gFindBar.browser.finder.addResultListener(listener); + gFindBar.toggleHighlight(shouldHighlight); + }); +} + +function findAgainAndWait() { + return new Promise(resolve => { + let listener = { + onFindResult() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + }, + onHighlightFinished() {}, + onMatchesCountResult() {}, + }; + gFindBar.browser.finder.addResultListener(listener); + gFindBar.onFindAgainCommand(); + }); +} + +async function openFindBarAndWait() { + await gFindBarPromise; + let awaitTransitionEnd = BrowserTestUtils.waitForEvent( + gFindBar, + "transitionend" + ); + gFindBar.open(); + await awaitTransitionEnd; +} + +// This test is comparing snapshots. It is necessary to wait for the gFindBar +// to close before taking the snapshot so the gFindBar does not take up space +// on the new snapshot. +async function closeFindBarAndWait() { + let awaitTransitionEnd = BrowserTestUtils.waitForEvent( + gFindBar, + "transitionend", + false, + event => { + return event.propertyName == "visibility"; + } + ); + gFindBar.close(); + await awaitTransitionEnd; +} diff --git a/toolkit/content/tests/browser/browser_bug594509.js b/toolkit/content/tests/browser/browser_bug594509.js new file mode 100644 index 0000000000..cf93d2c71c --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug594509.js @@ -0,0 +1,15 @@ +add_task(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:rights" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + Assert.ok( + content.document.getElementById("your-rights"), + "about:rights content loaded" + ); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_bug982298.js b/toolkit/content/tests/browser/browser_bug982298.js new file mode 100644 index 0000000000..99a4dc5e96 --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug982298.js @@ -0,0 +1,72 @@ +const scrollHtml = + '<textarea id="textarea1" row=2>Firefox\n\nFirefox\n\n\n\n\n\n\n\n\n\n' + + '</textarea><a href="about:blank">blank</a>'; + +add_task(async function() { + let url = "data:text/html;base64," + btoa(scrollHtml); + await BrowserTestUtils.withNewTab({ gBrowser, url }, async function(browser) { + let awaitFindResult = new Promise(resolve => { + let listener = { + onFindResult(aData) { + info("got find result"); + browser.finder.removeResultListener(listener); + + ok( + aData.result == Ci.nsITypeAheadFind.FIND_FOUND, + "should find string" + ); + resolve(); + }, + onCurrentSelection() {}, + onMatchesCountResult() {}, + }; + info("about to add results listener, open find bar, and send 'F' string"); + browser.finder.addResultListener(listener); + }); + await gFindBarPromise; + gFindBar.onFindCommand(); + EventUtils.sendString("F"); + info("added result listener and sent string 'F'"); + await awaitFindResult; + + // scroll textarea to bottom + await SpecialPowers.spawn(browser, [], () => { + let textarea = content.document.getElementById("textarea1"); + textarea.scrollTop = textarea.scrollHeight; + }); + BrowserTestUtils.loadURI(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser); + + ok( + browser.currentURI.spec == "about:blank", + "got load event for about:blank" + ); + + let awaitFindResult2 = new Promise(resolve => { + let listener = { + onFindResult(aData) { + info("got find result #2"); + browser.finder.removeResultListener(listener); + resolve(); + }, + onCurrentSelection() {}, + onMatchesCountResult() {}, + }; + + browser.finder.addResultListener(listener); + info("added result listener"); + }); + // find again needs delay for crash test + setTimeout(function() { + // ignore exception if occured + try { + info("about to send find again command"); + gFindBar.onFindAgainCommand(false); + info("sent find again command"); + } catch (e) { + info("got exception from onFindAgainCommand: " + e); + } + }, 0); + await awaitFindResult2; + }); +}); diff --git a/toolkit/content/tests/browser/browser_charsetMenu_swapBrowsers.js b/toolkit/content/tests/browser/browser_charsetMenu_swapBrowsers.js new file mode 100644 index 0000000000..8eda01de8e --- /dev/null +++ b/toolkit/content/tests/browser/browser_charsetMenu_swapBrowsers.js @@ -0,0 +1,34 @@ +/* Test that the charset menu is properly enabled when swapping browsers. */ +add_task(async function test() { + // NB: This test cheats and calls updateCharacterEncodingMenuState directly + // instead of opening the "View" menu. + function charsetMenuEnabled() { + updateCharacterEncodingMenuState(); + return !document.getElementById("charsetMenu").hasAttribute("disabled"); + } + + const PAGE = "data:text/html,<!DOCTYPE html><body>hello"; + let tab1 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: PAGE, + }); + ok(charsetMenuEnabled(), "should have a charset menu here"); + + let tab2 = await BrowserTestUtils.openNewForegroundTab({ gBrowser }); + ok(!charsetMenuEnabled(), "about:blank shouldn't have a charset menu"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + + let swapped = BrowserTestUtils.waitForEvent( + tab2.linkedBrowser, + "SwapDocShells" + ); + + // NB: Closes tab1. + gBrowser.swapBrowsersAndCloseOther(tab2, tab1); + await swapped; + + ok(charsetMenuEnabled(), "should have a charset after the swap"); + + BrowserTestUtils.removeTab(tab2); +}); diff --git a/toolkit/content/tests/browser/browser_contentTitle.js b/toolkit/content/tests/browser/browser_contentTitle.js new file mode 100644 index 0000000000..abd70809fc --- /dev/null +++ b/toolkit/content/tests/browser/browser_contentTitle.js @@ -0,0 +1,17 @@ +var url = + "https://example.com/browser/toolkit/content/tests/browser/file_contentTitle.html"; + +add_task(async function() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url)); + await BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "TestLocationChange", + true, + null, + true + ); + + is(gBrowser.contentTitle, "Test Page", "Should have the right title."); + + gBrowser.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_content_url_annotation.js b/toolkit/content/tests/browser/browser_content_url_annotation.js new file mode 100644 index 0000000000..72180ec2d0 --- /dev/null +++ b/toolkit/content/tests/browser/browser_content_url_annotation.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/* global Services, requestLongerTimeout, TestUtils, BrowserTestUtils, + ok, info, dump, is, Ci, Cu, Components, ctypes, + gBrowser, add_task, addEventListener, removeEventListener, ContentTask */ + +"use strict"; + +// Running this test in ASAN is slow. +requestLongerTimeout(2); + +/** + * Removes a file from a directory. This is a no-op if the file does not + * exist. + * + * @param directory + * The nsIFile representing the directory to remove from. + * @param filename + * A string for the file to remove from the directory. + */ +function removeFile(directory, filename) { + let file = directory.clone(); + file.append(filename); + if (file.exists()) { + file.remove(false); + } +} + +/** + * Returns the directory where crash dumps are stored. + * + * @return nsIFile + */ +function getMinidumpDirectory() { + let dir = Services.dirsvc.get("ProfD", Ci.nsIFile); + dir.append("minidumps"); + return dir; +} + +/** + * Checks that the URL is correctly annotated on a content process crash. + */ +add_task(async function test_content_url_annotation() { + let url = + "https://example.com/browser/toolkit/content/tests/browser/file_redirect.html"; + let redirect_url = + "https://example.com/browser/toolkit/content/tests/browser/file_redirect_to.html"; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + }, + async function(browser) { + ok(browser.isRemoteBrowser, "Should be a remote browser"); + + // file_redirect.html should send us to file_redirect_to.html + let promise = BrowserTestUtils.waitForContentEvent( + browser, + "RedirectDone", + true, + null, + true + ); + BrowserTestUtils.loadURI(browser, url); + await promise; + + // Crash the tab + let annotations = await BrowserTestUtils.crashFrame(browser); + + ok("URL" in annotations, "annotated a URL"); + is( + annotations.URL, + redirect_url, + "Should have annotated the URL after redirect" + ); + } + ); +}); diff --git a/toolkit/content/tests/browser/browser_crash_previous_frameloader.js b/toolkit/content/tests/browser/browser_crash_previous_frameloader.js new file mode 100644 index 0000000000..6870cd825d --- /dev/null +++ b/toolkit/content/tests/browser/browser_crash_previous_frameloader.js @@ -0,0 +1,131 @@ +"use strict"; + +/** + * Returns the id of the crash minidump. + * + * @param subject (nsISupports) + * The subject passed through the ipc:content-shutdown + * observer notification when a content process crash has + * occurred. + * @returns {String} The crash dump id. + */ +function getCrashDumpId(subject) { + Assert.ok( + subject instanceof Ci.nsIPropertyBag2, + "Subject needs to be a nsIPropertyBag2 to clean up properly" + ); + + return subject.getPropertyAsAString("dumpID"); +} + +/** + * Cleans up the .dmp and .extra file from a crash. + * + * @param id {String} The crash dump id. + */ +function cleanUpMinidump(id) { + let dir = Services.dirsvc.get("ProfD", Ci.nsIFile); + dir.append("minidumps"); + + let file = dir.clone(); + file.append(id + ".dmp"); + file.remove(true); + + file = dir.clone(); + file.append(id + ".extra"); + file.remove(true); +} + +/** + * This test ensures that if a remote frameloader crashes after + * the frameloader owner swaps it out for a new frameloader, + * that a oop-browser-crashed event is not sent to the new + * frameloader's browser element. + */ +add_task(async function test_crash_in_previous_frameloader() { + // On debug builds, crashing tabs results in much thinking, which + // slows down the test and results in intermittent test timeouts, + // so we'll pump up the expected timeout for this test. + requestLongerTimeout(2); + + if (!gMultiProcessBrowser) { + Assert.ok(false, "This test should only be run in multi-process mode."); + return; + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "http://example.com", + }, + async function(browser) { + // First, sanity check... + Assert.ok( + browser.isRemoteBrowser, + "This browser needs to be remote if this test is going to " + + "work properly." + ); + + // We will wait for the oop-browser-crashed event to have + // a chance to appear. That event is fired when RemoteTabs + // are destroyed, and that occurs _before_ ContentParents + // are destroyed, so we'll wait on the ipc:content-shutdown + // observer notification, which is fired when a ContentParent + // goes away. After we see this notification, oop-browser-crashed + // events should have fired. + let contentProcessGone = TestUtils.topicObserved("ipc:content-shutdown"); + let sawTabCrashed = false; + let onTabCrashed = () => { + sawTabCrashed = true; + }; + + browser.addEventListener("oop-browser-crashed", onTabCrashed); + + // The name of the game is to cause a crash in a remote browser, + // and then immediately swap out the browser for a non-remote one. + await SpecialPowers.spawn(browser, [], function() { + const { ctypes } = ChromeUtils.import( + "resource://gre/modules/ctypes.jsm" + ); + + let dies = function() { + ChromeUtils.privateNoteIntentionalCrash(); + let zero = new ctypes.intptr_t(8); + let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t)); + badptr.contents; + }; + + // When the parent flips the remoteness of the browser, the + // page should receive the pagehide event, which we'll then + // use to crash the frameloader. + docShell.chromeEventHandler.addEventListener("pagehide", function() { + dump("\nEt tu, Brute?\n"); + dies(); + }); + }); + + gBrowser.updateBrowserRemoteness(browser, { + remoteType: E10SUtils.NOT_REMOTE, + }); + info("Waiting for content process to go away."); + let [subject /* , data */] = await contentProcessGone; + + // If we don't clean up the minidump, the harness will + // complain. + let dumpID = getCrashDumpId(subject); + + Assert.ok(dumpID, "There should be a dumpID"); + if (dumpID) { + await Services.crashmanager.ensureCrashIsPresent(dumpID); + cleanUpMinidump(dumpID); + } + + info("Content process is gone!"); + Assert.ok( + !sawTabCrashed, + "Should not have seen the oop-browser-crashed event." + ); + browser.removeEventListener("oop-browser-crashed", onTabCrashed); + } + ); +}); diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker.js b/toolkit/content/tests/browser/browser_datetime_datepicker.js new file mode 100644 index 0000000000..a354dda0ec --- /dev/null +++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js @@ -0,0 +1,791 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const MONTH_YEAR = ".month-year", + DAYS_VIEW = ".days-view", + BTN_PREV_MONTH = ".prev", + BTN_NEXT_MONTH = ".next"; +const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + timeZone: "UTC", +}).format; +const DATE_FORMAT_LOCAL = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", +}).format; + +// Create a list of abbreviations for calendar class names +const W = "weekend", + O = "outside", + S = "selection", + R = "out-of-range", + T = "today", + P = "off-step"; + +// Calendar classlist for 2016-12. Used to verify the classNames are correct. +const calendarClasslist_201612 = [ + [W, O], + [O], + [O], + [O], + [], + [], + [W], + [W], + [], + [], + [], + [], + [], + [W], + [W], + [], + [], + [], + [S], + [], + [W], + [W], + [], + [], + [], + [], + [], + [W], + [W], + [], + [], + [], + [], + [], + [W], + [W, O], + [O], + [O], + [O], + [O], + [O], + [W, O], +]; + +function getCalendarText() { + return helper.getChildren(DAYS_VIEW).map(child => child.textContent); +} + +function getCalendarClassList() { + return helper + .getChildren(DAYS_VIEW) + .map(child => Array.from(child.classList)); +} + +function mergeArrays(a, b) { + return a.map((classlist, index) => classlist.concat(b[index])); +} + +async function verifyPickerPosition(browsingContext, inputId) { + let inputRect = await SpecialPowers.spawn( + browsingContext, + [inputId], + async function(inputIdChild) { + let rect = content.document + .getElementById(inputIdChild) + .getBoundingClientRect(); + return { + left: content.mozInnerScreenX + rect.left, + bottom: content.mozInnerScreenY + rect.bottom, + }; + } + ); + + function is_close(got, exp, msg) { + // on some platforms we see differences of a fraction of a pixel - so + // allow any difference of < 1 pixels as being OK. + ok( + Math.abs(got - exp) < 1, + msg + ": " + got + " should be equal(-ish) to " + exp + ); + } + is_close(helper.panel.screenX, inputRect.left, "datepicker x position"); + is_close(helper.panel.screenY, inputRect.bottom, "datepicker y position"); +} + +let helper = new DateTimeTestHelper(); + +registerCleanupFunction(() => { + helper.cleanup(); +}); + +/** + * Test that date picker opens to today's date when input field is blank + */ +add_task(async function test_datepicker_today() { + const date = new Date(); + + await helper.openPicker("data:text/html, <input type='date'>"); + + if (date.getMonth() === new Date().getMonth()) { + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT_LOCAL(date) + ); + } else { + Assert.ok( + true, + "Skipping datepicker today test if month changes when opening picker." + ); + } + + await helper.tearDown(); +}); + +/** + * Test that date picker opens to the correct month, with calendar days + * displayed correctly, given a date value is set. + */ +add_task(async function test_datepicker_open() { + const inputValue = "2016-12-15"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)) + ); + Assert.deepEqual( + getCalendarText(), + [ + "27", + "28", + "29", + "30", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + ], + "2016-12" + ); + Assert.deepEqual( + getCalendarClassList(), + calendarClasslist_201612, + "2016-12 classNames" + ); + + await helper.tearDown(); +}); + +/** + * Ensure picker closes when focus moves to a different input. + */ +add_task(async function test_datepicker_focus_change() { + await helper.openPicker( + `data:text/html,<input id=date type=date><input id=other>` + ); + let browser = helper.tab.linkedBrowser; + await verifyPickerPosition(browser, "date"); + + isnot(helper.panel.hidden, "Panel should be visible"); + + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("#other").focus(); + }); + + // NOTE: Would be cool to be able to use promisePickerClosed(), but + // popuphidden isn't really triggered for this code path it seems, so oh + // well. + await BrowserTestUtils.waitForCondition( + () => helper.panel.hidden, + "waiting for close" + ); + await helper.tearDown(); +}); + +/** + * Ensure picker opens and closes with key bindings appropriately. + */ +/* +disabled for bug 1676078 +add_task(async function test_datepicker_keyboard_open() { + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + await helper.openPicker( + `data:text/html,<input id=date type=date value=${inputValue}>` + ); + let browser = helper.tab.linkedBrowser; + await verifyPickerPosition(browser, "date"); + + BrowserTestUtils.synthesizeKey(" ", {}, browser); + + await BrowserTestUtils.waitForCondition( + () => helper.panel.hidden, + "waiting for close" + ); + + BrowserTestUtils.synthesizeKey(" ", {}, browser); + + await BrowserTestUtils.waitForCondition( + () => !helper.panel.hidden, + "waiting for open" + ); + + // NOTE: After the click, the first input field (the month one) is focused, + // so down arrow will change the selected month. + // + // This assumes en-US locale, which seems fine for testing purposes (as + // DATE_FORMAT and other bits around do the same). + BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + + // It'd be good to use something else than waitForCondition for this but + // there's no exposed event atm when the value changes from the child. + await BrowserTestUtils.waitForCondition(() => { + return ( + helper.getElement(MONTH_YEAR).textContent == + DATE_FORMAT(new Date(prevMonth)) + ); + }, "Should update date when updating months"); + + await helper.tearDown(); +}); +*/ + +/** + * When the prev month button is clicked, calendar should display the dates for + * the previous month. + */ +add_task(async function test_datepicker_prev_month_btn() { + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + helper.click(helper.getElement(BTN_PREV_MONTH)); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(prevMonth)) + ); + Assert.deepEqual( + getCalendarText(), + [ + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + ], + "2016-11" + ); + + await helper.tearDown(); +}); + +/** + * When the next month button is clicked, calendar should display the dates for + * the next month. + */ +add_task(async function test_datepicker_next_month_btn() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + helper.click(helper.getElement(BTN_NEXT_MONTH)); + + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextMonth)) + ); + Assert.deepEqual( + getCalendarText(), + [ + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + ], + "2017-01" + ); + + await helper.tearDown(); +}); + +/** + * When a date on the calendar is clicked, date picker should close and set + * value to the input box. + */ +add_task(async function test_datepicker_clicked() { + const inputValue = "2016-12-15"; + const firstDayOnCalendar = "2016-11-27"; + + await helper.openPicker( + `data:text/html, <input id="date" type="date" value="${inputValue}">` + ); + + let browser = helper.tab.linkedBrowser; + await verifyPickerPosition(browser, "date"); + + // Click the first item (top-left corner) of the calendar + let promise = BrowserTestUtils.waitForContentEvent( + helper.tab.linkedBrowser, + "input" + ); + helper.click(helper.getElement(DAYS_VIEW).children[0]); + await promise; + + let value = await SpecialPowers.spawn( + browser, + [], + () => content.document.querySelector("input").value + ); + Assert.equal(value, firstDayOnCalendar); + + await helper.tearDown(); +}); + +/** + * Ensure that the datepicker popop appears correctly positioned when + * the input field has been transformed. + */ +add_task(async function test_datepicker_transformed_position() { + const inputValue = "2016-12-15"; + + const style = + "transform: translateX(7px) translateY(13px); border-top: 2px; border-left: 5px; margin: 30px;"; + const iframeContent = `<input id="date" type="date" value="${inputValue}" style="${style}">`; + await helper.openPicker( + "data:text/html,<iframe id='iframe' src='http://example.net/document-builder.sjs?html=" + + encodeURI(iframeContent) + + "'>", + true + ); + + let bc = helper.tab.linkedBrowser.browsingContext.children[0]; + await verifyPickerPosition(bc, "date"); + + await helper.tearDown(); +}); + +/** + * Make sure picker is in correct state when it is reopened. + */ +add_task(async function test_datepicker_reopen_state() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + // Navigate to the next month but does not commit the change + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)) + ); + helper.click(helper.getElement(BTN_NEXT_MONTH)); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(nextMonth)) + ); + EventUtils.synthesizeKey("VK_ESCAPE", {}, window); + + // Ensures the picker opens to the month of the input value + await BrowserTestUtils.synthesizeMouseAtCenter( + "input", + {}, + gBrowser.selectedBrowser + ); + await helper.waitForPickerReady(); + Assert.equal( + helper.getElement(MONTH_YEAR).textContent, + DATE_FORMAT(new Date(inputValue)) + ); + + await helper.tearDown(); +}); + +/** + * When min and max attributes are set, calendar should show some dates as + * out-of-range. + */ +add_task(async function test_datepicker_min_max() { + const inputValue = "2016-12-15"; + const inputMin = "2016-12-05"; + const inputMax = "2016-12-25"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">` + ); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // R denotes out-of-range + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + [R], + ]), + "2016-12 with min & max" + ); + + await helper.tearDown(); +}); + +/** + * When step attribute is set, calendar should show some dates as off-step. + */ +add_task(async function test_datepicker_step() { + const inputValue = "2016-12-15"; + const inputStep = "5"; + + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}" step="${inputStep}">` + ); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // P denotes off-step + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + [P], + [], + [P], + [P], + [P], + ]), + "2016-12 with step" + ); + + await helper.tearDown(); +}); + +add_task(async function test_datepicker_abs_min() { + const inputValue = "0001-01-01"; + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + Assert.deepEqual( + getCalendarText(), + [ + "", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + ], + "0001-01" + ); + + await helper.tearDown(); +}); + +// This test checks if the change event is considered as user input event. +add_task(async function test_datepicker_handling_user_input() { + await helper.openPicker(`data:text/html, <input type="date">`); + + let changeEventPromise = SpecialPowers.spawn( + helper.tab.linkedBrowser, + [], + async () => { + let input = content.document.querySelector("input"); + await ContentTaskUtils.waitForEvent(input, "change", false, e => { + ok( + content.window.windowUtils.isHandlingUserInput, + "isHandlingUserInput should be true" + ); + return true; + }); + } + ); + // Click the first item (top-left corner) of the calendar + helper.click(helper.getElement(DAYS_VIEW).children[0]); + await changeEventPromise; + + await helper.tearDown(); +}); + +add_task(async function test_datepicker_abs_max() { + const inputValue = "275760-09-13"; + await helper.openPicker( + `data:text/html, <input type="date" value="${inputValue}">` + ); + + Assert.deepEqual( + getCalendarText(), + [ + "31", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + ], + "275760-09" + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/browser_default_image_filename.js b/toolkit/content/tests/browser/browser_default_image_filename.js new file mode 100644 index 0000000000..dc6eb8559e --- /dev/null +++ b/toolkit/content/tests/browser/browser_default_image_filename.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +/** + * TestCase for bug 564387 + * <https://bugzilla.mozilla.org/show_bug.cgi?id=564387> + */ +add_task(async function() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: + "data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7", + }, + async function(browser) { + let popupShownPromise = BrowserTestUtils.waitForEvent( + document, + "popupshown" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "img", + { + type: "contextmenu", + button: 2, + }, + browser + ); + + await popupShownPromise; + + let showFilePickerPromise = new Promise(resolve => { + MockFilePicker.showCallback = function(fp) { + is(fp.defaultString, "index.gif"); + resolve(); + }; + }); + + registerCleanupFunction(function() { + MockFilePicker.cleanup(); + }); + + // Select "Save Image As" option from context menu + var saveImageAsCommand = document.getElementById("context-saveimage"); + saveImageAsCommand.doCommand(); + + await showFilePickerPromise; + + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + } + ); +}); diff --git a/toolkit/content/tests/browser/browser_default_image_filename_redirect.js b/toolkit/content/tests/browser/browser_default_image_filename_redirect.js new file mode 100644 index 0000000000..a17d3dc3ee --- /dev/null +++ b/toolkit/content/tests/browser/browser_default_image_filename_redirect.js @@ -0,0 +1,53 @@ +/** + * TestCase for bug 1406253 + * <https://bugzilla.mozilla.org/show_bug.cgi?id=1406253> + * + * Load firebird.png, redirect it to doggy.png, and verify the filename is + * doggy.png in file picker dialog. + */ + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); +add_task(async function() { + // This URL will redirect to doggy.png. + const URL_FIREBIRD = + "http://mochi.test:8888/browser/toolkit/content/tests/browser/firebird.png"; + + await BrowserTestUtils.withNewTab(URL_FIREBIRD, async function(browser) { + // Click image to show context menu. + let popupShownPromise = BrowserTestUtils.waitForEvent( + document, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "img", + { type: "contextmenu", button: 2 }, + browser + ); + await popupShownPromise; + + // Prepare mock file picker. + let showFilePickerPromise = new Promise(resolve => { + MockFilePicker.showCallback = fp => resolve(fp.defaultString); + }); + registerCleanupFunction(function() { + MockFilePicker.cleanup(); + }); + + // Select "Save Image As" option from context menu + var saveImageAsCommand = document.getElementById("context-saveimage"); + saveImageAsCommand.doCommand(); + + let filename = await showFilePickerPromise; + is(filename, "doggy.png", "Verify image filename."); + + // Close context menu. + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + }); +}); diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_media.js b/toolkit/content/tests/browser/browser_delay_autoplay_media.js new file mode 100644 index 0000000000..acaf4ed16a --- /dev/null +++ b/toolkit/content/tests/browser/browser_delay_autoplay_media.js @@ -0,0 +1,145 @@ +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_multipleAudio.html"; +const PAGE_NO_AUTOPLAY = + "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html"; + +function check_audio_paused(browser, shouldBePaused) { + return SpecialPowers.spawn(browser, [shouldBePaused], shouldBePaused => { + var autoPlay = content.document.getElementById("autoplay"); + if (!autoPlay) { + ok(false, "Can't get the audio element!"); + } + is( + autoPlay.paused, + shouldBePaused, + "autoplay audio should " + (!shouldBePaused ? "not " : "") + "be paused." + ); + }); +} + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.useAudioChannelService.testing", true], + ["media.block-autoplay-until-in-foreground", true], + ], + }); +}); + +function set_media_autoplay() { + let audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + return; + } + audio.autoplay = true; +} + +add_task(async function delay_media_with_autoplay_keyword() { + info("- open new background tab -"); + const tab = BrowserTestUtils.addTab(window.gBrowser, PAGE_NO_AUTOPLAY); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- set media's autoplay property -"); + await SpecialPowers.spawn(tab.linkedBrowser, [], set_media_autoplay); + + info("- should delay autoplay media -"); + await waitForTabBlockEvent(tab, true); + + info("- switch tab to foreground -"); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + + info("- media should be resumed because tab has been visited -"); + await waitForTabPlayingEvent(tab, true); + await waitForTabBlockEvent(tab, false); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function delay_media_with_play_invocation() { + info("- open new background tab1 -"); + let tab1 = BrowserTestUtils.addTab(window.gBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab1.linkedBrowser); + + info("- should delay autoplay media for non-visited tab1 -"); + await waitForTabBlockEvent(tab1, true); + + info("- open new background tab2 -"); + let tab2 = BrowserTestUtils.addTab(window.gBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab2.linkedBrowser); + + info("- should delay autoplay for non-visited tab2 -"); + await waitForTabBlockEvent(tab2, true); + + info("- switch to tab1 -"); + await BrowserTestUtils.switchTab(window.gBrowser, tab1); + + info("- media in tab1 should be unblocked because the tab was visited -"); + await waitForTabPlayingEvent(tab1, true); + await waitForTabBlockEvent(tab1, false); + + info("- open another new foreground tab3 -"); + let tab3 = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "about:blank" + ); + info("- should still play media from tab1 -"); + await waitForTabPlayingEvent(tab1, true); + + info("- should still block media from tab2 -"); + await waitForTabPlayingEvent(tab2, false); + await waitForTabBlockEvent(tab2, true); + + info("- remove tabs -"); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); + +add_task(async function resume_delayed_media_when_enable_blocking_autoplay() { + // Disable autoplay and verify that when a tab is opened in the + // background and has had its playback start delayed, resuming via the audio + // tab indicator overrides the autoplay blocking logic. + // + // Clicking "play" on the audio tab indicator should always start playback + // in that tab, even if it's in an autoplay-blocked origin. + // + // Also test that that this block-autoplay logic override doesn't survive + // a new document being loaded into the tab; the new document should have + // to satisfy the autoplay requirements on its own. + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0], + ], + }); + + info("- open new background tab -"); + let tab = BrowserTestUtils.addTab(window.gBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- should block autoplay for non-visited tab -"); + await waitForTabBlockEvent(tab, true); + await check_audio_paused(tab.linkedBrowser, true); + tab.linkedBrowser.resumeMedia(); + + info("- should not block media from tab -"); + await waitForTabPlayingEvent(tab, true); + await check_audio_paused(tab.linkedBrowser, false); + + info( + "- check that loading a new URI in page clears gesture activation status -" + ); + BrowserTestUtils.loadURI(tab.linkedBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- should block autoplay again as gesture activation status cleared -"); + await check_audio_paused(tab.linkedBrowser, true); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); + + // Clear the block-autoplay pref. + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_media_pausedAfterPlay.js b/toolkit/content/tests/browser/browser_delay_autoplay_media_pausedAfterPlay.js new file mode 100644 index 0000000000..09dc5f9691 --- /dev/null +++ b/toolkit/content/tests/browser/browser_delay_autoplay_media_pausedAfterPlay.js @@ -0,0 +1,121 @@ +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html"; + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.useAudioChannelService.testing", true], + ["media.block-autoplay-until-in-foreground", true], + ], + }); +}); + +/** + * When media starts in an unvisited tab, we would delay its playback and resume + * media playback when the tab goes to foreground first time. There are two test + * cases are used to check different situations. + * + * The first one is used to check if the delayed media has been paused before + * the tab goes to foreground. Then, when the tab goes to foreground, media + * should still be paused. + * + * The second one is used to check if the delayed media has been paused, but + * it eventually was started again before the tab goes to foreground. Then, when + * the tab goes to foreground, media should be resumed. + */ +add_task(async function testShouldNotResumePausedMedia() { + info("- open new background tab and wait until it finishes loading -"); + const tab = BrowserTestUtils.addTab(window.gBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- play media and then immediately pause it -"); + await doPlayThenPauseOnMedia(tab); + + info("- show delay media playback icon on tab -"); + await waitForTabBlockEvent(tab, true); + + info("- selecting tab as foreground tab would resume the tab -"); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + + info("- resuming tab should dismiss delay autoplay icon -"); + await waitForTabBlockEvent(tab, false); + + info("- paused media should still be paused -"); + await checkAudioPauseState(tab, true /* should be paused */); + + info("- paused media won't generate tab playing icon -"); + await waitForTabPlayingEvent(tab, false); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testShouldResumePlayedMedia() { + info("- open new background tab and wait until it finishes loading -"); + const tab = BrowserTestUtils.addTab(window.gBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- play media, pause it, then play it again -"); + await doPlayPauseThenPlayOnMedia(tab); + + info("- show delay media playback icon on tab -"); + await waitForTabBlockEvent(tab, true); + + info("- select tab as foreground tab -"); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + + info("- resuming tab should dismiss delay autoplay icon -"); + await waitForTabBlockEvent(tab, false); + + info("- played media should still be played -"); + await checkAudioPauseState(tab, false /* should be played */); + + info("- played media would generate tab playing icon -"); + await waitForTabPlayingEvent(tab, true); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Helper functions. + */ +async function checkAudioPauseState(tab, expectedPaused) { + await SpecialPowers.spawn( + tab.linkedBrowser, + [expectedPaused], + expectedPaused => { + const audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + is(audio.paused, expectedPaused, "The pause state of audio is corret."); + } + ); +} + +async function doPlayThenPauseOnMedia(tab) { + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + const audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + audio.play(); + audio.pause(); + }); +} + +async function doPlayPauseThenPlayOnMedia(tab) { + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + const audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + audio.play(); + audio.pause(); + audio.play(); + }); +} diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_multipleMedia.js b/toolkit/content/tests/browser/browser_delay_autoplay_multipleMedia.js new file mode 100644 index 0000000000..3d60156cf1 --- /dev/null +++ b/toolkit/content/tests/browser/browser_delay_autoplay_multipleMedia.js @@ -0,0 +1,77 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_multipleAudio.html"; + +function check_autoplay_audio_onplay() { + let autoPlay = content.document.getElementById("autoplay"); + if (!autoPlay) { + ok(false, "Can't get the audio element!"); + } + + return new Promise((resolve, reject) => { + autoPlay.onplay = () => { + ok(false, "Should not receive play event!"); + this.onplay = null; + reject(); + }; + + autoPlay.pause(); + autoPlay.play(); + + content.setTimeout(() => { + ok(true, "Doesn't receive play event when media was blocked."); + autoPlay.onplay = null; + resolve(); + }, 1000); + }); +} + +function play_nonautoplay_audio_should_be_blocked() { + let nonAutoPlay = content.document.getElementById("nonautoplay"); + if (!nonAutoPlay) { + ok(false, "Can't get the audio element!"); + } + + nonAutoPlay.play(); + ok(nonAutoPlay.paused, "The blocked audio can't be playback."); +} + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.useAudioChannelService.testing", true], + ["media.block-autoplay-until-in-foreground", true], + ], + }); +}); + +add_task(async function block_multiple_media() { + info("- open new background tab -"); + let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + BrowserTestUtils.loadURI(browser, PAGE); + await BrowserTestUtils.browserLoaded(browser); + + info("- tab should be blocked -"); + await waitForTabBlockEvent(tab, true); + + info("- autoplay media should be blocked -"); + await SpecialPowers.spawn(browser, [], check_autoplay_audio_onplay); + + info("- non-autoplay can't start playback when the tab is blocked -"); + await SpecialPowers.spawn( + browser, + [], + play_nonautoplay_audio_should_be_blocked + ); + + info("- select tab as foreground tab -"); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + + info("- tab should be resumed -"); + await waitForTabPlayingEvent(tab, true); + await waitForTabBlockEvent(tab, false); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_notInTreeAudio.js b/toolkit/content/tests/browser/browser_delay_autoplay_notInTreeAudio.js new file mode 100644 index 0000000000..c1cca96b2d --- /dev/null +++ b/toolkit/content/tests/browser/browser_delay_autoplay_notInTreeAudio.js @@ -0,0 +1,66 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html"; + +function check_audio_pause_state(expectPause) { + var audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + is(audio.paused, expectPause, "The pause state of audio is corret."); +} + +function play_not_in_tree_audio() { + var audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + content.document.body.removeChild(audio); + + // Add timeout to ensure the audio is removed from DOM tree. + content.setTimeout(function() { + info("Prepare to start playing audio."); + audio.play(); + }, 1000); +} + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.useAudioChannelService.testing", true], + ["media.block-autoplay-until-in-foreground", true], + ], + }); +}); + +add_task(async function block_not_in_tree_media() { + info("- open new background tab -"); + let tab = BrowserTestUtils.addTab(window.gBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- tab should not be blocked -"); + await waitForTabBlockEvent(tab, false); + + info("- check audio's playing state -"); + await SpecialPowers.spawn(tab.linkedBrowser, [true], check_audio_pause_state); + + info("- playing audio -"); + await SpecialPowers.spawn(tab.linkedBrowser, [], play_not_in_tree_audio); + + info("- tab should be blocked -"); + await waitForTabBlockEvent(tab, true); + + info("- switch tab -"); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + + info("- tab should be resumed -"); + await waitForTabBlockEvent(tab, false); + + info("- tab should be audible -"); + await waitForTabPlayingEvent(tab, true); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_playAfterTabVisible.js b/toolkit/content/tests/browser/browser_delay_autoplay_playAfterTabVisible.js new file mode 100644 index 0000000000..e1f3f8f814 --- /dev/null +++ b/toolkit/content/tests/browser/browser_delay_autoplay_playAfterTabVisible.js @@ -0,0 +1,68 @@ +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html"; + +function check_audio_pause_state(expectPause) { + var audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + is(audio.paused, expectPause, "The pause state of audio is corret."); +} + +function play_audio() { + var audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + audio.play(); + return new Promise(resolve => { + audio.onplay = function() { + audio.onplay = null; + ok(true, "Audio starts playing."); + resolve(); + }; + }); +} + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.useAudioChannelService.testing", true], + ["media.block-autoplay-until-in-foreground", true], + ], + }); +}); + +/** + * This test is used for testing the visible tab which was not resumed yet. + * If the tab doesn't have any media component, it won't be resumed even it + * has already gone to foreground until we start audio. + */ +add_task(async function media_should_be_able_to_play_in_visible_tab() { + info("- open new background tab, and check tab's media pause state -"); + let tab = BrowserTestUtils.addTab(window.gBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await SpecialPowers.spawn(tab.linkedBrowser, [true], check_audio_pause_state); + + info( + "- select tab as foreground tab, and tab's media should still be paused -" + ); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + await SpecialPowers.spawn(tab.linkedBrowser, [true], check_audio_pause_state); + + info("- start audio in tab -"); + await SpecialPowers.spawn(tab.linkedBrowser, [], play_audio); + + info("- audio should be playing -"); + await waitForTabBlockEvent(tab, false); + await SpecialPowers.spawn( + tab.linkedBrowser, + [false], + check_audio_pause_state + ); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_playMediaInMuteTab.js b/toolkit/content/tests/browser/browser_delay_autoplay_playMediaInMuteTab.js new file mode 100644 index 0000000000..c333021697 --- /dev/null +++ b/toolkit/content/tests/browser/browser_delay_autoplay_playMediaInMuteTab.js @@ -0,0 +1,97 @@ +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_nonAutoplayAudio.html"; + +function wait_for_event(browser, event) { + return BrowserTestUtils.waitForEvent(browser, event, false, event => { + is( + event.originalTarget, + browser, + "Event must be dispatched to correct browser." + ); + return true; + }); +} + +function check_audio_volume_and_mute(expectedMute) { + var audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + let expectedVolume = expectedMute ? 0.0 : 1.0; + is(expectedVolume, audio.computedVolume, "Audio's volume is correct!"); + is(expectedMute, audio.computedMuted, "Audio's mute state is correct!"); +} + +function check_audio_pause_state(expectedPauseState) { + var audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + is(audio.paused, expectedPauseState, "Audio is paused."); +} + +function play_audio() { + var audio = content.document.getElementById("testAudio"); + if (!audio) { + ok(false, "Can't get the audio element!"); + } + + audio.play(); + ok(audio.paused, "Can't play audio, because the tab was still blocked."); +} + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.useAudioChannelService.testing", true], + ["media.block-autoplay-until-in-foreground", true], + ], + }); +}); + +add_task(async function unblock_icon_should_disapear_after_resume_tab() { + info("- open new background tab -"); + let tab = BrowserTestUtils.addTab(window.gBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- audio doesn't be started in beginning -"); + await SpecialPowers.spawn(tab.linkedBrowser, [true], check_audio_pause_state); + + info("- audio shouldn't be muted -"); + await SpecialPowers.spawn( + tab.linkedBrowser, + [false], + check_audio_volume_and_mute + ); + + info("- tab shouldn't display unblocking icon -"); + await waitForTabBlockEvent(tab, false); + + info("- mute tab -"); + tab.linkedBrowser.mute(); + ok(tab.linkedBrowser.audioMuted, "Audio should be muted now"); + + info("- try to start audio in background tab -"); + await SpecialPowers.spawn(tab.linkedBrowser, [], play_audio); + + info("- tab should display unblocking icon -"); + await waitForTabBlockEvent(tab, true); + + info("- select tab as foreground tab -"); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + + info("- audio shoule be muted, but not be blocked -"); + await SpecialPowers.spawn( + tab.linkedBrowser, + [true], + check_audio_volume_and_mute + ); + + info("- tab should not display unblocking icon -"); + await waitForTabBlockEvent(tab, false); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_silentAudioTrack_media.js b/toolkit/content/tests/browser/browser_delay_autoplay_silentAudioTrack_media.js new file mode 100644 index 0000000000..24b48d4999 --- /dev/null +++ b/toolkit/content/tests/browser/browser_delay_autoplay_silentAudioTrack_media.js @@ -0,0 +1,63 @@ +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_silentAudioTrack.html"; + +async function click_unblock_icon(tab) { + let icon = tab.soundPlayingIcon; + + await hover_icon(icon, document.getElementById("tabbrowser-tab-tooltip")); + EventUtils.synthesizeMouseAtCenter(icon, { button: 0 }); + leave_icon(icon); +} + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.useAudioChannelService.testing", true], + ["media.block-autoplay-until-in-foreground", true], + ], + }); +}); + +add_task(async function unblock_icon_should_disapear_after_resume_tab() { + info("- open new background tab -"); + let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank"); + BrowserTestUtils.loadURI(tab.linkedBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- tab should display unblocking icon -"); + await waitForTabBlockEvent(tab, true); + + info("- select tab as foreground tab -"); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + + info("- should not display unblocking icon -"); + await waitForTabBlockEvent(tab, false); + + info("- should not display sound indicator icon -"); + await waitForTabPlayingEvent(tab, false); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function should_not_show_sound_indicator_after_resume_tab() { + info("- open new background tab -"); + let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank"); + BrowserTestUtils.loadURI(tab.linkedBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- tab should display unblocking icon -"); + await waitForTabBlockEvent(tab, true); + + info("- click play tab icon -"); + await click_unblock_icon(tab); + + info("- should not display unblocking icon -"); + await waitForTabBlockEvent(tab, false); + + info("- should not display sound indicator icon -"); + await waitForTabPlayingEvent(tab, false); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_delay_autoplay_webAudio.js b/toolkit/content/tests/browser/browser_delay_autoplay_webAudio.js new file mode 100644 index 0000000000..8758cf2629 --- /dev/null +++ b/toolkit/content/tests/browser/browser_delay_autoplay_webAudio.js @@ -0,0 +1,40 @@ +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_webAudio.html"; + +// The tab closing code leaves an uncaught rejection. This test has been +// whitelisted until the issue is fixed. +if (!gMultiProcessBrowser) { + ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm", this); + PromiseTestUtils.expectUncaughtRejection(/is no longer, usable/); +} + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.useAudioChannelService.testing", true], + ["media.block-autoplay-until-in-foreground", true], + ], + }); +}); + +add_task(async function block_web_audio() { + info("- open new background tab -"); + let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank"); + BrowserTestUtils.loadURI(tab.linkedBrowser, PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("- tab should be blocked -"); + await waitForTabBlockEvent(tab, true); + + info("- switch tab -"); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + + info("- tab should be resumed -"); + await waitForTabBlockEvent(tab, false); + + info("- tab should be audible -"); + await waitForTabPlayingEvent(tab, true); + + info("- remove tab -"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_f7_caret_browsing.js b/toolkit/content/tests/browser/browser_f7_caret_browsing.js new file mode 100644 index 0000000000..334d40a1f8 --- /dev/null +++ b/toolkit/content/tests/browser/browser_f7_caret_browsing.js @@ -0,0 +1,278 @@ +var gListener = null; +const kURL = + "data:text/html;charset=utf-8,Caret browsing is fun.<input id='in'>"; + +const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled"; +const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret"; +const kPrefCaretBrowsingOn = "accessibility.browsewithcaret"; + +var oldPrefs = {}; +for (let pref of [ + kPrefShortcutEnabled, + kPrefWarnOnEnable, + kPrefCaretBrowsingOn, +]) { + oldPrefs[pref] = Services.prefs.getBoolPref(pref); +} + +Services.prefs.setBoolPref(kPrefShortcutEnabled, true); +Services.prefs.setBoolPref(kPrefWarnOnEnable, true); +Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false); + +registerCleanupFunction(function() { + for (let pref of [ + kPrefShortcutEnabled, + kPrefWarnOnEnable, + kPrefCaretBrowsingOn, + ]) { + Services.prefs.setBoolPref(pref, oldPrefs[pref]); + } +}); + +// NB: not using BrowserTestUtils.domWindowOpened here because there's no way to +// undo waiting for a window open. If we don't want the window to be opened, and +// wait for it to verify that it indeed does not open, we need to be able to +// then "stop" waiting so that when we next *do* want it to open, our "old" +// listener doesn't fire and do things we don't want (like close the window...). +let gCaretPromptOpeningObserver; +function promiseCaretPromptOpened() { + return new Promise(resolve => { + function observer(subject, topic, data) { + if (topic == "domwindowopened") { + Services.ww.unregisterNotification(observer); + let win = subject; + BrowserTestUtils.waitForEvent( + win, + "load", + false, + e => e.target.location.href != "about:blank" + ).then(() => resolve(win)); + gCaretPromptOpeningObserver = null; + } + } + Services.ww.registerNotification(observer); + gCaretPromptOpeningObserver = observer; + }); +} + +function hitF7(async = true) { + let f7 = () => EventUtils.sendKey("F7"); + // Need to not stop execution inside this task: + if (async) { + executeSoon(f7); + } else { + f7(); + } +} + +function syncToggleCaretNoDialog(expected) { + let openedDialog = false; + promiseCaretPromptOpened().then(function(win) { + openedDialog = true; + win.close(); // This will eventually return focus here and allow the test to continue... + }); + // Cause the dialog to appear sync, if it still does. + hitF7(false); + + let expectedStr = expected ? "on." : "off."; + ok( + !openedDialog, + "Shouldn't open a dialog to turn caret browsing " + expectedStr + ); + // Need to clean up if the dialog wasn't opened, so the observer doesn't get + // re-triggered later on causing "issues". + if (!openedDialog) { + Services.ww.unregisterNotification(gCaretPromptOpeningObserver); + gCaretPromptOpeningObserver = null; + } + let prefVal = Services.prefs.getBoolPref(kPrefCaretBrowsingOn); + is(prefVal, expected, "Caret browsing should now be " + expectedStr); +} + +function waitForFocusOnInput(browser) { + return SpecialPowers.spawn(browser, [], async function() { + let textEl = content.document.getElementById("in"); + return ContentTaskUtils.waitForCondition(() => { + return content.document.activeElement == textEl; + }, "Input should get focused."); + }); +} + +function focusInput(browser) { + return SpecialPowers.spawn(browser, [], async function() { + let textEl = content.document.getElementById("in"); + textEl.focus(); + }); +} + +add_task(async function checkTogglingCaretBrowsing() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kURL); + await focusInput(tab.linkedBrowser); + + let promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + let prompt = await promiseGotKey; + let doc = prompt.document; + let dialog = doc.getElementById("commonDialog"); + is(dialog.defaultButton, "cancel", "No button should be the default"); + ok( + !doc.getElementById("checkbox").checked, + "Checkbox shouldn't be checked by default." + ); + let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + + dialog.cancelDialog(); + await promiseDialogUnloaded; + info("Dialog unloaded"); + await waitForFocusOnInput(tab.linkedBrowser); + ok( + !Services.prefs.getBoolPref(kPrefCaretBrowsingOn), + "Caret browsing should still be off after cancelling the dialog." + ); + + promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + prompt = await promiseGotKey; + + doc = prompt.document; + dialog = doc.getElementById("commonDialog"); + is(dialog.defaultButton, "cancel", "No button should be the default"); + ok( + !doc.getElementById("checkbox").checked, + "Checkbox shouldn't be checked by default." + ); + promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + + dialog.acceptDialog(); + await promiseDialogUnloaded; + info("Dialog unloaded"); + await waitForFocusOnInput(tab.linkedBrowser); + ok( + Services.prefs.getBoolPref(kPrefCaretBrowsingOn), + "Caret browsing should be on after accepting the dialog." + ); + + syncToggleCaretNoDialog(false); + + promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + prompt = await promiseGotKey; + doc = prompt.document; + dialog = doc.getElementById("commonDialog"); + + is(dialog.defaultButton, "cancel", "No button should be the default"); + ok( + !doc.getElementById("checkbox").checked, + "Checkbox shouldn't be checked by default." + ); + + promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + dialog.cancelDialog(); + await promiseDialogUnloaded; + info("Dialog unloaded"); + await waitForFocusOnInput(tab.linkedBrowser); + + ok( + !Services.prefs.getBoolPref(kPrefCaretBrowsingOn), + "Caret browsing should still be off after cancelling the dialog." + ); + + Services.prefs.setBoolPref(kPrefShortcutEnabled, true); + Services.prefs.setBoolPref(kPrefWarnOnEnable, true); + Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function toggleCheckboxNoCaretBrowsing() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kURL); + await focusInput(tab.linkedBrowser); + + let promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + let prompt = await promiseGotKey; + let doc = prompt.document; + let dialog = doc.getElementById("commonDialog"); + is(dialog.defaultButton, "cancel", "No button should be the default"); + let checkbox = doc.getElementById("checkbox"); + ok(!checkbox.checked, "Checkbox shouldn't be checked by default."); + + // Check the box: + checkbox.click(); + + let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + + // Say no: + dialog.getButton("cancel").click(); + + await promiseDialogUnloaded; + info("Dialog unloaded"); + await waitForFocusOnInput(tab.linkedBrowser); + ok( + !Services.prefs.getBoolPref(kPrefCaretBrowsingOn), + "Caret browsing should still be off." + ); + ok( + !Services.prefs.getBoolPref(kPrefShortcutEnabled), + "Shortcut should now be disabled." + ); + + syncToggleCaretNoDialog(false); + ok( + !Services.prefs.getBoolPref(kPrefShortcutEnabled), + "Shortcut should still be disabled." + ); + + Services.prefs.setBoolPref(kPrefShortcutEnabled, true); + Services.prefs.setBoolPref(kPrefWarnOnEnable, true); + Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function toggleCheckboxWantCaretBrowsing() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kURL); + await focusInput(tab.linkedBrowser); + + let promiseGotKey = promiseCaretPromptOpened(); + hitF7(); + let prompt = await promiseGotKey; + let doc = prompt.document; + let dialog = doc.getElementById("commonDialog"); + is(dialog.defaultButton, "cancel", "No button should be the default"); + let checkbox = doc.getElementById("checkbox"); + ok(!checkbox.checked, "Checkbox shouldn't be checked by default."); + + // Check the box: + checkbox.click(); + + let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload"); + + // Say yes: + dialog.acceptDialog(); + await promiseDialogUnloaded; + info("Dialog unloaded"); + await waitForFocusOnInput(tab.linkedBrowser); + ok( + Services.prefs.getBoolPref(kPrefCaretBrowsingOn), + "Caret browsing should now be on." + ); + ok( + Services.prefs.getBoolPref(kPrefShortcutEnabled), + "Shortcut should still be enabled." + ); + ok( + !Services.prefs.getBoolPref(kPrefWarnOnEnable), + "Should no longer warn when enabling." + ); + + syncToggleCaretNoDialog(false); + syncToggleCaretNoDialog(true); + syncToggleCaretNoDialog(false); + + Services.prefs.setBoolPref(kPrefShortcutEnabled, true); + Services.prefs.setBoolPref(kPrefWarnOnEnable, true); + Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_findbar.js b/toolkit/content/tests/browser/browser_findbar.js new file mode 100644 index 0000000000..b6f116245e --- /dev/null +++ b/toolkit/content/tests/browser/browser_findbar.js @@ -0,0 +1,523 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +ChromeUtils.import("resource://gre/modules/Timer.jsm", this); + +const TEST_PAGE_URI = "data:text/html;charset=utf-8,The letter s."; +// Using 'javascript' schema to bypass E10SUtils.canLoadURIInRemoteType, because +// it does not allow 'data:' URI to be loaded in the parent process. +const E10S_PARENT_TEST_PAGE_URI = + getRootDirectory(gTestPath) + "file_empty.html"; +const TEST_PAGE_URI_WITHIFRAME = + "https://example.com/browser/toolkit/content/tests/browser/file_findinframe.html"; + +/** + * Makes sure that the findbar hotkeys (' and /) event listeners + * are added to the system event group and do not get blocked + * by calling stopPropagation on a keypress event on a page. + */ +add_task(async function test_hotkey_event_propagation() { + info("Ensure hotkeys are not affected by stopPropagation."); + + // Opening new tab + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE_URI + ); + let browser = gBrowser.getBrowserForTab(tab); + let findbar = await gBrowser.getFindBar(); + + // Pressing these keys open the findbar. + const HOTKEYS = ["/", "'"]; + + // Checking if findbar appears when any hotkey is pressed. + for (let key of HOTKEYS) { + is(findbar.hidden, true, "Findbar is hidden now."); + gBrowser.selectedTab = tab; + await SimpleTest.promiseFocus(gBrowser.selectedBrowser); + await BrowserTestUtils.sendChar(key, browser); + is(findbar.hidden, false, "Findbar should not be hidden."); + await closeFindbarAndWait(findbar); + } + + // Stop propagation for all keyboard events. + await SpecialPowers.spawn(browser, [], () => { + const stopPropagation = e => { + e.stopImmediatePropagation(); + }; + let window = content.document.defaultView; + window.addEventListener("keydown", stopPropagation); + window.addEventListener("keypress", stopPropagation); + window.addEventListener("keyup", stopPropagation); + }); + + // Checking if findbar still appears when any hotkey is pressed. + for (let key of HOTKEYS) { + is(findbar.hidden, true, "Findbar is hidden now."); + gBrowser.selectedTab = tab; + await SimpleTest.promiseFocus(gBrowser.selectedBrowser); + await BrowserTestUtils.sendChar(key, browser); + is(findbar.hidden, false, "Findbar should not be hidden."); + await closeFindbarAndWait(findbar); + } + + gBrowser.removeTab(tab); +}); + +add_task(async function test_not_found() { + info("Check correct 'Phrase not found' on new tab"); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE_URI + ); + + // Search for the first word. + await promiseFindFinished("--- THIS SHOULD NEVER MATCH ---", false); + let findbar = gBrowser.getCachedFindBar(); + is( + findbar._findStatusDesc.textContent, + findbar._notFoundStr, + "Findbar status text should be 'Phrase not found'" + ); + + gBrowser.removeTab(tab); +}); + +add_task(async function test_found() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE_URI + ); + + // Search for a string that WILL be found, with 'Highlight All' on + await promiseFindFinished("S", true); + ok( + !gBrowser.getCachedFindBar()._findStatusDesc.textContent, + "Findbar status should be empty" + ); + + gBrowser.removeTab(tab); +}); + +// Setting first findbar to case-sensitive mode should not affect +// new tab find bar. +add_task(async function test_tabwise_case_sensitive() { + let tab1 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE_URI + ); + let findbar1 = await gBrowser.getFindBar(); + + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE_URI + ); + let findbar2 = await gBrowser.getFindBar(); + + // Toggle case sensitivity for first findbar + findbar1.getElement("find-case-sensitive").click(); + + gBrowser.selectedTab = tab1; + + // Not found for first tab. + await promiseFindFinished("S", true); + is( + findbar1._findStatusDesc.textContent, + findbar1._notFoundStr, + "Findbar status text should be 'Phrase not found'" + ); + + gBrowser.selectedTab = tab2; + + // But it didn't affect the second findbar. + await promiseFindFinished("S", true); + ok(!findbar2._findStatusDesc.textContent, "Findbar status should be empty"); + + gBrowser.removeTab(tab1); + gBrowser.removeTab(tab2); +}); + +/** + * Navigating from a web page (for example mozilla.org) to an internal page + * (like about:addons) might trigger a change of browser's remoteness. + * 'Remoteness change' means that rendering page content moves from child + * process into the parent process or the other way around. + * This test ensures that findbar properly handles such a change. + */ +add_task(async function test_reinitialization_at_remoteness_change() { + // This test only makes sence in e10s evironment. + if (!gMultiProcessBrowser) { + info("Skipping this test because of non-e10s environment."); + return; + } + + info("Ensure findbar re-initialization at remoteness change."); + + // Load a remote page and trigger findbar construction. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE_URI + ); + let browser = gBrowser.getBrowserForTab(tab); + let findbar = await gBrowser.getFindBar(); + + // Findbar should operate normally. + await promiseFindFinished("z", false); + is( + findbar._findStatusDesc.textContent, + findbar._notFoundStr, + "Findbar status text should be 'Phrase not found'" + ); + + await promiseFindFinished("s", false); + ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty"); + + // Moving browser into the parent process and reloading sample data. + ok(browser.isRemoteBrowser, "Browser should be remote now."); + await promiseRemotenessChange(tab, false); + let docLoaded = BrowserTestUtils.browserLoaded( + browser, + false, + E10S_PARENT_TEST_PAGE_URI + ); + BrowserTestUtils.loadURI(browser, E10S_PARENT_TEST_PAGE_URI); + await docLoaded; + ok(!browser.isRemoteBrowser, "Browser should not be remote any more."); + browser.contentDocument.body.append("The letter s."); + browser.contentDocument.body.clientHeight; // Force flush. + + // Findbar should keep operating normally after remoteness change. + await promiseFindFinished("z", false); + is( + findbar._findStatusDesc.textContent, + findbar._notFoundStr, + "Findbar status text should be 'Phrase not found'" + ); + + await promiseFindFinished("s", false); + ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty"); + + BrowserTestUtils.removeTab(tab); +}); + +/** + * Ensure that the initial typed characters aren't lost immediately after + * opening the find bar. + */ +add_task(async function e10sLostKeys() { + // This test only makes sence in e10s evironment. + if (!gMultiProcessBrowser) { + info("Skipping this test because of non-e10s environment."); + return; + } + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE_URI + ); + + ok(!gFindBarInitialized, "findbar isn't initialized yet"); + + await gFindBarPromise; + let findBar = gFindBar; + let initialValue = findBar._findField.value; + + await EventUtils.synthesizeAndWaitKey( + "F", + { accelKey: true }, + window, + null, + () => { + // We can't afford to wait for the promise to resolve, by then the + // find bar is visible and focused, so sending characters to the + // content browser wouldn't work. + isnot( + document.activeElement, + findBar._findField, + "findbar is not yet focused" + ); + EventUtils.synthesizeKey("a"); + EventUtils.synthesizeKey("b"); + EventUtils.synthesizeKey("c"); + is( + findBar._findField.value, + initialValue, + "still has initial find query" + ); + } + ); + + await BrowserTestUtils.waitForCondition( + () => findBar._findField.value.length == 3 + ); + is(document.activeElement, findBar._findField, "findbar is now focused"); + is(findBar._findField.value, "abc", "abc fully entered as find query"); + + BrowserTestUtils.removeTab(tab); +}); + +/** + * This test makes sure that keyboard operations still occur + * after the findbar is opened and closed. + */ +add_task(async function test_open_and_close_keys() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,<body style='height: 5000px;'>Hello There</body>" + ); + + await gFindBarPromise; + let findBar = gFindBar; + + is(findBar.hidden, true, "Findbar is hidden now."); + let openedPromise = BrowserTestUtils.waitForEvent(findBar, "findbaropen"); + await EventUtils.synthesizeKey("f", { accelKey: true }); + await openedPromise; + + is(findBar.hidden, false, "Findbar should not be hidden."); + + let closedPromise = BrowserTestUtils.waitForEvent(findBar, "findbarclose"); + await EventUtils.synthesizeKey("KEY_Escape"); + await closedPromise; + + let scrollPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "scroll" + ); + await EventUtils.synthesizeKey("KEY_ArrowDown"); + await scrollPromise; + + let scrollPosition = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function() { + return content.document.body.scrollTop; + } + ); + + ok(scrollPosition > 0, "Scrolled ok to " + scrollPosition); + + BrowserTestUtils.removeTab(tab); +}); + +// This test loads an editable area within an iframe and then +// performs a search. Focusing the editable area should still +// allow keyboard events to be received. +add_task(async function test_hotkey_insubframe() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE_URI_WITHIFRAME + ); + + await gFindBarPromise; + let findBar = gFindBar; + + // Focus the editable area within the frame. + let browser = gBrowser.selectedBrowser; + let frameBC = browser.browsingContext.children[0]; + await SpecialPowers.spawn(frameBC, [], async () => { + content.document.body.focus(); + content.document.defaultView.focus(); + }); + + // Start a find and wait for the findbar to open. + let findBarOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser, + "findbaropen" + ); + EventUtils.synthesizeKey("f", { accelKey: true }); + await findBarOpenPromise; + + // Opening the findbar would have focused the find textbox. + // Focus the editable area again. + let cursorPos = await SpecialPowers.spawn(frameBC, [], async () => { + content.document.body.focus(); + content.document.defaultView.focus(); + return content.getSelection().anchorOffset; + }); + is(cursorPos, 0, "initial cursor position"); + + // Try moving the caret. + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, frameBC); + + cursorPos = await SpecialPowers.spawn(frameBC, [], async () => { + return content.getSelection().anchorOffset; + }); + is(cursorPos, 1, "cursor moved"); + + await closeFindbarAndWait(findBar); + gBrowser.removeTab(tab); +}); + +/** + * Reloading a page should use the same match case / whole word + * state for the search. + */ +add_task(async function test_preservestate_on_reload() { + for (let stateChange of ["case-sensitive", "entire-word"]) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,<p>There is a cat named Theo in the kitchen with another cat named Catherine. The two of them are thirsty." + ); + + // Start a find and wait for the findbar to open. + let findBarOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser, + "findbaropen" + ); + EventUtils.synthesizeKey("f", { accelKey: true }); + await findBarOpenPromise; + + let isEntireWord = stateChange == "entire-word"; + + let findbar = await gBrowser.getFindBar(); + + // Find some text. + let promiseMatches = promiseGetMatchCount(findbar); + await promiseFindFinished("The", true); + + let matches = await promiseMatches; + is(matches.current, 1, "Correct match position " + stateChange); + is(matches.total, 7, "Correct number of matches " + stateChange); + + // Turn on the case sensitive or entire word option. + findbar.getElement("find-" + stateChange).click(); + + promiseMatches = promiseGetMatchCount(findbar); + gFindBar.onFindAgainCommand(); + matches = await promiseMatches; + is( + matches.current, + 2, + "Correct match position after state change matches " + stateChange + ); + is( + matches.total, + isEntireWord ? 2 : 3, + "Correct number after state change matches " + stateChange + ); + + // Reload the page. + let loadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + true + ); + gBrowser.reload(); + await loadedPromise; + + // Perform a find again. The state should be preserved. + promiseMatches = promiseGetMatchCount(findbar); + gFindBar.onFindAgainCommand(); + matches = await promiseMatches; + is( + matches.current, + 1, + "Correct match position after reload and find again " + stateChange + ); + is( + matches.total, + isEntireWord ? 2 : 3, + "Correct number of matches after reload and find again " + stateChange + ); + + // Turn off the case sensitive or entire word option and find again. + findbar.getElement("find-" + stateChange).click(); + + promiseMatches = promiseGetMatchCount(findbar); + gFindBar.onFindAgainCommand(); + matches = await promiseMatches; + + is( + matches.current, + isEntireWord ? 4 : 2, + "Correct match position after reload and find again reset " + stateChange + ); + is( + matches.total, + 7, + "Correct number of matches after reload and find again reset " + + stateChange + ); + + findbar.clear(); + await closeFindbarAndWait(findbar); + + gBrowser.removeTab(tab); + } +}); + +async function promiseFindFinished(searchText, highlightOn) { + let findbar = await gBrowser.getFindBar(); + findbar.startFind(findbar.FIND_NORMAL); + let highlightElement = findbar.getElement("highlight"); + if (highlightElement.checked != highlightOn) { + highlightElement.click(); + } + return new Promise(resolve => { + executeSoon(() => { + findbar._findField.value = searchText; + + let resultListener; + // When highlighting is on the finder sends a second "FOUND" message after + // the search wraps. This causes timing problems with e10s. waitMore + // forces foundOrTimeout wait for the second "FOUND" message before + // resolving the promise. + let waitMore = highlightOn; + let findTimeout = setTimeout(() => foundOrTimedout(null), 2000); + let foundOrTimedout = function(aData) { + if (aData !== null && waitMore) { + waitMore = false; + return; + } + if (aData === null) { + info("Result listener not called, timeout reached."); + } + clearTimeout(findTimeout); + findbar.browser.finder.removeResultListener(resultListener); + resolve(); + }; + + resultListener = { + onFindResult: foundOrTimedout, + onCurrentSelection() {}, + onMatchesCountResult() {}, + onHighlightFinished() {}, + }; + findbar.browser.finder.addResultListener(resultListener); + findbar._find(); + }); + }); +} + +function promiseGetMatchCount(findbar) { + return new Promise(resolve => { + let resultListener = { + onFindResult() {}, + onCurrentSelection() {}, + onHighlightFinished() {}, + onMatchesCountResult(response) { + if (response.total > 0) { + findbar.browser.finder.removeResultListener(resultListener); + resolve(response); + } + }, + }; + findbar.browser.finder.addResultListener(resultListener); + }); +} + +function promiseRemotenessChange(tab, shouldBeRemote) { + return new Promise(resolve => { + let browser = gBrowser.getBrowserForTab(tab); + tab.addEventListener( + "TabRemotenessChange", + function() { + resolve(); + }, + { once: true } + ); + let remoteType = shouldBeRemote + ? E10SUtils.DEFAULT_REMOTE_TYPE + : E10SUtils.NOT_REMOTE; + gBrowser.updateBrowserRemoteness(browser, { remoteType }); + }); +} diff --git a/toolkit/content/tests/browser/browser_findbar_disabled_manual.js b/toolkit/content/tests/browser/browser_findbar_disabled_manual.js new file mode 100644 index 0000000000..ef37f7dc9a --- /dev/null +++ b/toolkit/content/tests/browser/browser_findbar_disabled_manual.js @@ -0,0 +1,33 @@ +const TEST_PAGE_URI = "data:text/html;charset=utf-8,The letter s."; + +// Disable manual (FAYT) findbar hotkeys. +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [["accessibility.typeaheadfind.manual", false]], + }); +}); + +// Makes sure that the findbar hotkeys (' and /) have no effect. +add_task(async function test_hotkey_disabled() { + // Opening new tab. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE_URI + ); + let browser = gBrowser.getBrowserForTab(tab); + let findbar = await gBrowser.getFindBar(); + + // Pressing these keys open the findbar normally. + const HOTKEYS = ["/", "'"]; + + // Make sure no findbar appears when pressed. + for (let key of HOTKEYS) { + is(findbar.hidden, true, "Findbar is hidden now."); + gBrowser.selectedTab = tab; + await SimpleTest.promiseFocus(gBrowser.selectedBrowser); + await BrowserTestUtils.sendChar(key, browser); + is(findbar.hidden, true, "Findbar should still be hidden."); + } + + gBrowser.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_isSynthetic.js b/toolkit/content/tests/browser/browser_isSynthetic.js new file mode 100644 index 0000000000..f2207cb2b6 --- /dev/null +++ b/toolkit/content/tests/browser/browser_isSynthetic.js @@ -0,0 +1,66 @@ +function LocationChangeListener(browser) { + this.browser = browser; + browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); +} + +LocationChangeListener.prototype = { + wasSynthetic: false, + browser: null, + + destroy() { + this.browser.removeProgressListener(this); + }, + + onLocationChange(webProgress, request, location, flags) { + this.wasSynthetic = this.browser.isSyntheticDocument; + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; + +const FILES = gTestPath + .replace("browser_isSynthetic.js", "") + .replace("chrome://mochitests/content/", "http://example.com/"); + +function waitForPageShow(browser) { + return BrowserTestUtils.waitForContentEvent(browser, "pageshow", true); +} + +add_task(async function() { + let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + let listener = new LocationChangeListener(browser); + + is(browser.isSyntheticDocument, false, "Should not be synthetic"); + + let loadPromise = waitForPageShow(browser); + BrowserTestUtils.loadURI(browser, "data:text/html;charset=utf-8,<html/>"); + await loadPromise; + is(listener.wasSynthetic, false, "Should not be synthetic"); + is(browser.isSyntheticDocument, false, "Should not be synthetic"); + + loadPromise = waitForPageShow(browser); + BrowserTestUtils.loadURI(browser, FILES + "empty.png"); + await loadPromise; + is(listener.wasSynthetic, true, "Should be synthetic"); + is(browser.isSyntheticDocument, true, "Should be synthetic"); + + loadPromise = waitForPageShow(browser); + browser.goBack(); + await loadPromise; + is(listener.wasSynthetic, false, "Should not be synthetic"); + is(browser.isSyntheticDocument, false, "Should not be synthetic"); + + loadPromise = waitForPageShow(browser); + browser.goForward(); + await loadPromise; + is(listener.wasSynthetic, true, "Should be synthetic"); + is(browser.isSyntheticDocument, true, "Should be synthetic"); + + listener.destroy(); + gBrowser.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js b/toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js new file mode 100644 index 0000000000..4eda24f531 --- /dev/null +++ b/toolkit/content/tests/browser/browser_keyevents_during_autoscrolling.js @@ -0,0 +1,129 @@ +add_task(async function() { + const kPrefName_AutoScroll = "general.autoScroll"; + Services.prefs.setBoolPref(kPrefName_AutoScroll, true); + registerCleanupFunction(() => + Services.prefs.clearUserPref(kPrefName_AutoScroll) + ); + + const kNoKeyEvents = 0; + const kKeyDownEvent = 1; + const kKeyPressEvent = 2; + const kKeyUpEvent = 4; + const kAllKeyEvents = 7; + + var expectedKeyEvents; + var dispatchedKeyEvents; + var key; + + /** + * Encapsulates EventUtils.sendChar(). + */ + function sendChar(aChar) { + key = aChar; + dispatchedKeyEvents = kNoKeyEvents; + EventUtils.sendChar(key); + is( + dispatchedKeyEvents, + expectedKeyEvents, + "unexpected key events were dispatched or not dispatched: " + key + ); + } + + /** + * Encapsulates EventUtils.sendKey(). + */ + function sendKey(aKey) { + key = aKey; + dispatchedKeyEvents = kNoKeyEvents; + EventUtils.sendKey(key); + is( + dispatchedKeyEvents, + expectedKeyEvents, + "unexpected key events were dispatched or not dispatched: " + key + ); + } + + function onKey(aEvent) { + // if (aEvent.target != root && aEvent.target != root.ownerDocument.body) { + // ok(false, "unknown target: " + aEvent.target.tagName); + // return; + // } + + var keyFlag; + switch (aEvent.type) { + case "keydown": + keyFlag = kKeyDownEvent; + break; + case "keypress": + keyFlag = kKeyPressEvent; + break; + case "keyup": + keyFlag = kKeyUpEvent; + break; + default: + ok(false, "Unknown events: " + aEvent.type); + return; + } + dispatchedKeyEvents |= keyFlag; + is(keyFlag, expectedKeyEvents & keyFlag, aEvent.type + " fired: " + key); + } + + var dataUri = 'data:text/html,<body style="height:10000px;"></body>'; + + await BrowserTestUtils.withNewTab(dataUri, async function(browser) { + info("Loaded data URI in new tab"); + await SimpleTest.promiseFocus(browser); + info("Focused selected browser"); + + window.addEventListener("keydown", onKey); + window.addEventListener("keypress", onKey); + window.addEventListener("keyup", onKey); + registerCleanupFunction(() => { + window.removeEventListener("keydown", onKey); + window.removeEventListener("keypress", onKey); + window.removeEventListener("keyup", onKey); + }); + + // Test whether the key events are handled correctly under normal condition + expectedKeyEvents = kAllKeyEvents; + sendChar("A"); + + // Start autoscrolling by middle button click on the page + info("Creating popup shown promise"); + let shownPromise = BrowserTestUtils.waitForEvent( + window, + "popupshown", + false, + event => event.originalTarget.className == "autoscroller" + ); + await BrowserTestUtils.synthesizeMouseAtPoint( + 10, + 10, + { button: 1 }, + gBrowser.selectedBrowser + ); + info("Waiting for autoscroll popup to show"); + await shownPromise; + + // Most key events should be eaten by the browser. + expectedKeyEvents = kNoKeyEvents; + sendChar("A"); + sendKey("DOWN"); + sendKey("RETURN"); + sendKey("RETURN"); + sendKey("HOME"); + sendKey("END"); + sendKey("TAB"); + sendKey("RETURN"); + + // Finish autoscrolling by ESC key. Note that only keydown and keypress + // events are eaten because keyup event is fired *after* the autoscrolling + // is finished. + expectedKeyEvents = kKeyUpEvent; + sendKey("ESCAPE"); + + // Test whether the key events are handled correctly under normal condition + expectedKeyEvents = kAllKeyEvents; + sendChar("A"); + }); +}); diff --git a/toolkit/content/tests/browser/browser_label_textlink.js b/toolkit/content/tests/browser/browser_label_textlink.js new file mode 100644 index 0000000000..e6b967cbe9 --- /dev/null +++ b/toolkit/content/tests/browser/browser_label_textlink.js @@ -0,0 +1,63 @@ +add_task(async function() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences" }, + async function(browser) { + let newTabURL = "http://www.example.com/"; + await SpecialPowers.spawn(browser, [newTabURL], async function( + newTabURL + ) { + let doc = content.document; + let label = doc.createXULElement("label", { is: "text-link" }); + label.href = newTabURL; + label.id = "textlink-test"; + label.textContent = "click me"; + doc.body.append(label); + }); + + // Test that click will open tab in foreground. + let awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#textlink-test", + {}, + browser + ); + let newTab = await awaitNewTab; + is( + newTab.linkedBrowser, + gBrowser.selectedBrowser, + "selected tab should be example page" + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Test that ctrl+shift+click/meta+shift+click will open tab in background. + awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#textlink-test", + { ctrlKey: true, metaKey: true, shiftKey: true }, + browser + ); + await awaitNewTab; + is( + gBrowser.selectedBrowser, + browser, + "selected tab should be original tab" + ); + BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + + // Middle-clicking should open tab in foreground. + awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, newTabURL); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#textlink-test", + { button: 1 }, + browser + ); + newTab = await awaitNewTab; + is( + newTab.linkedBrowser, + gBrowser.selectedBrowser, + "selected tab should be example page" + ); + BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + } + ); +}); diff --git a/toolkit/content/tests/browser/browser_license_links.js b/toolkit/content/tests/browser/browser_license_links.js new file mode 100644 index 0000000000..3eff69ba75 --- /dev/null +++ b/toolkit/content/tests/browser/browser_license_links.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that we can reach about:rights and about:buildconfig using links + * from about:license. + */ +add_task(async function check_links() { + await BrowserTestUtils.withNewTab("about:license", async browser => { + for (let otherPage of ["about:rights", "about:buildconfig"]) { + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, otherPage); + await BrowserTestUtils.synthesizeMouse( + `a[href='${otherPage}']`, + 2, + 2, + { accelKey: true }, + browser + ); + info("Clicked " + otherPage + " link"); + let tab = await tabPromise; + ok(true, otherPage + " tab opened correctly"); + BrowserTestUtils.removeTab(tab); + } + }); +}); diff --git a/toolkit/content/tests/browser/browser_mediaStreamPlayback.html b/toolkit/content/tests/browser/browser_mediaStreamPlayback.html new file mode 100644 index 0000000000..09685d488e --- /dev/null +++ b/toolkit/content/tests/browser/browser_mediaStreamPlayback.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<body> +<video id="v" controls></video> +<script> +const v = document.getElementById("v"); + +function audioTrack() { + const ctx = new AudioContext(), oscillator = ctx.createOscillator(); + const dst = oscillator.connect(ctx.createMediaStreamDestination()); + oscillator.start(); + return dst.stream.getAudioTracks()[0]; +} + +function videoTrack(width = 640, height = 480) { + const canvas = Object.assign(document.createElement("canvas"), {width, height}); + canvas.getContext('2d').fillRect(0, 0, width, height); + return canvas.captureStream(10).getVideoTracks()[0]; +} + +onload = () => v.srcObject = new MediaStream([videoTrack(), audioTrack()]); +</script> +</body> +</html> diff --git a/toolkit/content/tests/browser/browser_mediaStreamPlaybackWithoutAudio.html b/toolkit/content/tests/browser/browser_mediaStreamPlaybackWithoutAudio.html new file mode 100644 index 0000000000..fbc9fcb033 --- /dev/null +++ b/toolkit/content/tests/browser/browser_mediaStreamPlaybackWithoutAudio.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<body> +<video id="v" controls></video> +<script> +const v = document.getElementById("v"); + +function videoTrack(width = 640, height = 480) { + const canvas = Object.assign(document.createElement("canvas"), {width, height}); + canvas.getContext('2d').fillRect(0, 0, width, height); + return canvas.captureStream(10).getVideoTracks()[0]; +} + +onload = () => v.srcObject = new MediaStream([videoTrack()]); +</script> +</body> +</html> diff --git a/toolkit/content/tests/browser/browser_media_wakelock.js b/toolkit/content/tests/browser/browser_media_wakelock.js new file mode 100644 index 0000000000..dee4b1a26f --- /dev/null +++ b/toolkit/content/tests/browser/browser_media_wakelock.js @@ -0,0 +1,160 @@ +/** + * Test whether the wakelock state is correct under different situations. However, + * the lock state of power manager doesn't equal to the actual platform lock. + * Now we don't have any way to detect whether platform lock is set correctly or + * not, but we can at least make sure the specific topic's state in power manager + * is correct. + */ +"use strict"; + +// Import this in order to use `triggerPictureInPicture()`. +/* import-globals-from ../../../../toolkit/components/pictureinpicture/tests/head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js", + this +); + +const LOCATION = "https://example.com/browser/toolkit/content/tests/browser/"; +const AUDIO_WAKELOCK_NAME = "audio-playing"; +const VIDEO_WAKELOCK_NAME = "video-playing"; + +add_task(async function testCheckWakelockWhenChangeTabVisibility() { + await checkWakelockWhenChangeTabVisibility({ + description: "playing video", + url: "file_video.html", + lockAudio: true, + lockVideo: true, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "playing muted video", + url: "file_video.html", + additionalParams: { + muted: true, + }, + lockAudio: false, + lockVideo: true, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "playing volume=0 video", + url: "file_video.html", + additionalParams: { + volume: 0.0, + }, + lockAudio: false, + lockVideo: true, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "playing video without audio in it", + url: "file_videoWithoutAudioTrack.html", + lockAudio: false, + lockVideo: false, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "playing audio in video element", + url: "file_videoWithAudioOnly.html", + lockAudio: true, + lockVideo: false, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "playing audio in audio element", + url: "file_mediaPlayback2.html", + lockAudio: true, + lockVideo: false, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "playing video from media stream with audio and video tracks", + url: "browser_mediaStreamPlayback.html", + lockAudio: true, + lockVideo: true, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "playing video from media stream without audio track", + url: "browser_mediaStreamPlaybackWithoutAudio.html", + lockAudio: true, + lockVideo: true, + }); +}); + +/** + * Following are helper functions. + */ +async function checkWakelockWhenChangeTabVisibility({ + description, + url, + additionalParams, + lockAudio, + lockVideo, +}) { + const originalTab = gBrowser.selectedTab; + info(`start a new tab for '${description}'`); + const mediaTab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + LOCATION + url + ); + + info(`wait for media starting playing`); + await waitUntilVideoStarted(mediaTab, additionalParams); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock: lockAudio, + isForegroundLock: true, + }); + await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { + needLock: lockVideo, + isForegroundLock: true, + }); + + info(`switch media tab to background`); + await BrowserTestUtils.switchTab(window.gBrowser, originalTab); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock: lockAudio, + isForegroundLock: false, + }); + await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { + needLock: lockVideo, + isForegroundLock: false, + }); + + info(`switch media tab to foreground again`); + await BrowserTestUtils.switchTab(window.gBrowser, mediaTab); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock: lockAudio, + isForegroundLock: true, + }); + await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { + needLock: lockVideo, + isForegroundLock: true, + }); + + info(`remove tab`); + if (mediaTab.PIPWindow) { + await BrowserTestUtils.closeWindow(mediaTab.PIPWindow); + } + BrowserTestUtils.removeTab(mediaTab); +} + +async function waitUntilVideoStarted(tab, { muted, volume } = {}) { + await SpecialPowers.spawn( + tab.linkedBrowser, + [muted, volume], + async (muted, volume) => { + const video = content.document.getElementById("v"); + if (!video) { + ok(false, "can't get media element!"); + return; + } + if (muted) { + video.muted = muted; + } + if (volume !== undefined) { + video.volume = volume; + } + ok( + await video.play().then( + () => true, + () => false + ), + `video started playing.` + ); + } + ); +} diff --git a/toolkit/content/tests/browser/browser_media_wakelock_PIP.js b/toolkit/content/tests/browser/browser_media_wakelock_PIP.js new file mode 100644 index 0000000000..d75c1ef1d7 --- /dev/null +++ b/toolkit/content/tests/browser/browser_media_wakelock_PIP.js @@ -0,0 +1,156 @@ +/** + * Test the wakelock usage for video being used in the picture-in-picuture (PIP) + * mode. When video is playing in PIP window, we would always request a video + * wakelock, and request audio wakelock only when video is audible. + */ +add_task(async function testCheckWakelockForPIPVideo() { + await checkWakelockWhenChangeTabVisibility({ + description: "playing a PIP video", + lockAudio: true, + lockVideo: true, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "playing a muted PIP video", + additionalParams: { + muted: true, + }, + lockAudio: false, + lockVideo: true, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "playing a volume=0 PIP video", + additionalParams: { + volume: 0.0, + }, + lockAudio: false, + lockVideo: true, + }); +}); + +/** + * Following are helper functions and variables. + */ +const PAGE_URL = + "https://example.com/browser/toolkit/content/tests/browser/file_video.html"; +const AUDIO_WAKELOCK_NAME = "audio-playing"; +const VIDEO_WAKELOCK_NAME = "video-playing"; +const TEST_VIDEO_ID = "v"; + +// Import this in order to use `triggerPictureInPicture()`. +/* import-globals-from ../../../../toolkit/components/pictureinpicture/tests/head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js", + this +); + +async function checkWakelockWhenChangeTabVisibility({ + description, + additionalParams, + lockAudio, + lockVideo, +}) { + const originalTab = gBrowser.selectedTab; + info(`start a new tab for '${description}'`); + const tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + PAGE_URL + ); + + info(`wait for PIP video starting playing`); + await startPIPVideo(tab, additionalParams); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock: lockAudio, + isForegroundLock: true, + }); + await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { + needLock: lockVideo, + isForegroundLock: true, + }); + + info( + `switch tab to background and still own foreground locks due to visible PIP video` + ); + await BrowserTestUtils.switchTab(window.gBrowser, originalTab); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock: lockAudio, + isForegroundLock: true, + }); + await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { + needLock: lockVideo, + isForegroundLock: true, + }); + + info(`pausing PIP video should release all locks`); + await pausePIPVideo(tab); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock: false, + }); + await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { + needLock: false, + }); + + info(`resuming PIP video should request locks again`); + await resumePIPVideo(tab); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock: lockAudio, + isForegroundLock: true, + }); + await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { + needLock: lockVideo, + isForegroundLock: true, + }); + + info(`switch tab to foreground again`); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock: lockAudio, + isForegroundLock: true, + }); + await waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { + needLock: lockVideo, + isForegroundLock: true, + }); + + info(`remove tab`); + await BrowserTestUtils.closeWindow(tab.PIPWindow); + BrowserTestUtils.removeTab(tab); +} + +async function startPIPVideo(tab, { muted, volume } = {}) { + tab.PIPWindow = await triggerPictureInPicture( + tab.linkedBrowser, + TEST_VIDEO_ID + ); + await SpecialPowers.spawn( + tab.linkedBrowser, + [muted, volume, TEST_VIDEO_ID], + async (muted, volume, Id) => { + const video = content.document.getElementById(Id); + if (muted) { + video.muted = muted; + } + if (volume !== undefined) { + video.volume = volume; + } + ok( + await video.play().then( + () => true, + () => false + ), + `video started playing.` + ); + } + ); +} + +function pausePIPVideo(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [TEST_VIDEO_ID], Id => { + content.document.getElementById(Id).pause(); + }); +} + +function resumePIPVideo(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [TEST_VIDEO_ID], async Id => { + await content.document.getElementById(Id).play(); + }); +} diff --git a/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js b/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js new file mode 100644 index 0000000000..7c40b5fe1a --- /dev/null +++ b/toolkit/content/tests/browser/browser_media_wakelock_webaudio.js @@ -0,0 +1,127 @@ +/** + * Test if wakelock can be required correctly when we play web audio. The + * wakelock should only be required when web audio is audible. + */ + +const AUDIO_WAKELOCK_NAME = "audio-playing"; +const VIDEO_WAKELOCK_NAME = "video-playing"; + +add_task(async function testCheckAudioWakelockWhenChangeTabVisibility() { + await checkWakelockWhenChangeTabVisibility({ + description: "playing audible web audio", + needLock: true, + }); + await checkWakelockWhenChangeTabVisibility({ + description: "suspended web audio", + additionalParams: { + suspend: true, + }, + needLock: false, + }); +}); + +add_task( + async function testBrieflyAudibleAudioContextReleasesAudioWakeLockWhenInaudible() { + const tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "about:blank" + ); + + info(`make a short noise on web audio`); + await Promise.all([ + // As the sound would only happen for a really short period, calling + // checking wakelock first helps to ensure that we won't miss that moment. + waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock: true, + isForegroundLock: true, + }), + createWebAudioDocument(tab, { stopTimeOffset: 0.1 }), + ]); + await ensureNeverAcquireVideoWakelock(); + + info(`audio wakelock should be released after web audio becomes silent`); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, false, { + needLock: false, + }); + await ensureNeverAcquireVideoWakelock(); + + await BrowserTestUtils.removeTab(tab); + } +); + +/** + * Following are helper functions. + */ +async function checkWakelockWhenChangeTabVisibility({ + description, + additionalParams, + needLock, + elementIdForEnteringPIPMode, +}) { + const originalTab = gBrowser.selectedTab; + info(`start a new tab for '${description}'`); + const mediaTab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "about:blank" + ); + await createWebAudioDocument(mediaTab, additionalParams); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock, + isForegroundLock: true, + }); + await ensureNeverAcquireVideoWakelock(); + + info(`switch media tab to background`); + await BrowserTestUtils.switchTab(window.gBrowser, originalTab); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock, + isForegroundLock: false, + }); + await ensureNeverAcquireVideoWakelock(); + + info(`switch media tab to foreground again`); + await BrowserTestUtils.switchTab(window.gBrowser, mediaTab); + await waitForExpectedWakeLockState(AUDIO_WAKELOCK_NAME, { + needLock, + isForegroundLock: true, + }); + await ensureNeverAcquireVideoWakelock(); + + info(`remove media tab`); + BrowserTestUtils.removeTab(mediaTab); +} + +function createWebAudioDocument(tab, { stopTimeOffset, suspend } = {}) { + return SpecialPowers.spawn( + tab.linkedBrowser, + [suspend, stopTimeOffset], + async (suspend, stopTimeOffset) => { + // Create an oscillatorNode to produce sound. + content.ac = new content.AudioContext(); + const ac = content.ac; + const dest = ac.destination; + const source = new content.OscillatorNode(ac); + source.start(ac.currentTime); + source.connect(dest); + + if (stopTimeOffset) { + source.stop(ac.currentTime + 0.1); + } + + if (suspend) { + await content.ac.suspend(); + } else { + while (ac.state != "running") { + info(`wait until AudioContext starts running`); + await new Promise(r => (ac.onstatechange = r)); + } + info("AudioContext is running"); + } + } + ); +} + +function ensureNeverAcquireVideoWakelock() { + // Web audio won't play any video, we never need video wakelock. + return waitForExpectedWakeLockState(VIDEO_WAKELOCK_NAME, { needLock: false }); +} diff --git a/toolkit/content/tests/browser/browser_quickfind_editable.js b/toolkit/content/tests/browser/browser_quickfind_editable.js new file mode 100644 index 0000000000..7ece285602 --- /dev/null +++ b/toolkit/content/tests/browser/browser_quickfind_editable.js @@ -0,0 +1,59 @@ +const PAGE = + "data:text/html,<div contenteditable>foo</div><input><textarea></textarea>"; +const DESIGNMODE_PAGE = + "data:text/html,<body onload='document.designMode=\"on\";'>"; +const HOTKEYS = ["/", "'"]; + +async function test_hotkeys(browser, expected) { + let findbar = await gBrowser.getFindBar(); + for (let key of HOTKEYS) { + is(findbar.hidden, true, "findbar is hidden"); + await BrowserTestUtils.sendChar(key, gBrowser.selectedBrowser); + is( + findbar.hidden, + expected, + "findbar should" + (expected ? "" : " not") + " be hidden" + ); + if (!expected) { + await closeFindbarAndWait(findbar); + } + } +} + +async function focus_element(browser, query) { + await SpecialPowers.spawn(browser, [query], async function focus(query) { + let element = content.document.querySelector(query); + element.focus(); + }); +} + +add_task(async function test_hotkey_on_editable_element() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + async function do_tests(browser) { + await test_hotkeys(browser, false); + const ELEMENTS = ["div", "input", "textarea"]; + for (let elem of ELEMENTS) { + await focus_element(browser, elem); + await test_hotkeys(browser, true); + await focus_element(browser, ":root"); + await test_hotkeys(browser, false); + } + } + ); +}); + +add_task(async function test_hotkey_on_designMode_document() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: DESIGNMODE_PAGE, + }, + async function do_tests(browser) { + await test_hotkeys(browser, true); + } + ); +}); diff --git a/toolkit/content/tests/browser/browser_remoteness_change_listeners.js b/toolkit/content/tests/browser/browser_remoteness_change_listeners.js new file mode 100644 index 0000000000..34c3c4112e --- /dev/null +++ b/toolkit/content/tests/browser/browser_remoteness_change_listeners.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that adding progress listeners to a browser doesn't break things + * when switching the remoteness of that browser. + */ +add_task(async function test_remoteness_switch_listeners() { + await BrowserTestUtils.withNewTab("about:support", async function(browser) { + let wpl; + let navigated = new Promise(resolve => { + wpl = { + onLocationChange() { + is(browser.currentURI.spec, "https://example.com/"); + if (browser.currentURI?.spec == "https://example.com/") { + resolve(); + } + }, + QueryInterface: ChromeUtils.generateQI([ + Ci.nsISupportsWeakReference, + Ci.nsIWebProgressListener2, + Ci.nsIWebProgressListener, + ]), + }; + browser.addProgressListener(wpl); + }); + + let loaded = BrowserTestUtils.browserLoaded( + browser, + null, + "https://example.com/" + ); + BrowserTestUtils.loadURI(browser, "https://example.com/"); + await Promise.all([loaded, navigated]); + browser.removeProgressListener(wpl); + }); +}); diff --git a/toolkit/content/tests/browser/browser_resume_bkg_video_on_tab_hover.js b/toolkit/content/tests/browser/browser_resume_bkg_video_on_tab_hover.js new file mode 100644 index 0000000000..f8087f6a60 --- /dev/null +++ b/toolkit/content/tests/browser/browser_resume_bkg_video_on_tab_hover.js @@ -0,0 +1,154 @@ +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_silentAudioTrack.html"; + +async function check_video_decoding_state(args) { + let video = content.document.getElementById("autoplay"); + if (!video) { + ok(false, "Can't get the video element!"); + } + + let isSuspended = args.suspend; + let reload = args.reload; + + if (reload) { + // It is too late to register event handlers when playback is half + // way done. Let's start playback from the beginning so we won't + // miss any events. + video.load(); + video.play(); + } + + let state = isSuspended ? "suspended" : "resumed"; + let event = isSuspended ? "mozentervideosuspend" : "mozexitvideosuspend"; + return new Promise(resolve => { + video.addEventListener( + event, + function() { + ok(true, `Video decoding is ${state}.`); + resolve(); + }, + { once: true } + ); + }); +} + +async function check_should_send_unselected_tab_hover_msg(browser) { + info("did not update the value now, wait until it changes."); + if (browser.shouldHandleUnselectedTabHover) { + ok( + true, + "Should send unselected tab hover msg, someone is listening for it." + ); + return true; + } + return BrowserTestUtils.waitForCondition( + () => browser.shouldHandleUnselectedTabHover, + "Should send unselected tab hover msg, someone is listening for it." + ); +} + +async function check_should_not_send_unselected_tab_hover_msg(browser) { + info("did not update the value now, wait until it changes."); + return BrowserTestUtils.waitForCondition( + () => !browser.shouldHandleUnselectedTabHover, + "Should not send unselected tab hover msg, no one is listening for it." + ); +} + +function get_video_decoding_suspend_promise(browser, reload) { + let suspend = true; + return SpecialPowers.spawn( + browser, + [{ suspend, reload }], + check_video_decoding_state + ); +} + +function get_video_decoding_resume_promise(browser) { + let suspend = false; + let reload = false; + return ContentTask.spawn( + browser, + { suspend, reload }, + check_video_decoding_state + ); +} + +/** + * Because of bug1029451, we can't receive "mouseover" event correctly when + * we disable non-test mouse event. Therefore, we can't synthesize mouse event + * to simulate cursor hovering, so we temporarily use a hacky way to resume and + * suspend video decoding. + */ +function cursor_hover_over_tab_and_resume_video_decoding(browser) { + // TODO : simulate cursor hovering over the tab after fixing bug1029451. + browser.unselectedTabHover(true /* hover */); +} + +function cursor_leave_tab_and_suspend_video_decoding(browser) { + // TODO : simulate cursor leaveing the tab after fixing bug1029451. + browser.unselectedTabHover(false /* leave */); +} + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.block-autoplay-until-in-foreground", false], + ["media.suspend-bkgnd-video.enabled", true], + ["media.suspend-bkgnd-video.delay-ms", 0], + ["media.resume-bkgnd-video-on-tabhover", true], + ], + }); +}); + +/** + * TODO : add the following user-level tests after fixing bug1029451. + * test1 - only affect the unselected tab + * test2 - only affect the tab with suspended video + */ +add_task(async function resume_and_suspend_background_video_decoding() { + info("- open new background tab -"); + let tab = BrowserTestUtils.addTab(window.gBrowser, "about:blank"); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + info("- before loading media, we shoudn't send the tab hover msg for tab -"); + await check_should_not_send_unselected_tab_hover_msg(browser); + BrowserTestUtils.loadURI(browser, PAGE); + await BrowserTestUtils.browserLoaded(browser); + + info("- should suspend background video decoding -"); + await get_video_decoding_suspend_promise(browser, true); + await check_should_send_unselected_tab_hover_msg(browser); + + info("- when cursor is hovering over the tab, resuming the video decoding -"); + let promise = get_video_decoding_resume_promise(browser); + await cursor_hover_over_tab_and_resume_video_decoding(browser); + await promise; + await check_should_send_unselected_tab_hover_msg(browser); + + info("- when cursor leaves the tab, suspending the video decoding -"); + promise = get_video_decoding_suspend_promise(browser); + await cursor_leave_tab_and_suspend_video_decoding(browser); + await promise; + await check_should_send_unselected_tab_hover_msg(browser); + + info("- select video's owner tab as foreground tab, should resume video -"); + promise = get_video_decoding_resume_promise(browser); + await BrowserTestUtils.switchTab(window.gBrowser, tab); + await promise; + await check_should_send_unselected_tab_hover_msg(browser); + + info("- video's owner tab goes to background again, should suspend video -"); + promise = get_video_decoding_suspend_promise(browser); + let blankTab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + "about:blank" + ); + await promise; + await check_should_send_unselected_tab_hover_msg(browser); + + info("- remove tabs -"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(blankTab); +}); diff --git a/toolkit/content/tests/browser/browser_saveImageURL.js b/toolkit/content/tests/browser/browser_saveImageURL.js new file mode 100644 index 0000000000..764e7e9aa5 --- /dev/null +++ b/toolkit/content/tests/browser/browser_saveImageURL.js @@ -0,0 +1,75 @@ +"use strict"; + +const IMAGE_PAGE = + "https://example.com/browser/toolkit/content/tests/browser/image_page.html"; + +var MockFilePicker = SpecialPowers.MockFilePicker; + +MockFilePicker.init(window); +MockFilePicker.returnValue = MockFilePicker.returnCancel; + +registerCleanupFunction(function() { + MockFilePicker.cleanup(); +}); + +function waitForFilePicker() { + return new Promise(resolve => { + MockFilePicker.showCallback = () => { + MockFilePicker.showCallback = null; + ok(true, "Saw the file picker"); + resolve(); + }; + }); +} + +/** + * Test that internalSave works when saving an image like the context menu does. + */ +add_task(async function preferred_API() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: IMAGE_PAGE, + }, + async function(browser) { + let url = await SpecialPowers.spawn(browser, [], async function() { + let image = content.document.getElementById("image"); + return image.href; + }); + + let filePickerPromise = waitForFilePicker(); + internalSave( + url, + null, // document + "image.jpg", + null, // content disposition + "image/jpeg", + true, // bypass cache + null, // dialog title key + null, // chosen data + null, // no referrer info + null, // no document + false, // don't skip the filename prompt + null, // cache key + false, // not private. + gBrowser.contentPrincipal + ); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + let channel = docShell.currentDocumentChannel; + if (channel) { + todo( + channel.QueryInterface(Ci.nsIHttpChannelInternal) + .channelIsForDownload + ); + + // Throttleable is the only class flag assigned to downloads. + todo( + channel.QueryInterface(Ci.nsIClassOfService).classFlags == + Ci.nsIClassOfService.Throttleable + ); + } + }); + await filePickerPromise; + } + ); +}); diff --git a/toolkit/content/tests/browser/browser_save_resend_postdata.js b/toolkit/content/tests/browser/browser_save_resend_postdata.js new file mode 100644 index 0000000000..c519c19880 --- /dev/null +++ b/toolkit/content/tests/browser/browser_save_resend_postdata.js @@ -0,0 +1,169 @@ +/* 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 MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +/** + * Test for bug 471962 <https://bugzilla.mozilla.org/show_bug.cgi?id=471962>: + * When saving an inner frame as file only, the POST data of the outer page is + * sent to the address of the inner page. + * + * Test for bug 485196 <https://bugzilla.mozilla.org/show_bug.cgi?id=485196>: + * Web page generated by POST is retried as GET when Save Frame As used, and the + * page is no longer in the cache. + */ +function test() { + waitForExplicitFinish(); + + BrowserTestUtils.loadURI( + gBrowser, + "http://mochi.test:8888/browser/toolkit/content/tests/browser/data/post_form_outer.sjs" + ); + + gBrowser.addEventListener("pageshow", function pageShown(event) { + if (event.target.location == "about:blank") { + return; + } + gBrowser.removeEventListener("pageshow", pageShown); + + // Submit the form in the outer page, then wait for both the outer + // document and the inner frame to be loaded again. + gBrowser.addEventListener("DOMContentLoaded", handleOuterSubmit); + gBrowser.contentDocument.getElementById("postForm").submit(); + }); + + var framesLoaded = 0; + var innerFrame; + + function handleOuterSubmit() { + if (++framesLoaded < 2) { + return; + } + + gBrowser.removeEventListener("DOMContentLoaded", handleOuterSubmit); + + innerFrame = gBrowser.contentDocument.getElementById("innerFrame"); + + // Submit the form in the inner page. + gBrowser.addEventListener("DOMContentLoaded", handleInnerSubmit); + innerFrame.contentDocument.getElementById("postForm").submit(); + } + + function handleInnerSubmit() { + gBrowser.removeEventListener("DOMContentLoaded", handleInnerSubmit); + + // Create the folder the page will be saved into. + var destDir = createTemporarySaveDirectory(); + var file = destDir.clone(); + file.append("no_default_file_name"); + MockFilePicker.setFiles([file]); + MockFilePicker.showCallback = function(fp) { + MockFilePicker.filterIndex = 1; // kSaveAsType_URL + }; + + mockTransferCallback = onTransferComplete; + mockTransferRegisterer.register(); + + registerCleanupFunction(function() { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + destDir.remove(true); + }); + + var docToSave = innerFrame.contentDocument; + // We call internalSave instead of saveDocument to bypass the history + // cache. + internalSave( + docToSave.location.href, + docToSave, + null, + null, + docToSave.contentType, + false, + null, + null, + docToSave.referrer ? makeURI(docToSave.referrer) : null, + docToSave, + false, + null + ); + } + + function onTransferComplete(downloadSuccess) { + ok( + downloadSuccess, + "The inner frame should have been downloaded successfully" + ); + + // Read the entire saved file. + var file = MockFilePicker.getNsIFile(); + var fileContents = readShortFile(file); + + // Check if outer POST data is found (bug 471962). + is( + fileContents.indexOf("inputfield=outer"), + -1, + "The saved inner frame does not contain outer POST data" + ); + + // Check if inner POST data is found (bug 485196). + isnot( + fileContents.indexOf("inputfield=inner"), + -1, + "The saved inner frame was generated using the correct POST data" + ); + + finish(); + } +} + +/* import-globals-from common/mockTransfer.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this +); + +function createTemporarySaveDirectory() { + var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) { + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + return saveDir; +} + +/** + * Reads the contents of the provided short file (up to 1 MiB). + * + * @param aFile + * nsIFile object pointing to the file to be read. + * + * @return + * String containing the raw octets read from the file. + */ +function readShortFile(aFile) { + var inputStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + inputStream.init(aFile, -1, 0, 0); + try { + var scrInputStream = Cc[ + "@mozilla.org/scriptableinputstream;1" + ].createInstance(Ci.nsIScriptableInputStream); + scrInputStream.init(inputStream); + try { + // Assume that the file is much shorter than 1 MiB. + return scrInputStream.read(1048576); + } finally { + // Close the scriptable stream after reading, even if the operation + // failed. + scrInputStream.close(); + } + } finally { + // Close the stream after reading, if it is still open, even if the read + // operation failed. + inputStream.close(); + } +} diff --git a/toolkit/content/tests/browser/browser_suspend_videos_outside_viewport.js b/toolkit/content/tests/browser/browser_suspend_videos_outside_viewport.js new file mode 100644 index 0000000000..333bd0d41c --- /dev/null +++ b/toolkit/content/tests/browser/browser_suspend_videos_outside_viewport.js @@ -0,0 +1,39 @@ +/** + * This test is used to ensure we suspend video decoding if video is not in the + * viewport. + */ +"use strict"; + +const PAGE = + "https://example.com/browser/toolkit/content/tests/browser/file_outside_viewport_videos.html"; + +async function test_suspend_video_decoding() { + let videos = content.document.getElementsByTagName("video"); + for (let video of videos) { + info(`- start video on the ${video.id} side and outside the viewport -`); + await video.play(); + ok(true, `video started playing`); + ok(video.isVideoDecodingSuspended, `video decoding is suspended`); + } +} + +add_task(async function setup_test_preference() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.suspend-bkgnd-video.enabled", true], + ["media.suspend-bkgnd-video.delay-ms", 0], + ], + }); +}); + +add_task(async function start_test() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + async browser => { + await SpecialPowers.spawn(browser, [], test_suspend_video_decoding); + } + ); +}); diff --git a/toolkit/content/tests/browser/common/mockTransfer.js b/toolkit/content/tests/browser/common/mockTransfer.js new file mode 100644 index 0000000000..f4afa44903 --- /dev/null +++ b/toolkit/content/tests/browser/common/mockTransfer.js @@ -0,0 +1,85 @@ +/* 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/. */ + +Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/MockObjects.js", + this +); + +var mockTransferCallback; + +/** + * This "transfer" object implementation continues the currently running test + * when the download is completed, reporting true for success or false for + * failure as the first argument of the testRunner.continueTest function. + */ +function MockTransfer() { + this._downloadIsSuccessful = true; +} + +MockTransfer.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsIWebProgressListener2", + "nsITransfer", + ]), + + /* nsIWebProgressListener */ + onStateChange: function MTFC_onStateChange( + aWebProgress, + aRequest, + aStateFlags, + aStatus + ) { + // If at least one notification reported an error, the download failed. + if (!Components.isSuccessCode(aStatus)) { + this._downloadIsSuccessful = false; + } + + // If the download is finished + if ( + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK + ) { + // Continue the test, reporting the success or failure condition. + mockTransferCallback(this._downloadIsSuccessful); + } + }, + onProgressChange() {}, + onLocationChange() {}, + onStatusChange: function MTFC_onStatusChange( + aWebProgress, + aRequest, + aStatus, + aMessage + ) { + // If at least one notification reported an error, the download failed. + if (!Components.isSuccessCode(aStatus)) { + this._downloadIsSuccessful = false; + } + }, + onSecurityChange() {}, + onContentBlockingEvent() {}, + + /* nsIWebProgressListener2 */ + onProgressChange64() {}, + onRefreshAttempted() {}, + + /* nsITransfer */ + init() {}, + initWithBrowsingContext() {}, + setSha256Hash() {}, + setSignatureInfo() {}, +}; + +// Create an instance of a MockObjectRegisterer whose methods can be used to +// temporarily replace the default "@mozilla.org/transfer;1" object factory with +// one that provides the mock implementation above. To activate the mock object +// factory, call the "register" method. Starting from that moment, all the +// transfer objects that are requested will be mock objects, until the +// "unregister" method is called. +var mockTransferRegisterer = new MockObjectRegisterer( + "@mozilla.org/transfer;1", + MockTransfer +); diff --git a/toolkit/content/tests/browser/data/post_form_inner.sjs b/toolkit/content/tests/browser/data/post_form_inner.sjs new file mode 100644 index 0000000000..ce72159d81 --- /dev/null +++ b/toolkit/content/tests/browser/data/post_form_inner.sjs @@ -0,0 +1,31 @@ +/* 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/. */ + +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) +{ + var body = + '<html>\ + <body>\ + Inner POST data: '; + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bytes = [], avail = 0; + while ((avail = bodyStream.available()) > 0) + body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail)); + + body += + '<form id="postForm" action="post_form_inner.sjs" method="post">\ + <input type="text" name="inputfield" value="inner">\ + <input type="submit">\ + </form>\ + </body>\ + </html>'; + + response.bodyOutputStream.write(body, body.length); +} diff --git a/toolkit/content/tests/browser/data/post_form_outer.sjs b/toolkit/content/tests/browser/data/post_form_outer.sjs new file mode 100644 index 0000000000..89256fcfb2 --- /dev/null +++ b/toolkit/content/tests/browser/data/post_form_outer.sjs @@ -0,0 +1,34 @@ +/* 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/. */ + +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) +{ + var body = + '<html>\ + <body>\ + Outer POST data: '; + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bytes = [], avail = 0; + while ((avail = bodyStream.available()) > 0) + body += String.fromCharCode.apply(String, bodyStream.readByteArray(avail)); + + body += + '<form id="postForm" action="post_form_outer.sjs" method="post">\ + <input type="text" name="inputfield" value="outer">\ + <input type="submit">\ + </form>\ + \ + <iframe id="innerFrame" src="post_form_inner.sjs" width="400" height="200">\ + \ + </body>\ + </html>'; + + response.bodyOutputStream.write(body, body.length); +} diff --git a/toolkit/content/tests/browser/doggy.png b/toolkit/content/tests/browser/doggy.png Binary files differnew file mode 100644 index 0000000000..73632d3229 --- /dev/null +++ b/toolkit/content/tests/browser/doggy.png diff --git a/toolkit/content/tests/browser/empty.png b/toolkit/content/tests/browser/empty.png Binary files differnew file mode 100644 index 0000000000..17ddf0c3ee --- /dev/null +++ b/toolkit/content/tests/browser/empty.png diff --git a/toolkit/content/tests/browser/file_contentTitle.html b/toolkit/content/tests/browser/file_contentTitle.html new file mode 100644 index 0000000000..8d330aa0f2 --- /dev/null +++ b/toolkit/content/tests/browser/file_contentTitle.html @@ -0,0 +1,14 @@ +<html> +<head><title>Test Page</title></head> +<body> +<script type="text/javascript"> +dump("Script!\n"); +addEventListener("load", () => { + // Trigger an onLocationChange event. We want to make sure the title is still correct afterwards. + location.hash = "#x2"; + var event = new Event("TestLocationChange"); + document.dispatchEvent(event); +}, false); +</script> +</body> +</html> diff --git a/toolkit/content/tests/browser/file_document_open_audio.html b/toolkit/content/tests/browser/file_document_open_audio.html new file mode 100644 index 0000000000..1234299c67 --- /dev/null +++ b/toolkit/content/tests/browser/file_document_open_audio.html @@ -0,0 +1,11 @@ +<!doctype html> +<title>Test for bug 1572798</title> +<script> + function openVideo() { + var w = window.open('', '', 'width = 640, height = 480, scrollbars=yes, menubar=no, toolbar=no, resizable=yes'); + w.document.open(); + w.document.write('<!DOCTYPE html><title>test popup</title><audio controls src="audio.ogg"></audio>'); + w.document.close(); + } +</script> +<button onclick="openVideo()">Open video</button> diff --git a/toolkit/content/tests/browser/file_empty.html b/toolkit/content/tests/browser/file_empty.html new file mode 100644 index 0000000000..d2b0361f09 --- /dev/null +++ b/toolkit/content/tests/browser/file_empty.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> + <head> + <title>Page left intentionally blank...</title> + </head> + <body> + </body> +</html> diff --git a/toolkit/content/tests/browser/file_findinframe.html b/toolkit/content/tests/browser/file_findinframe.html new file mode 100644 index 0000000000..27a9d00a97 --- /dev/null +++ b/toolkit/content/tests/browser/file_findinframe.html @@ -0,0 +1,5 @@ +<html> + <body> + <iframe src="data:text/html,<body contenteditable>Test</body>"></iframe> + </body> +</html> diff --git a/toolkit/content/tests/browser/file_mediaPlayback2.html b/toolkit/content/tests/browser/file_mediaPlayback2.html new file mode 100644 index 0000000000..890b494a05 --- /dev/null +++ b/toolkit/content/tests/browser/file_mediaPlayback2.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<body> +<script type="text/javascript"> +var audio = new Audio(); +audio.oncanplay = function() { + audio.oncanplay = null; + audio.play(); +}; +audio.src = "audio.ogg"; +audio.loop = true; +audio.id = "v"; +document.body.appendChild(audio); +</script> +</body> diff --git a/toolkit/content/tests/browser/file_multipleAudio.html b/toolkit/content/tests/browser/file_multipleAudio.html new file mode 100644 index 0000000000..5dc37febb4 --- /dev/null +++ b/toolkit/content/tests/browser/file_multipleAudio.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> +</head> +<body> +<audio id="autoplay" src="audio.ogg"></audio> +<audio id="nonautoplay" src="audio.ogg"></audio> +<script type="text/javascript"> + +// In linux debug on try server, sometimes the download process would fail, so +// we can't activate the "auto-play" or playing after receving "oncanplay". +// Therefore, we just call play here. +var audio = document.getElementById("autoplay"); +audio.loop = true; +audio.play(); + +</script> +</body> diff --git a/toolkit/content/tests/browser/file_multiplePlayingAudio.html b/toolkit/content/tests/browser/file_multiplePlayingAudio.html new file mode 100644 index 0000000000..ae122506fb --- /dev/null +++ b/toolkit/content/tests/browser/file_multiplePlayingAudio.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> +</head> +<body> +<audio id="audio1" src="audio.ogg" controls></audio> +<audio id="audio2" src="audio.ogg" controls></audio> +<script type="text/javascript"> + +// In linux debug on try server, sometimes the download process would fail, so +// we can't activate the "auto-play" or playing after receving "oncanplay". +// Therefore, we just call play here. +var audio1 = document.getElementById("audio1"); +audio1.loop = true; +audio1.play(); + +var audio2 = document.getElementById("audio2"); +audio2.loop = true; +audio2.play(); + +</script> +</body> diff --git a/toolkit/content/tests/browser/file_nonAutoplayAudio.html b/toolkit/content/tests/browser/file_nonAutoplayAudio.html new file mode 100644 index 0000000000..4d2641021a --- /dev/null +++ b/toolkit/content/tests/browser/file_nonAutoplayAudio.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> +</head> +<body> +<audio id="testAudio" src="audio.ogg" loop></audio> diff --git a/toolkit/content/tests/browser/file_outside_viewport_videos.html b/toolkit/content/tests/browser/file_outside_viewport_videos.html new file mode 100644 index 0000000000..84aa34358d --- /dev/null +++ b/toolkit/content/tests/browser/file_outside_viewport_videos.html @@ -0,0 +1,41 @@ +<html> +<head> + <title>outside viewport videos</title> +<style> +/** + * These CSS would move elements to the far left/right/top/bottom where user + * can not see elements in the viewport if user doesn't scroll the page. + */ +.outside-left { + position: absolute; + left: -1000%; +} +.outside-right { + position: absolute; + right: -1000%; +} +.outside-top { + position: absolute; + top: -1000%; +} +.outside-bottom { + position: absolute; + bottom: -1000%; +} +</style> +</head> +<body> + <div class="outside-left"> + <video id="left" src="gizmo.mp4"> + </div> + <div class="outside-right"> + <video id="right" src="gizmo.mp4"> + </div> + <div class="outside-top"> + <video id="top" src="gizmo.mp4"> + </div> + <div class="outside-bottom"> + <video id="bottom" src="gizmo.mp4"> + </div> +</body> +</html> diff --git a/toolkit/content/tests/browser/file_redirect.html b/toolkit/content/tests/browser/file_redirect.html new file mode 100644 index 0000000000..4d5fa9dfd1 --- /dev/null +++ b/toolkit/content/tests/browser/file_redirect.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>redirecting...</title> +<script> +window.addEventListener("load", + () => window.location = "file_redirect_to.html"); +</script> +<body> +redirectin u bro +</body> +</html> diff --git a/toolkit/content/tests/browser/file_redirect_to.html b/toolkit/content/tests/browser/file_redirect_to.html new file mode 100644 index 0000000000..28c0b53713 --- /dev/null +++ b/toolkit/content/tests/browser/file_redirect_to.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>redirected!</title> +<script> +window.addEventListener("load", () => { + var event = new Event("RedirectDone"); + document.dispatchEvent(event); +}); +</script> +<body> +u got redirected, bro +</body> +</html> diff --git a/toolkit/content/tests/browser/file_silentAudioTrack.html b/toolkit/content/tests/browser/file_silentAudioTrack.html new file mode 100644 index 0000000000..afdf2c5297 --- /dev/null +++ b/toolkit/content/tests/browser/file_silentAudioTrack.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> +</head> +<body> +<video id="autoplay" src="silentAudioTrack.webm"></video> +<script type="text/javascript"> + +// In linux debug on try server, sometimes the download process would fail, so +// we can't activate the "auto-play" or playing after receving "oncanplay". +// Therefore, we just call play here. +var video = document.getElementById("autoplay"); +video.loop = true; +video.play(); + +</script> +</body> diff --git a/toolkit/content/tests/browser/file_video.html b/toolkit/content/tests/browser/file_video.html new file mode 100644 index 0000000000..3c70268fbb --- /dev/null +++ b/toolkit/content/tests/browser/file_video.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<title>video</title> +</head> +<body> +<video id="v" src="gizmo.mp4" controls loop></video> +</body> +</html> diff --git a/toolkit/content/tests/browser/file_videoWithAudioOnly.html b/toolkit/content/tests/browser/file_videoWithAudioOnly.html new file mode 100644 index 0000000000..be84d60c34 --- /dev/null +++ b/toolkit/content/tests/browser/file_videoWithAudioOnly.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<title>video</title> +</head> +<body> +<video id="v" src="audio.ogg" controls loop></video> +</body> +</html> diff --git a/toolkit/content/tests/browser/file_videoWithoutAudioTrack.html b/toolkit/content/tests/browser/file_videoWithoutAudioTrack.html new file mode 100644 index 0000000000..a732b7c9d0 --- /dev/null +++ b/toolkit/content/tests/browser/file_videoWithoutAudioTrack.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<title>video without audio track</title> +</head> +<body> +<video id="v" src="gizmo-noaudio.webm" controls loop></video> +</body> +</html> diff --git a/toolkit/content/tests/browser/file_webAudio.html b/toolkit/content/tests/browser/file_webAudio.html new file mode 100644 index 0000000000..f6fb5e7c07 --- /dev/null +++ b/toolkit/content/tests/browser/file_webAudio.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> +</head> +<body> +<pre id=state></pre> +<button id="start" onclick="start_webaudio()">Start</button> +<button id="stop" onclick="stop_webaudio()">Stop</button> +<script type="text/javascript"> + var ac = new AudioContext(); + var dest = ac.destination; + var osc = ac.createOscillator(); + osc.connect(dest); + osc.start(); + document.querySelector("pre").innerText = ac.state; + ac.onstatechange = function() { + document.querySelector("pre").innerText = ac.state; + } + + function start_webaudio() { + ac.resume(); + } + + function stop_webaudio() { + ac.suspend(); + } +</script> +</body> diff --git a/toolkit/content/tests/browser/firebird.png b/toolkit/content/tests/browser/firebird.png Binary files differnew file mode 100644 index 0000000000..de5c22f8ce --- /dev/null +++ b/toolkit/content/tests/browser/firebird.png diff --git a/toolkit/content/tests/browser/firebird.png^headers^ b/toolkit/content/tests/browser/firebird.png^headers^ new file mode 100644 index 0000000000..2918fdbe5f --- /dev/null +++ b/toolkit/content/tests/browser/firebird.png^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Found +Location: doggy.png diff --git a/toolkit/content/tests/browser/gizmo-noaudio.webm b/toolkit/content/tests/browser/gizmo-noaudio.webm Binary files differnew file mode 100644 index 0000000000..9f412cb6e3 --- /dev/null +++ b/toolkit/content/tests/browser/gizmo-noaudio.webm diff --git a/toolkit/content/tests/browser/gizmo.mp4 b/toolkit/content/tests/browser/gizmo.mp4 Binary files differnew file mode 100644 index 0000000000..87efad5ade --- /dev/null +++ b/toolkit/content/tests/browser/gizmo.mp4 diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js new file mode 100644 index 0000000000..8973f1a6b4 --- /dev/null +++ b/toolkit/content/tests/browser/head.js @@ -0,0 +1,338 @@ +"use strict"; + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); + +/** + * A wrapper for the findbar's method "close", which is not synchronous + * because of animation. + */ +function closeFindbarAndWait(findbar) { + return new Promise(resolve => { + if (findbar.hidden) { + resolve(); + return; + } + findbar.addEventListener("transitionend", function cont(aEvent) { + if (aEvent.propertyName != "visibility") { + return; + } + findbar.removeEventListener("transitionend", cont); + resolve(); + }); + let close = findbar.getElement("find-closebutton"); + close.doCommand(); + }); +} + +function pushPrefs(...aPrefs) { + return new Promise(resolve => { + SpecialPowers.pushPrefEnv({ set: aPrefs }, resolve); + }); +} + +/** + * Used to check whether the audio unblocking icon is in the tab. + */ +async function waitForTabBlockEvent(tab, expectBlocked) { + if (tab.activeMediaBlocked == expectBlocked) { + ok(true, "The tab should " + (expectBlocked ? "" : "not ") + "be blocked"); + } else { + info("Block state doens't match, wait for attributes changes."); + await BrowserTestUtils.waitForEvent( + tab, + "TabAttrModified", + false, + event => { + if (event.detail.changed.includes("activemedia-blocked")) { + is( + tab.activeMediaBlocked, + expectBlocked, + "The tab should " + (expectBlocked ? "" : "not ") + "be blocked" + ); + return true; + } + return false; + } + ); + } +} + +/** + * Used to check whether the tab has soundplaying attribute. + */ +async function waitForTabPlayingEvent(tab, expectPlaying) { + if (tab.soundPlaying == expectPlaying) { + ok(true, "The tab should " + (expectPlaying ? "" : "not ") + "be playing"); + } else { + info("Playing state doesn't match, wait for attributes changes."); + await BrowserTestUtils.waitForEvent( + tab, + "TabAttrModified", + false, + event => { + if (event.detail.changed.includes("soundplaying")) { + is( + tab.soundPlaying, + expectPlaying, + "The tab should " + (expectPlaying ? "" : "not ") + "be playing" + ); + return true; + } + return false; + } + ); + } +} + +function getTestPlugin(pluginName) { + var ph = SpecialPowers.Cc["@mozilla.org/plugin/host;1"].getService( + SpecialPowers.Ci.nsIPluginHost + ); + var tags = ph.getPluginTags(); + var name = pluginName || "Test Plug-in"; + for (var tag of tags) { + if (tag.name == name) { + return tag; + } + } + + ok(false, "Could not find plugin tag with plugin name '" + name + "'"); + return null; +} + +async function setTestPluginEnabledState(newEnabledState, pluginName) { + var oldEnabledState = await SpecialPowers.setTestPluginEnabledState( + newEnabledState, + pluginName + ); + if (!oldEnabledState) { + return; + } + var plugin = getTestPlugin(pluginName); + // Run a nested event loop to wait for the preference change to + // propagate to the child. Yuck! + SpecialPowers.Services.tm.spinEventLoopUntil(() => { + return plugin.enabledState == newEnabledState; + }); + SimpleTest.registerCleanupFunction(function() { + return SpecialPowers.setTestPluginEnabledState(oldEnabledState, pluginName); + }); +} + +function disable_non_test_mouse(disable) { + let utils = window.windowUtils; + utils.disableNonTestMouseEvents(disable); +} + +function hover_icon(icon, tooltip) { + disable_non_test_mouse(true); + + let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown"); + EventUtils.synthesizeMouse(icon, 1, 1, { type: "mouseover" }); + EventUtils.synthesizeMouse(icon, 2, 2, { type: "mousemove" }); + EventUtils.synthesizeMouse(icon, 3, 3, { type: "mousemove" }); + EventUtils.synthesizeMouse(icon, 4, 4, { type: "mousemove" }); + return popupShownPromise; +} + +function leave_icon(icon) { + EventUtils.synthesizeMouse(icon, 0, 0, { type: "mouseout" }); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mousemove", + }); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mousemove", + }); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mousemove", + }); + + disable_non_test_mouse(false); +} + +/** + * Helper class for testing datetime input picker widget + */ +class DateTimeTestHelper { + constructor() { + this.panel = gBrowser._getAndMaybeCreateDateTimePickerPanel(); + this.panel.setAttribute("animate", false); + this.tab = null; + this.frame = null; + } + + /** + * Opens a new tab with the URL of the test page, and make sure the picker is + * ready for testing. + * + * @param {String} pageUrl + * @param {bool} inFrame true if input is in the first child frame + */ + async openPicker(pageUrl, inFrame) { + this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + let bc = gBrowser.selectedBrowser; + if (inFrame) { + await SpecialPowers.spawn(bc, [], async function() { + const iframe = content.document.querySelector("iframe"); + // Ensure the iframe's position is correct before doing any + // other operations + iframe.getBoundingClientRect(); + }); + bc = bc.browsingContext.children[0]; + } + await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, bc); + this.frame = this.panel.querySelector("#dateTimePopupFrame"); + await this.waitForPickerReady(); + } + + promisePickerClosed() { + return new Promise(resolve => { + this.panel.addEventListener("popuphidden", resolve, { once: true }); + }); + } + + async waitForPickerReady() { + let readyPromise; + let loadPromise = new Promise(resolve => { + this.frame.addEventListener( + "load", + () => { + // Add the PickerReady event listener directly inside the load event + // listener to avoid missing the event. + readyPromise = BrowserTestUtils.waitForEvent( + this.frame.contentDocument, + "PickerReady" + ); + resolve(); + }, + { capture: true, once: true } + ); + }); + + await loadPromise; + // Wait for picker elements to be ready + await readyPromise; + } + + /** + * Find an element on the picker. + * + * @param {String} selector + * @return {DOMElement} + */ + getElement(selector) { + return this.frame.contentDocument.querySelector(selector); + } + + /** + * Find the children of an element on the picker. + * + * @param {String} selector + * @return {Array<DOMElement>} + */ + getChildren(selector) { + return Array.from(this.getElement(selector).children); + } + + /** + * Click on an element + * + * @param {DOMElement} element + */ + click(element) { + EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow); + } + + /** + * Close the panel and the tab + */ + async tearDown() { + if (!this.panel.hidden) { + let pickerClosePromise = this.promisePickerClosed(); + this.panel.hidePopup(); + await pickerClosePromise; + } + BrowserTestUtils.removeTab(this.tab); + this.tab = null; + } + + /** + * Clean up after tests. Remove the frame to prevent leak. + */ + cleanup() { + this.frame.remove(); + this.frame = null; + this.panel.removeAttribute("animate"); + this.panel = null; + } +} + +/** + * Used to listen events if you just need it once + */ +function once(target, name) { + var p = new Promise(function(resolve, reject) { + target.addEventListener( + name, + function() { + resolve(); + }, + { once: true } + ); + }); + return p; +} + +/** + * check if current wakelock is equal to expected state, if not, then wait until + * the wakelock changes its state to expected state. + * @param needLock + * the wakolock should be locked or not + * @param isForegroundLock + * when the lock is on, the wakelock should be in the foreground or not + */ +async function waitForExpectedWakeLockState( + topic, + { needLock, isForegroundLock } +) { + const powerManagerService = Cc["@mozilla.org/power/powermanagerservice;1"]; + const powerManager = powerManagerService.getService( + Ci.nsIPowerManagerService + ); + const wakelockState = powerManager.getWakeLockState(topic); + let expectedLockState = "unlocked"; + if (needLock) { + expectedLockState = isForegroundLock + ? "locked-foreground" + : "locked-background"; + } + if (wakelockState != expectedLockState) { + info(`wait until wakelock becomes ${expectedLockState}`); + await wakeLockObserved( + powerManager, + topic, + state => state == expectedLockState + ); + } + is( + powerManager.getWakeLockState(topic), + expectedLockState, + `the wakelock state for '${topic}' is equal to '${expectedLockState}'` + ); +} + +function wakeLockObserved(powerManager, observeTopic, checkFn) { + return new Promise(resolve => { + function wakeLockListener() {} + wakeLockListener.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIDOMMozWakeLockListener"]), + callback(topic, state) { + if (topic == observeTopic && checkFn(state)) { + powerManager.removeWakeLockListener(wakeLockListener.prototype); + resolve(); + } + }, + }; + powerManager.addWakeLockListener(wakeLockListener.prototype); + }); +} diff --git a/toolkit/content/tests/browser/image.jpg b/toolkit/content/tests/browser/image.jpg Binary files differnew file mode 100644 index 0000000000..5031808ad2 --- /dev/null +++ b/toolkit/content/tests/browser/image.jpg diff --git a/toolkit/content/tests/browser/image_page.html b/toolkit/content/tests/browser/image_page.html new file mode 100644 index 0000000000..522a1d8cf9 --- /dev/null +++ b/toolkit/content/tests/browser/image_page.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>OHAI</title> +<body> +<img id="image" src="image.jpg" /> +</body> +</html> diff --git a/toolkit/content/tests/browser/silentAudioTrack.webm b/toolkit/content/tests/browser/silentAudioTrack.webm Binary files differnew file mode 100644 index 0000000000..8e08a86c45 --- /dev/null +++ b/toolkit/content/tests/browser/silentAudioTrack.webm diff --git a/toolkit/content/tests/chrome/RegisterUnregisterChrome.js b/toolkit/content/tests/chrome/RegisterUnregisterChrome.js new file mode 100644 index 0000000000..c5c1fceab7 --- /dev/null +++ b/toolkit/content/tests/chrome/RegisterUnregisterChrome.js @@ -0,0 +1,140 @@ +/* This code is mostly copied from chrome/test/unit/head_crtestutils.js */ + +// This file assumes chrome-harness.js is loaded in the global scope. +/* import-globals-from ../../../../testing/mochitest/chrome-harness.js */ + +const NS_CHROME_MANIFESTS_FILE_LIST = "ChromeML"; +const XUL_CACHE_PREF = "nglayout.debug.disable_xul_cache"; + +var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIXULChromeRegistry +); + +// Create the temporary file in the profile, instead of in TmpD, because +// we know the mochitest harness kills off the profile when it's done. +function copyToTemporaryFile(f) { + let tmpd = Services.dirsvc.get("ProfD", Ci.nsIFile); + let tmpf = tmpd.clone(); + tmpf.append("temp.manifest"); + tmpf.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + tmpf.remove(false); + f.copyTo(tmpd, tmpf.leafName); + return tmpf; +} + +function* dirIter(directory) { + var testsDir = Services.io.newURI(directory).QueryInterface(Ci.nsIFileURL) + .file; + + let en = testsDir.directoryEntries; + while (en.hasMoreElements()) { + yield en.nextFile; + } +} + +function getParent(path) { + let lastSlash = path.lastIndexOf("/"); + if (lastSlash == -1) { + lastSlash = path.lastIndexOf("\\"); + if (lastSlash == -1) { + return ""; + } + return "/" + path.substring(0, lastSlash).replace(/\\/g, "/"); + } + return path.substring(0, lastSlash); +} + +function copyDirToTempProfile(path, subdirname) { + if (subdirname === undefined) { + subdirname = "mochikit-tmp"; + } + + let tmpdir = Services.dirsvc.get("ProfD", Ci.nsIFile); + tmpdir.append(subdirname); + tmpdir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o777); + + let rootDir = getParent(path); + if (rootDir == "") { + return tmpdir; + } + + // The SimpleTest directory is hidden + var files = Array.from(dirIter("file://" + rootDir)); + for (let f in files) { + files[f].copyTo(tmpdir, ""); + } + return tmpdir; +} + +function convertChromeURI(chromeURI) { + let uri = Services.io.newURI(chromeURI); + return gChromeReg.convertChromeURL(uri); +} + +function chromeURIToFile(chromeURI) { + var jar = getJar(chromeURI); + if (jar) { + var tmpDir = extractJarToTmp(jar); + let parts = chromeURI.split("/"); + if (parts[parts.length - 1] != "") { + tmpDir.append(parts[parts.length - 1]); + } + return tmpDir; + } + + return convertChromeURI(chromeURI).QueryInterface(Ci.nsIFileURL).file; +} + +// Register a chrome manifest temporarily and return a function which un-does +// the registrarion when no longer needed. +function createManifestTemporarily(tempDir, manifestText) { + Services.prefs.setBoolPref(XUL_CACHE_PREF, true); + + tempDir.append("temp.manifest"); + + let foStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + foStream.init(tempDir, 0x02 | 0x08 | 0x20, 0o664, 0); // write, create, truncate + foStream.write(manifestText, manifestText.length); + foStream.close(); + let tempfile = copyToTemporaryFile(tempDir); + + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .autoRegister(tempfile); + + return function() { + tempfile.fileSize = 0; // truncate the manifest + gChromeReg.checkForNewChrome(); + Services.prefs.clearUserPref(XUL_CACHE_PREF); + }; +} + +// Register a chrome manifest temporarily and return a function which un-does +// the registrarion when no longer needed. +function registerManifestTemporarily(manifestURI) { + Services.prefs.setBoolPref(XUL_CACHE_PREF, true); + + let file = chromeURIToFile(manifestURI); + + let tempfile = copyToTemporaryFile(file); + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .autoRegister(tempfile); + + return function() { + tempfile.fileSize = 0; // truncate the manifest + gChromeReg.checkForNewChrome(); + Services.prefs.clearUserPref(XUL_CACHE_PREF); + }; +} + +function registerManifestPermanently(manifestURI) { + var chromepath = chromeURIToFile(manifestURI); + + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .autoRegister(chromepath); + return chromepath; +} diff --git a/toolkit/content/tests/chrome/bug263683_window.xhtml b/toolkit/content/tests/chrome/bug263683_window.xhtml new file mode 100644 index 0000000000..f2c25524e9 --- /dev/null +++ b/toolkit/content/tests/chrome/bug263683_window.xhtml @@ -0,0 +1,203 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="263683test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="SimpleTest.executeSoon(startTest);" + title="263683 test"> + + <script type="application/javascript"><![CDATA[ + const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + + var gPrefsvc = SpecialPowers.Services.prefs.nsIPrefBranch; + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var info = window.arguments[0].info; + var is = window.arguments[0].is; + + function startTest() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + // Testing on a remote browser has been disabled due to frequent + // intermittent failures. + for (let browserId of ["content"/*, "content-remote"*/]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + // We're bailing out when testing a remote browser on OSX 10.6, because it + // fails permanently. + if (browserId.endsWith("remote") && AppConstants.isPlatformAndVersionAtMost("macosx", 11)) { + return; + } + + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = BrowserTestUtils.browserLoaded(gBrowser); + BrowserTestUtils.loadURI(gBrowser, 'data:text/html,<h2>Text mozilla</h2><input id="inp" type="text" />'); + await promise; + await onDocumentLoaded(); + } + + function toggleHighlightAndWait(highlight) { + return new Promise(resolve => { + let listener = { + onHighlightFinished() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + gFindBar.toggleHighlight(highlight); + }); + } + + async function onDocumentLoaded() { + gFindBar.open(); + var search = "mozilla"; + gFindBar._findField.focus(); + gFindBar._findField.value = search; + var matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + matchCase.doCommand(); + } + + let promise = toggleHighlightAndWait(true); + gFindBar._find(); + await promise; + + await SpecialPowers.spawn(gBrowser, [{ search }], async function(args) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + Assert.ok("SELECTION_FIND" in controller, "Correctly detects new selection type"); + let selection = controller.getSelection(controller.SELECTION_FIND); + + Assert.equal(selection.rangeCount, 1, + "Correctly added a match to the selection type"); + Assert.equal(selection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + }); + + await toggleHighlightAndWait(false); + + await SpecialPowers.spawn(gBrowser, [{ search }], async function(args) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + let selection = controller.getSelection(controller.SELECTION_FIND); + Assert.equal(selection.rangeCount, 0, "Correctly removed the range"); + + let input = content.document.getElementById("inp"); + input.value = args.search; + }); + + await toggleHighlightAndWait(true); + + await SpecialPowers.spawn(gBrowser, [{ search }], async function(args) { + let input = content.document.getElementById("inp"); + let inputController = input.editor.selectionController; + let inputSelection = inputController.getSelection(inputController.SELECTION_FIND); + + Assert.equal(inputSelection.rangeCount, 1, + "Correctly added a match from input to the selection type"); + Assert.equal(inputSelection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + }); + + await toggleHighlightAndWait(false); + + await SpecialPowers.spawn(gBrowser, [], async function() { + let input = content.document.getElementById("inp"); + let inputController = input.editor.selectionController; + let inputSelection = inputController.getSelection(inputController.SELECTION_FIND); + + Assert.equal(inputSelection.rangeCount, 0, "Correctly removed the range"); + }); + + // For posterity, test iframes too. + + promise = BrowserTestUtils.browserLoaded(gBrowser); + BrowserTestUtils.loadURI(gBrowser, 'data:text/html,<h2>Text mozilla</h2><iframe id="leframe" ' + + 'src="data:text/html,Text mozilla"></iframe>'); + await promise; + + await toggleHighlightAndWait(true); + + await SpecialPowers.spawn(gBrowser, [{ search }], async function(args) { + function getSelection(docShell) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + return controller.getSelection(controller.SELECTION_FIND); + } + + let selection = getSelection(docShell); + Assert.equal(selection.rangeCount, 1, + "Correctly added a match to the selection type"); + Assert.equal(selection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + + // Check the iframe too: + let frame = content.document.getElementById("leframe"); + // Hoops! Get the docShell first, then the selection. + selection = getSelection(frame.contentWindow.docShell); + Assert.equal(selection.rangeCount, 1, + "Correctly added a match to the selection type"); + Assert.equal(selection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + }); + + await toggleHighlightAndWait(false); + + let matches = gFindBar._foundMatches.value.match(/([\d]*)\sof\s([\d]*)/); + is(matches[1], "2", "Found correct amount of matches") + + await SpecialPowers.spawn(gBrowser, [], async function(args) { + function getSelection(docShell) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + return controller.getSelection(controller.SELECTION_FIND); + } + + let selection = getSelection(docShell); + Assert.equal(selection.rangeCount, 0, "Correctly removed the range"); + + // Check the iframe too: + let frame = content.document.getElementById("leframe"); + // Hoops! Get the docShell first, then the selection. + selection = getSelection(frame.contentWindow.docShell); + Assert.equal(selection.rangeCount, 0, "Correctly removed the range"); + + content.document.documentElement.focus(); + }); + + gFindBar.close(true); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug304188_window.xhtml b/toolkit/content/tests/chrome/bug304188_window.xhtml new file mode 100644 index 0000000000..51553849f6 --- /dev/null +++ b/toolkit/content/tests/chrome/bug304188_window.xhtml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="FindbarTest for bug 304188 - +find-menu appears in editor element which has had makeEditable() called but designMode not set"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + const {ContentTask} = ChromeUtils.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.arguments[0]); + + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var info = window.arguments[0].info; + var ok = window.arguments[0].ok; + + function onLoad() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = ContentTask.spawn(gBrowser, [], async function() { + return new Promise(resolve => { + addEventListener("DOMContentLoaded", () => resolve(), { once: true }); + }); + }); + BrowserTestUtils.loadURI(gBrowser, "data:text/html;charset=utf-8,some%20random%20text"); + await promise; + await onDocumentLoaded(); + } + + async function onDocumentLoaded() { + await ContentTask.spawn(gBrowser, [], async function() { + var edsession = content.docShell.editingSession; + edsession.makeWindowEditable(content, "html", false, true, false); + content.focus(); + }); + + await enterStringIntoEditor("'"); + await enterStringIntoEditor("/"); + + ok(gFindBar.hidden, + "Findfield should have stayed hidden after entering editor test"); + } + + async function enterStringIntoEditor(aString) { + for (let i = 0; i < aString.length; i++) { + await ContentTask.spawn(gBrowser, [{ charCode: aString.charCodeAt(i) }], async function(args) { + let event = content.document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, args.charCode); + content.document.body.dispatchEvent(event); + }); + } + } + ]]></script> + + <browser id="content" flex="1" src="about:blank" type="content" primary="true" messagemanagergroup="test"/> + <browser id="content-remote" remote="true" flex="1" src="about:blank" type="content" primary="true" messagemanagergroup="test"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug331215_window.xhtml b/toolkit/content/tests/chrome/bug331215_window.xhtml new file mode 100644 index 0000000000..c3db332466 --- /dev/null +++ b/toolkit/content/tests/chrome/bug331215_window.xhtml @@ -0,0 +1,97 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="331215test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="SimpleTest.executeSoon(startTest);" + title="331215 test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var info = window.arguments[0].info; + var ok = window.arguments[0].ok; + SimpleTest.requestLongerTimeout(2); + + function startTest() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = BrowserTestUtils.browserLoaded(gBrowser); + BrowserTestUtils.loadURI(gBrowser, "data:text/plain,latest"); + await promise; + await onDocumentLoaded(); + } + + async function onDocumentLoaded() { + document.getElementById("cmd_find").doCommand(); + await promiseEnterStringIntoFindField("test"); + document.commandDispatcher + .getControllerForCommand("cmd_moveTop") + .doCommand("cmd_moveTop"); + await promiseEnterStringIntoFindField("l"); + ok(gFindBar._findField.getAttribute("status") == "notfound", + "Findfield status attribute should have been 'notfound' after entering test"); + await promiseEnterStringIntoFindField("a"); + ok(gFindBar._findField.getAttribute("status") != "notfound", + "Findfield status attribute should not have been 'notfound' after entering latest"); + } + + function promiseEnterStringIntoFindField(aString) { + return new Promise(resolve => { + let listener = { + onFindResult(result) { + if (result.result == Ci.nsITypeAheadFind.FIND_FOUND && result.searchString != aString) + return; + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + + for (let c of aString) { + let code = c.charCodeAt(0); + let ev = new KeyboardEvent("keypress", { + keyCode: code, + charCode: code, + bubbles: true + }); + gFindBar._findField.dispatchEvent(ev); + } + }); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug360437_window.xhtml b/toolkit/content/tests/chrome/bug360437_window.xhtml new file mode 100644 index 0000000000..a4968c6099 --- /dev/null +++ b/toolkit/content/tests/chrome/bug360437_window.xhtml @@ -0,0 +1,121 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="360437Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + width="600" + height="600" + onload="startTest();" + title="360437 test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + const {ContentTask} = ChromeUtils.import("resource://testing-common/ContentTask.jsm"); + ContentTask.setTestScope(window.arguments[0]); + + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var info = window.arguments[0].info; + + function startTest() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + + let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser); + let contentLoadedPromise = ContentTask.spawn(gBrowser, null, async function() { + return new Promise(resolve => { + addEventListener("DOMContentLoaded", () => resolve(), { once: true }); + }); + }); + BrowserTestUtils.loadURI(gBrowser, "data:text/html,<form><input id='input' type='text' value='text inside an input element'></form>"); + await loadedPromise; + await contentLoadedPromise; + + gFindBar.onFindCommand(); + + // Make sure the findfield is correctly focused on open + var searchStr = "text inside an input element"; + await promiseEnterStringIntoFindField(searchStr); + is(document.commandDispatcher.focusedElement, + gFindBar._findField, "Find field isn't focused"); + + // Make sure "find again" correctly transfers focus to the content element + // when the find bar is closed. + await new Promise(resolve => { + window.addEventListener("findbarclose", resolve, { once: true }); + gFindBar.close(); + }); + gFindBar.onFindAgainCommand(false); + await SpecialPowers.spawn(gBrowser, [], async function() { + Assert.equal(content.document.activeElement, + content.document.getElementById("input"), "Input Element isn't focused"); + }); + + // Make sure "find again" doesn't focus the content element if focus + // isn't in the content document. + var textbox = document.getElementById("textbox"); + textbox.focus(); + + ok(gFindBar.hidden, "Findbar is hidden"); + gFindBar.onFindAgainCommand(false); + is(document.activeElement, textbox, + "Focus was stolen from a chrome element"); + } + + function promiseFindResult(str = null) { + return new Promise(resolve => { + let listener = { + onFindResult({ searchString }) { + if (str !== null && str != searchString) { + return; + } + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + function promiseEnterStringIntoFindField(str) { + let promise = promiseFindResult(str); + for (let i = 0; i < str.length; i++) { + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, str.charCodeAt(i)); + gFindBar._findField.dispatchEvent(event); + } + return promise; + } + ]]></script> + <html:input id="textbox"/> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug366992_window.xhtml b/toolkit/content/tests/chrome/bug366992_window.xhtml new file mode 100644 index 0000000000..698d26b43a --- /dev/null +++ b/toolkit/content/tests/chrome/bug366992_window.xhtml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="366992 test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="onLoad();" + width="600" + height="600" + title="366992 test"> + + <commandset id="editMenuCommands"> + <commandset id="editMenuCommandSetAll" commandupdater="true" events="focus,select" + oncommandupdate="goUpdateGlobalEditMenuItems()"/> + <commandset id="editMenuCommandSetUndo" commandupdater="true" events="undo" + oncommandupdate="goUpdateUndoEditMenuItems()"/> + <commandset id="editMenuCommandSetPaste" commandupdater="true" events="clipboard" + oncommandupdate="goUpdatePasteMenuItems()"/> + <command id="cmd_undo" oncommand="goDoCommand('cmd_undo')"/> + <command id="cmd_redo" oncommand="goDoCommand('cmd_redo')"/> + <command id="cmd_cut" oncommand="goDoCommand('cmd_cut')"/> + <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')"/> + <command id="cmd_paste" oncommand="goDoCommand('cmd_paste')"/> + <command id="cmd_delete" oncommand="goDoCommand('cmd_delete')"/> + <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')"/> + <command id="cmd_switchTextDirection" oncommand="goDoCommand('cmd_switchTextDirection');"/> + </commandset> + + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/editMenuOverlay.js"/> + <script type="application/javascript"><![CDATA[ + // Without the fix for bug 366992, the delete command would be enabled + // for the input even though the input's controller for this command + // disables it. + var gShouldNotBeReachedController = { + supportsCommand(aCommand) { + return aCommand == "cmd_delete"; + }, + isCommandEnabled(aCommand) { + return aCommand == "cmd_delete"; + }, + doCommand(aCommand) { } + } + + function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); + } + function finish() { + window.controllers.removeController(gShouldNotBeReachedController); + window.close(); + window.arguments[0].SimpleTest.finish(); + } + + function onLoad() { + document.getElementById("input").focus(); + var deleteDisabled = document.getElementById("cmd_delete") + .getAttribute("disabled") == "true"; + ok(deleteDisabled, + "cmd_delete should be disabled when the empty input is focused"); + finish(); + } + + window.controllers.appendController(gShouldNotBeReachedController); + ]]></script> + + <html:input id="input"/> +</window> diff --git a/toolkit/content/tests/chrome/bug409624_window.xhtml b/toolkit/content/tests/chrome/bug409624_window.xhtml new file mode 100644 index 0000000000..ced0aee9e1 --- /dev/null +++ b/toolkit/content/tests/chrome/bug409624_window.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="409624test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + title="409624 test"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + + function finish() { + window.close(); + SimpleTest.finish(); + } + + function startTest() { + gFindBar = document.getElementById("FindToolbar"); + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", onPageShow, { once: true }); + BrowserTestUtils.loadURI(gBrowser, 'data:text/html,<h2>Text mozilla</h2><input id="inp" type="text" />'); + } + + function onPageShow() { + gFindBar.clear(); + let textbox = gFindBar.getElement("findbar-textbox"); + + // Clear should work regardless of whether the editor has been lazily + // initialised yet + ok(!gFindBar.hasTransactions, "No transactions when findbar empty"); + textbox.value = "mozilla"; + ok(gFindBar.hasTransactions, "Has transactions when findbar value set without editor init"); + gFindBar.clear(); + is(textbox.value, '', "findbar input value cleared after clear() call without editor init"); + ok(!gFindBar.hasTransactions, "No transactions after clear() call"); + + gFindBar.open(); + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden && matchCaseCheckbox.checked) + matchCaseCheckbox.click(); + ok(!matchCaseCheckbox.checked, "case-insensitivity correctly set"); + + // Simulate typical input + textbox.focus(); + gFindBar.clear(); + sendChar("m"); + ok(gFindBar.hasTransactions, "Has transactions after input"); + let preSelection = gBrowser.contentWindow.getSelection(); + ok(!preSelection.isCollapsed, "Found item and selected range"); + gFindBar.clear(); + is(textbox.value, '', "findbar input value cleared after clear() call"); + let postSelection = gBrowser.contentWindow.getSelection(); + ok(postSelection.isCollapsed, "item found deselected after clear() call"); + let fp = gFindBar.getElement("find-previous"); + ok(fp.disabled, "find-previous button disabled after clear() call"); + let fn = gFindBar.getElement("find-next"); + ok(fn.disabled, "find-next button disabled after clear() call"); + + // Test status updated after a search for text not in page + textbox.focus(); + sendChar("x"); + gFindBar.clear(); + let ftext = gFindBar.getElement("find-status"); + is(ftext.textContent, "", "status text disabled after clear() call"); + + // Test input empty with undo stack non-empty + textbox.focus(); + sendChar("m"); + sendKey("BACK_SPACE"); + ok(gFindBar.hasTransactions, "Has transactions when undo available"); + gFindBar.clear(); + gFindBar.close(); + + finish(); + } + + SimpleTest.waitForFocus(startTest, window); + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug429723_window.xhtml b/toolkit/content/tests/chrome/bug429723_window.xhtml new file mode 100644 index 0000000000..9408c7b1a1 --- /dev/null +++ b/toolkit/content/tests/chrome/bug429723_window.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="429723Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="429723 test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + var gFindBar = null; + var gBrowser; + + function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); + } + + function finish() { + window.close(); + window.arguments[0].SimpleTest.finish(); + } + + function onLoad() { + var _delayedOnLoad = function() { + gFindBar = document.getElementById("FindToolbar"); + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", onPageShow, { once: true }); + BrowserTestUtils.loadURI(gBrowser, "data:text/html,<h2 id='h2'>mozilla</h2>"); + } + setTimeout(_delayedOnLoad, 1000); + } + + function enterStringIntoFindField(aString) { + for (var i=0; i < aString.length; i++) { + var event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, aString.charCodeAt(i)); + gFindBar._findField.dispatchEvent(event); + } + } + + function onPageShow() { + var findField = gFindBar._findField; + document.getElementById("cmd_find").doCommand(); + + var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden & matchCaseCheckbox.checked) + matchCaseCheckbox.click(); + + // Perform search + var searchStr = "z"; + enterStringIntoFindField(searchStr); + + // Highlight search term + var highlight = gFindBar.getElement("highlight"); + if (!highlight.checked) + highlight.click(); + + // Delete search term + var event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, KeyEvent.DOM_VK_BACK_SPACE, 0); + gFindBar._findField.dispatchEvent(event); + + var notRed = !findField.hasAttribute("status") || + (findField.getAttribute("status") != "notfound"); + ok(notRed, "Find Bar textbox is correct colour"); + finish(); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug451540_window.xhtml b/toolkit/content/tests/chrome/bug451540_window.xhtml new file mode 100644 index 0000000000..651fac8291 --- /dev/null +++ b/toolkit/content/tests/chrome/bug451540_window.xhtml @@ -0,0 +1,252 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"?> + +<window id="451540test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + title="451540 test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + const SEARCH_TEXT = "minefield"; + + let gFindBar = null; + let gPrefsvc = SpecialPowers.Services.prefs.nsIPrefBranch; + let gBrowser; + + let sendCtrl = true; + let sendMeta = false; + if (navigator.platform.includes("Mac")) { + sendCtrl = false; + sendMeta = true; + } + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var info = window.arguments[0].info; + + SimpleTest.requestLongerTimeout(2); + + function startTest() { + gFindBar = document.getElementById("FindToolbar"); + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", onPageShow, { once: true }); + let data = `data:text/html,<input id="inp" type="text" /> + <textarea id="tarea"/>`; + BrowserTestUtils.loadURI(gBrowser, data); + } + + function promiseHighlightFinished() { + return new Promise(resolve => { + let listener = { + onHighlightFinished() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + async function resetForNextTest(elementId, aText) { + if (!aText) + aText = SEARCH_TEXT; + + // Turn off highlighting + let highlightButton = gFindBar.getElement("highlight"); + if (highlightButton.checked) { + highlightButton.click(); + } + + // Initialise input + info(`setting element value to ${aText}`); + await SpecialPowers.spawn(gBrowser, [{elementId, aText}], async function(args) { + let {elementId, aText} = args; + let doc = content.document; + let element = doc.getElementById(elementId); + element.value = aText; + element.focus(); + }); + info(`just set element value to ${aText}`); + gFindBar._findField.value = SEARCH_TEXT; + + // Perform search and turn on highlighting + gFindBar._find(); + highlightButton.click(); + await promiseHighlightFinished(); + + // Move caret to start of element + info(`focusing element`); + await SpecialPowers.spawn(gBrowser, [elementId], async function(elementId) { + let doc = content.document; + let element = doc.getElementById(elementId); + element.focus(); + }); + info(`focused element`); + if (navigator.platform.includes("Mac")) { + await BrowserTestUtils.synthesizeKey("VK_LEFT", { metaKey: true }, gBrowser); + } else { + await BrowserTestUtils.synthesizeKey("VK_HOME", {}, gBrowser); + } + } + + async function testSelection(elementId, expectedRangeCount, message) { + await SpecialPowers.spawn(gBrowser, [{elementId, expectedRangeCount, message}], async function(args) { + let {elementId, expectedRangeCount, message} = args; + let doc = content.document; + let element = doc.getElementById(elementId); + let controller = element.editor.selectionController; + let selection = controller.getSelection(controller.SELECTION_FIND); + Assert.equal(selection.rangeCount, expectedRangeCount, message); + }); + } + + async function testInput(elementId, testTypeText) { + let isEditableElement = await SpecialPowers.spawn(gBrowser, [elementId], async function(elementId) { + let doc = content.document; + let element = doc.getElementById(elementId); + let elementClass = ChromeUtils.getClassName(element); + return elementClass === "HTMLInputElement" || + elementClass === "HTMLTextAreaElement"; + }); + if (!isEditableElement) { + return; + } + + // Initialize the findbar + let matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + matchCase.doCommand(); + } + + // First check match has been correctly highlighted + await resetForNextTest(elementId); + + await testSelection(elementId, 1, testTypeText + " correctly highlighted match"); + + // Test 2: check highlight removed when text added within the highlight + await BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + + await testSelection(elementId, 0, testTypeText + " correctly removed highlight on text insertion"); + + // Test 3: check highlighting remains when text added before highlight + await resetForNextTest(elementId); + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " highlight correctly remained on text insertion at start"); + + // Test 4: check highlighting remains when text added after highlight + await resetForNextTest(elementId); + for (let x = 0; x < SEARCH_TEXT.length; x++) { + await BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + } + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " highlight correctly remained on text insertion at end"); + + // Test 5: deleting text within the highlight + await resetForNextTest(elementId); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed highlight on text deletion"); + + // Test 6: deleting text at end of highlight + await resetForNextTest(elementId, SEARCH_TEXT + "A"); + for (let x = 0; x < (SEARCH_TEXT + "A").length; x++) { + await BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + } + await BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " highlight correctly remained on text deletion at end"); + + // Test 7: deleting text at start of highlight + await resetForNextTest(elementId, "A" + SEARCH_TEXT); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " highlight correctly remained on text deletion at start"); + + // Test 8: deleting selection + await resetForNextTest(elementId); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("x", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed highlight on selection deletion"); + + // Test 9: Multiple matches within one editor (part 1) + // Check second match remains highlighted after inserting text into + // first match, and that its highlighting gets removed when the + // second match is edited + await resetForNextTest(elementId, SEARCH_TEXT + " " + SEARCH_TEXT); + await testSelection(elementId, 2, testTypeText + " correctly highlighted both matches"); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " correctly removed only the first highlight on text insertion"); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_LEFT", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed second highlight on text insertion"); + + // Test 10: Multiple matches within one editor (part 2) + // Check second match remains highlighted after deleting text in + // first match, and that its highlighting gets removed when the + // second match is edited + await resetForNextTest(elementId, SEARCH_TEXT + " " + SEARCH_TEXT); + await testSelection(elementId, 2, testTypeText + " correctly highlighted both matches"); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " correctly removed only the first highlight on text deletion"); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_LEFT", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_BACK_SPACE", {}, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed second highlight on text deletion"); + + // Test 11: Multiple matches within one editor (part 3) + // Check second match remains highlighted after deleting selection + // in first match, and that second match highlighting gets correctly + // removed when it has a selection deleted from it + await resetForNextTest(elementId, SEARCH_TEXT + " " + SEARCH_TEXT); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("x", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + await testSelection(elementId, 1, testTypeText + " correctly removed only first highlight on selection deletion"); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_RIGHT", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_LEFT", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("VK_LEFT", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("x", { ctrlKey: sendCtrl, metaKey: sendMeta }, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed second highlight on selection deletion"); + + // Turn off highlighting + let highlightButton = gFindBar.getElement("highlight"); + if (highlightButton.checked) { + highlightButton.click(); + } + + } + + function onPageShow() { + (async function() { + gFindBar.open(); + await testInput("inp", "Input:"); + await testInput("tarea", "Textarea:"); + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForFocus(startTest, window); + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug624329_window.xhtml b/toolkit/content/tests/chrome/bug624329_window.xhtml new file mode 100644 index 0000000000..8cef32e4e5 --- /dev/null +++ b/toolkit/content/tests/chrome/bug624329_window.xhtml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Test for bug 624329 context menu position" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + context="menu"> + + <script> + window.arguments[0].SimpleTest.waitForFocus(window.arguments[0].childFocused, window); + </script> + + <menupopup id="menu"> + <!-- The bug demonstrated only when the accesskey was presented separately + from the label. + e.g. because the accesskey is not a letter in the label. + + The bug demonstrates only on the first show of the context menu + unless menu items are removed/added each time the menu is + constructed. --> + <menuitem label="Long label to ensure the popup would hit the right of the screen" accesskey="1"/> + </menupopup> +</window> diff --git a/toolkit/content/tests/chrome/chrome.ini b/toolkit/content/tests/chrome/chrome.ini new file mode 100644 index 0000000000..fc9171278f --- /dev/null +++ b/toolkit/content/tests/chrome/chrome.ini @@ -0,0 +1,204 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + ../widgets/popup_shared.js + ../widgets/tree_shared.js + RegisterUnregisterChrome.js + bug263683_window.xhtml + bug304188_window.xhtml + bug331215_window.xhtml + bug360437_window.xhtml + bug366992_window.xhtml + bug409624_window.xhtml + bug429723_window.xhtml + bug624329_window.xhtml + dialog_dialogfocus.xhtml + dialog_dialogfocus2.xhtml + file_empty.xhtml + file_edit_contextmenu.xhtml + file_about_networking_wsh.py + file_autocomplete_with_composition.js + file_editor_with_autocomplete.js + findbar_entireword_window.xhtml + findbar_events_window.xhtml + findbar_window.xhtml + frame_popup_anchor.xhtml + frame_popupremoving_frame.xhtml + frame_subframe_origin_subframe1.xhtml + frame_subframe_origin_subframe2.xhtml + popup_childframe_node.xhtml + popup_trigger.js + sample_entireword_latin1.html + window_browser_drop.xhtml + window_keys.xhtml + window_largemenu.xhtml + window_panel.xhtml + window_panel_anchoradjust.xhtml + window_popup_anchor.xhtml + window_popup_anchoratrect.xhtml + window_popup_attribute.xhtml + window_popup_button.xhtml + window_popup_preventdefault_chrome.xhtml + window_preferences.xhtml + window_preferences2.xhtml + window_preferences3.xhtml + window_preferences_commandretarget.xhtml + window_preferences_disabled.xhtml + window_screenPosSize.xhtml + window_showcaret.xhtml + window_subframe_origin.xhtml + window_titlebar.xhtml + window_tooltip.xhtml + xul_selectcontrol.js + rtlchrome/rtl.css + rtlchrome/rtl.dtd + rtlchrome/rtl.manifest +prefs = + gfx.font_rendering.fallback.async=false + +[test_about_networking.html] +[test_arrowpanel.xhtml] +skip-if = (verify && (os == 'win')) || (debug && (os == 'win') && (bits == 32)) # Bug 1546256 +[test_autocomplete2.xhtml] +[test_autocomplete3.xhtml] +[test_autocomplete4.xhtml] +[test_autocomplete5.xhtml] +[test_autocomplete_emphasis.xhtml] +[test_autocomplete_with_composition_on_input.html] +[test_autocomplete_placehold_last_complete.xhtml] +[test_browser_drop.xhtml] +[test_bug263683.xhtml] +skip-if = debug && (os == 'win' || os == 'linux') +[test_bug304188.xhtml] +skip-if = webrender +[test_bug331215.xhtml] +skip-if = webrender || (os == 'win' && debug) || (os == 'mac' && debug) #Bug 1339326 #Bug 1582327 +[test_bug360220.xhtml] +[test_bug360437.xhtml] +skip-if = os == 'linux' # Bug 1264604 +[test_bug365773.xhtml] +[test_bug366992.xhtml] +[test_bug382990.xhtml] +[test_bug409624.xhtml] +[test_bug418874.xhtml] +[test_bug429723.xhtml] +[test_bug437844.xhtml] +skip-if = (verify && debug && (os == 'mac' || os == 'win')) +[test_bug451540.xhtml] +support-files = bug451540_window.xhtml +[test_bug457632.xhtml] +[test_bug460942.xhtml] +[test_bug471776.xhtml] +[test_bug509732.xhtml] +[test_bug557987.xhtml] +[test_bug562554.xhtml] +[test_bug624329.xhtml] +fail-if = (os == 'linux' && os_version == '18.04') # Bug 1600194 +[test_bug792324.xhtml] +[test_bug1048178.xhtml] +skip-if = toolkit == "cocoa" +[test_button.xhtml] +[test_closemenu_attribute.xhtml] +[test_contextmenu_list.xhtml] +[test_custom_element_base.xhtml] +[test_custom_element_delay_connection.xhtml] +[test_custom_element_parts.html] +[test_deck.xhtml] +[test_dialogfocus.xhtml] +[test_edit_contextmenu.html] +[test_editor_for_input_with_autocomplete.html] +[test_findbar.xhtml] +skip-if = os == 'mac' && os_version == '10.14' # macosx1014 due to 1550078 +[test_findbar_entireword.xhtml] +[test_findbar_events.xhtml] +[test_frames.xhtml] +[test_hiddenitems.xhtml] +[test_hiddenpaging.xhtml] +[test_keys.xhtml] +[test_labelcontrol.xhtml] +[test_largemenu.html] +skip-if = os == 'linux' && !debug # Bug 1207174 +[test_maximized_persist.xhtml] +support-files = window_maximized_persist.xhtml +[test_navigate_persist.html] +support-files = window_navigate_persist.html +[test_menu.xhtml] +[test_menu_withcapture.xhtml] +[test_menu_hide.xhtml] +[test_menuchecks.xhtml] +[test_menuitem_blink.xhtml] +[test_menuitem_commands.xhtml] +[test_menulist.xhtml] +[test_menulist_keynav.xhtml] +[test_menulist_null_value.xhtml] +[test_menulist_paging.xhtml] +[test_menulist_position.xhtml] +[test_mousescroll.xhtml] +[test_mozinputbox_dictionary.xhtml] +[test_notificationbox.xhtml] +skip-if = (os == 'linux' && debug) || (os == 'win') # Bug 1429649 +[test_panel.xhtml] +skip-if = os == 'mac' && os_version == '10.14' # macosx1014 due to 1550078 +[test_panel_anchoradjust.xhtml] +[test_panelfrommenu.xhtml] +[test_popup_anchor.xhtml] +[test_popup_anchoratrect.xhtml] +skip-if = os == 'linux' # 1167694 +[test_popup_attribute.xhtml] +skip-if = os == 'linux' && asan || (os == 'mac' && debug) # Bug 1281360, 1582610 +[test_popup_button.xhtml] +skip-if = os == 'linux' && asan || (os == 'mac' && debug) # Bug 1281360, 1582610 +[test_popup_coords.xhtml] +[test_popup_keys.xhtml] +[test_popup_moveToAnchor.xhtml] +[test_popup_preventdefault.xhtml] +[test_popup_preventdefault_chrome.xhtml] +[test_popup_recreate.xhtml] +[test_popup_scaled.xhtml] +[test_popup_tree.xhtml] +[test_popuphidden.xhtml] +[test_popupincontent.xhtml] +skip-if = (verify && (os == 'win')) +[test_popupremoving.xhtml] +[test_popupremoving_frame.xhtml] +[test_position.xhtml] +[test_preferences.xhtml] +[test_preferences_beforeaccept.xhtml] +support-files = window_preferences_beforeaccept.xhtml +[test_preferences_onsyncfrompreference.xhtml] +support-files = window_preferences_onsyncfrompreference.xhtml +[test_props.xhtml] +[test_radio.xhtml] +[test_richlistbox.xhtml] +[test_screenPersistence.xhtml] +[test_scrollbar.xhtml] +[test_showcaret.xhtml] +[test_subframe_origin.xhtml] +[test_tabbox.xhtml] +[test_tabindex.xhtml] +[test_textbox_search.xhtml] +[test_titlebar.xhtml] +skip-if = os == "linux" || (os == 'mac' && os_version == '10.14') # macosx1014 due to 1550078 +[test_tooltip.xhtml] +skip-if = (os == 'mac' || os == 'win') # Bug 1141245, frequent timeouts on OSX 10.14, Windows +[test_tooltip_noautohide.xhtml] +[test_tree.xhtml] +[test_tree_hier.xhtml] +[test_tree_single.xhtml] +[test_tree_view.xhtml] +[test_window_intrinsic_size.xhtml] +support-files = window_intrinsic_size.xhtml +# test_panel_focus.xhtml won't work if the Full Keyboard Access preference is set to +# textboxes and lists only, so skip this test on Mac +[test_panel_focus.xhtml] +support-files = window_panel_focus.xhtml +skip-if = toolkit == "cocoa" +[test_chromemargin.xhtml] +support-files = window_chromemargin.xhtml +skip-if = toolkit == "cocoa" +[test_autocomplete_mac_caret.xhtml] +skip-if = toolkit != "cocoa" +[test_cursorsnap.xhtml] +disabled = +#skip-if = os != "win" +support-files = window_cursorsnap_dialog.xhtml window_cursorsnap_wizard.xhtml diff --git a/toolkit/content/tests/chrome/dialog_dialogfocus.xhtml b/toolkit/content/tests/chrome/dialog_dialogfocus.xhtml new file mode 100644 index 0000000000..29e3c2d856 --- /dev/null +++ b/toolkit/content/tests/chrome/dialog_dialogfocus.xhtml @@ -0,0 +1,64 @@ +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window onload="loaded()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> +<dialog id="dialog-focus" + buttons="extra2,accept,cancel"> + +<tabbox id="tabbox" hidden="true"> + <tabs> + <tab id="tab" label="Tab"/> + </tabs> + <tabpanels> + <tabpanel> + <button id="tabbutton" label="Tab Button"/> + <button id="tabbutton2" label="Tab Button 2"/> + </tabpanel> + </tabpanels> +</tabbox> + +<html:input id="textbox-yes" value="textbox-yes" hidden="true"/> +<html:input id="textbox-no" value="textbox-no" noinitialfocus="true" hidden="true"/> +<button id="one" label="One"/> +<button id="two" label="Two" hidden="true"/> + +<script> +function loaded() +{ + if (window.arguments) { + var step = window.arguments[0]; + switch (step) { + case 2: + document.getElementById("one").setAttribute("noinitialfocus", "true"); + break; + case 3: + document.getElementById("one").hidden = true; + // no-fallthrough + case 4: + document.getElementById("tabbutton2").setAttribute("noinitialfocus", "true"); + // no-fallthrough + case 5: + document.getElementById("tabbutton").setAttribute("noinitialfocus", "true"); + // no-fallthrough + case 6: + document.getElementById("tabbox").hidden = false; + break; + case 7: + var two = document.getElementById("two"); + two.hidden = false; + two.focus(); + break; + case 8: + document.getElementById("textbox-yes").hidden = false; + break; + case 9: + document.getElementById("textbox-no").hidden = false; + break; + } + } +} +</script> + +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/dialog_dialogfocus2.xhtml b/toolkit/content/tests/chrome/dialog_dialogfocus2.xhtml new file mode 100644 index 0000000000..da4b3dbdfa --- /dev/null +++ b/toolkit/content/tests/chrome/dialog_dialogfocus2.xhtml @@ -0,0 +1,8 @@ +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="root" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<dialog id="dialog-focus" + buttons="none"> + <button id="nonbutton" noinitialfocus="true"/> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/file_about_networking_wsh.py b/toolkit/content/tests/chrome/file_about_networking_wsh.py new file mode 100644 index 0000000000..57bd353c21 --- /dev/null +++ b/toolkit/content/tests/chrome/file_about_networking_wsh.py @@ -0,0 +1,10 @@ +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + while not request.client_terminated: + msgutil.receive_message(request) diff --git a/toolkit/content/tests/chrome/file_autocomplete_with_composition.js b/toolkit/content/tests/chrome/file_autocomplete_with_composition.js new file mode 100644 index 0000000000..fe595d00e4 --- /dev/null +++ b/toolkit/content/tests/chrome/file_autocomplete_with_composition.js @@ -0,0 +1,712 @@ +// nsDoTestsForAutoCompleteWithComposition tests autocomplete with composition. +// Users must include SimpleTest.js and EventUtils.js. + +function waitForCondition(condition, nextTest) { + var tries = 0; + var interval = setInterval(function() { + if (condition() || tries >= 30) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { + clearInterval(interval); + nextTest(); + }; +} + +function nsDoTestsForAutoCompleteWithComposition( + aDescription, + aWindow, + aTarget, + aAutoCompleteController, + aIsFunc, + aGetTargetValueFunc, + aOnFinishFunc +) { + this._description = aDescription; + this._window = aWindow; + this._target = aTarget; + this._controller = aAutoCompleteController; + + this._is = aIsFunc; + this._getTargetValue = aGetTargetValueFunc; + this._onFinish = aOnFinishFunc; + + this._target.focus(); + + this._DefaultCompleteDefaultIndex = this._controller.input.completeDefaultIndex; + + this._doTests(); +} + +nsDoTestsForAutoCompleteWithComposition.prototype = { + _window: null, + _target: null, + _controller: null, + _DefaultCompleteDefaultIndex: false, + _description: "", + + _is: null, + _getTargetValue() { + return "not initialized"; + }, + _onFinish: null, + + _doTests() { + if (++this._testingIndex == this._tests.length) { + this._controller.input.completeDefaultIndex = this._DefaultCompleteDefaultIndex; + this._onFinish(); + return; + } + + var test = this._tests[this._testingIndex]; + if ( + this._controller.input.completeDefaultIndex != test.completeDefaultIndex + ) { + this._controller.input.completeDefaultIndex = test.completeDefaultIndex; + } + test.execute(this._window); + + if (test.popup) { + waitForCondition( + () => this._controller.input.popupOpen, + this._checkResult.bind(this) + ); + } else { + waitForCondition(() => { + this._controller.searchStatus >= + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH; + }, this._checkResult.bind(this)); + } + }, + + _checkResult() { + var test = this._tests[this._testingIndex]; + this._is( + this._getTargetValue(), + test.value, + this._description + ", " + test.description + ": value" + ); + this._is( + this._controller.searchString, + test.searchString, + this._description + ", " + test.description + ": searchString" + ); + this._is( + this._controller.input.popupOpen, + test.popup, + this._description + ", " + test.description + ": popupOpen" + ); + this._doTests(); + }, + + _testingIndex: -1, + _tests: [ + // Simple composition when popup hasn't been shown. + // The autocomplete popup should not be shown during composition, but + // after compositionend, the popup should be shown. + { + description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "M", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "M" }, + }, + aWindow + ); + }, + popup: false, + value: "M", + searchString: "", + }, + { + description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "Mo", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "o" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "", + }, + { + description: "compositionend should open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Enter" } }, + aWindow + ); + }, + popup: true, + value: "Mo", + searchString: "Mo", + }, + // If composition starts when popup is shown, the compositionstart event + // should cause closing the popup. + { + description: "compositionstart should close the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "z", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "z" }, + }, + aWindow + ); + }, + popup: false, + value: "Moz", + searchString: "Mo", + }, + { + description: "modifying composition string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "zi", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "i" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozi", + searchString: "Mo", + }, + { + description: + "compositionend should research the result and open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Enter" } }, + aWindow + ); + }, + popup: true, + value: "Mozi", + searchString: "Mozi", + }, + // If composition is cancelled, the value shouldn't be changed. + { + description: "compositionstart should reclose the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "l", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "l" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozil", + searchString: "Mozi", + }, + { + description: "modifying composition string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "ll", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "l" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozill", + searchString: "Mozi", + }, + { + description: + "modifying composition string to empty string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozi", + searchString: "Mozi", + }, + { + description: "cancled compositionend should reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommit", data: "", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: true, + value: "Mozi", + searchString: "Mozi", + }, + // But if composition replaces some characters and canceled, the search + // string should be the latest value. + { + description: + "compositionstart with selected string should close the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("VK_LEFT", { shiftKey: true }, aWindow); + synthesizeKey("VK_LEFT", { shiftKey: true }, aWindow); + synthesizeCompositionChange( + { + composition: { + string: "z", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "z" }, + }, + aWindow + ); + }, + popup: false, + value: "Moz", + searchString: "Mozi", + }, + { + description: "modifying composition string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "zi", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "i" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozi", + searchString: "Mozi", + }, + { + description: + "modifying composition string to empty string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "Mozi", + }, + { + description: + "canceled compositionend should search the result with the latest value", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: true, + value: "Mo", + searchString: "Mo", + }, + // If all characters are removed, the popup should be closed. + { + description: + "the value becomes empty by backspace, the popup should be closed", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + popup: false, + value: "", + searchString: "", + }, + // composition which is canceled shouldn't cause opening the popup. + { + description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "M", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "M" }, + }, + aWindow + ); + }, + popup: false, + value: "M", + searchString: "", + }, + { + description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "Mo", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "o" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "", + }, + { + description: + "modifying composition string to empty string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "", + searchString: "", + }, + { + description: + "canceled compositionend shouldn't open the popup if it was closed", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: false, + value: "", + searchString: "", + }, + // Down key should open the popup even if the editor is empty. + { + description: "DOWN key should open the popup even if the value is empty", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + }, + popup: true, + value: "", + searchString: "", + }, + // If popup is open at starting composition, the popup should be reopened + // after composition anyway. + { + description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "M", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "M" }, + }, + aWindow + ); + }, + popup: false, + value: "M", + searchString: "", + }, + { + description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "Mo", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "o" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "", + }, + { + description: + "modifying composition string to empty string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "", + searchString: "", + }, + { + description: + "canceled compositionend should open the popup if it was opened", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: true, + value: "", + searchString: "", + }, + // Type normally, and hit escape, the popup should be closed. + { + description: "ESCAPE should close the popup after typing something", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("M", {}, aWindow); + synthesizeKey("o", {}, aWindow); + synthesizeKey("KEY_Escape", {}, aWindow); + }, + popup: false, + value: "Mo", + searchString: "Mo", + }, + // Even if the popup is closed, composition which is canceled should open + // the popup if the value isn't empty. + // XXX This might not be good behavior, but anyway, this is minor issue... + { + description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "z", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "z" }, + }, + aWindow + ); + }, + popup: false, + value: "Moz", + searchString: "Mo", + }, + { + description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "zi", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "i" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozi", + searchString: "Mo", + }, + { + description: + "modifying composition string to empty string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "Mo", + }, + { + description: + "canceled compositionend shouldn't open the popup if the popup was closed", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: true, + value: "Mo", + searchString: "Mo", + }, + // House keeping... + { + description: "house keeping for next tests", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + popup: false, + value: "", + searchString: "", + }, + // Testing for nsIAutoCompleteInput.completeDefaultIndex being true. + { + description: + "compositionstart shouldn't open the popup (completeDefaultIndex is true)", + completeDefaultIndex: true, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "M", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "M" }, + }, + aWindow + ); + }, + popup: false, + value: "M", + searchString: "", + }, + { + description: + "modifying composition string shouldn't open the popup (completeDefaultIndex is true)", + completeDefaultIndex: true, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "Mo", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "o" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "", + }, + { + description: + "compositionend should open the popup (completeDefaultIndex is true)", + completeDefaultIndex: true, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Enter" } }, + aWindow + ); + }, + popup: true, + value: "Mozilla", + searchString: "Mo", + }, + // House keeping... + { + description: "house keeping for next tests", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + popup: false, + value: "", + searchString: "", + }, + ], +}; diff --git a/toolkit/content/tests/chrome/file_edit_contextmenu.xhtml b/toolkit/content/tests/chrome/file_edit_contextmenu.xhtml new file mode 100644 index 0000000000..e4d1a79040 --- /dev/null +++ b/toolkit/content/tests/chrome/file_edit_contextmenu.xhtml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> +<script> +customElements.define("shadow-input", class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(document.createElement("input")); + } +}); +</script> +<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> +<!-- Copied from toolkit/content/editMenuCommands.inc.xul --> +<script type="application/javascript" src="chrome://global/content/editMenuOverlay.js"/> +<commandset id="editMenuCommands"> + <commandset id="editMenuCommandSetAll" commandupdater="true" events="focus,select" + oncommandupdate="goUpdateGlobalEditMenuItems()"/> + <commandset id="editMenuCommandSetUndo" commandupdater="true" events="undo" + oncommandupdate="goUpdateUndoEditMenuItems()"/> + <commandset id="editMenuCommandSetPaste" commandupdater="true" events="clipboard" + oncommandupdate="goUpdatePasteMenuItems()"/> + <command id="cmd_undo" oncommand="goDoCommand('cmd_undo')"/> + <command id="cmd_redo" oncommand="goDoCommand('cmd_redo')"/> + <command id="cmd_cut" oncommand="goDoCommand('cmd_cut')"/> + <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')"/> + <command id="cmd_paste" oncommand="goDoCommand('cmd_paste')"/> + <command id="cmd_delete" oncommand="goDoCommand('cmd_delete')"/> + <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')"/> + <command id="cmd_switchTextDirection" oncommand="goDoCommand('cmd_switchTextDirection');"/> +</commandset> + +<menupopup id="outer-context-menu"> + <menuseparator id="customizeMailToolbarMenuSeparator"/> + <menuitem id="hello" label="Hello" accesskey="H"/> +</menupopup> + +<hbox context="outer-context-menu"> +<html:textarea /> +<html:input /> +<search-textbox /> +<html:shadow-input /> +</hbox> + +</window> diff --git a/toolkit/content/tests/chrome/file_editor_with_autocomplete.js b/toolkit/content/tests/chrome/file_editor_with_autocomplete.js new file mode 100644 index 0000000000..d6f1cf6c01 --- /dev/null +++ b/toolkit/content/tests/chrome/file_editor_with_autocomplete.js @@ -0,0 +1,625 @@ +// nsDoTestsForEditorWithAutoComplete tests basic functions of editor with autocomplete. +// Users must include SimpleTest.js and EventUtils.js, and register "Mozilla" to the autocomplete for the target. + +async function waitForCondition(condition) { + return new Promise(resolve => { + var tries = 0; + var interval = setInterval(function() { + if (condition() || tries >= 60) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { + clearInterval(interval); + resolve(); + }; + }); +} + +function nsDoTestsForEditorWithAutoComplete( + aDescription, + aWindow, + aTarget, + aAutoCompleteController, + aIsFunc, + aTodoIsFunc, + aGetTargetValueFunc +) { + this._description = aDescription; + this._window = aWindow; + this._target = aTarget; + this._controller = aAutoCompleteController; + + this._is = aIsFunc; + this._todo_is = aTodoIsFunc; + this._getTargetValue = aGetTargetValueFunc; + + this._target.focus(); + + this._DefaultCompleteDefaultIndex = this._controller.input.completeDefaultIndex; +} + +nsDoTestsForEditorWithAutoComplete.prototype = { + _window: null, + _target: null, + _controller: null, + _DefaultCompleteDefaultIndex: false, + _description: "", + + _is: null, + _getTargetValue() { + return "not initialized"; + }, + + run: async function runTestsImpl() { + for (let test of this._tests) { + if ( + this._controller.input.completeDefaultIndex != test.completeDefaultIndex + ) { + this._controller.input.completeDefaultIndex = test.completeDefaultIndex; + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + this._target.addEventListener("beforeinput", onBeforeInput); + this._target.addEventListener("input", onInput); + + if (test.execute(this._window, this._target) === false) { + this._target.removeEventListener("beforeinput", onBeforeInput); + this._target.removeEventListener("input", onInput); + continue; + } + + await waitForCondition(() => { + return ( + this._controller.searchStatus >= + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH + ); + }); + this._target.removeEventListener("beforeinput", onBeforeInput); + this._target.removeEventListener("input", onInput); + this._checkResult(test, beforeInputEvents, inputEvents); + } + this._controller.input.completeDefaultIndex = this._DefaultCompleteDefaultIndex; + }, + + _checkResult(aTest, aBeforeInputEvents, aInputEvents) { + this._is( + this._getTargetValue(), + aTest.value, + this._description + ", " + aTest.description + ": value" + ); + this._is( + this._controller.searchString, + aTest.searchString, + this._description + ", " + aTest.description + ": searchString" + ); + this._is( + this._controller.input.popupOpen, + aTest.popup, + this._description + ", " + aTest.description + ": popupOpen" + ); + this._is( + this._controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH, + this._description + ", " + aTest.description + ": status" + ); + this._is( + aBeforeInputEvents.length, + aTest.inputEvents.length, + this._description + + ", " + + aTest.description + + ": number of beforeinput events wrong" + ); + this._is( + aInputEvents.length, + aTest.inputEvents.length, + this._description + + ", " + + aTest.description + + ": number of input events wrong" + ); + for (let events of [aBeforeInputEvents, aInputEvents]) { + for (let i = 0; i < events.length; i++) { + if (aTest.inputEvents[i] === undefined) { + this._is( + true, + false, + this._description + + ", " + + aTest.description + + ': "beforeinput" and "input" event shouldn\'t be dispatched anymore' + ); + return; + } + this._is( + events[i] instanceof this._window.InputEvent, + true, + `${this._description}, ${aTest.description}: "${events[i].type}" event should be dispatched with InputEvent interface` + ); + let expectCancelable = + events[i].type === "beforeinput" && + (aTest.inputEvents[i].inputType !== "insertReplacementText" || + SpecialPowers.getBoolPref( + "dom.input_event.allow_to_cancel_set_user_input" + )); + + this._is( + events[i].cancelable, + expectCancelable, + `${this._description}, ${aTest.description}: "${ + events[i].type + }" event should ${expectCancelable ? "be" : "be never"} cancelable` + ); + this._is( + events[i].bubbles, + true, + `${this._description}, ${aTest.description}: "${events[i].type}" event should always bubble` + ); + this._is( + events[i].inputType, + aTest.inputEvents[i].inputType, + `${this._description}, ${aTest.description}: inputType of "${events[i].type}" event should be "${aTest.inputEvents[i].inputType}"` + ); + this._is( + events[i].data, + aTest.inputEvents[i].data, + `${this._description}, ${aTest.description}: data of "${events[i].type}" event should be ${aTest.inputEvents[i].data}` + ); + this._is( + events[i].dataTransfer, + null, + `${this._description}, ${aTest.description}: dataTransfer of "${events[i].type}" event should be null` + ); + this._is( + events[i].getTargetRanges().length, + 0, + `${this._description}, ${aTest.description}: getTargetRanges() of "${events[i].type}" event should return empty array` + ); + } + } + }, + + _tests: [ + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: type 'Mo'", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("M", { shiftKey: true }, aWindow); + synthesizeKey("o", {}, aWindow); + return true; + }, + popup: true, + value: "Mo", + searchString: "Mo", + inputEvents: [ + { inputType: "insertText", data: "M" }, + { inputType: "insertText", data: "o" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: select 'Mozilla' to complete the word", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + synthesizeKey("KEY_Enter", {}, aWindow); + return true; + }, + popup: false, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "insertReplacementText", data: "Mozilla" }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: undo the word, but typed text shouldn't be canceled", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mo", + searchString: "Mo", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: undo the typed text", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: redo the typed text", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mo", + searchString: "Mo", + inputEvents: [{ inputType: "historyRedo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: redo the word", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "historyRedo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: removing all text for next test...", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("a", { accelKey: true }, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "deleteContentBackward", data: null }], + }, + + { + description: + "Undo/Redo behavior check when typed text does not match the case: type 'mo'", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("m", {}, aWindow); + synthesizeKey("o", {}, aWindow); + return true; + }, + popup: true, + value: "mo", + searchString: "mo", + inputEvents: [ + { inputType: "insertText", data: "m" }, + { inputType: "insertText", data: "o" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: select 'Mozilla' to complete the word", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + synthesizeKey("KEY_Enter", {}, aWindow); + return true; + }, + popup: false, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "insertReplacementText", data: "Mozilla" }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: undo the word, but typed text shouldn't be canceled", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "mo", + searchString: "mo", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: undo the typed text", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: redo the typed text", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "mo", + searchString: "mo", + inputEvents: [{ inputType: "historyRedo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: redo the word", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "historyRedo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: removing all text for next test...", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("a", { accelKey: true }, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "deleteContentBackward", data: null }], + }, + + // Testing for nsIAutoCompleteInput.completeDefaultIndex being true. + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): type 'Mo'", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("M", { shiftKey: true }, aWindow); + synthesizeKey("o", {}, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mo", + inputEvents: [ + { inputType: "insertText", data: "M" }, + { inputType: "insertText", data: "o" }, + { inputType: "insertReplacementText", data: "Mozilla" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): select 'Mozilla' to complete the word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + synthesizeKey("KEY_Enter", {}, aWindow); + return true; + }, + popup: false, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mo", + searchString: "Mo", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the typed text", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the typed text", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mo", + inputEvents: [ + { inputType: "historyRedo", data: null }, + { inputType: "insertReplacementText", data: "Mozilla" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mo", + inputEvents: [], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): removing all text for next test...", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("a", { accelKey: true }, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "deleteContentBackward", data: null }], + }, + + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): type 'mo'", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("m", {}, aWindow); + synthesizeKey("o", {}, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mo", + inputEvents: [ + { inputType: "insertText", data: "m" }, + { inputType: "insertText", data: "o" }, + { inputType: "insertReplacementText", data: "mozilla" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): select 'Mozilla' to complete the word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + synthesizeKey("KEY_Enter", {}, aWindow); + return true; + }, + popup: false, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "insertReplacementText", data: "Mozilla" }], + }, + // Different from "exactly matches the case" case, modifying the case causes one additional transaction. + // Although we could make this transaction ignored. + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the selected word, but typed text shouldn't be canceled", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mozilla", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "mo", + searchString: "mo", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the typed text", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + // XXX This is odd case. Consistency with undo behavior, this should restore "mo". + // However, looks like that autocomplete automatically restores "mozilla". + // Additionally, looks like that it causes clearing the redo stack. + // Therefore, the following redo operations do nothing. + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the typed text", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mo", + inputEvents: [ + { inputType: "historyRedo", data: null }, + { inputType: "insertReplacementText", data: "mozilla" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the default index word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mo", + inputEvents: [], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mo", + inputEvents: [], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): removing all text for next test...", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("a", { accelKey: true }, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "deleteContentBackward", data: null }], + }, + ], +}; diff --git a/toolkit/content/tests/chrome/file_empty.xhtml b/toolkit/content/tests/chrome/file_empty.xhtml new file mode 100644 index 0000000000..cda0d080bd --- /dev/null +++ b/toolkit/content/tests/chrome/file_empty.xhtml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> +<html xmlns='http://www.w3.org/1999/xhtml'> +</html>
\ No newline at end of file diff --git a/toolkit/content/tests/chrome/findbar_entireword_window.xhtml b/toolkit/content/tests/chrome/findbar_entireword_window.xhtml new file mode 100644 index 0000000000..404f0bb5fd --- /dev/null +++ b/toolkit/content/tests/chrome/findbar_entireword_window.xhtml @@ -0,0 +1,271 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"?> + +<window id="FindbarEntireWordTest" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="findbar test - entire words only"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js"/> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + const {ContentTask} = ChromeUtils.import("resource://testing-common/ContentTask.jsm"); + + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var SpecialPowers = window.arguments[0].SpecialPowers; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var isnot = window.arguments[0].isnot; + var info = window.arguments[0].info; + SimpleTest.requestLongerTimeout(2); + + const kBaseURL = "chrome://mochitests/content/chrome/toolkit/content/tests/chrome"; + const kTests = { + latin1: { + testSimpleEntireWord: { + "and": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'and' should've been found"); + is(results.matches.total, 6, "should've found 6 matches"); + }, + "an": results => { + is(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'an' shouldn't have been found"); + is(results.matches.total, 0, "should've found 0 matches"); + }, + "darkness": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'darkness' should've been found"); + is(results.matches.total, 3, "should've found 3 matches"); + }, + "mammon": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'mammon' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + }, + testCaseSensitive: { + "And": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'And' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + }, + "and": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'and' should've been found"); + is(results.matches.total, 5, "should've found 5 matches"); + }, + "Mammon": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'mammon' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + }, + testWordBreakChars: { + "a": results => { + // 'a' is a common charactar, but there should only be one occurrence + // separated by word boundaries. + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'a' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + }, + "quarrelled": results => { + // 'quarrelled' is denoted as a word by a period char. + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'quarrelled' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + }, + testQuickfind: { + "and": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'and' should've been found"); + is(results.matches.total, 6, "should've found 6 matches"); + }, + "an": results => { + is(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'an' shouldn't have been found"); + is(results.matches.total, 0, "should've found 0 matches"); + }, + "darkness": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'darkness' should've been found"); + is(results.matches.total, 3, "should've found 3 matches"); + }, + "mammon": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'mammon' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + } + } + }; + + function onLoad() { + (async function() { + await SpecialPowers.pushPrefEnv( + { set: [["findbar.entireword", true]] }); + + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + // XXXmikedeboer: when multiple test samples are available, make this + // a nested loop that iterates over them. For now, only + // latin is available. + await startTestWithBrowser("latin1", browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(testName, browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + + let promise = ContentTask.spawn(gBrowser, null, () => + new Promise(resolve => { + addEventListener("DOMContentLoaded", () => resolve(), { once: true }); + }) + ); + BrowserTestUtils.loadURI(gBrowser, kBaseURL + "/sample_entireword_" + testName + ".html"); + await promise; + await onDocumentLoaded(testName); + } + + async function onDocumentLoaded(testName) { + let suite = kTests[testName]; + await testSimpleEntireWord(suite.testSimpleEntireWord); + await testCaseSensitive(suite.testCaseSensitive); + await testWordBreakChars(suite.testWordBreakChars); + await testQuickfind(suite.testQuickfind); + } + + var enterStringIntoFindField = async function(str, waitForResult = true) { + for (let promise, i = 0; i < str.length; i++) { + if (waitForResult) { + promise = promiseFindResult(); + } + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, str.charCodeAt(i)); + gFindBar._findField.dispatchEvent(event); + if (waitForResult) { + await promise; + } + } + }; + + function openFindbar() { + document.getElementById("cmd_find").doCommand(); + return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise; + } + + function promiseFindResult(searchString) { + return new Promise(resolve => { + let data = {}; + let listener = { + onFindResult: res => { + if (searchString && res.searchString != searchString) + return; + + gFindBar.browser.finder.removeResultListener(listener); + data.find = res; + if (res.result == Ci.nsITypeAheadFind.FIND_NOTFOUND) { + data.matches = { total: 0, current: 0 }; + resolve(data); + return; + } + listener = { + onMatchesCountResult: res => { + if (searchString && res.searchString != searchString) + return; + + gFindBar.browser.finder.removeResultListener(listener); + data.matches = res; + resolve(data); + } + }; + gFindBar.browser.finder.addResultListener(listener); + } + }; + + gFindBar.browser.finder.addResultListener(listener); + }); + } + + async function testIterator(tests) { + for (let searchString of Object.getOwnPropertyNames(tests)) { + gFindBar.clear(); + + let promise = promiseFindResult(searchString); + + await enterStringIntoFindField(searchString, false); + + let result = await promise; + tests[searchString](result); + } + } + + async function testSimpleEntireWord(tests) { + await openFindbar(); + ok(!gFindBar.hidden, "testSimpleEntireWord: findbar should be open"); + + await testIterator(tests); + + gFindBar.close(); + } + + async function testCaseSensitive(tests) { + await openFindbar(); + ok(!gFindBar.hidden, "testCaseSensitive: findbar should be open"); + + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden && !matchCaseCheckbox.checked) + matchCaseCheckbox.click(); + + await testIterator(tests); + + if (!matchCaseCheckbox.hidden) + matchCaseCheckbox.click(); + gFindBar.close(); + } + + async function testWordBreakChars(tests) { + await openFindbar(); + ok(!gFindBar.hidden, "testWordBreakChars: findbar should be open"); + + await testIterator(tests); + + gFindBar.close(); + } + + async function testQuickfind(tests) { + await SpecialPowers.spawn(gBrowser, [], async function() { + let document = content.document; + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, "/".charCodeAt(0)); + document.documentElement.dispatchEvent(event); + }); + + ok(!gFindBar.hidden, "testQuickfind: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testQuickfind: find field is not focused"); + ok(!gFindBar.getElement("entire-word-status").hidden, + "testQuickfind: entire word mode status text should be visible"); + + await testIterator(tests); + + gFindBar.close(); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/findbar_events_window.xhtml b/toolkit/content/tests/chrome/findbar_events_window.xhtml new file mode 100644 index 0000000000..92d8037590 --- /dev/null +++ b/toolkit/content/tests/chrome/findbar_events_window.xhtml @@ -0,0 +1,189 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="FindbarTest" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="SimpleTest.executeSoon(startTest);" + title="findbar events test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + + var gFindBar = null; + var gBrowser; + const kTimeout = 5000; // 5 seconds. + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var info = window.arguments[0].info; + SimpleTest.requestLongerTimeout(2); + + function startTest() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + const url = "data:text/html,hello there"; + let promise = BrowserTestUtils.browserLoaded(gBrowser, false, url); + BrowserTestUtils.loadURI(gBrowser, url); + await promise; + await onDocumentLoaded(); + } + + async function onDocumentLoaded() { + gFindBar.open(); + gFindBar.onFindCommand(); + + await testFind(); + await testFindAgain(); + await testCaseSensitivity(); + await testDiacriticMatching(); + await testHighlight(); + } + + function checkSelection() { + return new Promise(resolve => { + SimpleTest.executeSoon(() => { + SpecialPowers.spawn(gBrowser, [], async function() { + let selected = content.getSelection(); + Assert.equal(String(selected), "", "No text is selected"); + + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + let selection = controller.getSelection(controller.SELECTION_FIND); + Assert.equal(selection.rangeCount, 0, "No text is highlighted"); + }).then(resolve); + }); + }); + } + + function once(node, eventName, preventDefault = true) { + return new Promise((resolve, reject) => { + let timeout = window.setTimeout(() => { + reject("Event wasn't fired within " + kTimeout + "ms for event '" + + eventName + "'."); + }, kTimeout); + + node.addEventListener(eventName, function clb(e) { + window.clearTimeout(timeout); + node.removeEventListener(eventName, clb); + if (preventDefault) + e.preventDefault(); + resolve(e); + }); + }); + } + + async function testFind() { + info("Testing normal find."); + let query = "t"; + let promise = once(gFindBar, "find"); + + // Put some text in the find box. + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, query.charCodeAt(0)); + gFindBar._findField.dispatchEvent(event); + + let e = await promise; + ok(e.detail.query === query, "find event query should match '" + query + "'"); + // Since we're preventing the default make sure nothing was selected. + await checkSelection(); + } + + async function testFindAgain() { + info("Testing repeating normal find."); + let promise = once(gFindBar, "findagain"); + + gFindBar.onFindAgainCommand(); + + await promise; + // Since we're preventing the default make sure nothing was selected. + await checkSelection(); + } + + async function testCaseSensitivity() { + info("Testing normal case sensitivity."); + let promise = once(gFindBar, "findcasesensitivitychange", false); + + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + matchCaseCheckbox.click(); + + let e = await promise; + ok(e.detail.caseSensitive, "find should be case sensitive"); + + // Toggle it back to the original setting. + matchCaseCheckbox.click(); + + // Changing case sensitivity does the search so clear the selected text + // before the next test. + await SpecialPowers.spawn(gBrowser, [], () => content.getSelection().removeAllRanges()); + } + + async function testDiacriticMatching() { + info("Testing normal diacritic matching."); + let promise = once(gFindBar, "finddiacriticmatchingchange", false); + + let matchDiacriticsCheckbox = gFindBar.getElement("find-match-diacritics"); + matchDiacriticsCheckbox.click(); + + let e = await promise; + ok(e.detail.matchDiacritics, "find should match diacritics"); + + // Toggle it back to the original setting. + matchDiacriticsCheckbox.click(); + + // Changing diacritic matching does the search so clear the selected text + // before the next test. + await SpecialPowers.spawn(gBrowser, [], () => content.getSelection().removeAllRanges()); + } + + async function testHighlight() { + info("Testing find with highlight all."); + // Update the find state so the highlight button is clickable. + gFindBar.updateControlState(Ci.nsITypeAheadFind.FIND_FOUND, false); + + let promise = once(gFindBar, "findhighlightallchange"); + + let highlightButton = gFindBar.getElement("highlight"); + if (!highlightButton.checked) + highlightButton.click(); + + let e = await promise; + ok(e.detail.highlightAll, "find event should have highlight all set"); + // Since we're preventing the default make sure nothing was highlighted. + await checkSelection(); + + // Toggle it back to the original setting. + if (highlightButton.checked) + highlightButton.click(); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/findbar_window.xhtml b/toolkit/content/tests/chrome/findbar_window.xhtml new file mode 100644 index 0000000000..320bc99b2e --- /dev/null +++ b/toolkit/content/tests/chrome/findbar_window.xhtml @@ -0,0 +1,777 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="FindbarTest" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="findbar test"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript"><![CDATA[ + const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + const SAMPLE_URL = "http://www.mozilla.org/"; + const SAMPLE_TEXT = "Some text in a text field."; + const SEARCH_TEXT = "Text Test (δοκιμή)"; + const NOT_FOUND_TEXT = "This text is not on the page." + const ITERATOR_TIMEOUT = Services.prefs.getIntPref("findbar.iteratorTimeout"); + + var gFindBar = null; + var gBrowser; + + var gHasFindClipboard = Services.clipboard.supportsFindClipboard(); + + var gStatusText; + var gXULBrowserWindow = { + QueryInterface: ChromeUtils.generateQI(["nsIXULBrowserWindow"]), + + setOverLink(aStatusText) { + gStatusText = aStatusText; + }, + + onBeforeLinkTraversal() { } + }; + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var info = window.arguments[0].info; + SimpleTest.requestLongerTimeout(2); + + function onLoad() { + (async function() { + window.docShell + .treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow) + .XULBrowserWindow = gXULBrowserWindow; + + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + + // Tests delays the loading of a document for one second. + await new Promise(resolve => setTimeout(resolve, 1000)); + + let promise = BrowserTestUtils.browserLoaded(gBrowser); + BrowserTestUtils.loadURI(gBrowser, "data:text/html;charset=utf-8,<h2 id='h2'>" + SEARCH_TEXT + + "</h2><h2><a href='" + SAMPLE_URL + "'>Link Test</a></h2><input id='text' type='text' value='" + + SAMPLE_TEXT + "'></input><input id='button' type='button'></input><img id='img' width='50' height='50'/>", + { triggeringPrincipal: window.document.nodePrincipal }); + await promise; + await onDocumentLoaded(); + } + + async function onDocumentLoaded() { + await testNormalFind(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testNormalFind"); + await openFindbar(); + await testNormalFindWithComposition(); + gFindBar.close(); + ok(gFindBar.hidden, "findbar should be hidden after testNormalFindWithComposition"); + await openFindbar(); + await testAutoCaseSensitivityUI(); + await testQuickFindText(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testQuickFindText"); + // TODO: `testFindWithHighlight` tests fastFind integrity, which can not + // be accessed with RemoteFinder. We're skipping it for now. + if (gFindBar._browser.finder._fastFind) { + await testFindWithHighlight(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testFindWithHighlight"); + } + await testFindbarSelection(); + ok(gFindBar.hidden, "Failed to close findbar after testFindbarSelection"); + // TODO: I don't know how to drop a content element on a chrome input. + if (!gBrowser.hasAttribute("remote")) + testDrop(); + await testQuickFindLink(); + if (gHasFindClipboard) { + await testStatusText(); + } + + if (!AppConstants.DEBUG) { + await testFindCountUI(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testFindCountUI"); + await testFindCountUI(true); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testFindCountUI - linksOnly"); + } + + await openFindbar(); + await testFindAfterCaseChanged(); + gFindBar.close(); + await openFindbar(); + await testFailedStringReset(); + gFindBar.close(); + await testQuickFindClose(); + // TODO: This doesn't seem to work when the findbar is connected to a + // remote browser element. + if (!gBrowser.hasAttribute("remote")) + await testFindAgainNotFound(); + await testToggleEntireWord(); + } + + async function testFindbarSelection() { + function checkFindbarState(aTestName, aExpSelection) { + ok(!gFindBar.hidden, "testFindbarSelection: failed to open findbar: " + aTestName); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testFindbarSelection: find field is not focused: " + aTestName); + if (!gHasFindClipboard) { + ok(gFindBar._findField.value == aExpSelection, + "Incorrect selection in testFindbarSelection: " + aTestName + + ". Selection: " + gFindBar._findField.value); + } + + // Clear the value, close the findbar. + gFindBar._findField.value = ""; + gFindBar.close(); + } + + // Test normal selected text. + await SpecialPowers.spawn(gBrowser, [], async function() { + let document = content.document; + let cH2 = document.getElementById("h2"); + let cSelection = content.getSelection(); + let cRange = document.createRange(); + cRange.setStart(cH2, 0); + cRange.setEnd(cH2, 1); + cSelection.removeAllRanges(); + cSelection.addRange(cRange); + }); + await openFindbar(); + checkFindbarState("plain text", SEARCH_TEXT); + + // Test editable element with selection. + await SpecialPowers.spawn(gBrowser, [], async function() { + let textInput = content.document.getElementById("text"); + textInput.focus(); + textInput.select(); + }); + await openFindbar(); + checkFindbarState("text input", SAMPLE_TEXT); + + // Test non-editable input element (type="button"). + await SpecialPowers.spawn(gBrowser, [], async function() { + content.document.getElementById("button").focus(); + }); + await openFindbar(); + checkFindbarState("button", ""); + } + + function testDrop() { + gFindBar.open(); + // use an dummy image to start the drag so it doesn't get interrupted by a selection + var img = gBrowser.contentDocument.getElementById("img"); + synthesizeDrop(img, gFindBar._findField, [[ {type: "text/plain", data: "Rabbits" } ]], "copy", window); + is(gFindBar._findField.value, "Rabbits", "drop on findbar"); + gFindBar.close(); + } + + function testQuickFindClose() { + return new Promise(resolve => { + var _isClosedCallback = function() { + ok(gFindBar.hidden, + "_isClosedCallback: Failed to auto-close quick find bar after " + + gFindBar._quickFindTimeoutLength + "ms"); + resolve(); + }; + setTimeout(_isClosedCallback, gFindBar._quickFindTimeoutLength + 100); + }); + } + + function testStatusText() { + return new Promise(resolve => { + var _delayedCheckStatusText = function() { + ok(gStatusText == SAMPLE_URL, "testStatusText: Failed to set status text of found link"); + resolve(); + }; + setTimeout(_delayedCheckStatusText, 100); + }); + } + + function promiseFindResult(expectedString) { + return new Promise(resolve => { + let listener = { + onFindResult(result) { + if (expectedString && result.searchString != expectedString) { + return; + } + + gFindBar.browser.finder.removeResultListener(listener); + resolve(result); + }, + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + function promiseMatchesCountResult(expectedString) { + return new Promise(resolve => { + let listener = { + onMatchesCountResult(result) { + if (expectedString && result.searchString != expectedString) + return; + + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + }, + }; + gFindBar.browser.finder.addResultListener(listener); + // Make sure we resolve _at least_ after five times the find iterator timeout. + setTimeout(resolve, (ITERATOR_TIMEOUT * 5) + 20); + }); + } + + function promiseHighlightFinished(expectedString) { + return new Promise(resolve => { + let listener = { + onHighlightFinished(result) { + if (expectedString && result.searchString != expectedString) + return; + + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + var enterStringIntoFindField = async function(str, waitForResult = true) { + for (let promise, i = 0; i < str.length; i++) { + if (waitForResult) { + promise = promiseFindResult(); + } + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, str.charCodeAt(i)); + gFindBar._findField.dispatchEvent(event); + if (waitForResult) { + await promise; + } + } + }; + + function promiseExpectRangeCount(rangeCount) { + return SpecialPowers.spawn(gBrowser, [{ rangeCount }], async function(args) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND); + Assert.equal(sel.rangeCount, args.rangeCount, + "Expected the correct amount of ranges inside the Find selection"); + }); + } + + // also test match-case + async function testNormalFind() { + document.getElementById("cmd_find").doCommand(); + + ok(!gFindBar.hidden, "testNormalFind: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testNormalFind: find field is not focused"); + + let promise; + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden && matchCaseCheckbox.checked) { + promise = promiseFindResult(); + matchCaseCheckbox.click(); + await promise; + } + + var searchStr = "text tes"; + await enterStringIntoFindField(searchStr); + + let sel = await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + let sel = content.getSelection().toString(); + Assert.equal(sel.toLowerCase(), args.searchStr, + "testNormalFind: failed to find '" + args.searchStr + "'"); + return sel; + }); + testClipboardSearchString(sel); + + if (!matchCaseCheckbox.hidden) { + promise = promiseFindResult(); + matchCaseCheckbox.click(); + await promise; + enterStringIntoFindField("t"); + await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + Assert.notEqual(content.getSelection().toString(), args.searchStr, + "testNormalFind: Case-sensitivy is broken '" + args.searchStr + "'"); + }); + promise = promiseFindResult(); + matchCaseCheckbox.click(); + await promise; + } + } + + function openFindbar() { + document.getElementById("cmd_find").doCommand(); + return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise; + } + + async function testNormalFindWithComposition() { + ok(!gFindBar.hidden, "testNormalFindWithComposition: findbar should be open"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testNormalFindWithComposition: find field should be focused"); + + var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + var clicked = false; + if (!matchCaseCheckbox.hidden & matchCaseCheckbox.checked) { + matchCaseCheckbox.click(); + clicked = true; + } + + gFindBar._findField.focus(); + + var searchStr = "text"; + + synthesizeCompositionChange( + { "composition": + { "string": searchStr, + "clauses": + [ + { "length": searchStr.length, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": searchStr.length, "length": 0 } + }); + + await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + Assert.notEqual(content.getSelection().toString().toLowerCase(), args.searchStr, + "testNormalFindWithComposition: text shouldn't be found during composition"); + }); + + synthesizeComposition({ type: "compositioncommitasis" }); + + let sel = await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + let sel = content.getSelection().toString(); + Assert.equal(sel.toLowerCase(), args.searchStr, + "testNormalFindWithComposition: text should be found after committing composition"); + return sel; + }); + testClipboardSearchString(sel); + + if (clicked) { + matchCaseCheckbox.click(); + } + } + + async function testAutoCaseSensitivityUI() { + var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + var matchCaseLabel = gFindBar.getElement("match-case-status"); + ok(!matchCaseCheckbox.hidden, "match case box is hidden in manual mode"); + ok(matchCaseLabel.hidden, "match case label is visible in manual mode"); + + await changeCase(2); + + ok(matchCaseCheckbox.hidden, + "match case box is visible in automatic mode"); + ok(!matchCaseLabel.hidden, + "match case label is hidden in automatic mode"); + + await enterStringIntoFindField("a"); + var insensitiveLabel = matchCaseLabel.value; + await enterStringIntoFindField("A"); + var sensitiveLabel = matchCaseLabel.value; + ok(insensitiveLabel != sensitiveLabel, + "Case Sensitive label was not correctly updated"); + + // bug 365551 + gFindBar.onFindAgainCommand(); + ok(matchCaseCheckbox.hidden && !matchCaseLabel.hidden, + "bug 365551: case sensitivity UI is broken after find-again"); + await changeCase(0); + gFindBar.close(); + } + + async function clearFocus() { + document.commandDispatcher.focusedElement = null; + document.commandDispatcher.focusedWindow = null; + await SpecialPowers.spawn(gBrowser, [], async function() { + content.focus(); + }); + } + + async function testQuickFindLink() { + await clearFocus(); + + await SpecialPowers.spawn(gBrowser, [], async function() { + let document = content.document; + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, "'".charCodeAt(0)); + document.documentElement.dispatchEvent(event); + }); + + ok(!gFindBar.hidden, "testQuickFindLink: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testQuickFindLink: find field is not focused"); + + var searchStr = "Link Test"; + await enterStringIntoFindField(searchStr); + await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + Assert.equal(content.getSelection().toString(), args.searchStr, + "testQuickFindLink: failed to find sample link"); + }); + testClipboardSearchString(searchStr); + } + + // See bug 963925 for more details on this test. + async function testFindWithHighlight() { + gFindBar._findField.value = ""; + + // For this test, we want to closely control the selection. The easiest + // way to do so is to replace the implementation of + // Finder.getInitialSelection with a no-op and call the findbar's callback + // (onCurrentSelection(..., true)) ourselves with our hand-picked + // selection. + let oldGetInitialSelection = gFindBar.browser.finder.getInitialSelection; + let searchStr; + gFindBar.browser.finder.getInitialSelection = function(){}; + + let findCommand = document.getElementById("cmd_find"); + findCommand.doCommand(); + + gFindBar.onCurrentSelection("", true); + + searchStr = "e"; + await enterStringIntoFindField(searchStr); + + let a = gFindBar._findField.value; + let b = gFindBar._browser.finder._fastFind.searchString; + let c = gFindBar._browser.finder.searchString; + ok(a == b && b == c, "testFindWithHighlight 1: " + a + ", " + b + ", " + c + "."); + + searchStr = "t"; + findCommand.doCommand(); + + gFindBar.onCurrentSelection(searchStr, true); + gFindBar.browser.finder.getInitialSelection = oldGetInitialSelection; + + a = gFindBar._findField.value; + b = gFindBar._browser.finder._fastFind.searchString; + c = gFindBar._browser.finder.searchString; + ok(a == searchStr && b == c, "testFindWithHighlight 2: " + searchStr + + ", " + a + ", " + b + ", " + c + "."); + + let highlightButton = gFindBar.getElement("highlight"); + highlightButton.click(); + ok(highlightButton.checked, "testFindWithHighlight 3: Highlight All should be checked."); + + a = gFindBar._findField.value; + b = gFindBar._browser.finder._fastFind.searchString; + c = gFindBar._browser.finder.searchString; + ok(a == searchStr && b == c, "testFindWithHighlight 4: " + a + ", " + b + ", " + c + "."); + + gFindBar.onFindAgainCommand(); + a = gFindBar._findField.value; + b = gFindBar._browser.finder._fastFind.searchString; + c = gFindBar._browser.finder.searchString; + ok(a == b && b == c, "testFindWithHighlight 5: " + a + ", " + b + ", " + c + "."); + + highlightButton.click(); + ok(!highlightButton.checked, "testFindWithHighlight: Highlight All should be unchecked."); + + // Regression test for bug 1316515. + searchStr = "e"; + gFindBar.clear(); + await enterStringIntoFindField(searchStr); + await promiseExpectRangeCount(0); + + highlightButton.click(); + ok(highlightButton.checked, "testFindWithHighlight: Highlight All should be checked."); + await promiseHighlightFinished(searchStr); + await promiseExpectRangeCount(3); + + synthesizeKey("KEY_Backspace"); + await promiseExpectRangeCount(0); + + // Regression test for bug 1316513. + highlightButton.click(); + ok(!highlightButton.checked, "testFindWithHighlight - 1316513: Highlight All should be unchecked."); + await enterStringIntoFindField(searchStr); + + highlightButton.click(); + ok(highlightButton.checked, "testFindWithHighlight - 1316513: Highlight All should be checked."); + await promiseHighlightFinished(searchStr); + await promiseExpectRangeCount(3); + + let promise = BrowserTestUtils.browserLoaded(gBrowser); + gBrowser.reload(); + await promise; + + ok(highlightButton.checked, "testFindWithHighlight - 1316513: Highlight All " + + "should still be checked after a reload."); + synthesizeKey("KEY_Enter"); + await promiseHighlightFinished(searchStr); + await promiseExpectRangeCount(3); + + // Uncheck at test end to not interfere with other test functions that are + // run after this one. + highlightButton.click(); + } + + async function testQuickFindText() { + await clearFocus(); + + await SpecialPowers.spawn(gBrowser, [], async function() { + let document = content.document; + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, "/".charCodeAt(0)); + document.documentElement.dispatchEvent(event); + }); + + ok(!gFindBar.hidden, "testQuickFindText: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testQuickFindText: find field is not focused"); + + await enterStringIntoFindField(SEARCH_TEXT); + await SpecialPowers.spawn(gBrowser, [{ SEARCH_TEXT }], async function(args) { + Assert.equal(content.getSelection().toString(), args.SEARCH_TEXT, + "testQuickFindText: failed to find '" + args.SEARCH_TEXT + "'"); + }); + testClipboardSearchString(SEARCH_TEXT); + } + + async function testFindCountUI(linksOnly = false) { + await clearFocus(); + + if (linksOnly) { + await SpecialPowers.spawn(gBrowser, [], async function() { + let document = content.document; + let event = document.createEvent("KeyboardEvent"); + event.initKeyEvent("keypress", true, true, null, false, false, + false, false, 0, "'".charCodeAt(0)); + document.documentElement.dispatchEvent(event); + }); + } else { + document.getElementById("cmd_find").doCommand(); + } + + ok(!gFindBar.hidden, "testFindCountUI: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testFindCountUI: find field is not focused"); + + let promise; + let matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + promise = promiseFindResult(); + matchCase.click(); + await new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); + await promise; + } + + let foundMatches = gFindBar._foundMatches; + let tests = [{ + text: "t", + current: linksOnly ? 1 : 5, + total: linksOnly ? 2 : 10, + }, { + text: "te", + current: linksOnly ? 1 : 3, + total: linksOnly ? 1 : 5, + }, { + text: "tes", + current: 1, + total: linksOnly ? 1 : 2, + }, { + text: "texxx", + current: 0, + total: 0 + }]; + let regex = /([\d]*)\sof\s([\d]*)/; + + function assertMatches(aTest, aMatches) { + is(aMatches[1], String(aTest.current), + `${linksOnly ? "[Links-only] " : ""}Currently highlighted match should be at ${aTest.current} for '${aTest.text}'`); + is(aMatches[2], String(aTest.total), + `${linksOnly ? "[Links-only] " : ""}Total amount of matches should be ${aTest.total} for '${aTest.text}'`); + } + + for (let test of tests) { + gFindBar._findField.select(); + gFindBar._findField.focus(); + + let timeout = ITERATOR_TIMEOUT; + if (test.text.length == 1) + timeout *= 4; + else if (test.text.length == 2) + timeout *= 2; + timeout += 20; + await new Promise(resolve => setTimeout(resolve, timeout)); + await enterStringIntoFindField(test.text, false); + await promiseMatchesCountResult(test.text); + let matches = foundMatches.value.match(regex); + if (!test.total) { + ok(!matches, "No message should be shown when 0 matches are expected"); + } else { + assertMatches(test, matches); + for (let i = 1; i < test.total; i++) { + await new Promise(resolve => setTimeout(resolve, timeout)); + gFindBar.onFindAgainCommand(); + await promiseMatchesCountResult(test.text); + // test.current + 1, test.current + 2, ..., test.total, 1, ..., test.current + let current = (test.current + i - 1) % test.total + 1; + assertMatches({ + text: test.text, + current, + total: test.total + }, foundMatches.value.match(regex)); + } + } + } + } + + // See bug 1051187. + async function testFindAfterCaseChanged() { + // Search to set focus on "Text Test" so that searching for "t" selects first + // (upper case!) "T". + await enterStringIntoFindField(SEARCH_TEXT); + gFindBar.clear(); + + // Case-insensitive should already be the current value. + Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 0); + + await enterStringIntoFindField("t"); + await SpecialPowers.spawn(gBrowser, [], async function() { + Assert.equal(content.getSelection().toString(), "T", "First T should be selected."); + }); + + await changeCase(1); + await SpecialPowers.spawn(gBrowser, [], async function() { + Assert.equal(content.getSelection().toString(), "t", "First t should be selected."); + }); + } + + // Make sure that _findFailedString is cleared: + // 1. Do a search that fails with case sensitivity but matches with no case sensitivity. + // 2. Uncheck case sensitivity button to match the string. + async function testFailedStringReset() { + Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 1); + + let promise = promiseFindResult(gBrowser.hasAttribute("remote") ? SEARCH_TEXT.toUpperCase() : ""); + await enterStringIntoFindField(SEARCH_TEXT.toUpperCase(), false); + await promise; + await SpecialPowers.spawn(gBrowser, [], async function() { + Assert.equal(content.getSelection().toString(), "", "Not found."); + }); + + await changeCase(0); + await SpecialPowers.spawn(gBrowser, [{ SEARCH_TEXT }], async function(args) { + Assert.equal(content.getSelection().toString(), args.SEARCH_TEXT, + "Search text should be selected."); + }); + } + + function testClipboardSearchString(aExpected) { + if (!gHasFindClipboard) + return; + + if (!aExpected) + aExpected = ""; + var searchStr = gFindBar.browser.finder.clipboardSearchString; + ok(searchStr.toLowerCase() == aExpected.toLowerCase(), + "testClipboardSearchString: search string not set to '" + aExpected + + "', instead found '" + searchStr + "'"); + } + + // See bug 967982. + async function testFindAgainNotFound() { + await openFindbar(); + await enterStringIntoFindField(NOT_FOUND_TEXT, false); + gFindBar.close(); + ok(gFindBar.hidden, "The findbar is closed."); + let promise = promiseFindResult(); + gFindBar.onFindAgainCommand(); + await promise; + ok(!gFindBar.hidden, "Unsuccessful Find Again opens the find bar."); + + await enterStringIntoFindField(SEARCH_TEXT); + gFindBar.close(); + ok(gFindBar.hidden, "The findbar is closed."); + promise = promiseFindResult(); + gFindBar.onFindAgainCommand(); + await promise; + ok(gFindBar.hidden, "Successful Find Again leaves the find bar closed."); + } + + async function testToggleDiacriticMatching() { + await openFindbar(); + let promise = promiseFindResult(); + await enterStringIntoFindField("δοκιμη", false); + let result = await promise; + is(result.result, Ci.nsITypeAheadFind.FIND_FOUND, "Text should be found"); + + await new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); + promise = promiseFindResult(); + let check = gFindBar.getElement("find-match-diacritics"); + check.click(); + result = await promise; + is(result.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "Text should NOT be found"); + + check.click(); + gFindBar.close(true); + } + + async function testToggleEntireWord() { + await openFindbar(); + let promise = promiseFindResult(); + await enterStringIntoFindField("Tex", false); + let result = await promise; + is(result.result, Ci.nsITypeAheadFind.FIND_FOUND, "Text should be found"); + + await new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); + promise = promiseFindResult(); + let check = gFindBar.getElement("find-entire-word"); + check.click(); + result = await promise; + is(result.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "Text should NOT be found"); + + check.click(); + gFindBar.close(true); + } + + function changeCase(value) { + let promise = gBrowser.hasAttribute("remote") ? promiseFindResult() : Promise.resolve(); + Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", value); + return promise; + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/frame_popup_anchor.xhtml b/toolkit/content/tests/chrome/frame_popup_anchor.xhtml new file mode 100644 index 0000000000..142bc7b0af --- /dev/null +++ b/toolkit/content/tests/chrome/frame_popup_anchor.xhtml @@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<menupopup id="popup" onpopupshowing="if (isSecondTest) popupShowing(event)" onpopupshown="popupShown()" + onpopuphidden="nextTest()"> + <menuitem label="One"/> + <menuitem label="Two"/> +</menupopup> + +<button id="button" label="OK" popup="popup"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var isSecondTest = false; + +function openPopup() +{ + document.getElementById("popup").openPopup(parent.document.getElementById("outerbutton"), "after_start", 3, 1); +} + +function popupShowing(event) +{ + var buttonrect = document.getElementById("button").getBoundingClientRect(); + parent.arguments[0].SimpleTest.is(event.clientX, buttonrect.left + 6, "popup clientX with mouse"); + parent.arguments[0].SimpleTest.is(event.clientY, buttonrect.top + 6, "popup clientY with mouse"); +} + +function popupShown() +{ + var left, top; + var popuprect = document.getElementById("popup").getBoundingClientRect(); + if (isSecondTest) { + let buttonrect = document.getElementById("button").getBoundingClientRect(); + left = buttonrect.left + 6; + top = buttonrect.top + 6; + } + else { + let iframerect = parent.document.getElementById("frame").getBoundingClientRect(); + let buttonrect = parent.document.getElementById("outerbutton").getBoundingClientRect(); + + // The popup should appear anchored on the bottom left edge of the button, however + // the client rectangle is relative to the iframe's document. Thus the coordinates + // are: + // left = iframe's left - anchor button's left - 3 pixel offset passed to openPopup + + // iframe border (17px) + iframe padding (0) + // top = iframe's top - anchor button's bottom - 1 pixel offset passed to openPopup + + // iframe border (0) + iframe padding (3px); + left = -(Math.round(iframerect.left) - Math.round(buttonrect.left) + 14); + top = -(Math.round(iframerect.top) - Math.round(buttonrect.bottom) + 2); + } + + var testid = isSecondTest ? "with mouse" : "anchored to parent frame"; + parent.arguments[0].SimpleTest.is(Math.round(popuprect.left), left, "popup left " + testid); + parent.arguments[0].SimpleTest.is(Math.round(popuprect.top), top, "popup top " + testid); + + document.getElementById("popup").hidePopup(); +} + +function nextTest() +{ + if (isSecondTest) { + parent.arguments[0].SimpleTest.finish(); + parent.close(); + } + else { + // this second test ensures that the popupshowing coordinates when a popup in + // a frame is opened are correct + isSecondTest = true; + synthesizeMouse(document.getElementById("button"), 6, 6, { }); + } +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/frame_popupremoving_frame.xhtml b/toolkit/content/tests/chrome/frame_popupremoving_frame.xhtml new file mode 100644 index 0000000000..e8f00ce7a9 --- /dev/null +++ b/toolkit/content/tests/chrome/frame_popupremoving_frame.xhtml @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Removing Frame Tests" + onload="setTimeout(init, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<hbox> + +<menu id="separatemenu1" label="Menu"> + <menupopup id="separatepopup1" onpopupshown="document.getElementById('separatemenu2').open = true"> + <menuitem label="L1 One"/> + <menuitem label="L1 Two"/> + <menuitem label="L1 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu2" label="Menu"> + <menupopup id="separatepopup2" onpopupshown="document.getElementById('separatemenu3').open = true"> + <menuitem label="L2 One"/> + <menuitem label="L2 Two"/> + <menuitem label="L2 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu3" label="Menu" onpopupshown="document.getElementById('separatemenu4').open = true"> + <menupopup id="separatepopup3"> + <menuitem label="L3 One"/> + <menuitem label="L3 Two"/> + <menuitem label="L3 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu4" label="Menu" onpopupshown="document.getElementById('nestedmenu1').open = true"> + <menupopup id="separatepopup3"> + <menuitem label="L4 One"/> + <menuitem label="L4 Two"/> + <menuitem label="L4 Three"/> + </menupopup> +</menu> + +</hbox> + +<menu id="nestedmenu1" label="Menu"> + <menupopup id="nestedpopup1" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu2" label="Menu"> + <menupopup id="nestedpopup2" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu3" label="Menu"> + <menupopup id="nestedpopup3" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu4" label="Menu" onpopupshown="parent.popupsOpened()"> + <menupopup id="nestedpopup4"> + <menuitem label="Nested One"/> + <menuitem label="Nested Two"/> + <menuitem label="Nested Three"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> +</menu> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function init() +{ + document.getElementById("separatemenu1").open = true; +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xhtml b/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xhtml new file mode 100644 index 0000000000..f68a06d85d --- /dev/null +++ b/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xhtml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="frame1" + style="background-color:green;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<spacer height="10px"/> +<iframe + style="margin:10px; min-height:170px; max-width:200px; max-height:200px; border:solid 1px white;" + src="frame_subframe_origin_subframe2.xhtml"></iframe> +<spacer height="3px"/> +<caption id="cap1" style="min-width:200px; max-width:200px; background-color:white;" label=""/> +<script class="testbody" type="application/javascript"> + +// Fire a mouse move event aimed at this window, and check to be +// sure the client coords translate from widget to the dom correctly. + +function runTests() +{ + synthesizeMouse(document.getElementById("frame1"), 3, 4, { type: "mousemove" }); +} + +function mouseMove(e) { + e.stopPropagation(); + var el = document.getElementById("cap1"); + el.label = "client: (" + e.clientX + "," + e.clientY + ")"; + parent.arguments[0].SimpleTest.is(e.clientX, 3, "mouse event clientX on sub frame 1"); + parent.arguments[0].SimpleTest.is(e.clientY, 4, "mouse event clientY on sub frame 1"); + // fire the next test on the sub frame + frames[0].runTests(); +} + +window.addEventListener("mousemove", mouseMove); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xhtml b/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xhtml new file mode 100644 index 0000000000..338877ff96 --- /dev/null +++ b/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="frame2" + style="background-color:red;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<spacer height="10px"/> +<caption id="cap2" style="background-color:white;" label=""/> +<script class="testbody" type="application/javascript"> + +// Fire a mouse move event aimed at this window, and check to be +// sure the client coords translate from widget to the dom correctly. + +function runTests() +{ + synthesizeMouse(document.getElementById("frame2"), 6, 5, { type: "mousemove" }); +} + +function mouseMove(e) { + e.stopPropagation(); + var el = document.getElementById("cap2"); + el.label = "client: (" + e.clientX + "," + e.clientY + ")"; + parent.parent.arguments[0].SimpleTest.is(e.clientX, 6, "mouse event clientX on sub frame 2"); + parent.parent.arguments[0].SimpleTest.is(e.clientY, 5, "mouse event clientY on sub frame 2"); + parent.parent.arguments[0].SimpleTest.finish(); + parent.parent.close(); +} + +window.addEventListener("mousemove", mouseMove); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/popup_childframe_node.xhtml b/toolkit/content/tests/chrome/popup_childframe_node.xhtml new file mode 100644 index 0000000000..512f5f8c26 --- /dev/null +++ b/toolkit/content/tests/chrome/popup_childframe_node.xhtml @@ -0,0 +1,2 @@ +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" width="80" height="80" + onclick="document.documentElement.setAttribute('data', 'x' + document.popupNode)"/> diff --git a/toolkit/content/tests/chrome/popup_trigger.js b/toolkit/content/tests/chrome/popup_trigger.js new file mode 100644 index 0000000000..1777b37039 --- /dev/null +++ b/toolkit/content/tests/chrome/popup_trigger.js @@ -0,0 +1,1262 @@ +/* import-globals-from ../widgets/popup_shared.js */ + +var gMenuPopup = null; +var gTrigger = null; +var gIsMenu = false; +var gScreenX = -1, + gScreenY = -1; +var gCachedEvent = null; +var gCachedEvent2 = null; + +function cacheEvent(modifiers) { + var cachedEvent = null; + + var mouseFn = function(event) { + cachedEvent = event; + }; + + window.addEventListener("mousedown", mouseFn); + synthesizeMouse(document.documentElement, 0, 0, modifiers); + window.removeEventListener("mousedown", mouseFn); + + return cachedEvent; +} + +function runTests() { + if (screen.height < 768) { + ok( + false, + "popup tests are likely to fail for screen heights less than 768 pixels" + ); + } + + gMenuPopup = document.getElementById("thepopup"); + gTrigger = document.getElementById("trigger"); + + gIsMenu = gTrigger.hasMenu(); + + // a hacky way to get the screen position of the document. Cache the event + // so that we can use it in calls to openPopup. + gCachedEvent = cacheEvent({ shiftKey: true }); + gScreenX = gCachedEvent.screenX; + gScreenY = gCachedEvent.screenY; + gCachedEvent2 = cacheEvent({ + altKey: true, + ctrlKey: true, + shiftKey: true, + metaKey: true, + }); + + startPopupTests(popupTests); +} + +var popupTests = [ + { + testname: "mouse click on trigger", + events: ["popupshowing thepopup", "popupshown thepopup"], + test() { + // for menus, no trigger will be set. For non-menus using the popup + // attribute, the trigger will be set to the node with the popup attribute + gExpectedTriggerNode = gIsMenu ? "notset" : gTrigger; + synthesizeMouse(gTrigger, 4, 4, {}); + }, + async result(testname) { + gExpectedTriggerNode = null; + // menus are the anchor but non-menus are opened at screen coordinates + is( + gMenuPopup.anchorNode, + gIsMenu ? gTrigger : null, + testname + " anchorNode" + ); + // menus are opened internally, but non-menus have a mouse event which + // triggered them + is( + gMenuPopup.triggerNode, + gIsMenu ? null : gTrigger, + testname + " triggerNode" + ); + is( + document.popupNode, + gIsMenu ? null : gTrigger, + testname + " document.popupNode" + ); + is(document.tooltipNode, null, testname + " document.tooltipNode"); + // check to ensure the popup node for a different document isn't used + if (window.opener) { + is( + window.opener.document.popupNode, + null, + testname + " opener.document.popupNode" + ); + } + + // Popup may have wrong initial size in non e10s mode tests, because + // layout is not yet ready for popup content lazy population on + // popupshowing event. + await new Promise(r => + requestAnimationFrame(() => requestAnimationFrame(r)) + ); + + // this will be used in some tests to ensure the size doesn't change + var popuprect = gMenuPopup.getBoundingClientRect(); + gPopupWidth = Math.round(popuprect.width); + gPopupHeight = Math.round(popuprect.height); + + checkActive(gMenuPopup, "", testname); + checkOpen("trigger", testname); + // if a menu, the popup should be opened underneath the menu in the + // 'after_start' position, otherwise it is opened at the mouse position + if (gIsMenu) { + compareEdge(gTrigger, gMenuPopup, "after_start", 0, 0, testname); + } + }, + }, + { + // check that pressing cursor down while there is no selection + // highlights the first item + testname: "cursor down no selection", + events: ["DOMMenuItemActive item1"], + test() { + synthesizeKey("KEY_ArrowDown"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // check that pressing cursor up wraps and highlights the last item + testname: "cursor up wrap", + events() { + // No wrapping on menus on Mac + return platformIsMac() + ? [] + : ["DOMMenuItemInactive item1", "DOMMenuItemActive last"]; + }, + test() { + synthesizeKey("KEY_ArrowUp"); + }, + result(testname) { + checkActive(gMenuPopup, platformIsMac() ? "item1" : "last", testname); + }, + }, + { + // check that pressing cursor down wraps and highlights the first item + testname: "cursor down wrap", + condition() { + return !platformIsMac(); + }, + events: ["DOMMenuItemInactive last", "DOMMenuItemActive item1"], + test() { + synthesizeKey("KEY_ArrowDown"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // check that pressing cursor down highlights the second item + testname: "cursor down", + events: ["DOMMenuItemInactive item1", "DOMMenuItemActive item2"], + test() { + synthesizeKey("KEY_ArrowDown"); + }, + result(testname) { + checkActive(gMenuPopup, "item2", testname); + }, + }, + { + // check that pressing cursor up highlights the second item + testname: "cursor up", + events: ["DOMMenuItemInactive item2", "DOMMenuItemActive item1"], + test() { + synthesizeKey("KEY_ArrowUp"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // cursor left should not do anything + testname: "cursor left", + test() { + synthesizeKey("KEY_ArrowLeft"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // cursor right should not do anything + testname: "cursor right", + test() { + synthesizeKey("KEY_ArrowRight"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // check cursor down when a disabled item exists in the menu + testname: "cursor down disabled", + events() { + // On Windows, disabled items are included when navigating, but on + // other platforms, disabled items are skipped over + if (navigator.platform.indexOf("Win") == 0) { + return ["DOMMenuItemInactive item1", "DOMMenuItemActive item2"]; + } + return ["DOMMenuItemInactive item1", "DOMMenuItemActive amenu"]; + }, + test() { + document.getElementById("item2").disabled = true; + synthesizeKey("KEY_ArrowDown"); + }, + }, + { + // check cursor up when a disabled item exists in the menu + testname: "cursor up disabled", + events() { + if (navigator.platform.indexOf("Win") == 0) { + return [ + "DOMMenuItemInactive item2", + "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", + "DOMMenuItemActive item2", + "DOMMenuItemInactive item2", + "DOMMenuItemActive item1", + ]; + } + return ["DOMMenuItemInactive amenu", "DOMMenuItemActive item1"]; + }, + test() { + if (navigator.platform.indexOf("Win") == 0) { + synthesizeKey("KEY_ArrowDown"); + } + synthesizeKey("KEY_ArrowUp"); + if (navigator.platform.indexOf("Win") == 0) { + synthesizeKey("KEY_ArrowUp"); + } + }, + }, + { + testname: "mouse click outside", + events: [ + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuItemInactive item1", + "DOMMenuInactive thepopup", + ], + test() { + gMenuPopup.hidePopup(); + // XXXndeakin event simulation fires events outside of the platform specific + // widget code so the popup capturing isn't handled. Thus, the menu won't + // rollup this way. + // synthesizeMouse(gTrigger, 0, -12, { }); + }, + result(testname, step) { + is(gMenuPopup.anchorNode, null, testname + " anchorNode"); + is(gMenuPopup.triggerNode, null, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + checkClosed("trigger", testname); + }, + }, + { + // these tests check to ensure that passing an anchor and position + // puts the popup in the right place + testname: "open popup anchored", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + "topleft topleft", + "topcenter topleft", + "topright topleft", + "leftcenter topright", + "rightcenter topright", + "bottomleft bottomleft", + "bottomcenter bottomleft", + "bottomright bottomleft", + "topleft bottomright", + "bottomcenter bottomright", + "rightcenter topright", + ], + test(testname, step) { + gExpectedTriggerNode = "notset"; + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + // no triggerNode because it was opened without passing an event + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, gTrigger, testname + " anchorNode"); + is(gMenuPopup.triggerNode, null, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + compareEdge(gTrigger, gMenuPopup, step, 0, 0, testname); + }, + }, + { + // these tests check the same but with a 10 pixel margin on the popup + testname: "open popup anchored with margin", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + "topleft topleft", + "topcenter topleft", + "topright topleft", + "leftcenter topright", + "rightcenter topright", + "bottomleft bottomleft", + "bottomcenter bottomleft", + "bottomright bottomleft", + "topleft bottomright", + "bottomcenter bottomright", + "rightcenter topright", + ], + test(testname, step) { + gMenuPopup.setAttribute("style", "margin: 10px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + var rightmod = + step == "before_end" || + step == "after_end" || + step == "start_before" || + step == "start_after" || + step.match(/topright$/) || + step.match(/bottomright$/); + var bottommod = + step == "before_start" || + step == "before_end" || + step == "start_after" || + step == "end_after" || + step.match(/bottomleft$/) || + step.match(/bottomright$/); + compareEdge( + gTrigger, + gMenuPopup, + step, + rightmod ? -10 : 10, + bottommod ? -10 : 10, + testname + ); + gMenuPopup.removeAttribute("style"); + }, + }, + { + // these tests check the same but with a -8 pixel margin on the popup + testname: "open popup anchored with negative margin", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + ], + test(testname, step) { + gMenuPopup.setAttribute("style", "margin: -8px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + var rightmod = + step == "before_end" || + step == "after_end" || + step == "start_before" || + step == "start_after"; + var bottommod = + step == "before_start" || + step == "before_end" || + step == "start_after" || + step == "end_after"; + compareEdge( + gTrigger, + gMenuPopup, + step, + rightmod ? 8 : -8, + bottommod ? 8 : -8, + testname + ); + gMenuPopup.removeAttribute("style"); + }, + }, + { + testname: "open popup with large positive margin", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + ], + test(testname, step) { + gMenuPopup.setAttribute("style", "margin: 1000px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + var popuprect = gMenuPopup.getBoundingClientRect(); + // as there is more room on the 'end' or 'after' side, popups will always + // appear on the right or bottom corners, depending on which side they are + // allowed to be flipped by. + var expectedleft = + step == "before_end" || step == "after_end" + ? 0 + : Math.round(window.innerWidth - gPopupWidth); + var expectedtop = + step == "start_after" || step == "end_after" + ? 0 + : Math.round(window.innerHeight - gPopupHeight); + is( + Math.round(popuprect.left), + expectedleft, + testname + " x position " + step + ); + is( + Math.round(popuprect.top), + expectedtop, + testname + " y position " + step + ); + gMenuPopup.removeAttribute("style"); + }, + }, + { + testname: "open popup with large negative margin", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + ], + test(testname, step) { + gMenuPopup.setAttribute("style", "margin: -1000px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + var popuprect = gMenuPopup.getBoundingClientRect(); + // using negative margins causes the reverse of positive margins, and + // popups will appear on the left or top corners. + var expectedleft = + step == "before_end" || step == "after_end" + ? Math.round(window.innerWidth - gPopupWidth) + : 0; + var expectedtop = + step == "start_after" || step == "end_after" + ? Math.round(window.innerHeight - gPopupHeight) + : 0; + is( + Math.round(popuprect.left), + expectedleft, + testname + " x position " + step + ); + is( + Math.round(popuprect.top), + expectedtop, + testname + " y position " + step + ); + gMenuPopup.removeAttribute("style"); + }, + }, + { + testname: "popup with unknown step", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test() { + gMenuPopup.openPopup(gTrigger, "other", 0, 0, false, false); + }, + result(testname) { + var triggerrect = gMenuPopup.getBoundingClientRect(); + var popuprect = gMenuPopup.getBoundingClientRect(); + is( + Math.round(popuprect.left), + triggerrect.left, + testname + " x position " + ); + is(Math.round(popuprect.top), triggerrect.top, testname + " y position "); + }, + }, + { + // these tests check to ensure that the position attribute can be used + // to set the position of a popup instead of passing it as an argument + testname: "open popup anchored with attribute", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + "topcenter topleft", + "topright bottomright", + "leftcenter topright", + ], + test(testname, step) { + gMenuPopup.setAttribute("position", step); + gMenuPopup.openPopup(gTrigger, "", 0, 0, false, false); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, step, 0, 0, testname); + }, + }, + { + // this test checks to ensure that the attributes override flag to openPopup + // can be used to override the popup's position. This test also passes an + // event to openPopup to check the trigger node. + testname: "open popup anchored with override", + events: ["popupshowing thepopup 0010", "popupshown thepopup"], + test(testname, step) { + // attribute overrides the position passed in + gMenuPopup.setAttribute("position", "end_after"); + gExpectedTriggerNode = gCachedEvent.target; + gMenuPopup.openPopup( + gTrigger, + "before_start", + 0, + 0, + false, + true, + gCachedEvent + ); + }, + result(testname, step) { + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, gTrigger, testname + " anchorNode"); + is( + gMenuPopup.triggerNode, + gCachedEvent.target, + testname + " triggerNode" + ); + is( + document.popupNode, + gCachedEvent.target, + testname + " document.popupNode" + ); + compareEdge(gTrigger, gMenuPopup, "end_after", 0, 0, testname); + }, + }, + { + testname: "close popup with escape", + events: [ + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuInactive thepopup", + ], + test(testname, step) { + synthesizeKey("KEY_Escape"); + checkClosed("trigger", testname); + }, + }, + { + // check that offsets may be supplied to the openPopup method + testname: "open popup anchored with offsets", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + // attribute is empty so does not override + gMenuPopup.setAttribute("position", ""); + gMenuPopup.openPopup(gTrigger, "before_start", 5, 10, true, true); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, "before_start", 5, 10, testname); + }, + }, + { + // if no anchor is supplied to openPopup, it should be opened relative + // to the viewport. + testname: "open popup unanchored", + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + gMenuPopup.openPopup(null, "after_start", 6, 8, false); + }, + result(testname, step) { + var rect = gMenuPopup.getBoundingClientRect(); + ok( + rect.left == 6 && rect.top == 8 && rect.right && rect.bottom, + testname + ); + }, + }, + { + testname: "activate menuitem with mouse", + events: [ + "DOMMenuInactive thepopup", + "command item3", + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuItemInactive item3", + ], + test(testname, step) { + var item3 = document.getElementById("item3"); + synthesizeMouse(item3, 4, 4, {}); + }, + result(testname, step) { + checkClosed("trigger", testname); + }, + }, + { + testname: "close popup", + condition() { + return false; + }, + events: [ + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuInactive thepopup", + ], + test(testname, step) { + gMenuPopup.hidePopup(); + }, + }, + { + testname: "open popup at screen", + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + gExpectedTriggerNode = "notset"; + gMenuPopup.openPopupAtScreen(gScreenX + 24, gScreenY + 20, false); + }, + result(testname, step) { + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, null, testname + " anchorNode"); + is(gMenuPopup.triggerNode, null, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + var rect = gMenuPopup.getBoundingClientRect(); + is(rect.left, 24, testname + " left"); + is(rect.top, 20, testname + " top"); + ok(rect.right, testname + " right is " + rect.right); + ok(rect.bottom, testname + " bottom is " + rect.bottom); + }, + }, + { + // check that pressing a menuitem's accelerator selects it. Note that + // the menuitem with the M accesskey overrides the earlier menuitem that + // begins with M. + testname: "menuitem accelerator", + events: [ + "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", + "DOMMenuInactive thepopup", + "command amenu", + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuItemInactive amenu", + ], + test() { + sendString("M"); + }, + result(testname) { + checkClosed("trigger", testname); + }, + }, + { + testname: "open context popup at screen", + events: ["popupshowing thepopup 0010", "popupshown thepopup"], + test(testname, step) { + gExpectedTriggerNode = gCachedEvent.target; + gMenuPopup.openPopupAtScreen( + gScreenX + 8, + gScreenY + 16, + true, + gCachedEvent + ); + }, + result(testname, step) { + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, null, testname + " anchorNode"); + is( + gMenuPopup.triggerNode, + gCachedEvent.target, + testname + " triggerNode" + ); + is( + document.popupNode, + gCachedEvent.target, + testname + " document.popupNode" + ); + + var childframe = document.getElementById("childframe"); + if (childframe) { + for (var t = 0; t < 2; t++) { + var child = childframe.contentDocument; + var evt = child.createEvent("Event"); + evt.initEvent("click", true, true); + child.documentElement.dispatchEvent(evt); + is( + child.documentElement.getAttribute("data"), + "xundefined", + "cannot get popupNode from other document" + ); + child.documentElement.setAttribute("data", "none"); + // now try again with document.popupNode set explicitly + document.popupNode = gCachedEvent.target; + } + } + + var openX = 8; + var openY = 16; + var rect = gMenuPopup.getBoundingClientRect(); + is(rect.left, openX + (platformIsMac() ? 1 : 2), testname + " left"); + is(rect.top, openY + (platformIsMac() ? -6 : 2), testname + " top"); + ok(rect.right, testname + " right is " + rect.right); + ok(rect.bottom, testname + " bottom is " + rect.bottom); + }, + }, + { + // pressing a letter that doesn't correspond to an accelerator, but does + // correspond to the first letter in a menu's label. The menu should not + // close because there is more than one item corresponding to that letter + testname: "menuitem with non accelerator", + events: ["DOMMenuItemActive one"], + test() { + sendString("O"); + }, + result(testname) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "one", testname); + }, + }, + { + // pressing the letter again should select the next one that starts with + // that letter + testname: "menuitem with non accelerator again", + events: ["DOMMenuItemInactive one", "DOMMenuItemActive submenu"], + test() { + sendString("O"); + }, + result(testname) { + // 'submenu' is a menu but it should not be open + checkOpen("trigger", testname); + checkClosed("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + }, + }, + { + // open the submenu with the cursor right key + testname: "open submenu with cursor right", + events: [ + "popupshowing submenupopup", + "DOMMenuItemActive submenuitem", + "popupshown submenupopup", + ], + test() { + synthesizeKey("KEY_ArrowRight"); + }, + result(testname) { + checkOpen("trigger", testname); + checkOpen("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive( + document.getElementById("submenupopup"), + "submenuitem", + testname + ); + }, + }, + { + // close the submenu with the cursor left key + testname: "close submenu with cursor left", + events: [ + "popuphiding submenupopup", + "popuphidden submenupopup", + "DOMMenuItemInactive submenuitem", + "DOMMenuInactive submenupopup", + "DOMMenuItemActive submenu", + ], + test() { + synthesizeKey("KEY_ArrowLeft"); + }, + result(testname) { + checkOpen("trigger", testname); + checkClosed("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive(document.getElementById("submenupopup"), "", testname); + }, + }, + { + // open the submenu with the enter key + testname: "open submenu with enter", + events: [ + "popupshowing submenupopup", + "DOMMenuItemActive submenuitem", + "popupshown submenupopup", + ], + test() { + synthesizeKey("KEY_Enter"); + }, + result(testname) { + checkOpen("trigger", testname); + checkOpen("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive( + document.getElementById("submenupopup"), + "submenuitem", + testname + ); + }, + }, + { + // close the submenu with the escape key + testname: "close submenu with escape", + events: [ + "popuphiding submenupopup", + "popuphidden submenupopup", + "DOMMenuItemInactive submenuitem", + "DOMMenuInactive submenupopup", + "DOMMenuItemActive submenu", + ], + test() { + synthesizeKey("KEY_Escape"); + }, + result(testname) { + checkOpen("trigger", testname); + checkClosed("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive(document.getElementById("submenupopup"), "", testname); + }, + }, + { + // pressing the letter again when the next item is disabled should still + // select the disabled item on Windows, but select the next item on other + // platforms + testname: "menuitem with non accelerator disabled", + events() { + if (navigator.platform.indexOf("Win") == 0) { + return [ + "DOMMenuItemInactive submenu", + "DOMMenuItemActive other", + "DOMMenuItemInactive other", + "DOMMenuItemActive item1", + ]; + } + return [ + "DOMMenuItemInactive submenu", + "DOMMenuItemActive last", + "DOMMenuItemInactive last", + "DOMMenuItemActive item1", + ]; + }, + test() { + sendString("OF"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // pressing a letter that doesn't correspond to an accelerator nor the + // first letter of a menu. This should have no effect. + testname: "menuitem with keypress no accelerator found", + test() { + sendString("G"); + }, + result(testname) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // when only one menuitem starting with that letter exists, it should be + // selected and the menu closed + testname: "menuitem with non accelerator single", + events: [ + "DOMMenuItemInactive item1", + "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", + "DOMMenuInactive thepopup", + "command amenu", + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuItemInactive amenu", + ], + test() { + sendString("M"); + }, + result(testname) { + checkClosed("trigger", testname); + checkActive(gMenuPopup, "", testname); + }, + }, + { + testname: "open context popup at screen with all modifiers set", + events: ["popupshowing thepopup 1111", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.openPopupAtScreen( + gScreenX + 8, + gScreenY + 16, + true, + gCachedEvent2 + ); + }, + }, + { + testname: "open popup with open property", + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + openMenu(gTrigger); + }, + result(testname, step) { + checkOpen("trigger", testname); + if (gIsMenu) { + compareEdge(gTrigger, gMenuPopup, "after_start", 0, 0, testname); + } + }, + }, + { + testname: "open submenu with open property", + events: [ + "popupshowing submenupopup", + "DOMMenuItemActive submenu", + "popupshown submenupopup", + ], + test(testname, step) { + openMenu(document.getElementById("submenu")); + }, + result(testname, step) { + checkOpen("trigger", testname); + checkOpen("submenu", testname); + // XXXndeakin + // getBoundingClientRect doesn't seem to working right for submenus + // so disable this test for now + // compareEdge(document.getElementById("submenu"), + // document.getElementById("submenupopup"), "end_before", 0, 0, testname); + }, + }, + { + testname: "hidePopup hides entire chain", + events: [ + "popuphiding submenupopup", + "popuphidden submenupopup", + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuInactive submenupopup", + "DOMMenuItemInactive submenu", + "DOMMenuItemInactive submenu", + "DOMMenuInactive thepopup", + ], + test() { + gMenuPopup.hidePopup(); + }, + result(testname, step) { + checkClosed("trigger", testname); + checkClosed("submenu", testname); + }, + }, + { + testname: "open submenu with open property without parent open", + test(testname, step) { + openMenu(document.getElementById("submenu")); + }, + result(testname, step) { + checkClosed("trigger", testname); + checkClosed("submenu", testname); + }, + }, + { + testname: "open popup with open property and position", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + gMenuPopup.setAttribute("position", "before_start"); + openMenu(gTrigger); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, "before_start", 0, 0, testname); + }, + }, + { + testname: "close popup with open property", + condition() { + return gIsMenu; + }, + events: [ + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuInactive thepopup", + ], + test(testname, step) { + closeMenu(gTrigger, gMenuPopup); + }, + result(testname, step) { + checkClosed("trigger", testname); + }, + }, + { + testname: "open popup with open property, position, anchor and alignment", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.setAttribute("position", "start_after"); + gMenuPopup.setAttribute("popupanchor", "topright"); + gMenuPopup.setAttribute("popupalign", "bottomright"); + openMenu(gTrigger); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, "start_after", 0, 0, testname); + }, + }, + { + testname: "open popup with open property, anchor and alignment", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.removeAttribute("position"); + gMenuPopup.setAttribute("popupanchor", "bottomright"); + gMenuPopup.setAttribute("popupalign", "topright"); + openMenu(gTrigger); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, "after_end", 0, 0, testname); + gMenuPopup.removeAttribute("popupanchor"); + gMenuPopup.removeAttribute("popupalign"); + }, + }, + { + testname: "focus and cursor down on trigger", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gTrigger.focus(); + synthesizeKey("KEY_ArrowDown", { altKey: !platformIsMac() }); + }, + result(testname, step) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "", testname); + }, + }, + { + testname: "focus and cursor up on trigger", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + gTrigger.focus(); + synthesizeKey("KEY_ArrowUp", { altKey: !platformIsMac() }); + }, + result(testname, step) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "", testname); + }, + }, + { + testname: "select and enter on menuitem", + condition() { + return gIsMenu; + }, + events: [ + "DOMMenuItemActive item1", + "DOMMenuItemInactive item1", + "DOMMenuInactive thepopup", + "command item1", + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuItemInactive item1", + ], + test(testname, step) { + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + }, + result(testname, step) { + checkClosed("trigger", testname); + }, + }, + { + testname: "focus trigger and key to open", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gTrigger.focus(); + synthesizeKey(platformIsMac() ? " " : "KEY_F4"); + }, + result(testname, step) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "", testname); + }, + }, + { + // the menu should only open when the meta or alt key is not pressed + testname: "focus trigger and key wrong modifier", + condition() { + return gIsMenu; + }, + test(testname, step) { + gTrigger.focus(); + if (platformIsMac()) { + synthesizeKey("KEY_F4", { altKey: true }); + } else { + synthesizeKey("", { metaKey: true }); + } + }, + result(testname, step) { + checkClosed("trigger", testname); + }, + }, + { + testname: "mouse click on disabled menu", + condition() { + return gIsMenu; + }, + test(testname, step) { + gTrigger.setAttribute("disabled", "true"); + synthesizeMouse(gTrigger, 4, 4, {}); + }, + result(testname, step) { + checkClosed("trigger", testname); + gTrigger.removeAttribute("disabled"); + }, + }, + { + // openPopup using object as position argument + testname: "openPopup with object argument", + events: ["popupshowing thepopup 0000", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.openPopup(gTrigger, { position: "before_start", x: 5, y: 7 }); + checkOpen("trigger", testname); + }, + result(testname, step) { + var triggerrect = gTrigger.getBoundingClientRect(); + var popuprect = gMenuPopup.getBoundingClientRect(); + is( + Math.round(popuprect.left), + Math.round(triggerrect.left + 5), + testname + " x position " + ); + is( + Math.round(popuprect.bottom), + Math.round(triggerrect.top + 7), + testname + " y position " + ); + }, + }, + { + // openPopup using object as position argument with event + testname: "openPopup with object argument with event", + events: ["popupshowing thepopup 1000", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.openPopup(gTrigger, { + position: "after_start", + x: 0, + y: 0, + triggerEvent: new MouseEvent("mousedown", { altKey: true }), + }); + checkOpen("trigger", testname); + }, + }, + { + // openPopup with no arguments + testname: "openPopup with no arguments", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.openPopup(); + }, + result(testname, step) { + let isMenu = gTrigger.type == "menu"; + // With no arguments, open in default menu position + var triggerrect = gTrigger.getBoundingClientRect(); + var popuprect = gMenuPopup.getBoundingClientRect(); + is( + Math.round(popuprect.left), + isMenu ? Math.round(triggerrect.left) : 0, + testname + " x position " + ); + is( + Math.round(popuprect.top), + isMenu ? Math.round(triggerrect.bottom) : 0, + testname + " y position " + ); + }, + }, + { + // openPopup should open the menu synchronously, however popupshown + // is fired asynchronously + testname: "openPopup synchronous", + events: [ + "popupshowing thepopup", + "popupshowing submenupopup", + "popupshown thepopup", + "DOMMenuItemActive submenu", + "popupshown submenupopup", + ], + test(testname, step) { + gMenuPopup.openPopup(gTrigger, "after_start", 0, 0, false, true); + document + .getElementById("submenupopup") + .openPopup(gTrigger, "end_before", 0, 0, false, true); + checkOpen("trigger", testname); + checkOpen("submenu", testname); + }, + }, + { + // remove the content nodes for the popup + testname: "remove content", + test(testname, step) { + var submenupopup = document.getElementById("submenupopup"); + submenupopup.remove(); + var popup = document.getElementById("thepopup"); + popup.remove(); + }, + }, +]; + +function platformIsMac() { + return navigator.platform.indexOf("Mac") > -1; +} diff --git a/toolkit/content/tests/chrome/rtlchrome/rtl.css b/toolkit/content/tests/chrome/rtlchrome/rtl.css new file mode 100644 index 0000000000..0fea010019 --- /dev/null +++ b/toolkit/content/tests/chrome/rtlchrome/rtl.css @@ -0,0 +1,2 @@ +/* Imitate RTL UI */
+window { direction: rtl; }
diff --git a/toolkit/content/tests/chrome/rtlchrome/rtl.dtd b/toolkit/content/tests/chrome/rtlchrome/rtl.dtd new file mode 100644 index 0000000000..8b32de6746 --- /dev/null +++ b/toolkit/content/tests/chrome/rtlchrome/rtl.dtd @@ -0,0 +1 @@ +<!ENTITY locale.dir "rtl"> diff --git a/toolkit/content/tests/chrome/rtlchrome/rtl.manifest b/toolkit/content/tests/chrome/rtlchrome/rtl.manifest new file mode 100644 index 0000000000..a4cc6929be --- /dev/null +++ b/toolkit/content/tests/chrome/rtlchrome/rtl.manifest @@ -0,0 +1,5 @@ +content rtlchrome /
+
+# Override intl.css with our own CSS file
+override chrome://global/locale/intl.css chrome://rtlchrome/rtl.css
+override chrome://global/locale/global.dtd chrome://rtlchrome/rtl.dtd
diff --git a/toolkit/content/tests/chrome/sample_entireword_latin1.html b/toolkit/content/tests/chrome/sample_entireword_latin1.html new file mode 100644 index 0000000000..b2d66fa3c4 --- /dev/null +++ b/toolkit/content/tests/chrome/sample_entireword_latin1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head><title>Latin entire-word find test page</title></head> + <body> + <!-- Feel free to extend the contents of this page with more comprehensive + - Latin punctuation and/ or word markers. + --> + <p>The twins of Mammon quarrelled. Their warring plunged the world into a new darkness, and the beast abhorred the darkness. So it began to move swiftly, and grew more powerful, and went forth and multiplied. And the beasts brought fire and light to the darkness.</p> + <p>from The Book of Mozilla, 15:1</p> + </body> +</html> diff --git a/toolkit/content/tests/chrome/test_about_networking.html b/toolkit/content/tests/chrome/test_about_networking.html new file mode 100644 index 0000000000..5465c07751 --- /dev/null +++ b/toolkit/content/tests/chrome/test_about_networking.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=912103 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + var dashboard = Cc["@mozilla.org/network/dashboard;1"] + .getService(Ci.nsIDashboard); + dashboard.enableLogging = true; + + var wsURI = "ws://mochi.test:8888/chrome/toolkit/content/tests/chrome/file_about_networking"; + var websocket = new WebSocket(wsURI); + + websocket.addEventListener("open", function() { + dashboard.requestWebsocketConnections(function(data) { + var found = false; + for (var i = 0; i < data.websockets.length; i++) { + if (data.websockets[i].hostport == "mochi.test:8888") { + found = true; + break; + } + } + isnot(found, false, "tested websocket entry not found"); + websocket.close(); + SimpleTest.finish(); + }); + }); + } + + window.addEventListener("DOMContentLoaded", function() { + runTest(); + }, {once: true}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=912103">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_arrowpanel.xhtml b/toolkit/content/tests/chrome/test_arrowpanel.xhtml new file mode 100644 index 0000000000..cf83d5c947 --- /dev/null +++ b/toolkit/content/tests/chrome/test_arrowpanel.xhtml @@ -0,0 +1,322 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Arrow Panels" + style="padding: 10px;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<stack flex="1"> + <label id="topleft" value="Top Left Corner" style="justify-self: left; margin-left: 15px; align-self: start; margin-top: 15px;"/> + <label id="topright" value="Top Right" style="justify-self: right; margin-right: 15px; align-self: start; margin-top: 15px;"/> + <label id="bottomleft" value="Bottom Left Corner" style="justify-self: left; margin-left: 15px; align-self: end; margin-bottom: 15px;"/> + <label id="bottomright" value="Bottom Right" style="justify-self: right; margin-right: 15px; align-self: end; margin-bottom: 15px;"/> + <!-- Our SimpleTest/TestRunner.js runs tests inside an iframe which sizes are W=500 H=300. + 'left' and 'top' values need to be set so that the panel (popup) has enough room to display on its 4 sides. --> + <label id="middle" value="+/- Centered" style="justify-self: left; margin-left: 225px; align-self: start; margin-top: 135px;"/> + <iframe id="frame" type="content" + src="data:text/html,<input id='input'>" style="width: 100px; height: 100px; justify-self: left; margin-left: 225px; align-self: start; margin-top: 120px;"/> +</stack> + +<panel id="panel" type="arrow" animate="false" + onpopupshown="checkPanelPosition(this)" onpopuphidden="runNextTest.next()"> + <box width="115" height="65"/> +</panel> + +<panel id="bigpanel" type="arrow" animate="false" + onpopupshown="checkBigPanel(this)" onpopuphidden="runNextTest.next()"> + <box width="125" height="3000"/> +</panel> + +<panel id="animatepanel" type="arrow" + onpopupshown="animatedPopupShown = true;" + onpopuphidden="animatedPopupHidden = true; runNextTest.next();"> + <label value="Animate Closed" height="40"/> +</panel> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +const isOSXYosemite = navigator.userAgent.includes("Mac OS X 10.10"); + +var expectedAnchor = null; +var expectedSide = "", expectedAnchorEdge = "", expectedPack = ""; +var zoomFactor = 1; +var animatedPopupShown = false; +var animatedPopupHidden = false; +var runNextTest; + +function startTest() +{ + runNextTest = nextTest(); + runNextTest.next(); +} + +function* nextTest() +{ + var panel = $("panel"); + + function openPopup(position, anchor, expected, anchorEdge, pack, alignment) + { + expectedAnchor = anchor instanceof Node ? anchor : $(anchor); + expectedSide = expected; + expectedAnchorEdge = anchorEdge; + expectedPack = pack; + + panel.removeAttribute("side"); + panel.openPopup(expectedAnchor, position, 0, 0, false, false, null); + } + + for (var iter = 0; iter < 2; iter++) { + openPopup("after_start", "topleft", "top", "left", "start"); + yield; + openPopup("after_start", "bottomleft", "bottom", "left", "start", "before_start"); + yield; + openPopup("before_start", "topleft", "top", "left", "start", "after_start"); + yield; + openPopup("before_start", "bottomleft", "bottom", "left", "start"); + yield; + openPopup("after_start", "middle", "top", "left", "start"); + yield; + openPopup("before_start", "middle", "bottom", "left", "start"); + yield; + + openPopup("after_start", "topright", "top", "right", "end", "after_end"); + yield; + openPopup("after_start", "bottomright", "bottom", "right", "end", "before_end"); + yield; + openPopup("before_start", "topright", "top", "right", "end", "after_end"); + yield; + openPopup("before_start", "bottomright", "bottom", "right", "end", "before_end"); + yield; + + openPopup("after_end", "middle", "top", "right", "end"); + yield; + openPopup("before_end", "middle", "bottom", "right", "end"); + yield; + + openPopup("start_before", "topleft", "left", "top", "start", "end_before"); + yield; + openPopup("start_before", "topright", "right", "top", "start"); + yield; + openPopup("end_before", "topleft", "left", "top", "start"); + yield; + openPopup("end_before", "topright", "right", "top", "start", "start_before"); + yield; + openPopup("start_before", "middle", "right", "top", "start"); + yield; + openPopup("end_before", "middle", "left", "top", "start"); + yield; + + openPopup("start_before", "bottomleft", "left", "bottom", "end", "end_after"); + yield; + openPopup("start_before", "bottomright", "right", "bottom", "end", "start_after"); + yield; + openPopup("end_before", "bottomleft", "left", "bottom", "end", "end_after"); + yield; + openPopup("end_before", "bottomright", "right", "bottom", "end", "start_after"); + yield; + + openPopup("start_after", "middle", "right", "bottom", "end"); + yield; + openPopup("end_after", "middle", "left", "bottom", "end"); + yield; + + openPopup("topcenter bottomleft", "bottomleft", "bottom", "center left", "start", "before_start"); + yield; + openPopup("bottomcenter topleft", "topleft", "top", "center left", "start", "after_start"); + yield; + openPopup("topcenter bottomright", "bottomright", "bottom", "center right", "end", "before_end"); + yield; + openPopup("bottomcenter topright", "topright", "top", "center right", "end", "after_end"); + yield; + openPopup("topcenter bottomleft", "middle", "bottom", "center left", "start", "before_start"); + yield; + openPopup("bottomcenter topleft", "middle", "top", "center left", "start", "after_start"); + yield; + + openPopup("leftcenter topright", "middle", "right", "center top", "start", "start_before"); + yield; + openPopup("rightcenter bottomleft", "middle", "left", "center bottom", "end", "end_after"); + yield; + +/* + XXXndeakin disable these parts of the test which often cause problems, see bug 626563 + + openPopup("after_start", frames[0].document.getElementById("input"), "top", "left", "start"); + yield; + + setScale(frames[0], 1.5); + openPopup("after_start", frames[0].document.getElementById("input"), "top", "left", "start"); + yield; + + setScale(frames[0], 2.5); + openPopup("before_start", frames[0].document.getElementById("input"), "bottom", "left", "start"); + yield; + + setScale(frames[0], 1); +*/ + + $("bigpanel").openPopup($("topleft"), "after_start", 0, 0, false, false, null, "start"); + yield; + + // switch to rtl mode + document.documentElement.style.direction = "rtl"; + + $("topleft").style.marginRight = "15px"; + $("topleft").style.justifySelf = "right"; + + $("topright").style.marginLeft = "15px"; + $("topright").style.justifySelf = "left"; + + $("bottomleft").style.marginRight = "15px"; + $("bottomleft").style.justifySelf = "right"; + + $("bottomright").style.marginLeft = "15px"; + $("bottomright").style.justifySelf = "left"; + + $("topleft").style.removeProperty("marginLeft"); + $("topright").style.removeProperty("marginRight"); + $("bottomleft").style.removeProperty("marginLeft"); + $("bottomright").style.removeProperty("marginRight"); + } + + // Test that a transition occurs when opening or closing the popup. + if (SpecialPowers.getBoolPref("xul.panel-animations.enabled")) { + function transitionEnded(event) { + if ($("animatepanel").state != "open") { + is($("animatepanel").state, "showing", "state is showing during transitionend"); + ok(!animatedPopupShown, "popupshown not fired yet") + } else { + is($("animatepanel").state, "open", "state is open after transitionend"); + ok(animatedPopupShown, "popupshown now fired") + SimpleTest.executeSoon(() => runNextTest.next()); + } + } + + // Check that the transition occurs for an arrow panel with animate="true" + $("animatepanel").addEventListener("transitionend", transitionEnded); + $("animatepanel").openPopup($("topleft"), "after_start", 0, 0, false, false, null, "start"); + is($("animatepanel").state, "showing", "state is showing"); + yield; + $("animatepanel").removeEventListener("transitionend", transitionEnded); + + synthesizeKey("KEY_Escape"); + ok(!animatedPopupHidden, "animated popup not hidden yet"); + yield; + } + + SimpleTest.finish() +} + +function setScale(win, scale) +{ + SpecialPowers.setFullZoom(win, scale); + zoomFactor = scale; +} + +function checkPanelPosition(panel) +{ + let anchor = panel.anchorNode; + let adj = 0, hwinpos = 0, vwinpos = 0; + if (anchor.ownerDocument != document) { + var framerect = anchor.ownerGlobal.frameElement.getBoundingClientRect(); + hwinpos = framerect.left; + vwinpos = framerect.top; + } + + // Positions are reversed in rtl yet the coordinates used in the computations + // are not, so flip the expected label side and anchor edge. + var isRTL = (window.getComputedStyle(panel).direction == "rtl"); + if (isRTL) { + var flipLeftRight = val => val == "left" ? "right" : "left"; + expectedAnchorEdge = expectedAnchorEdge.replace(/(left|right)/, flipLeftRight); + expectedSide = expectedSide.replace(/(left|right)/, flipLeftRight); + } + + var panelRect = panel.getBoundingClientRect(); + var anchorRect = anchor.getBoundingClientRect(); + var contentRect = panel.firstChild.getBoundingClientRect(); + switch (expectedSide) { + case "top": + ok(contentRect.top > vwinpos + anchorRect.bottom * zoomFactor + 5, "panel content is below"); + break; + case "bottom": + ok(contentRect.bottom < vwinpos + anchorRect.top * zoomFactor - 5, "panel content is above"); + break; + case "left": + ok(contentRect.left > hwinpos + anchorRect.right * zoomFactor + 5, "panel content is right"); + break; + case "right": + ok(contentRect.right < hwinpos + anchorRect.left * zoomFactor - 5, "panel content is left"); + break; + } + + let iscentered = false; + if (expectedAnchorEdge.indexOf("center ") == 0) { + expectedAnchorEdge = expectedAnchorEdge.substring(7); + iscentered = true; + } + + switch (expectedAnchorEdge) { + case "top": + adj = vwinpos + parseInt(getComputedStyle(panel, "").marginTop); + if (iscentered) + adj += Math.round(anchorRect.height) / 2; + isWithinHalfPixel(panelRect.top, anchorRect.top * zoomFactor + adj, "anchored on top"); + break; + case "bottom": + adj = vwinpos + parseInt(getComputedStyle(panel, "").marginBottom); + if (iscentered) + adj += Math.round(anchorRect.height) / 2; + isWithinHalfPixel(panelRect.bottom, anchorRect.bottom * zoomFactor - adj, "anchored on bottom"); + break; + case "left": + adj = hwinpos + parseInt(getComputedStyle(panel, "").marginLeft); + if (iscentered) + adj += Math.round(anchorRect.width) / 2; + isWithinHalfPixel(panelRect.left, anchorRect.left * zoomFactor + adj, "anchored on left "); + break; + case "right": + adj = hwinpos + parseInt(getComputedStyle(panel, "").marginRight); + if (iscentered) + adj += Math.round(anchorRect.width) / 2; + if (!isOSXYosemite) + isWithinHalfPixel(panelRect.right, anchorRect.right * zoomFactor - adj, "anchored on right"); + break; + } + + is(anchor, expectedAnchor, "anchor"); + + var arrow = panel.shadowRoot.querySelector(".panel-arrow"); + is(panel.getAttribute("side"), expectedSide, "panel arrow side"); + is(arrow.hidden, false, "panel hidden"); + is(arrow.parentNode.getAttribute("pack"), expectedPack, "panel arrow pack"); + + panel.hidePopup(); +} + +function isWithinHalfPixel(a, b, desc) +{ + ok(Math.abs(a - b) <= 0.5, desc); +} + +function checkBigPanel(panel) +{ + ok(panel.firstChild.getBoundingClientRect().height < 2800, "big panel height"); + panel.hidePopup(); +} + +SimpleTest.waitForFocus(startTest); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"/> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete2.xhtml b/toolkit/content/tests/chrome/test_autocomplete2.xhtml new file mode 100644 index 0000000000..4a051357eb --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete2.xhtml @@ -0,0 +1,191 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 2" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// Set to indicate whether or not we want autoCompleteSimple to return a result +var returnResult = false; + +const ACR = Ci.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (returnResult) { + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "SUCCESS"; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAtn() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +var element = document.getElementById("autocomplete"); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + checkResult(); + }; +}(element.onSearchComplete); + +// Test Bug 441530 - correctly setting "nomatch" +// Test Bug 441526 - correctly setting style with "highlightnonmatches" + +SimpleTest.waitForExplicitFinish(); +setTimeout(startTest, 0); + +function startTest() { + var autocomplete = $("autocomplete"); + + // Ensure highlightNonMatches can be set correctly. + + // This should not be set by default. + is(autocomplete.hasAttribute("highlightnonmatches"), false, + "highlight nonmatches not set by default"); + + autocomplete.highlightNonMatches = "true"; + + is(autocomplete.getAttribute("highlightnonmatches"), "true", + "highlight non matches attribute set correctly"); + is(autocomplete.highlightNonMatches, true, + "highlight non matches getter returned correctly"); + + autocomplete.highlightNonMatches = "false"; + + is(autocomplete.getAttribute("highlightnonmatches"), "false", + "highlight non matches attribute set to false correctly"); + is(autocomplete.highlightNonMatches, false, + "highlight non matches getter returned false correctly"); + + check(); +} + +function check() { + var autocomplete = $("autocomplete"); + + // Toggle this value, so we can re-use the one function. + returnResult = !returnResult; + + // blur the field to ensure that the popup is closed and that the previous + // search has stopped, then start a new search. + autocomplete.blur(); + autocomplete.focus(); + sendString("r"); +} + +function checkResult() { + var autocomplete = $("autocomplete"); + var style = window.getComputedStyle(autocomplete); + + if (returnResult) { + // Result was returned, so there should not be a nomatch attribute + is(autocomplete.hasAttribute("nomatch"), false, + "nomatch attribute shouldn't be present here"); + + // Ensure that the style is set correctly whichever way highlightNonMatches + // is set. + autocomplete.highlightNonMatches = "true"; + + isnot(style.color, "rgb(255, 0, 0)", + "not nomatch and highlightNonMatches - should not be red"); + + autocomplete.highlightNonMatches = "false"; + + isnot(style.color, "rgb(255, 0, 0)", + "not nomatch and not highlightNonMatches - should not be red"); + + setTimeout(check, 0); + } + else { + // No result was returned, so there should be nomatch attribute + is(autocomplete.getAttribute("nomatch"), "true", + "nomatch attribute not correctly set when expected"); + + // Ensure that the style is set correctly whichever way highlightNonMatches + // is set. + autocomplete.highlightNonMatches = "true"; + + is(style.color, "rgb(255, 0, 0)", + "nomatch and highlightNonMatches - should be red"); + + autocomplete.highlightNonMatches = "false"; + + isnot(style.color, "rgb(255, 0, 0)", + "nomatch and not highlightNonMatches - should not be red"); + + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete3.xhtml b/toolkit/content/tests/chrome/test_autocomplete3.xhtml new file mode 100644 index 0000000000..04a5117b6a --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete3.xhtml @@ -0,0 +1,202 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 3" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// Set to indicate whether or not we want autoCompleteSimple to return a result +var returnResult = true; + +const ACR = Ci.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (returnResult) { + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "Result"; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +let element = document.getElementById("autocomplete"); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + checkResult(); + }; +}(element.onSearchComplete); + +// Test Bug 325842 - completeDefaultIndex + +SimpleTest.waitForExplicitFinish(); +setTimeout(startTest, 0); + +var currentTest = 0; + +// Note the entries for these tests (key) are incremental. +const tests = [ + { completeDefaultIndex: "false", key: "r", result: "r", + start: 1, end: 1 }, + { completeDefaultIndex: "true", key: "e", result: "result", + start: 2, end: 6 }, + { completeDefaultIndex: "true", key: "t", result: "ret >> Result", + start: 3, end: 13 } +]; + +function startTest() { + var autocomplete = $("autocomplete"); + + // These should not be set by default. + is(autocomplete.hasAttribute("completedefaultindex"), false, + "completedefaultindex not set by default"); + + autocomplete.completeDefaultIndex = "true"; + + is(autocomplete.getAttribute("completedefaultindex"), "true", + "completedefaultindex attribute set correctly"); + is(autocomplete.completeDefaultIndex, true, + "autoFill getter returned correctly"); + + autocomplete.completeDefaultIndex = "false"; + + is(autocomplete.getAttribute("completedefaultindex"), "false", + "completedefaultindex attribute set to false correctly"); + is(autocomplete.completeDefaultIndex, false, + "completeDefaultIndex getter returned false correctly"); + + checkNext(); +} + +function checkNext() { + var autocomplete = $("autocomplete"); + + autocomplete.completeDefaultIndex = tests[currentTest].completeDefaultIndex; + autocomplete.focus(); + + synthesizeKey(tests[currentTest].key); +} + +function checkResult() { + var autocomplete = $("autocomplete"); + + is(autocomplete.value, tests[currentTest].result, + "Test " + currentTest + ": autocomplete.value should equal '" + + tests[currentTest].result + "'"); + + is(autocomplete.selectionStart, tests[currentTest].start, + "Test " + currentTest + ": autocomplete selection should start at " + + tests[currentTest].start); + + is(autocomplete.selectionEnd, tests[currentTest].end, + "Test " + currentTest + ": autocomplete selection should end at " + + tests[currentTest].end); + + ++currentTest; + + if (currentTest < tests.length) { + setTimeout(checkNext, 0); + } else { + // TODO (bug 494809): Autocomplete-in-the-middle should take in count RTL + // and complete on KEY_ArrowRight or KEY_ArrowLeft based on that. It should also revert + // what user has typed to far if he moves in the opposite direction. + if (!autocomplete.value.includes(">>")) { + // Test result if user accepts autocomplete suggestion. + synthesizeKey("KEY_ArrowRight"); + is( + autocomplete.value, + "Result", + "Test complete: autocomplete.value should equal 'Result'" + ); + is( + autocomplete.selectionStart, + 6, + "Test complete: autocomplete selection should start at 6" + ); + is( + autocomplete.selectionEnd, + 6, + "Test complete: autocomplete selection should end at 6" + ); + } + + setTimeout(function () { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory( + autoCompleteSimpleID, + autoCompleteSimple + ); + SimpleTest.finish(); + }, 0); + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete4.xhtml b/toolkit/content/tests/chrome/test_autocomplete4.xhtml new file mode 100644 index 0000000000..9abf66ba19 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete4.xhtml @@ -0,0 +1,282 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 4" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + completedefaultindex="true" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// Set to indicate whether or not we want autoCompleteSimple to return a result +var returnResult = true; + +const IS_MAC = navigator.platform.includes("Mac"); + +const ACR = Ci.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (returnResult) { + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "Result"; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +let element = document.getElementById("autocomplete"); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + searchComplete(); + }; +}(element.onSearchComplete); + +// Test Bug 325842 - completeDefaultIndex + +SimpleTest.waitForExplicitFinish(); + +setTimeout(nextTest, 0); + +var currentTest = null; + +// Note the entries for these tests (key) are incremental. +const tests = [ + { + desc: "HOME key remove selection", + key: "KEY_Home", + removeSelection: true, + result: "re", + start: 0, end: 0 + }, + { + desc: "LEFT key remove selection", + key: "KEY_ArrowLeft", + removeSelection: true, + result: "re", + start: 1, end: 1 + }, + { desc: "RIGHT key remove selection", + key: "KEY_ArrowRight", + removeSelection: true, + result: "re", + start: 2, end: 2 + }, + { desc: "ENTER key remove selection", + key: "KEY_Enter", + removeSelection: true, + result: "re", + start: 2, end: 2 + }, + { + desc: "HOME key", + key: "KEY_Home", + removeSelection: false, + result: "Result", + start: 0, end: 0 + }, + { + desc: "LEFT key", + key: "KEY_ArrowLeft", + removeSelection: false, + result: "Result", + start: 5, end: 5 + }, + { desc: "RIGHT key", + key: "KEY_ArrowRight", + removeSelection: false, + result: "Result", + start: 6, end: 6 + }, + { desc: "RETURN key", + key: "KEY_Enter", + removeSelection: false, + result: "Result", + start: 6, end: 6 + }, + { desc: "TAB key should confirm suggestion when forcecomplete is set", + key: "KEY_Tab", + removeSelection: false, + forceComplete: true, + result: "Result", + start: 6, end: 6 + }, + + { desc: "RIGHT key complete from middle", + key: "KEY_ArrowRight", + forceComplete: true, + completeFromMiddle: true, + result: "Result", + start: 6, end: 6 + }, + { + desc: "RIGHT key w/ minResultsForPopup=2", + key: "KEY_ArrowRight", + removeSelection: false, + minResultsForPopup: 2, + result: "Result", + start: 6, end: 6 + }, +]; + +function nextTest() { + if (!tests.length) { + // No more tests to run, finish. + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); + return; + } + + var autocomplete = $("autocomplete"); + autocomplete.value = ""; + currentTest = tests.shift(); + + // HOME key works differently on Mac, so we skip tests using it. + if (currentTest.key == "KEY_Home" && IS_MAC) + nextTest(); + else + setTimeout(runCurrentTest, 0); +} + +function runCurrentTest() { + var autocomplete = $("autocomplete"); + if ("minResultsForPopup" in currentTest) + autocomplete.setAttribute("minresultsforpopup", currentTest.minResultsForPopup) + else + autocomplete.removeAttribute("minresultsforpopup"); + + autocomplete.focus(); + + if (!currentTest.completeFromMiddle) { + sendString("re"); + } + else { + sendString("lt"); + } +} + +function searchComplete() { + var autocomplete = $("autocomplete"); + autocomplete.setAttribute("forcecomplete", currentTest.forceComplete); + + if (currentTest.completeFromMiddle) { + if (!currentTest.forceComplete) { + synthesizeKey(currentTest.key); + } + else if (!/ >> /.test(autocomplete.value)) { + // At this point we should have a value like "lt >> Result" showing. + throw new Error("Expected an middle-completed value, got " + autocomplete.value); + } + + // For forceComplete a blur should cause a value from the results to get + // completed to. E.g. "lt >> Result" will turn into "Result". + if (currentTest.forceComplete) + autocomplete.blur(); + + checkResult(); + return; + } + + is(autocomplete.value, "result", + "Test '" + currentTest.desc + "': autocomplete.value should equal 'result'"); + + if (autocomplete.selectionStart == 2) { // Finished inserting "re" string. + if (currentTest.removeSelection) { + // remove current selection + synthesizeKey("KEY_Delete"); + } + + synthesizeKey(currentTest.key); + + checkResult(); + } +} + +function checkResult() { + var autocomplete = $("autocomplete"); + + is(autocomplete.value, currentTest.result, + "Test '" + currentTest.desc + "': autocomplete.value should equal '" + + currentTest.result + "'"); + + is(autocomplete.selectionStart, currentTest.start, + "Test '" + currentTest.desc + "': autocomplete selection should start at " + + currentTest.start); + + is(autocomplete.selectionEnd, currentTest.end, + "Test '" + currentTest.desc + "': autocomplete selection should end at " + + currentTest.end); + + setTimeout(nextTest, 0); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete5.xhtml b/toolkit/content/tests/chrome/test_autocomplete5.xhtml new file mode 100644 index 0000000000..72cb081f66 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete5.xhtml @@ -0,0 +1,166 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 5" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + autocompletesearch="simple" + notifylegacyevents="true"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const ACR = Ci.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "SUCCESS"; +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + + +let element = document.getElementById("autocomplete"); + +// Create stub to intercept `onSearchBegin` event. +element.onSearchBegin = function(original) { + return function() { + original.apply(this, arguments); + checkSearchBegin(); + }; +}(element.onSearchBegin); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + checkSearchCompleted(); + }; +}(element.onSearchComplete); + +element.addEventListener("textEntered", checkTextEntered); +element.addEventListener("textReverted", checkTextReverted); + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +SimpleTest.waitForExplicitFinish(); +setTimeout(startTest, 0); + +function startTest() { + let autocomplete = $("autocomplete"); + + // blur the field to ensure that the popup is closed and that the previous + // search has stopped, then start a new search. + autocomplete.blur(); + autocomplete.focus(); + sendString("r"); +} + +let hasTextEntered = false; +let hasSearchBegun = false; + +function checkSearchBegin() { + hasSearchBegun = true; +} + +let test = 0; +function checkSearchCompleted() { + is(hasSearchBegun, true, "onsearchbegin handler has been correctly called."); + + if (test == 0) { + hasSearchBegun = false; + synthesizeKey("KEY_Enter"); + } else if (test == 1) { + hasSearchBegun = false; + synthesizeKey("KEY_Escape"); + } else { + throw new Error("checkSearchCompleted should only be called twice."); + } +} + +function checkTextEntered() { + is(test, 0, "checkTextEntered should be reached from first test."); + is(hasSearchBegun, false, "onsearchbegin handler should not be called on text revert."); + + // fire second test + test++; + + let autocomplete = $("autocomplete"); + autocomplete.textValue = ""; + autocomplete.blur(); + autocomplete.focus(); + sendString("r"); +} + +function checkTextReverted() { + is(test, 1, "checkTextReverted should be the second test reached."); + is(hasSearchBegun, false, "onsearchbegin handler should not be called on text revert."); + + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_emphasis.xhtml b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xhtml new file mode 100644 index 0000000000..5155dee687 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xhtml @@ -0,0 +1,182 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete emphasis test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="richautocomplete" + is="autocomplete-input" + autocompletesearch="simple" + autocompletepopup="richpopup"/> +<panel is="autocomplete-richlistbox-popup" + id="richpopup" + type="autocomplete-richlistbox" + noautofocus="true"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const ACR = Ci.nsIAutoCompleteResult; + +// A global variable to hold the search result for the current search. +var resultText = ""; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; +} + +nsAutoCompleteSimpleResult.prototype = { + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: 0, + getValueAt() { return resultText; }, + getCommentAt() { return this.getValueAt(); }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return this.getValueAt(); }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that returns the string contained in 'resultText'. +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(outer, iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +var element = document.getElementById("richautocomplete"); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + checkSearchCompleted(); + }; +}(element.onSearchComplete); + +SimpleTest.waitForExplicitFinish(); +setTimeout(nextTest, 0); + +/* Test cases have the following attributes: + * - search: A search string, to be emphasized in the result. + * - result: A fixed result string, so we can hardcode the expected emphasis. + * - emphasis: A list of chunks that should be emphasized or not, in strict alternation. + * - emphasizeFirst: Whether the first element of 'emphasis' should be emphasized; + * The emphasis of the other elements is defined by the strict alternation rule. + */ +let testcases = [ + { search: "test", + result: "A test string", + emphasis: ["A ", "test", " string"], + emphasizeFirst: false + }, + { search: "tea two", + result: "Tea for two, and two for tea...", + emphasis: ["Tea", " for ", "two", ", and ", "two", " for ", "tea", "..."], + emphasizeFirst: true + }, + { search: "tat", + result: "tatatat", + emphasis: ["tatatat"], + emphasizeFirst: true + }, + { search: "cheval valise", + result: "chevalise", + emphasis: ["chevalise"], + emphasizeFirst: true + } +]; +let test = -1; +let currentTest = null; + +function nextTest() { + test++; + + if (test >= testcases.length) { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + return; + } + + // blur the field to ensure that the popup is closed and that the previous + // search has stopped, then start a new search. + let autocomplete = $("richautocomplete"); + autocomplete.blur(); + autocomplete.focus(); + + currentTest = testcases[test]; + resultText = currentTest.result; + autocomplete.value = currentTest.search; + synthesizeKey("KEY_ArrowDown"); +} + +function checkSearchCompleted() { + let autocomplete = $("richautocomplete"); + let result = autocomplete.popup.richlistbox.firstChild; + + for (let attribute of [result._titleText, result._urlText]) { + is(attribute.childNodes.length, currentTest.emphasis.length, + "The element should have the expected number of children."); + for (let i = 0; i < currentTest.emphasis.length; i++) { + let node = attribute.childNodes[i]; + // Emphasized parts strictly alternate. + if ((i % 2 == 0) == currentTest.emphasizeFirst) { + // Check that this part is correctly emphasized. + is(node.nodeName, "span", ". That child should be a span node"); + ok(node.classList.contains("ac-emphasize-text"), ". That child should be emphasized"); + is(node.textContent, currentTest.emphasis[i], ". That emphasis should be as expected."); + } else { + // Check that this part is _not_ emphasized. + is(node.nodeName, "#text", ". That child should be a text node"); + is(node.textContent, currentTest.emphasis[i], ". That text should be as expected."); + } + } + } + + setTimeout(nextTest, 0); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xhtml b/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xhtml new file mode 100644 index 0000000000..b49f8a1d5e --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test" + onload="setTimeout(keyCaretTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" is="autocomplete-input"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function keyCaretTest() +{ + var autocomplete = $("autocomplete"); + + autocomplete.focus(); + checkKeyCaretTest("KEY_ArrowUp", 0, 0, true, "no value up"); + checkKeyCaretTest("KEY_ArrowDown", 0, 0, true, "no value down"); + + autocomplete.value = "Sample"; + + autocomplete.selectionStart = 3; + autocomplete.selectionEnd = 3; + checkKeyCaretTest("KEY_ArrowUp", 0, 0, true, "value up with caret in middle"); + checkKeyCaretTest("KEY_ArrowUp", 0, 0, true, "value up with caret in middle again"); + + autocomplete.selectionStart = 2; + autocomplete.selectionEnd = 2; + checkKeyCaretTest("KEY_ArrowDown", 6, 6, true, "value down with caret in middle"); + checkKeyCaretTest("KEY_ArrowDown", 6, 6, true, "value down with caret in middle again"); + + autocomplete.selectionStart = 1; + autocomplete.selectionEnd = 4; + checkKeyCaretTest("KEY_ArrowUp", 0, 0, true, "value up with selection"); + + autocomplete.selectionStart = 1; + autocomplete.selectionEnd = 4; + checkKeyCaretTest("KEY_ArrowDown", 6, 6, true, "value down with selection"); + + SimpleTest.finish(); +} + +function checkKeyCaretTest(key, expectedStart, expectedEnd, result, testid) +{ + var autocomplete = $("autocomplete"); + var keypressFired = false; + function listener(event) { + if (event.target == autocomplete) { + keypressFired = true; + } + } + SpecialPowers.addSystemEventListener(window, "keypress", listener, false); + synthesizeKey(key, {}); + SpecialPowers.removeSystemEventListener(window, "keypress", listener, false); + is(keypressFired, result, `${testid} keypress event should${result ? "" : " not"} be fired`); + is(autocomplete.selectionStart, expectedStart, testid + " selectionStart"); + is(autocomplete.selectionEnd, expectedEnd, testid + " selectionEnd"); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xhtml b/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xhtml new file mode 100644 index 0000000000..15ed2dfda9 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xhtml @@ -0,0 +1,306 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="runTest();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + completedefaultindex="true" + timeout="0" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +function autoCompleteSimpleResult(aString, searchId) { + this.searchString = aString; + this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS; + this.matchCount = 1; + if (aString.startsWith('ret')) { + this._param = autoCompleteSimpleResult.retireCompletion; + } else { + this._param = "Result"; + } + this._searchId = searchId; +} +autoCompleteSimpleResult.retireCompletion = "Retire"; +autoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: Ci.nsIAutoCompleteResult.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +var searchCounter = 0; + +// A basic autocomplete implementation that returns one result. +let autoCompleteSimple = { + classID: Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"), + contractID: "@mozilla.org/autocomplete/search;1?name=simple", + searchAsync: false, + pendingSearch: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIFactory", + "nsIAutoCompleteSearch" + ]), + createInstance(outer, iid) { + return this.QueryInterface(iid); + }, + + registerFactory() { + let registrar = + Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(this.classID, "Test Simple Autocomplete", + this.contractID, this); + }, + unregisterFactory() { + let registrar = + Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(this.classID, this); + }, + + startSearch(aString, aParam, aResult, aListener) { + let result = new autoCompleteSimpleResult(aString); + + if (this.searchAsync) { + // Simulate an async search by using a timeout before invoking the + // |onSearchResult| callback. + // Store the searchTimeout such that it can be canceled if stopSearch is called. + this.pendingSearch = setTimeout(() => { + this.pendingSearch = null; + + aListener.onSearchResult(this, result); + + // Move to the next step in the async test. + asyncTest.next(); + }, 0); + } else { + aListener.onSearchResult(this, result); + } + }, + stopSearch() { + clearTimeout(this.pendingSearch); + } +}; + +SimpleTest.waitForExplicitFinish(); + +let gACTimer; +let gAutoComplete; +let asyncTest; + +let searchCompleteTimeoutId = null; + +function finishTest() { + // Unregister the factory so that we don't get in the way of other tests + autoCompleteSimple.unregisterFactory(); + SimpleTest.finish(); +} + +function runTest() { + autoCompleteSimple.registerFactory(); + gAutoComplete = $("autocomplete"); + gAutoComplete.focus(); + + // Return the search results synchronous, which also makes the completion + // happen synchronous. + autoCompleteSimple.searchAsync = false; + + sendString("r"); + is(gAutoComplete.value, "result", "Value should be autocompleted immediately"); + + sendString("e"); + is(gAutoComplete.value, "result", "Value should be autocompleted immediately"); + + synthesizeKey("KEY_Delete"); + is(gAutoComplete.value, "re", "Deletion should not complete value"); + + synthesizeKey("KEY_Backspace"); + is(gAutoComplete.value, "r", "Backspace should not complete value"); + + synthesizeKey("KEY_ArrowLeft"); + is(gAutoComplete.value, "r", "Value should stay same when navigating with cursor"); + + runAsyncTest(); +} + +function* asyncTestGenerator() { + sendString("re"); + is(gAutoComplete.value, "re", "Value should not be autocompleted immediately"); + + // Calling |yield undefined| makes this generator function wait until + // |asyncTest.next();| is called. This happens from within the + // |autoCompleteSimple.startSearch()| function once the simulated async + // search has finished. + // Therefore, the effect of the |yield undefined;| here (and the ones) below + // is to wait until the async search result comes back. + yield undefined; + + is(gAutoComplete.value, "result", "Value should be autocompleted"); + + // Test if typing the `s` character completes directly based on the last + // completion + sendString("s"); + is(gAutoComplete.value, "result", "Value should be completed immediately"); + + yield undefined; + + is(gAutoComplete.value, "result", "Value should be autocompleted to same value"); + synthesizeKey("KEY_Delete"); + is(gAutoComplete.value, "res", "Deletion should not complete value"); + + // No |yield undefined| needed here as no completion is triggered by the deletion. + + is(gAutoComplete.value, "res", "Still no complete value after deletion"); + + synthesizeKey("KEY_Backspace"); + is(gAutoComplete.value, "re", "Backspace should not complete value"); + + yield undefined; + + is(gAutoComplete.value, "re", "Value after search due to backspace should stay the same"); (3) + + // Typing a character that is not like the previous match. In this case, the + // completion cannot happen directly and therefore the value will be completed + // only after the search has finished. + sendString("t"); + is(gAutoComplete.value, "ret", "Value should not be autocompleted immediately"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + sendString("i"); + is(gAutoComplete.value, "retire", "Value should be autocompleted immediately"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted to the same value"); + + // Setup the scene to test how the completion behaves once the placeholder + // completion and the result from the search do not agree with each other. + gAutoComplete.value = 'r'; + // Need to type two characters as the input was reset and the autocomplete + // controller things, ther user hit the backspace button, in which case + // no completion is performed. But as a completion is desired, another + // character `t` is typed afterwards. + sendString("e"); + yield undefined; + sendString("t"); + is(gAutoComplete.value, "ret", "Value should not be autocompleted"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + // The placeholder string is now set to "retire". Changing the completion + // string to "retirement" and see what the completion will turn out like. + autoCompleteSimpleResult.retireCompletion = "Retirement"; + sendString("i"); + is(gAutoComplete.value, "retire", "Value should be autocompleted based on placeholder"); + + yield undefined; + + is(gAutoComplete.value, "retirement", "Value should be autocompleted based on search result"); + + // Change the search result to `Retire` again and see if the new result is + // complited. + autoCompleteSimpleResult.retireCompletion = "Retire"; + sendString("r"); + is(gAutoComplete.value, "retirement", "Value should be autocompleted based on placeholder"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted based on search result"); + + // Complete the value + gAutoComplete.value = 're'; + sendString("t"); + yield undefined; + sendString("i"); + is(gAutoComplete.value, "reti", "Value should not be autocompleted"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + // Remove the selected text "re" (1) and the "et" (2). Afterwards, add it again (3). + // This should not cause the completion to kick in. + synthesizeKey("KEY_Delete"); // (1) + + is(gAutoComplete.value, "reti", "Value should not complete after deletion"); + + gAutoComplete.selectionStart = 1; + gAutoComplete.selectionEnd = 3; + synthesizeKey("KEY_Delete"); // (2) + + is(gAutoComplete.value, "ri", "Value should stay unchanged after removing character in the middle"); + + yield undefined; + + sendString("e"); // (3.1) + is(gAutoComplete.value, "rei", "Inserting a character in the middle should not complete the value"); + + yield undefined; + + sendString("t"); // (3.2) + is(gAutoComplete.value, "reti", "Inserting a character in the middle should not complete the value"); + + yield undefined; + + // Adding a new character at the end should not cause the completion to happen again + // as the completion failed before. + gAutoComplete.selectionStart = 4; + gAutoComplete.selectionEnd = 4; + sendString("r"); + is(gAutoComplete.value, "retir", "Value should not be autocompleted immediately"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + finishTest(); + yield undefined; +} + +function runAsyncTest() { + gAutoComplete.value = ''; + autoCompleteSimple.searchAsync = true; + + asyncTest = asyncTestGenerator(); + asyncTest.next(); +} +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html new file mode 100644 index 0000000000..d15cd649e4 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>autocomplete with composition tests on HTML input element</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="file_autocomplete_with_composition.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <iframe id="formTarget" name="formTarget"></iframe> + <form action="data:text/html," target="formTarget"> + <input name="test" id="input"><input type="submit"> + </form> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +function runTests() { + var formFillController = + SpecialPowers.getFormFillController() + .QueryInterface(Ci.nsIAutoCompleteInput); + var originalFormFillTimeout = formFillController.timeout; + + SpecialPowers.attachFormFillControllerTo(window); + var target = document.getElementById("input"); + + // Register a word to the form history. + let chromeScript = SpecialPowers.loadChromeScript(function addEntry() { + let {FormHistory} = ChromeUtils.import("resource://gre/modules/FormHistory.jsm"); + FormHistory.update({ op: "add", fieldname: "test", value: "Mozilla" }); + }); + chromeScript.destroy(); + target.focus(); + + new nsDoTestsForAutoCompleteWithComposition( + "Testing on HTML input (asynchronously search)", + window, target, formFillController.controller, is, + function() { return target.value; }, + function() { + target.setAttribute("timeout", 0); + new nsDoTestsForAutoCompleteWithComposition( + "Testing on HTML input (synchronously search)", + window, target, formFillController.controller, is, + function() { return target.value; }, + function() { + formFillController.timeout = originalFormFillTimeout; + SpecialPowers.detachFormFillControllerFrom(window); + SimpleTest.finish(); + }); + }); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_browser_drop.xhtml b/toolkit/content/tests/chrome/test_browser_drop.xhtml new file mode 100644 index 0000000000..3b0f0fdb2b --- /dev/null +++ b/toolkit/content/tests/chrome/test_browser_drop.xhtml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Browser Drop Test" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script><![CDATA[ +SimpleTest.waitForExplicitFinish(); +function runTest() { + add_task(async function() { + let win = window.browsingContext.topChromeWindow.openDialog("window_browser_drop.xhtml", "_blank", "chrome,width=200,height=200", window); + await SimpleTest.promiseFocus(win); + for (let browserType of ["content", "remote-content"]) { + await win.dropLinksOnBrowser(win.document.getElementById(browserType + "child"), browserType); + } + await win.dropLinksOnBrowser(win.document.getElementById("chromechild"), "chrome"); + }); +} +//]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug1048178.xhtml b/toolkit/content/tests/chrome/test_bug1048178.xhtml new file mode 100644 index 0000000000..302835e3b9 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug1048178.xhtml @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1048178 +--> +<window title="Mozilla Bug 1048178" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"/> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1048178" + target="_blank">Mozilla Bug 1048178</a> + + <hbox> + <scrollbar id="scroller" + orient="horizontal" + curpos="0" + maxpos="500" + pageincrement="500" + width="500" + style="margin:0"/> + </hbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 1048178 **/ +var scrollbarTester = { + scrollbar: null, + startTest() { + this.scrollbar = $("scroller"); + this.setScrollToClick(false); + this.testThumbDragging(); + SimpleTest.finish(); + }, + testThumbDragging() { + var x = 400; // on the right half of the scroolbar + var y = 5; + + this.mousedown(x, y, 0); + this.mousedown(x, y, 2); + this.mouseup(x, y, 2); + this.mouseup(x, y, 0); + + var newPos = this.getPos(); // sould be '500' + + this.mousedown(x, y, 0); + this.mousemove(x-1, y, 0); + this.mouseup(x-1, y, 0); + + var newPos2 = this.getPos(); + ok(newPos2 < newPos, + "Scrollbar thumb should follow the mouse when dragged."); + }, + setScrollToClick(value) { + var prefService = SpecialPowers.Services.prefs.nsIPrefService; + var uiBranch = prefService.getBranch("ui."); + uiBranch.setIntPref("scrollToClick", value ? 1 : 0); + }, + getPos() { + return this.scrollbar.getAttribute("curpos"); + }, + mousedown(x, y, button) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousedown", 'button': button }); + }, + mousemove(x, y, button) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousemove", 'button': button }); + }, + mouseup(x, y, button) { + synthesizeMouse(this.scrollbar, x, y, { type: "mouseup", 'button': button }); + } +} + +function doTest() { + setTimeout(function() { scrollbarTester.startTest(); }, 0); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug263683.xhtml b/toolkit/content/tests/chrome/test_bug263683.xhtml new file mode 100644 index 0000000000..64c7df08ac --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug263683.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=263683 +--> +<window title="Mozilla Bug 263683" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=263683"> + Mozilla Bug 263683 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 263683 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("bug263683_window.xhtml", "263683test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug304188.xhtml b/toolkit/content/tests/chrome/test_bug304188.xhtml new file mode 100644 index 0000000000..1047528d20 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug304188.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=304188 +--> +<window title="Mozilla Bug 304188" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=304188">Mozilla Bug 304188</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 304188 **/ +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug304188_window.xhtml", "findbartest", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug331215.xhtml b/toolkit/content/tests/chrome/test_bug331215.xhtml new file mode 100644 index 0000000000..22560089c1 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug331215.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=331215 +--> +<window title="Mozilla Bug 331215" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=331215">Mozilla Bug 331215</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 331215 **/ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug331215_window.xhtml", "331215test", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug360220.xhtml b/toolkit/content/tests/chrome/test_bug360220.xhtml new file mode 100644 index 0000000000..a942e0acd6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug360220.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=360220 +--> +<window title="Mozilla Bug 360220" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=360220">Mozilla Bug 360220</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<menulist id="menulist"> + <menupopup> + <menuitem id="firstItem" label="foo" selected="true"/> + <menuitem id="secondItem" label="bar"/> + </menupopup> +</menulist> +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +/** Test for Bug 360220 **/ + +var menulist = document.getElementById("menulist"); +var secondItem = document.getElementById("secondItem"); +menulist.selectedItem = secondItem; + +is(menulist.label, "bar", "second item was not selected"); + +let mutObserver = new MutationObserver(() => { + is(menulist.label, "new label", "menulist label was not updated to the label of its selected item"); + done(); +}); +mutObserver.observe(menulist, { attributeFilter: ['label'] }); +secondItem.label = "new label"; + +let failureTimeout = setTimeout(function() { + ok(false, "menulist label should have updated"); + done(); +}, 2000); + +function done() { + mutObserver.disconnect(); + clearTimeout(failureTimeout); + SimpleTest.finish(); +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug360437.xhtml b/toolkit/content/tests/chrome/test_bug360437.xhtml new file mode 100644 index 0000000000..dc592e4141 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug360437.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=360437 +--> +<window title="Mozilla Bug 360437" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=360437">Mozilla Bug 360437</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 360437 **/ +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug360437_window.xhtml", "360437test", + "chrome,width=600,height=600,noopener", window); + + + + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug365773.xhtml b/toolkit/content/tests/chrome/test_bug365773.xhtml new file mode 100644 index 0000000000..e85a590608 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug365773.xhtml @@ -0,0 +1,67 @@ +<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=365773
+-->
+<window title="Mozilla Bug 365773"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=365773">Mozilla Bug 365773</a>
+<p id="display">
+ <radiogroup id="group" collapsed="true" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <radio id="item" label="Item"/>
+ </radiogroup>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 365773 **/
+
+function selectItem(item, isIndex, testName) {
+ var exception = null;
+ try {
+ if (isIndex)
+ document.getElementById("group").selectedIndex = item;
+ else
+ document.getElementById("group").selectedItem = item;
+ }
+ catch(e) {
+ exception = e;
+ }
+
+ ok(exception == null, testName);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+window.onload = function runTests() {
+ var item = document.getElementById("item");
+
+ selectItem(item, false, "Radio button selected with selectedItem (not focused)");
+ selectItem(null, false, "Radio button deselected with selectedItem (not focused)");
+ selectItem(0, true, "Radio button selected with selectedIndex (not focused)");
+ selectItem(-1, true, "Radio button deselected with selectedIndex (not focused)");
+
+ document.getElementById("group").focus();
+
+ selectItem(item, false, "Radio button selected with selectedItem (focused)");
+ selectItem(null, false, "Radio button deselected with selectedItem (focused)");
+ selectItem(0, true, "Radio button selected with selectedIndex (focused)");
+ selectItem(-1, true, "Radio button deselected with selectedIndex (focused)");
+
+ SimpleTest.finish();
+};
+]]>
+</script>
+
+</window>
diff --git a/toolkit/content/tests/chrome/test_bug366992.xhtml b/toolkit/content/tests/chrome/test_bug366992.xhtml new file mode 100644 index 0000000000..9b178f1abe --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug366992.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=366992 +--> +<window title="Mozilla Bug 366992" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=366992">Mozilla Bug 366992</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 366992 **/ +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug366992_window.xhtml", "findbartest", + "chrome,width=600,height=600,noopener", window); + + + + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug382990.xhtml b/toolkit/content/tests/chrome/test_bug382990.xhtml new file mode 100644 index 0000000000..1f937ac1b6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug382990.xhtml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=382990 +--> +<window title="Mozilla Bug 382990" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="startThisTest()"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=382990" + target="_blank">Mozilla Bug 382990</a> + </body> + + <tree id="testTree" height="200px"> + <treecols> + <treecol flex="1" label="Name" id="name"/> + </treecols> + <treechildren> + <treeitem><treerow><treecell label="a"/></treerow></treeitem> + <treeitem><treerow><treecell label="z"/></treerow></treeitem> + </treechildren> + </tree> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + /** Test for Bug 382990 **/ + + SimpleTest.waitForExplicitFinish(); + function startThisTest() + { + var treeElem = document.getElementById("testTree"); + treeElem.view.selection.select(0); + treeElem.focus(); + synthesizeKey("z", {ctrlKey: true}); + ok(!treeElem.view.selection.isSelected(1), "Tree selection should not change for key events with ctrl pressed."); + SimpleTest.finish(); + } + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug409624.xhtml b/toolkit/content/tests/chrome/test_bug409624.xhtml new file mode 100644 index 0000000000..6aa5f2ded9 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug409624.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=409624 +--> +<window title="Mozilla Bug 409624" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=409624"> + Mozilla Bug 409624 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 409624 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("bug409624_window.xhtml", "409624test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug418874.xhtml b/toolkit/content/tests/chrome/test_bug418874.xhtml new file mode 100644 index 0000000000..42d75b10c4 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug418874.xhtml @@ -0,0 +1,70 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Textbox with placeholder test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <hbox> + <html:input id="t1" placeholder="empty"/> + </hbox> + + <hbox> + <html:input id="t2" placeholder="empty"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + var t2 = $("t2"); + setTextboxValue(t1, "1"); + var t1Enabled = {}; + var t1CanUndo = {}; + t1.editor.canUndo(t1Enabled, t1CanUndo); + is(t1CanUndo.value, true, + "undo correctly enabled when placeholder was not changed through property"); + + t2.placeholder = "reallyempty"; + setTextboxValue(t2, "2"); + var t2Enabled = {}; + var t2CanUndo = {}; + t2.editor.canUndo(t2Enabled, t2CanUndo); + is(t2CanUndo.value, true, + "undo correctly enabled when placeholder explicitly changed through property"); + + SimpleTest.finish(); + } + + function setTextboxValue(textbox, value) { + textbox.focus(); + sendString(value); + textbox.blur(); + } + + SimpleTest.waitForFocus(doTest); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug429723.xhtml b/toolkit/content/tests/chrome/test_bug429723.xhtml new file mode 100644 index 0000000000..865f0d66f8 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug429723.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=429723 +--> +<window title="Mozilla Bug 429723" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=429723">Mozilla Bug 429723</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 429723 **/ +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug429723_window.xhtml", "429723test", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug437844.xhtml b/toolkit/content/tests/chrome/test_bug437844.xhtml new file mode 100644 index 0000000000..9053783754 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug437844.xhtml @@ -0,0 +1,89 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=437844 +https://bugzilla.mozilla.org/show_bug.cgi?id=348233 +--> +<window title="Mozilla Bug 437844 and Bug 348233" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="chrome://mochikit/content/chrome-harness.js"></script> + <script type="application/javascript" + src="RegisterUnregisterChrome.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=437844"> + Mozilla Bug 437844 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=348233"> + Mozilla Bug 348233 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.expectAssertions(0, 58); + + /** Test for Bug 437844 and Bug 348233 **/ + SimpleTest.waitForExplicitFinish(); + + let prefs = SpecialPowers.Services.prefs.nsIPrefBranch; + prefs.setCharPref("intl.l10n.pseudo", "bidi"); + + let rootDir = getRootDirectory(window.location.href); + let manifest = rootDir + "rtlchrome/rtl.manifest"; + + //copy rtlchrome to profile/rtlchrome and generate .manifest + let filePath = chromeURIToFile(manifest); + let tempProfileDir = copyDirToTempProfile(filePath.path, 'rtlchrome'); + if (tempProfileDir.path.lastIndexOf('\\') >= 0) { + manifest = "content rtlchrome /" + tempProfileDir.path.replace(/\\/g, '/') + "\n"; + } else { + manifest = "content rtlchrome " + tempProfileDir.path + "\n"; + } + manifest += "override chrome://global/locale/intl.css chrome://rtlchrome/content/rtlchrome/rtl.css\n"; + manifest += "override chrome://global/locale/global.dtd chrome://rtlchrome/content/rtlchrome/rtl.dtd\n"; + + let cleanupFunc = createManifestTemporarily(tempProfileDir, manifest); + + // Load about:plugins in an iframe + let frame = document.createXULElement("iframe"); + frame.setAttribute("src", "about:plugins"); + frame.addEventListener("load", function () { + is(frame.contentDocument.dir, "rtl", "about:plugins should be RTL in RTL locales"); + + // eslint-disable-next-line no-unused-vars + let tmpd = SpecialPowers.Services.dirsvc.get("ProfD", Ci.nsIFile); + + frame = document.createXULElement("iframe"); + frame.setAttribute("src", "file://" + tmpd.path); // a file:// URI, bug 348233 + frame.addEventListener("load", function () { + is(frame.contentDocument.body.dir, "rtl", "file:// listings should be RTL in RTL locales"); + + cleanupFunc(); + prefs.clearUserPref("intl.l10n.pseudo"); + SimpleTest.finish(); + }); + document.documentElement.appendChild(frame); + }, { once: true }); + document.documentElement.appendChild(frame); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug451540.xhtml b/toolkit/content/tests/chrome/test_bug451540.xhtml new file mode 100644 index 0000000000..debcadfe05 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug451540.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=451540 +--> +<window title="Mozilla Bug 451540" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=451540"> + Mozilla Bug 451540 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 451540 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("bug451540_window.xhtml", "451540test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug457632.xhtml b/toolkit/content/tests/chrome/test_bug457632.xhtml new file mode 100644 index 0000000000..bd8797b32d --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug457632.xhtml @@ -0,0 +1,180 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 457632 + --> +<window title="Bug 457632" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <vbox id="nb"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" + onload="test()"/> + + <!-- test code goes here --> +<script type="application/javascript"> +<![CDATA[ +var gNotificationBox; + +function completeAnimation(nextTest) { + if (!gNotificationBox._animating) { + nextTest(); + return; + } + + setTimeout(completeAnimation, 50, nextTest); +} + +function test() { + SimpleTest.waitForExplicitFinish(); + gNotificationBox = new MozElements.NotificationBox(e => { + document.getElementById("nb").appendChild(e); + }); + + is(gNotificationBox.allNotifications.length, 0, "There should be no initial notifications"); + gNotificationBox.appendNotification("Test notification", + "notification1", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + is(gNotificationBox.allNotifications.length, 1, "Notification exists while animating in"); + let notification = gNotificationBox.getNotificationWithValue("notification1"); + ok(notification, "Notification should exist while animating in"); + + // Wait for the notificaton to finish displaying + completeAnimation(test1); +} + +// Tests that a notification that is fully animated in gets removed immediately +function test1() { + let notification = gNotificationBox.getNotificationWithValue("notification1"); + gNotificationBox.removeNotification(notification); + notification = gNotificationBox.getNotificationWithValue("notification1"); + ok(!notification, "Test 1 showed notification was still present"); + ok(!gNotificationBox.currentNotification, "Test 1 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 1 should show no notifications present"); + + // Wait for the notificaton to finish hiding + completeAnimation(test2); +} + +// Tests that a notification that is animating in gets removed immediately +function test2() { + let notification = gNotificationBox.appendNotification("Test notification", + "notification2", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + gNotificationBox.removeNotification(notification); + notification = gNotificationBox.getNotificationWithValue("notification2"); + ok(!notification, "Test 2 showed notification was still present"); + ok(!gNotificationBox.currentNotification, "Test 2 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 2 should show no notifications present"); + + // Get rid of the hiding notifications + gNotificationBox.removeAllNotifications(true); + test3(); +} + +// Tests that a background notification goes away immediately +function test3() { + let notification = gNotificationBox.appendNotification("Test notification", + "notification3", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + let notification2 = gNotificationBox.appendNotification("Test notification", + "notification4", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + is(gNotificationBox.allNotifications.length, 2, "Test 3 should show 2 notifications present"); + gNotificationBox.removeNotification(notification); + is(gNotificationBox.allNotifications.length, 1, "Test 3 should show 1 notifications present"); + notification = gNotificationBox.getNotificationWithValue("notification3"); + ok(!notification, "Test 3 showed notification was still present"); + gNotificationBox.removeNotification(notification2); + is(gNotificationBox.allNotifications.length, 0, "Test 3 should show 0 notifications present"); + notification2 = gNotificationBox.getNotificationWithValue("notification4"); + ok(!notification2, "Test 3 showed notification2 was still present"); + ok(!gNotificationBox.currentNotification, "Test 3 said there was still a current notification"); + + // Get rid of the hiding notifications + gNotificationBox.removeAllNotifications(true); + test4(); +} + +// Tests that a foreground notification hiding a background one goes away +function test4() { + let notification = gNotificationBox.appendNotification("Test notification", + "notification5", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + let notification2 = gNotificationBox.appendNotification("Test notification", + "notification6", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + gNotificationBox.removeNotification(notification2); + notification2 = gNotificationBox.getNotificationWithValue("notification6"); + ok(!notification2, "Test 4 showed notification2 was still present"); + is(gNotificationBox.currentNotification, notification, "Test 4 said the current notification was wrong"); + is(gNotificationBox.allNotifications.length, 1, "Test 4 should show 1 notifications present"); + gNotificationBox.removeNotification(notification); + notification = gNotificationBox.getNotificationWithValue("notification5"); + ok(!notification, "Test 4 showed notification was still present"); + ok(!gNotificationBox.currentNotification, "Test 4 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 4 should show 0 notifications present"); + + // Get rid of the hiding notifications + gNotificationBox.removeAllNotifications(true); + test5(); +} + +// Tests that removeAllNotifications gets rid of everything +function test5() { + let notification = gNotificationBox.appendNotification("Test notification", + "notification7", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + let notification2 = gNotificationBox.appendNotification("Test notification", + "notification8", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + gNotificationBox.removeAllNotifications(); + notification = gNotificationBox.getNotificationWithValue("notification7"); + notification2 = gNotificationBox.getNotificationWithValue("notification8"); + ok(!notification, "Test 5 showed notification was still present"); + ok(!notification2, "Test 5 showed notification2 was still present"); + ok(!gNotificationBox.currentNotification, "Test 5 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 5 should show 0 notifications present"); + + gNotificationBox.appendNotification("Test notification", + "notification9", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + + // Wait for the notificaton to finish displaying + completeAnimation(test6); +} + +// Tests whether removing an already removed notification doesn't break things +function test6() { + let notification = gNotificationBox.getNotificationWithValue("notification9"); + ok(notification, "Test 6 should have an initial notification"); + gNotificationBox.removeNotification(notification); + gNotificationBox.removeNotification(notification); + + ok(!gNotificationBox.currentNotification, "Test 6 shouldn't be any current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 6 allNotifications.length should be 0"); + notification = gNotificationBox.appendNotification("Test notification", + "notification10", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + is(notification, gNotificationBox.currentNotification, "Test 6 should have made the current notification"); + gNotificationBox.removeNotification(notification); + + SimpleTest.finish(); +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug460942.xhtml b/toolkit/content/tests/chrome/test_bug460942.xhtml new file mode 100644 index 0000000000..53f33302be --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug460942.xhtml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=460942 +--> +<window title="Mozilla Bug 460942" + onload="runTests()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=460942" + target="_blank">Mozilla Bug 460942</a> + </body> + + <!-- test code goes here --> + + <richlistbox> + <richlistitem id="item1"> + <label value="one"/> + <box> + <label value="two"/> + </box> + </richlistitem> + <richlistitem id="item2"><description>one</description><description>two</description></richlistitem> + </richlistbox> + + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug 460942 **/ + function runTests() { + is ($("item1").label, "one two"); + is ($("item2").label, ""); + SimpleTest.finish(); + } + SimpleTest.waitForExplicitFinish(); + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug471776.xhtml b/toolkit/content/tests/chrome/test_bug471776.xhtml new file mode 100644 index 0000000000..6e21ffd9ba --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug471776.xhtml @@ -0,0 +1,48 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Textbox with placeholder undo test" width="500" height="600" + onload="doTest();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <hbox> + <html:input id="t1" placeholder="empty"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + t1.focus(); + var t1Enabled = {}; + var t1CanUndo = {}; + t1.editor.canUndo(t1Enabled, t1CanUndo); + ok(!t1CanUndo.value, "undo correctly disabled when no user edits"); + SimpleTest.finish(); + } + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug509732.xhtml b/toolkit/content/tests/chrome/test_bug509732.xhtml new file mode 100644 index 0000000000..3307d62450 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug509732.xhtml @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 509732 + --> +<window title="Bug 509732" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <vbox id="nb" hidden="true"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" + onload="test()"/> + + <!-- test code goes here --> +<script type="application/javascript"> +<![CDATA[ +var gNotificationBox; + +// Tests that a notification that is added in an hidden box didn't throw the animation +function test() { + SimpleTest.waitForExplicitFinish(); + gNotificationBox = new MozElements.NotificationBox(e => { + document.getElementById("nb").appendChild(e); + }); + + is(gNotificationBox.allNotifications.length, 0, "There should be no initial notifications"); + + gNotificationBox.appendNotification("Test notification", + "notification1", null, + gNotificationBox.PRIORITY_INFO_LOW, + null); + + is(gNotificationBox.allNotifications.length, 1, "Notification exists"); + is(gNotificationBox._animating, false, "Notification shouldn't be animating"); + + test1(); +} + +// Tests that a notification that is removed from an hidden box didn't throw the animation +function test1() { + let notification = gNotificationBox.getNotificationWithValue("notification1"); + gNotificationBox.removeNotification(notification); + ok(!gNotificationBox.currentNotification, "Test 1 should show no current animation"); + is(gNotificationBox._animating, false, "Notification shouldn't be animating"); + is(gNotificationBox.allNotifications.length, 0, "Test 1 should show no notifications present"); + + SimpleTest.finish(); +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug557987.xhtml b/toolkit/content/tests/chrome/test_bug557987.xhtml new file mode 100644 index 0000000000..f9ea841cfd --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug557987.xhtml @@ -0,0 +1,71 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 557987 + --> +<window title="Bug 557987" width="400" height="400" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <toolbarbutton id="button" type="menu" label="Test bug 557987" + onclick="eventReceived('click');" + oncommand="eventReceived('command');"> + <menupopup onpopupshowing="eventReceived('popupshowing'); return false;" /> + </toolbarbutton> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(test); + +// Tests that mouse events are correctly dispatched to <toolbarbutton type="menu"/> +// This used to test menu buttons, and was updated when this button type was removed. +function test() { + + disableNonTestMouseEvents(true); + + let button = $("button"); + let rightEdge = button.getBoundingClientRect().width - 2; + let centerX = button.getBoundingClientRect().width / 2; + let centerY = button.getBoundingClientRect().height / 2; + + synthesizeMouse(button, rightEdge, centerY, {}, window); + synthesizeMouse(button, centerX, centerY, {}, window); + + synthesizeMouse(document.getElementsByTagName("body")[0], 0, 0, {}, window); + + disableNonTestMouseEvents(false); + SimpleTest.executeSoon(finishTest); + +} + +function finishTest() { + is(eventCount.command, 0, "Correct number of command events received"); + is(eventCount.popupshowing, 1, "Correct number of popupshowing events received"); + is(eventCount.click, 2, "Correct number of click events received"); + is(eventCount.focus, 0, "Correct number of focus events received"); + + SimpleTest.finish(); +} + +let eventCount = { + command: 0, + popupshowing: 0, + click: 0, + focus: 0 +}; + +function eventReceived(eventName) { + eventCount[eventName]++; +} + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug562554.xhtml b/toolkit/content/tests/chrome/test_bug562554.xhtml new file mode 100644 index 0000000000..88f876f0df --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug562554.xhtml @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 562554 + --> +<window title="Bug 562554" width="400" height="400" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <toolbarbutton type="menu" id="toolbarmenu" height="200"> + <menupopup id="menupopup" onpopupshowing="eventReceived('popupshowing'); return false;"/> + <stack> + <button style="width: 100px; height: 30px; margin-left: 0; margin-top: 0;" allowevents="true" + onclick="eventReceived('clickbutton1'); return false;"/> + <button style="width: 100px; height: 30px; margin-left: 70px; margin-top: 0;" + onclick="eventReceived('clickbutton2'); return false;"/> + </stack> + </toolbarbutton> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(test); + +// Tests that mouse events are correctly dispatched to <toolbarbutton type="menu"/> +function test() { + disableNonTestMouseEvents(true); + nextTest(); +} + +let tests = [ + // Click on the toolbarbutton itself - should call popupshowing + () => synthesizeMouse($("toolbarmenu"), 10, 50, {}, window), + () => is(eventCount.popupshowing, 1, "Got first popupshowing event"), + + // Click on button1 which has allowevents="true" - should call clickbutton1 + () => synthesizeMouse($("toolbarmenu"), 10, 15, {}, window), + () => is(eventCount.clickbutton1, 1, "Button 1 clicked"), + + // Click on button2 where it intersects with button1 - should call popupshowing + () => synthesizeMouse($("toolbarmenu"), 85, 15, {}, window), + () => is(eventCount.popupshowing, 2, "Got second popupshowing event"), + + // Click on button2 outside of intersection - should call popupshowing + () => synthesizeMouse($("toolbarmenu"), 150, 15, {}, window) +]; + +function nextTest() { + if (tests.length) { + let func = tests.shift(); + func(); + SimpleTest.executeSoon(nextTest); + } else { + disableNonTestMouseEvents(false); + SimpleTest.executeSoon(finishTest); + } +} + +function finishTest() { + is(eventCount.clickbutton1, 1, "Correct number of clicks on button 1"); + is(eventCount.clickbutton2, 0, "Correct number of clicks on button 2"); + is(eventCount.popupshowing, 3, "Correct number of popupshowing events received"); + + SimpleTest.finish(); +} + +let eventCount = { + popupshowing: 0, + clickbutton1: 0, + clickbutton2: 0 +}; + +function eventReceived(eventName) { + eventCount[eventName]++; +} + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug624329.xhtml b/toolkit/content/tests/chrome/test_bug624329.xhtml new file mode 100644 index 0000000000..7000cbc66a --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug624329.xhtml @@ -0,0 +1,154 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=624329 +--> +<window title="Mozilla Bug 624329 context menu position" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" + onload="openTestWindow()"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=624329" + target="_blank">Mozilla Bug 624329</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug 624329 **/ + +SimpleTest.waitForExplicitFinish(); + +var win; +var timeoutID; +var menu; + +function openTestWindow() { + win = window.browsingContext.topChromeWindow.openDialog("bug624329_window.xhtml", "_blank", "width=300,resizable=yes,chrome", window); + // Close our window if the test times out so that it doesn't interfere + // with later tests. + timeoutID = setTimeout(function () { + ok(false, "Test timed out."); + // Provide some time for a screenshot + setTimeout(finish, 1000); + }, 20000); +} + +function listenOnce(event, callback) { + win.addEventListener(event, function listener() { + callback(); + }, { once: true}); +} + +function childFocused() { + // maximizing the window is a simple way to ensure that the menu is near + // the right edge of the screen. + + listenOnce("resize", childResized); + win.maximize(); +} + +function childResized() { + const isOSXMavericks = navigator.userAgent.includes("Mac OS X 10.9"); + const isOSXYosemite = navigator.userAgent.includes("Mac OS X 10.10"); + if (isOSXMavericks || isOSXYosemite) { + todo_is(win.windowState, win.STATE_MAXIMIZED, + "A resize before being maximized breaks this test on 10.9 and 10.10"); + finish(); + return; + } + + is(win.windowState, win.STATE_MAXIMIZED, + "window should be maximized"); + + isnot(win.innerWidth, 300, + "window inner width should have changed"); + + openContextMenu(); +} + +function openContextMenu() { + var mouseX = win.innerWidth - 10; + var mouseY = 10; + + menu = win.document.getElementById("menu"); + var screenX = menu.screenX; + var screenY = menu.screenY; + var utils = win.windowUtils; + + utils.sendMouseEvent("contextmenu", mouseX, mouseY, 2, 0, 0); + + var interval = setInterval(checkMoved, 200); + function checkMoved() { + if (menu.screenX != screenX || + menu.screenY != screenY) { + clearInterval(interval); + // Wait further to check that the window does not move again. + setTimeout(checkPosition, 1000); + } + } + + function checkPosition() { + var rootElement = win.document.documentElement; + var platformIsMac = navigator.userAgent.indexOf("Mac") > -1; + + var x = menu.screenX - rootElement.screenX; + var y = menu.screenY - rootElement.screenY; + + if (platformIsMac) + { + // This check is alterered slightly for OSX which adds padding to the top + // and bottom of its context menus. The menu position calculation must + // be changed to allow for the pointer to be outside this padding + // when the menu opens. + // (Bug 1075089) + ok(y + 6 >= mouseY, + "menu top " + (y + 6) + " should be below click point " + mouseY); + } + else + { + ok(y >= mouseY, + "menu top " + y + " should be below click point " + mouseY); + } + + ok(y <= mouseY + 20, + "menu top " + y + " should not be too far below click point " + mouseY); + + ok(x < mouseX, + "menu left " + x + " should be left of click point " + mouseX); + var right = x + menu.getBoundingClientRect().width; + + if (platformIsMac) { + // Rather than be constrained by the right hand screen edge, OSX menus flip + // horizontally and appear to the left of the mouse pointer + ok(right < mouseX, + "menu right " + right + " should be left of click point " + mouseX); + } + else { + ok(right > mouseX, + "menu right " + right + " should be right of click point " + mouseX); + } + + clearTimeout(timeoutID); + finish(); + } + +} + +function finish() { + if (menu && navigator.platform.includes("Win")) { + todo(false, "Should not have to hide popup before closing its window"); + // This avoids mochitest "Unable to restore focus" errors (bug 670053). + menu.hidePopup(); + } + win.close(); + SimpleTest.finish(); +} + + ]]> + </script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug792324.xhtml b/toolkit/content/tests/chrome/test_bug792324.xhtml new file mode 100644 index 0000000000..ddedf5907b --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug792324.xhtml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=792324 +--> +<window title="Mozilla Bug 792324" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +<body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=792324">Mozilla Bug 792324</a> + + <p id="display"></p> +<div id="content" style="display: none"> +</div> +</body> + +<panel id="panel-1"> + <button label="just a normal button"/> + <button id="button-1" + accesskey="X" + oncommand="clicked(event)" + label="Button in panel 1" + /> +</panel> + +<panel id="panel-2"> + <button label="just a normal button"/> + <button id="button-2" + accesskey="X" + oncommand="clicked(event)" + label="Button in panel 2" + /> +</panel> + +<script class="testbody" type="application/javascript"><![CDATA[ + +/** Test for Bug 792324 **/ +let after_click; + +function clicked(event) { + after_click(event); +} + +function checkAccessKeyOnPanel(panelid, buttonid, cb) { + let panel = document.getElementById(panelid); + panel.addEventListener("popupshown", function onpopupshown() { + panel.firstChild.focus(); + after_click = function(event) { + is(event.target.id, buttonid, "Accesskey was directed to the button '" + buttonid + "'"); + panel.hidePopup(); + cb(); + } + sendString("X"); + }); + panel.openPopup(null, "", 100, 100, false, false); +} + +function test() { + checkAccessKeyOnPanel("panel-1", "button-1", function() { + checkAccessKeyOnPanel("panel-2", "button-2", function() { + SimpleTest.finish(); + }); + }); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(test, window); + +]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_button.xhtml b/toolkit/content/tests/chrome/test_button.xhtml new file mode 100644 index 0000000000..58a0a50325 --- /dev/null +++ b/toolkit/content/tests/chrome/test_button.xhtml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for button + --> +<window title="Button Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<button id="one" label="One" /> +<button id="two" label="Two"/> +<hbox> + <button id="three" label="Three" open="true"/> +</hbox> +<hbox> + <button id="four" type="menu" label="Four"/> + <button id="five" label="Five"/> +</hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_button() +{ + synthesizeMouseExpectEvent($("one"), 2, 2, {}, $("one"), "command", "button press"); + $("one").focus(); + synthesizeKeyExpectEvent("VK_SPACE", { }, $("one"), "command", "key press"); + $("two").disabled = true; + synthesizeMouseExpectEvent($("two"), 2, 2, {}, $("two"), "!command", "button press command when disabled"); + synthesizeMouseExpectEvent($("two"), 2, 2, {}, $("two"), "click", "button press click when disabled"); + + if (!navigator.platform.includes("Mac")) { + $("one").focus(); + synthesizeKey("KEY_ArrowDown"); + is(document.activeElement, $("three"), "key cursor down on button"); + + synthesizeKey("KEY_ArrowRight"); + is(document.activeElement, $("four"), "key cursor right on button"); + synthesizeKey("KEY_ArrowDown"); + is(document.activeElement, $("four"), "key cursor down on menu button"); + + $("three").focus(); + synthesizeKey("KEY_ArrowUp"); + is(document.activeElement, $("one"), "key cursor up on button"); + } + + $("two").focus(); + ok(document.activeElement != $("two"), "focus disabled button"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(test_button); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_chromemargin.xhtml b/toolkit/content/tests/chrome/test_chromemargin.xhtml new file mode 100644 index 0000000000..d1a6a568be --- /dev/null +++ b/toolkit/content/tests/chrome/test_chromemargin.xhtml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Custom chrome margin tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> + +// Tests parsing of the chrome margin attrib on a window. + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_chromemargin.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_closemenu_attribute.xhtml b/toolkit/content/tests/chrome/test_closemenu_attribute.xhtml new file mode 100644 index 0000000000..7b29bd6c5d --- /dev/null +++ b/toolkit/content/tests/chrome/test_closemenu_attribute.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu closemenu Attribute Tests" + onload="setTimeout(nextTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<button id="menu" type="menu" label="Menu" onpopuphidden="popupHidden(event)"> + <menupopup id="p1" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="l1" label="One"> + <menupopup id="p2" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="l2" label="Two"> + <menupopup id="p3" onpopupshown="executeMenuItem()"> + <menuitem id="l3" label="Three"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> +</button> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gExpectedId = "p3"; +var gMode = -1; +var gModes = ["", "auto", "single", "none"]; + +function nextTest() +{ + gMode++; + if (gModes[gMode] != "none") + gExpectedId = "p3"; + + if (gMode != 0) + $("l3").setAttribute("closemenu", gModes[gMode]); + if (gModes[gMode] == "none") + $("l2").open = true; + else + $("menu").open = true; +} + +function executeMenuItem() +{ + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + // after a couple of seconds, end the test, as the 'none' closemenu value + // should not hide any popups + if (gModes[gMode] == "none") + setTimeout(function() { $("menu").open = false; }, 2000); +} + +function popupHidden(event) +{ + if (gModes[gMode] == "none") { + if (event.target.id == "p1") + SimpleTest.finish() + return; + } + + is(event.target.id, gExpectedId, + "Expected event " + gModes[gMode] + " " + gExpectedId); + + gExpectedId = ""; + if (event.target.id == "p3") { + if (gModes[gMode] == "" || gModes[gMode] == "auto") + gExpectedId = "p2"; + } + else if (event.target.id == "p2") { + if (gModes[gMode] == "" || gModes[gMode] == "auto") + gExpectedId = "p1"; + } + + if (!gExpectedId) + nextTest(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_contextmenu_list.xhtml b/toolkit/content/tests/chrome/test_contextmenu_list.xhtml new file mode 100644 index 0000000000..8fb5f112d0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_contextmenu_list.xhtml @@ -0,0 +1,289 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Context Menu on List Tests" + onload="setTimeout(startTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<spacer height="5"/> + +<hbox style="padding-left: 10px;"> + <spacer width="5"/> + <richlistbox id="list" context="themenu" style="padding: 0;" oncontextmenu="checkContextMenu(event)"> + <richlistitem id="item1" style="padding-top: 3px; margin: 0;"><button label="One"/></richlistitem> + <richlistitem id="item2" height="22"><checkbox label="Checkbox"/></richlistitem> + <richlistitem id="item3"><button label="Three"/></richlistitem> + <richlistitem id="item4"><checkbox label="Four"/></richlistitem> + </richlistbox> + + <tree id="tree" rows="5" flex="1" context="themenu" style="-moz-appearance: none; border: 0"> + <treecols> + <treecol label="Name" flex="1"/> + <splitter class="tree-splitter"/> + <treecol label="Moons"/> + </treecols> + <treechildren id="treechildren"> + <treeitem> + <treerow> + <treecell label="Mercury"/> + <treecell label="0"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Venus"/> + <treecell label="0"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Earth"/> + <treecell label="1"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mars"/> + <treecell label="2"/> + </treerow> + </treeitem> + </treechildren> + </tree> + + <menu id="menu" label="Menu"> + <menupopup id="menupopup" onpopupshown="menuTests()" onpopuphidden="nextTest()" + oncontextmenu="checkContextMenuForMenu(event)"> + <menuitem id="menu1" label="Menu 1"/> + <menuitem id="menu2" label="Menu 2"/> + <menuitem id="menu3" label="Menu 3"/> + </menupopup> + </menu> + +</hbox> + +<menupopup id="themenu" onpopupshowing="if (gTestId == -1) event.preventDefault()" + onpopupshown="checkPopup()" onpopuphidden="setTimeout(nextTest, 0);"> + <menuitem label="Item"/> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gTestId = -1; +var gTestElement = "list"; +var gSelectionStep = 0; +var gContextMenuFired = false; + +function startTest() +{ + // first, check if the richlistbox selection changes on a contextmenu mouse event + var element = $("list"); + synthesizeMouse(element.getItemAtIndex(3), 7, 1, { type : "mousedown", button: 2, ctrlKey: true }); + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + + gSelectionStep++; + synthesizeMouse(element.getItemAtIndex(1), 7, 1, { type : "mousedown", button: 2, ctrlKey: true, shiftKey: true }); + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + + gSelectionStep++; + synthesizeMouse(element.getItemAtIndex(1), 7, 1, { type : "mousedown", button: 2 }); + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + + $("menu").open = true; +} + +function menuTests() +{ + gSelectionStep = 0; + var element = $("menu"); + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + is(gContextMenuFired, true, "context menu fired when menu open"); + + gSelectionStep = 1; + $("menu").activeChild = $("menu2"); + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + + $("menu").open = false; +} + +function nextTest() +{ + gTestId++; + if (gTestId > 2) { + if (gTestElement == "list") { + gTestElement = "tree"; + gTestId = 0; + } + else { + SimpleTest.finish(); + return; + } + } + var element = $(gTestElement); + element.focus(); + if (gTestId == 0) { + if (gTestElement == "list") + element.selectedIndex = 2; + element.currentIndex = 2; + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + } + else if (gTestId == 1) { + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + } + else { + element.currentIndex = -1; + element.selectedIndex = -1; + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + } +} + +// This is nasty so I'd better explain what's going on. +// The basic problem is that the synthetic mouse coordinate generated +// by DOMWindowUtils.sendMouseEvent and also the synthetic mouse coordinate +// generated internally when contextmenu events are redirected to the focused +// element are rounded to the nearest device pixel. But this rounding is done +// while the coordinates are relative to the nearest widget. When this test +// is run in the mochitest harness, the nearest widget is the main mochitest +// window, and our document can have a fractional position within that +// mochitest window. So when we round coordinates for comparison in this +// test, we need to do so very carefully, especially if the target element +// also has a fractional position within our document. +// +// For example, if the y-offset of our containing IFRAME is 100.4px, +// and the offset of our expected point is 10.3px in our document, the actual +// mouse event is dispatched to round(110.7) == 111px. This comes back +// with a clientY of round(111 - 100.4) == round(10.6) == 11. This is not +// equal to round(10.3) as you might expect. + +function isRoundedX(a, b, msg) +{ + is(Math.round(a + window.mozInnerScreenX), Math.round(b + window.mozInnerScreenX), msg); +} + +function isRoundedY(a, b, msg) +{ + is(Math.round(a + window.mozInnerScreenY), Math.round(b + window.mozInnerScreenY), msg); +} + +function checkContextMenu(event) +{ + var rect = $(gTestElement).getBoundingClientRect(); + + var frombase = (gTestId == -1 || gTestId == 1); + if (!frombase) + rect = event.originalTarget.getBoundingClientRect(); + var left = frombase ? rect.left + 7 : rect.left; + var top = frombase ? rect.top + 4 : rect.bottom; + + isRoundedX(event.clientX, left, gTestElement + " clientX " + gSelectionStep + " " + gTestId + "," + frombase); + isRoundedY(event.clientY, top, gTestElement + " clientY " + gSelectionStep + " " + gTestId); + ok(event.screenX > left, gTestElement + " screenX " + gSelectionStep + " " + gTestId); + ok(event.screenY > top, gTestElement + " screenY " + gSelectionStep + " " + gTestId); + + // context menu from mouse click + switch (gTestId) { + case -1: + // eslint-disable-next-line no-nested-ternary + var expected = gSelectionStep == 2 ? 1 : (platformIsMac() ? 3 : 0); + is($(gTestElement).selectedIndex, expected, "index after click " + gSelectionStep); + break; + case 0: + if (gTestElement == "list") + is(event.originalTarget, $("item3"), "list selection target"); + else + is(event.originalTarget, $("treechildren"), "tree selection target"); + break; + case 1: + is(event.originalTarget.id, $("item1").id, "list mouse selection target"); + break; + case 2: + is(event.originalTarget, $("list"), "list no selection target"); + break; + } +} + +function checkContextMenuForMenu(event) +{ + gContextMenuFired = true; + + var popuprect = (gSelectionStep ? $("menu2") : $("menupopup")).getBoundingClientRect(); + is(event.clientX, Math.round(popuprect.left), "menu left " + gSelectionStep); + // the clientY is off by one sometimes on Windows (when loaded in the testing iframe + // but not when loaded separately) so just check for both cases for now + ok(event.clientY == Math.round(popuprect.bottom) || + event.clientY - 1 == Math.round(popuprect.bottom), "menu top " + gSelectionStep); +} + +function checkPopup() +{ + var menurect = $("themenu").getBoundingClientRect(); + + // Context menus are offset by a number of pixels from the mouse click + // which activates them. This is so that they don't appear exactly + // under the mouse which can cause them to be mistakenly dismissed. + // The number of pixels depends on the platform and is defined in + // each platform's nsLookAndFeel + var contextMenuOffsetX = platformIsMac() ? 1 : 2; + var contextMenuOffsetY = platformIsMac() ? -6 : 2; + + if (gTestId == 0) { + if (gTestElement == "list") { + var itemrect = $("item3").getBoundingClientRect(); + isRoundedX(menurect.left, itemrect.left + contextMenuOffsetX, + "list selection keyboard left"); + isRoundedY(menurect.top, itemrect.bottom + contextMenuOffsetY, + "list selection keyboard top"); + } + else { + var tree = $("tree"); + var bodyrect = $("treechildren").getBoundingClientRect(); + isRoundedX(menurect.left, bodyrect.left + contextMenuOffsetX, + "tree selection keyboard left"); + isRoundedY(menurect.top, bodyrect.top + + tree.rowHeight * 3 + contextMenuOffsetY, + "tree selection keyboard top"); + } + } + else if (gTestId == 1) { + // activating a context menu with the mouse from position (7, 4). + let elementrect = $(gTestElement).getBoundingClientRect(); + isRoundedX(menurect.left, elementrect.left + 7 + contextMenuOffsetX, + gTestElement + " mouse left"); + isRoundedY(menurect.top, elementrect.top + 4 + contextMenuOffsetY, + gTestElement + " mouse top"); + } + else { + let elementrect = $(gTestElement).getBoundingClientRect(); + isRoundedX(menurect.left, elementrect.left + contextMenuOffsetX, + gTestElement + " no selection keyboard left"); + isRoundedY(menurect.top, elementrect.bottom + contextMenuOffsetY, + gTestElement + " no selection keyboard top"); + } + + $("themenu").hidePopup(); +} + +function platformIsMac() +{ + return navigator.platform.indexOf("Mac") > -1; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_cursorsnap.xhtml b/toolkit/content/tests/chrome/test_cursorsnap.xhtml new file mode 100644 index 0000000000..f83938fbb7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_cursorsnap.xhtml @@ -0,0 +1,123 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window title="Cursor snapping test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +const kMaxRetryCount = 4; +const kTimeoutTime = [ + 100, 100, 1000, 1000, 5000 +]; + +var gRetryCount; + +var gTestingCount = 0; +var gTestingIndex = -1; +var gDisable = false; +var gHidden = false; + +function canRetryTest() +{ + return gRetryCount <= kMaxRetryCount; +} + +function getTimeoutTime() +{ + return kTimeoutTime[gRetryCount]; +} + +function runNextTest() +{ + gRetryCount = 0; + gTestingIndex++; + runCurrentTest(); +} + +function retryCurrentTest() +{ + ok(canRetryTest(), "retry the current test..."); + gRetryCount++; + runCurrentTest(); +} + +function runCurrentTest() +{ + var position = "top=" + gTestingCount + ",left=" + gTestingCount + ","; + gTestingCount++; + switch (gTestingIndex) { + case 0: + gDisable = false; + gHidden = false; + window.openDialog("window_cursorsnap_dialog.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 1: + gDisable = true; + gHidden = false; + window.openDialog("window_cursorsnap_dialog.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 2: + gDisable = false; + gHidden = true; + window.openDialog("window_cursorsnap_dialog.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 3: + gDisable = false; + gHidden = false; + window.openDialog("window_cursorsnap_wizard.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 4: + gDisable = true; + gHidden = false; + window.openDialog("window_cursorsnap_wizard.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 5: + gDisable = false; + gHidden = true; + window.openDialog("window_cursorsnap_wizard.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + default: + SetPrefs(false); + SimpleTest.finish(); + } +} + +function SetPrefs(aSet) +{ + var prefSvc = SpecialPowers.Services.prefs.nsIPrefBranch; + const kPrefName = "ui.cursor_snapping.always_enabled"; + if (aSet) { + prefSvc.setBoolPref(kPrefName, true); + } else if (prefSvc.prefHasUserValue(kPrefName)) { + prefSvc.clearUserPref(kPrefName); + } +} + +SetPrefs(true); +runNextTest(); + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_custom_element_base.xhtml b/toolkit/content/tests/chrome/test_custom_element_base.xhtml new file mode 100644 index 0000000000..d95828fe11 --- /dev/null +++ b/toolkit/content/tests/chrome/test_custom_element_base.xhtml @@ -0,0 +1,366 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Custom Element Base Class Tests" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <button id="one"/> + <simpleelement id="two" style="-moz-user-focus: normal;"/> + <simpleelement id="three" disabled="true" style="-moz-user-focus: normal;"/> + <button id="four"/> + <inherited-element-declarative foo="fuagra" empty-string=""></inherited-element-declarative> + <inherited-element-derived foo="fuagra"></inherited-element-derived> + <inherited-element-shadowdom-declarative foo="fuagra" empty-string=""></inherited-element-shadowdom-declarative> + <inherited-element-imperative foo="fuagra" empty-string=""></inherited-element-imperative> + <inherited-element-beforedomloaded foo="fuagra" empty-string=""></inherited-element-beforedomloaded> + + <!-- test code running before page load goes here --> + <script type="application/javascript"><![CDATA[ + class InheritAttrsChangeBeforDOMLoaded extends MozXULElement { + static get inheritedAttributes() { + return { + "label": "foo", + }; + } + connectedCallback() { + this.append(MozXULElement.parseXULToFragment(`<label />`)); + this.label = this.querySelector("label"); + + this.initializeAttributeInheritance(); + is(this.label.getAttribute("foo"), "fuagra", + "InheritAttrsChangeBeforDOMLoaded: attribute should be propagated #1"); + + this.setAttribute("foo", "chuk&gek"); + is(this.label.getAttribute("foo"), "chuk&gek", + "InheritAttrsChangeBeforDOMLoaded: attribute should be propagated #2"); + } + } + customElements.define("inherited-element-beforedomloaded", + InheritAttrsChangeBeforDOMLoaded); + ]]> + </script> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + ok(MozXULElement, "MozXULElement defined on the window"); + testMixin(); + testBaseControl(); + testBaseControlMixin(); + testBaseText(); + testParseXULToFragment(); + testInheritAttributes(); + await testCustomInterface(); + + let htmlWin = await new Promise(resolve => { + let htmlIframe = document.createXULElement("iframe"); + htmlIframe.src = "file_empty.xhtml"; + htmlIframe.onload = () => resolve(htmlIframe.contentWindow); + document.documentElement.appendChild(htmlIframe); + }); + + ok(htmlWin.MozXULElement, "MozXULElement defined on a chrome HTML window"); + SimpleTest.finish(); + } + + function testMixin() { + ok(MozElements.MozElementMixin, "Mixin exists"); + let MixedHTMLElement = MozElements.MozElementMixin(HTMLElement); + ok(MixedHTMLElement.insertFTLIfNeeded, "Mixed in class contains helper functions"); + } + + function testBaseControl() { + ok(MozElements.BaseControl, "BaseControl exists"); + ok("disabled" in MozElements.BaseControl.prototype, + "BaseControl prototype contains base control attributes"); + } + + function testBaseControlMixin() { + ok(MozElements.BaseControlMixin, "Mixin exists"); + let MixedHTMLSpanElement = MozElements.MozElementMixin(HTMLSpanElement); + let HTMLSpanBaseControl = MozElements.BaseControlMixin(MixedHTMLSpanElement); + ok("disabled" in HTMLSpanBaseControl.prototype, "Mixed in class prototype contains base control attributes"); + } + + function testBaseText() { + ok(MozElements.BaseText, "BaseText exists"); + ok("label" in MozElements.BaseText.prototype, + "BaseText prototype inherits BaseText attributes"); + ok("disabled" in MozElements.BaseText.prototype, + "BaseText prototype inherits BaseControl attributes"); + } + + function testParseXULToFragment() { + ok(MozXULElement.parseXULToFragment, "parseXULToFragment helper exists"); + + let frag = MozXULElement.parseXULToFragment(`<deck id='foo' />`); + ok(frag instanceof DocumentFragment); + + document.documentElement.appendChild(frag); + + let deck = document.documentElement.lastChild; + ok(deck instanceof MozXULElement, "instance of MozXULElement"); + ok(deck instanceof XULElement, "instance of XULElement"); + is(deck.id, "foo", "attribute set"); + is(deck.selectedIndex, "0", "Custom Element is property attached"); + deck.remove(); + + info("Checking that whitespace text is removed but non-whitespace text isn't"); + let boxWithWhitespaceText = MozXULElement.parseXULToFragment(`<box> </box>`).querySelector("box"); + is(boxWithWhitespaceText.textContent, "", "Whitespace removed"); + let boxWithNonWhitespaceText = MozXULElement.parseXULToFragment(`<box>foo</box>`).querySelector("box"); + is(boxWithNonWhitespaceText.textContent, "foo", "Non-whitespace not removed"); + + try { + // we didn't encode the & as & + MozXULElement.parseXULToFragment(`<box id="foo=1&bar=2"/>`); + ok(false, "parseXULToFragment should've thrown an exception for not-well-formed XML"); + } + catch (ex) { + is(ex.message, "not well-formed XML", "parseXULToFragment threw the wrong message"); + } + } + + function testInheritAttributes() { + class InheritsElementDeclarative extends MozXULElement { + static get inheritedAttributes() { + return { + "label": "text=label,foo,empty-string,bardo=bar", + "unmatched": "foo", // Make sure we don't throw on unmatched selectors + }; + } + + connectedCallback() { + this.textContent = ""; + this.append(MozXULElement.parseXULToFragment(`<label />`)); + this.label = this.querySelector("label"); + this.initializeAttributeInheritance(); + } + } + customElements.define("inherited-element-declarative", InheritsElementDeclarative); + let declarativeEl = document.querySelector("inherited-element-declarative"); + ok(declarativeEl, "declarative inheritance element exists"); + + class InheritsElementDerived extends InheritsElementDeclarative { + static get inheritedAttributes() { + return { label: "renamedfoo=foo" }; + } + } + customElements.define("inherited-element-derived", InheritsElementDerived); + + class InheritsElementShadowDOMDeclarative extends MozXULElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + static get inheritedAttributes() { + return { + "label": "text=label,foo,empty-string,bardo=bar", + "unmatched": "foo", // Make sure we don't throw on unmatched selectors + }; + } + + connectedCallback() { + this.shadowRoot.textContent = ""; + this.shadowRoot.append(MozXULElement.parseXULToFragment(`<label />`)); + this.label = this.shadowRoot.querySelector("label"); + this.initializeAttributeInheritance(); + } + } + customElements.define("inherited-element-shadowdom-declarative", InheritsElementShadowDOMDeclarative); + let shadowDOMDeclarativeEl = document.querySelector("inherited-element-shadowdom-declarative"); + ok(shadowDOMDeclarativeEl, "declarative inheritance element with shadow DOM exists"); + + class InheritsElementImperative extends MozXULElement { + static get observedAttributes() { + return [ "label", "foo", "empty-string", "bar" ]; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (this.label && oldValue != newValue) { + this.inherit(); + } + } + + inherit() { + let map = { + "label": [[ "label", "text" ]], + "foo": [[ "label", "foo" ]], + "empty-string": [[ "label", "empty-string" ]], + "bar": [[ "label", "bardo" ]], + }; + for (let attr of InheritsElementImperative.observedAttributes) { + this.inheritAttribute(map[attr], attr); + } + } + + connectedCallback() { + // Typically `initializeAttributeInheritance` handles this for us: + this._inheritedElements = null; + + this.textContent = ""; + this.append(MozXULElement.parseXULToFragment(`<label />`)); + this.label = this.querySelector("label"); + this.inherit(); + } + } + + customElements.define("inherited-element-imperative", InheritsElementImperative); + let imperativeEl = document.querySelector("inherited-element-imperative"); + ok(imperativeEl, "imperative inheritance element exists"); + + function checkElement(el) { + is(el.label.getAttribute("foo"), "fuagra", "predefined attribute @foo"); + ok(el.label.hasAttribute("empty-string"), "predefined attribute @empty-string"); + ok(!el.label.hasAttribute("bardo"), "predefined attribute @bardo"); + ok(!el.label.textContent, "predefined attribute @label"); + + el.setAttribute("empty-string", "not-empty-anymore"); + is(el.label.getAttribute("empty-string"), "not-empty-anymore", + "attribute inheritance: empty-string"); + + el.setAttribute("label", "label-test"); + is(el.label.textContent, "label-test", + "attribute inheritance: text=label attribute change"); + + el.setAttribute("bar", "bar-test"); + is(el.label.getAttribute("bardo"), "bar-test", + "attribute inheritance: `=` mapping"); + + el.label.setAttribute("bardo", "changed-from-child"); + is(el.label.getAttribute("bardo"), "changed-from-child", + "attribute inheritance: doesn't apply when host attr hasn't changed and child attr was changed"); + + el.label.removeAttribute("bardo"); + ok(!el.label.hasAttribute("bardo"), + "attribute inheritance: doesn't apply when host attr hasn't changed and child attr was removed"); + + el.setAttribute("bar", "changed-from-host"); + is(el.label.getAttribute("bardo"), "changed-from-host", + "attribute inheritance: does apply when host attr has changed and child attr was changed"); + + el.removeAttribute("bar"); + ok(!el.label.hasAttribute("bardo"), + "attribute inheritance: does apply when host attr has been removed"); + + el.setAttribute("bar", "changed-from-host-2"); + is(el.label.getAttribute("bardo"), "changed-from-host-2", + "attribute inheritance: does apply when host attr has changed after being removed"); + + // Restore to the original state so this can be ran again with the same element: + el.removeAttribute("label"); + el.removeAttribute("bar"); + } + + for (let el of [declarativeEl, shadowDOMDeclarativeEl, imperativeEl]) { + info(`Running checks for ${el.tagName}`); + checkElement(el); + info(`Remove and re-add ${el.tagName} to make sure attribute inheritance still works`); + el.replaceWith(el); + checkElement(el); + } + + let derivedEl = document.querySelector("inherited-element-derived"); + ok(derivedEl, "derived inheritance element exists"); + ok(!derivedEl.label.hasAttribute("foo"), + "attribute inheritance: base class attribute is not applied in derived class that overrides it"); + ok(derivedEl.label.hasAttribute("renamedfoo"), + "attribute inheritance: attribute defined in derived class is present"); + } + + async function testCustomInterface() { + class SimpleElement extends MozXULElement { + get disabled() { + return this.getAttribute("disabled") == "true"; + } + + set disabled(val) { + if (val) this.setAttribute("disabled", "true"); + else this.removeAttribute("disabled"); + return val; + } + + get tabIndex() { + return parseInt(this.getAttribute("tabIndex")) || 0; + } + + set tabIndex(val) { + if (val) this.setAttribute("tabIndex", val); + else this.removeAttribute("tabIndex"); + return val; + } + } + + MozXULElement.implementCustomInterface(SimpleElement, [Ci.nsIDOMXULControlElement]); + customElements.define("simpleelement", SimpleElement); + + let twoElement = document.getElementById("two"); + + is(document.documentElement.getCustomInterfaceCallback, undefined, + "No getCustomInterfaceCallback on non-custom element"); + is(typeof twoElement.getCustomInterfaceCallback, "function", + "getCustomInterfaceCallback available on custom element when set"); + is(document.documentElement.QueryInterface, undefined, + "Non-custom element should not have a QueryInterface implementation"); + + // Try various ways to get the custom interface. + + let asControl = twoElement.getCustomInterfaceCallback(Ci.nsIDOMXULControlElement); + + // XXX: Switched to from ok() to todo_is() in Bug 1467712. Follow up in 1500967 + // Not sure if this was suppose to simply check for existence or equality? + todo_is(asControl, twoElement, "getCustomInterface returns interface implementation "); + + asControl = twoElement.QueryInterface(Ci.nsIDOMXULControlElement); + ok(asControl, "QueryInterface to nsIDOMXULControlElement"); + ok(asControl instanceof Node, "Control is a Node"); + + // Now make sure that the custom element handles focus/tabIndex as needed by shitfing + // focus around and enabling/disabling the simple elements. + + // Enable Full Keyboard Access emulation on Mac + await SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]}); + + ok(!twoElement.disabled, "two is enabled"); + ok(document.getElementById("three").disabled, "three is disabled"); + + await SimpleTest.promiseFocus(); + ok(document.hasFocus(), "has focus"); + + // This should skip the disabled simpleelement. + synthesizeKey("VK_TAB"); + is(document.activeElement.id, "one", "Tab 1"); + synthesizeKey("VK_TAB"); + is(document.activeElement.id, "two", "Tab 2"); + synthesizeKey("VK_TAB"); + is(document.activeElement.id, "four", "Tab 3"); + + twoElement.disabled = true; + is(twoElement.getAttribute("disabled"), "true", "two disabled after change"); + + synthesizeKey("VK_TAB", { shiftKey: true }); + is(document.activeElement.id, "one", "Tab 1"); + + info("Checking that interfaces get inherited automatically with implementCustomInterface"); + class ExtendedElement extends SimpleElement { } + MozXULElement.implementCustomInterface(ExtendedElement, [Ci.nsIDOMXULSelectControlElement]); + customElements.define("extendedelement", ExtendedElement); + const extendedInstance = document.createXULElement("extendedelement"); + ok(extendedInstance.QueryInterface(Ci.nsIDOMXULSelectControlElement), "interface applied"); + ok(extendedInstance.QueryInterface(Ci.nsIDOMXULControlElement), "inherited interface applied"); + } + ]]> + </script> +</window> + diff --git a/toolkit/content/tests/chrome/test_custom_element_delay_connection.xhtml b/toolkit/content/tests/chrome/test_custom_element_delay_connection.xhtml new file mode 100644 index 0000000000..f40277d198 --- /dev/null +++ b/toolkit/content/tests/chrome/test_custom_element_delay_connection.xhtml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Custom Element Base Delayed Connected" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <script type="application/javascript"><![CDATA[ + let nativeDOMContentLoadedFired = false; + document.addEventListener("DOMContentLoaded", () => { + nativeDOMContentLoadedFired = true; + }, { once: true }); + + // To test `delayConnectedCallback` and `isConnectedAndReady` we have to run this before + // DOMContentLoaded, which is why this is done in a separate script that runs + // immediately and not in `runTests`. + let delayedConnectionPromise = new Promise(resolve => { + + let numSkippedAttributeChanges = 0; + let numDelayedConnections = 0; + let numDelayedDisconnections = 0; + let finishedWaitingForDOMReady = false; + + // Register this custom element before DOMContentLoaded has fired and before it's parsed in + // the markup: + customElements.define("delayed-connection", class DelayedConnection extends MozXULElement { + static get observedAttributes() { return ["foo"]; } + attributeChangedCallback() { + ok(!this.isConnectedAndReady, + "attributeChangedCallback fires before isConnectedAndReady"); + ok(!nativeDOMContentLoadedFired, + "attributeChangedCallback fires before nativeDOMContentLoadedFired"); + numSkippedAttributeChanges++; + } + connectedCallback() { + if (this.delayConnectedCallback()) { + ok(!finishedWaitingForDOMReady, + "connectedCallback with delayConnectedCallback fires before finishedWaitingForDOMReady"); + ok(!this.isConnectedAndReady, + "connectedCallback with delayConnectedCallback fires before isConnectedAndReady"); + ok(!nativeDOMContentLoadedFired, + "connectedCallback with delayConnectedCallback fires before nativeDOMContentLoadedFired"); + numDelayedConnections++; + return; + } + + ok(!finishedWaitingForDOMReady, + "connectedCallback only fires once when DOM is ready"); + ok(this.isConnectedAndReady, + "isConnectedAndReady during connectedCallback"); + ok(!nativeDOMContentLoadedFired, + "delayed connectedCallback fires before nativeDOMContentLoadedFired"); + + is(numSkippedAttributeChanges, 2, + "Correct number of skipped attribute changes"); + is(numDelayedConnections, 2, + "Correct number of delayed connections"); + is(numDelayedDisconnections, 1, + "Correct number of delated disconnections"); + + finishedWaitingForDOMReady = true; + resolve(); + } + disconnectedCallback() { + ok(this.delayConnectedCallback(), + "disconnectedCallback while DOM not ready"); + is(numDelayedDisconnections, 0, + "disconnectedCallback fired only once"); + numDelayedDisconnections++; + } + }); + }); + + // This should be called after the element is parsed below this. + function mutateDelayedConnection() { + // Fire connectedCallback and attributeChangedCallback twice before DOMContentLoaded + // fires. The first connectedCallback is due to the parse and the second due to re-appending. + let delayedConnection = document.querySelector("delayed-connection"); + delayedConnection.setAttribute("foo", "bar"); + delayedConnection.remove(); + delayedConnection.setAttribute("foo", "bat"); + document.documentElement.append(delayedConnection); + } + ]]> + </script> + + <delayed-connection></delayed-connection> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + mutateDelayedConnection(); + + async function runTests() { + info("Waiting for delayed connection to fire"); + ok(nativeDOMContentLoadedFired, + "nativeDOMContentLoadedFired is true in runTests"); + await delayedConnectionPromise; + SimpleTest.finish(); + } + ]]> + </script> +</window>
\ No newline at end of file diff --git a/toolkit/content/tests/chrome/test_custom_element_parts.html b/toolkit/content/tests/chrome/test_custom_element_parts.html new file mode 100644 index 0000000000..e46e93e206 --- /dev/null +++ b/toolkit/content/tests/chrome/test_custom_element_parts.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title><!-- Shadow Parts issue with xul/xbl domparser --></title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <style>custom-button::part(foo) { background: red; }</style> + <script> + add_task(async function test() { + // A simplified version of what MozXULElement.parseXULToFragment does + let parser = new DOMParser(); + parser.forceEnableXULXBL(); + let doc = parser.parseFromString(`<box xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><box part="foo">there</box></box>`, `application/xml`); + let range = doc.createRange(); + range.selectNodeContents(doc.querySelector("box")); + let frag = range.extractContents(); + + customElements.define("custom-button", class extends HTMLElement { + constructor() { + super(); + this.attachShadow({mode: "open"}); + this.shadowRoot.appendChild(document.importNode(frag, true)); + } + }); + let button = document.createElement("custom-button"); + document.body.appendChild(button); + + let box = button.shadowRoot.querySelector("box"); + + // XXX: this fixes it + // box.removeAttribute("part"); + // box.setAttribute("part", "foo"); + + is(window.getComputedStyle(box).getPropertyValue("background-color"), "rgb(255, 0, 0)", "part applied"); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_deck.xhtml b/toolkit/content/tests/chrome/test_deck.xhtml new file mode 100644 index 0000000000..a5adb992cd --- /dev/null +++ b/toolkit/content/tests/chrome/test_deck.xhtml @@ -0,0 +1,131 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for deck + --> +<window title="Deck Test" + onload="setTimeout(run_tests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<deck id="deck1" style="padding-top: 5px; padding-bottom: 12px;"> + <button id="d1b1" label="Button One"/> + <button id="d1b2" label="Button Two is larger" height="80" style="margin: 1px;"/> +</deck> +<deck id="deck2" selectedIndex="1"> + <button id="d2b1" label="Button One"/> + <button id="d2b2" label="Button Two"/> +</deck> +<deck id="deck3" selectedIndex="1"> + <button id="d3b1" label="Remove me"/> + <button id="d3b2" label="Keep me selected"/> +</deck> +<deck id="deck4" selectedIndex="5"> + <button id="d4b1" label="Remove me"/> + <button id="d4b2" label="Remove me"/> + <button id="d4b3" label="Remove me"/> + <button id="d4b4" label="Button 4"/> + <button id="d4b5" label="Button 5"/> + <button id="d4b6" label="Keep me selected"/> + <button id="d4b7" label="Button 7"/> +</deck> +<deck id="deck5" selectedIndex="2"> + <button id="d5b1" label="Button 1"/> + <button id="d5b2" label="Button 2"/> + <button id="d5b3" label="Keep me selected"/> + <button id="d5b4" label="Remove me"/> + <button id="d5b5" label="Remove me"/> + <button id="d5b6" label="Remove me"/> +</deck> + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function run_tests() { + test_deck(); + test_deck_child_removal(); + SimpleTest.finish(); +} + +function test_deck() +{ + var deck = $("deck1"); + ok(deck.selectedIndex === '0', "deck one selectedIndex"); + // this size is the button height, 80, plus the button padding of 1px on each side, + // plus the deck's 5px top padding and the 12px bottom padding. + var rect = deck.getBoundingClientRect(); + is(Math.round(rect.bottom) - Math.round(rect.top), 99, "deck size of largest child"); + synthesizeMouseExpectEvent(deck, 12, 12, { }, $("d1b1"), "click", "mouse on deck one"); + + // change the selected page of the deck and ensure that the mouse click goes + // to the button on that page + deck.selectedIndex = 1; + ok(deck.selectedIndex === '1', "deck one selectedIndex after change"); + synthesizeMouseExpectEvent(deck, 9, 9, { }, $("d1b2"), "click", "mouse on deck one after change"); + + deck = $("deck2"); + ok(deck.selectedIndex === '1', "deck two selectedIndex"); + synthesizeMouseExpectEvent(deck, 9, 9, { }, $("d2b2"), "click", "mouse on deck two"); +} + +function test_deck_child_removal() +{ + // Start with a simple case where we have two child nodes in a deck, with + // the second child (index 1) selected. Removing the first node should + // automatically set the selectedIndex at 0. + let deck = $("deck3"); + let child = $("d3b1"); + is(deck.selectedIndex, "1", "Should have the deck element at index 1 selected"); + + // Remove the child at the 0th index. The deck should automatically + // set the selectedIndex to "0". + child.remove(); + is(deck.selectedIndex, "0", "Should have the deck element at index 0 selected"); + + // Now scale it up by using a deck with 7 child nodes, and remove the + // first three, making sure that the selectedIndex is decremented + // each time. + deck = $("deck4"); + let expectedIndex = 5; + is(deck.selectedIndex, String(expectedIndex), + "Should have the deck element at index " + expectedIndex + " selected"); + + for (let i = 0; i < 3; ++i) { + deck.firstChild.remove(); + expectedIndex--; + is(deck.selectedIndex, String(expectedIndex), + "Should have the deck element at index " + expectedIndex + " selected"); + } + + // Check that removing the currently selected node doesn't change + // behaviour. + deck.childNodes[expectedIndex].remove(); + is(deck.selectedIndex, String(expectedIndex), + "The selectedIndex should not change when removing the node " + + "at the selected index."); + + // Finally, make sure we haven't changed the behaviour when removing + // nodes at indexes greater than the selected node. + deck = $("deck5"); + expectedIndex = 2; + is(deck.selectedIndex, String(expectedIndex), + "Should have the deck element at index " + expectedIndex + " selected"); + + // And then remove all of the nodes, starting from last to first, making + // sure that the selectedIndex does not change. + while (deck.lastChild) { + deck.lastChild.remove(); + is(deck.selectedIndex, String(expectedIndex), + "Should have the deck element at index " + expectedIndex + " selected"); + } +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_dialogfocus.xhtml b/toolkit/content/tests/chrome/test_dialogfocus.xhtml new file mode 100644 index 0000000000..a89dca2ba5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_dialogfocus.xhtml @@ -0,0 +1,137 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<button id="test" label="Test"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); + +var expected = [ "one", "_extra2", "tab", "one", "tabbutton2", "tabbutton", "two", "textbox-yes", "one", "root" ]; +// non-Mac will always focus the default button if any of the dialog buttons +// would be focused +if (!navigator.platform.includes("Mac")) + expected[1] = "_accept"; + +let extraDialog = "data:application/xhtml+xml,<window id='root'><dialog " + + "buttons='none' " + + "xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" + + "<button id='nonbutton' noinitialfocus='true'/></dialog></window>"; + +var step = 0; +var fullKeyboardAccess = false; + +function startTest() +{ + var testButton = document.getElementById("test"); + synthesizeKey("KEY_Tab"); + fullKeyboardAccess = (document.activeElement == testButton); + info("We " + (fullKeyboardAccess ? "have" : "don't have") + " full keyboard access"); + runTest(); +} + +function runTest() +{ + step++; + info("runTest(), step = " + step + ", expected = " + expected[step - 1]); + if (step > expected.length || (!fullKeyboardAccess && step == 2)) { + info("finishing"); + SimpleTest.finish(); + return; + } + + var expectedFocus = expected[step - 1]; + let filename = expectedFocus == "root" ? "dialog_dialogfocus2.xhtml" : "dialog_dialogfocus.xhtml"; + var win = window.browsingContext.topChromeWindow.openDialog(filename, "_new", "chrome,dialog", step); + + function checkDialogFocus(event) + { + info(`checkDialogFocus()`); + let match = false; + let activeElement = win.document.activeElement; + let dialog = win.document.getElementById("dialog-focus"); + + if (activeElement == dialog) { + let shadowActiveElement = + dialog.shadowRoot.activeElement; + if (shadowActiveElement) { + activeElement = shadowActiveElement; + } + } + // if full keyboard access is not on, just skip the tests + if (fullKeyboardAccess) { + if (!(event.target instanceof Element)) { + info("target not an Element"); + return; + } + + if (expectedFocus[0] == "_") + match = (activeElement.getAttribute("dlgtype") == expectedFocus.substring(1)); + else + match = (activeElement.id == expectedFocus); + info("match = " + match); + if (!match) + return; + } + else { + match = (activeElement == win.document.documentElement); + info("match = " + match); + } + + win.removeEventListener("focus", checkDialogFocusEvent, true); + dialog.shadowRoot.removeEventListener( + "focus", checkDialogFocusEvent, true); + ok(match, "focus step " + step); + + win.close(); + SimpleTest.waitForFocus(runTest, window); + } + + let finalCheckInitiated = false; + function checkDialogFocusEvent(event) { + // Delay to have time for focus/blur to occur. + if (expectedFocus == "root") { + if (!finalCheckInitiated) { + setTimeout(() => { + is(win.document.activeElement, win.document.documentElement, + "No other focus but root"); + win.close(); + SimpleTest.waitForFocus(runTest, window); + }, 0); + finalCheckInitiated = true; + } + } else { + checkDialogFocus(event); + } + } + win.addEventListener("focus", checkDialogFocusEvent, true); + win.addEventListener("load", () => { + win.document.getElementById("dialog-focus").shadowRoot.addEventListener( + "focus", checkDialogFocusEvent, true); + }); +} + +SimpleTest.waitForFocus(startTest, window); + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_edit_contextmenu.html b/toolkit/content/tests/chrome/test_edit_contextmenu.html new file mode 100644 index 0000000000..b9eb30a8eb --- /dev/null +++ b/toolkit/content/tests/chrome/test_edit_contextmenu.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1513343 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1513343</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + SimpleTest.waitForExplicitFinish(); + + async function runTest() { + let win = window.browsingContext.topChromeWindow.open("file_edit_contextmenu.xhtml", "context-menu", "chrome,width=600,height=600"); + await new Promise(r => win.addEventListener("load", r, { once: true})); + await SimpleTest.promiseFocus(win); + + const elements = [ + win.document.querySelector("textarea"), + win.document.querySelector("input"), + win.document.querySelector("search-textbox"), + win.document.querySelector("shadow-input").shadowRoot.querySelector("input"), + ]; + for (const element of elements) { + await testElement(element, win); + } + SimpleTest.finish(); + } + + async function testElement(element, win) { + ok(element, "element exists"); + + info("Synthesizing a key so 'Undo' will be enabled"); + element.focus(); + synthesizeKey("x", {}, win); + is(element.value, "x", "initial value"); + + element.select(); + synthesizeKey("c", { accelKey: true }, win); // copy to clipboard + synthesizeKey("KEY_ArrowRight", {}, win); // drop selection to disable cut and copy context menu items + + win.document.addEventListener("contextmenu", (e) => { + info("Calling prevent default on the first contextmenu event"); + e.preventDefault(); + }, { once: true }); + synthesizeMouseAtCenter(element, {type: "contextmenu"}, win); + ok(!win.document.getElementById("textbox-contextmenu"), "contextmenu with preventDefault() doesn't run"); + + let popupshown = new Promise(r => win.addEventListener("popupshown", r, { once: true })); + synthesizeMouseAtCenter(element, {type: "contextmenu"}, win); + let contextmenu = win.document.getElementById("textbox-contextmenu"); + ok(contextmenu, "context menu exists after right click"); + await popupshown; + + // Check that we only got the one context menu, and not two. + let outerContextmenu = win.document.getElementById("outer-context-menu"); + ok(outerContextmenu.state == "closed", "the outer context menu state is is not closed, it's: " + outerContextmenu.state); + + ok(!contextmenu.querySelector("[command=cmd_undo]").hasAttribute("disabled"), "undo enabled"); + ok(contextmenu.querySelector("[command=cmd_cut]").hasAttribute("disabled"), "cut disabled"); + ok(contextmenu.querySelector("[command=cmd_copy]").hasAttribute("disabled"), "copy disabled"); + ok(!contextmenu.querySelector("[command=cmd_paste]").hasAttribute("disabled"), "paste enabled"); + ok(contextmenu.querySelector("[command=cmd_delete]").hasAttribute("disabled"), "delete disabled"); + ok(!contextmenu.querySelector("[command=cmd_selectAll]").hasAttribute("disabled"), "select all enabled"); + + contextmenu.querySelector("[command=cmd_undo]").click(); + is(element.value, "", "undo worked"); + + // Close the context menu to avoid affecting next test + let popuphidden = new Promise(r => win.addEventListener("popuphidden", r, { once: true })); + contextmenu.hidePopup(); + await popuphidden; + contextmenu.remove(); + } + </script> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1513343">Mozilla Bug 1513343</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html b/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html new file mode 100644 index 0000000000..513207ecf3 --- /dev/null +++ b/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Basic editor behavior for HTML input element with autocomplete</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="file_editor_with_autocomplete.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <iframe id="formTarget" name="formTarget"></iframe> + <form action="data:text/html," target="formTarget"> + <input name="test" id="input"><input type="submit"> + </form> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +async function registerWord(aTarget, aAutoCompleteController) { + // Register a word to the form history. + let chromeScript = SpecialPowers.loadChromeScript(function addEntry() { + let {FormHistory} = ChromeUtils.import("resource://gre/modules/FormHistory.jsm"); + FormHistory.update({ op: "add", fieldname: "test", value: "Mozilla" }); + }); + aTarget.focus(); + aTarget.value = "Mozilla"; + + await waitForCondition(() => { + if (aAutoCompleteController.searchStatus == aAutoCompleteController.STATUS_NONE || + aAutoCompleteController.searchStatus == aAutoCompleteController.STATUS_COMPLETE_NO_MATCH) { + aAutoCompleteController.startSearch("Mozilla"); + } + return aAutoCompleteController.matchCount > 0; + }); + chromeScript.destroy(); + aTarget.value = ""; + synthesizeKey("KEY_Escape"); +} + +async function runTests() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.input_events.beforeinput.enabled", true]], + }); + + var formFillController = + SpecialPowers.getFormFillController() + .QueryInterface(Ci.nsIAutoCompleteInput); + var originalFormFillTimeout = formFillController.timeout; + + SpecialPowers.attachFormFillControllerTo(window); + var target = document.getElementById("input"); + + // Register a word to the form history. + await registerWord(target, formFillController.controller); + + let tests1 = new nsDoTestsForEditorWithAutoComplete( + "Testing on HTML input (asynchronously search)", + window, target, formFillController.controller, is, todo_is, + function() { return target.value; }); + await tests1.run(); + + target.setAttribute("timeout", 0); + let tests2 = new nsDoTestsForEditorWithAutoComplete( + "Testing on HTML input (synchronously search)", + window, target, formFillController.controller, is, todo_is, + function() { return target.value; }); + await tests2.run(); + + formFillController.timeout = originalFormFillTimeout; + SpecialPowers.detachFormFillControllerFrom(window); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_findbar.xhtml b/toolkit/content/tests/chrome/test_findbar.xhtml new file mode 100644 index 0000000000..a0329adfbd --- /dev/null +++ b/toolkit/content/tests/chrome/test_findbar.xhtml @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=257061 +https://bugzilla.mozilla.org/show_bug.cgi?id=288254 +--> +<window title="Mozilla Bug 257061 and Bug 288254" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=257061">Mozilla Bug 257061</a> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=288254">Mozilla Bug 288254</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 257061 and Bug 288254 **/ +SimpleTest.waitForExplicitFinish(); + +// Since bug 978861, this pref is set to `false` on OSX. For this test, we'll +// set it `true` to disable the find clipboard on OSX, which interferes with +// our tests. +SpecialPowers.pushPrefEnv({ + set: [["accessibility.typeaheadfind.prefillwithselection", true]] +}, () => { + window.openDialog("findbar_window.xhtml", "findbartest", "chrome,width=600,height=600,noopener", window); +}); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_findbar_entireword.xhtml b/toolkit/content/tests/chrome/test_findbar_entireword.xhtml new file mode 100644 index 0000000000..641e407aa0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_findbar_entireword.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=269442 +--> +<window title="Mozilla Bug 269442" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=269442"> + Mozilla Bug 269442 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 269442 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("findbar_entireword_window.xhtml", "269442test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_findbar_events.xhtml b/toolkit/content/tests/chrome/test_findbar_events.xhtml new file mode 100644 index 0000000000..f8e96d8ba8 --- /dev/null +++ b/toolkit/content/tests/chrome/test_findbar_events.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=793275 +--> +<window title="Mozilla Bug 793275" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=793275"> + Mozilla Bug 793275 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 793275 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("findbar_events_window.xhtml", "793275test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_frames.xhtml b/toolkit/content/tests/chrome/test_frames.xhtml new file mode 100644 index 0000000000..2ac68f1a06 --- /dev/null +++ b/toolkit/content/tests/chrome/test_frames.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script><![CDATA[ +SimpleTest.waitForExplicitFinish(); + +function runTest() { + for (let i = 1; i <= 3; i++) { + let frame = document.getElementById("frame" + i); + ok(frame instanceof XULFrameElement, "XULFrameElement " + i); + + // Check the various fields to ensure that they have the correct type. + ok(frame.docShell instanceof Ci.nsIDocShell, "docShell " + i); + ok(frame.webNavigation instanceof Ci.nsIWebNavigation, "webNavigation " + i); + + let contentWindow = frame.contentWindow; + let contentDocument = frame.contentDocument; + ok(contentWindow instanceof Window, "contentWindow " + i); + ok(contentDocument instanceof Document, "contentDocument " + i); + is(contentDocument.body.id, "thechildbody" + i, "right document body " + i); + + // These fields should all be read-only. + frame.docShell = null; + ok(frame.docShell instanceof Ci.nsIDocShell, "docShell after set " + i); + frame.webNavigation = null; + ok(frame.webNavigation instanceof Ci.nsIWebNavigation, "webNavigation after set " + i); + frame.contentWindow = window; + is(frame.contentWindow, contentWindow, "contentWindow after set " + i); + frame.contentDocument = document; + is(frame.contentDocument, contentDocument, "contentDocument after set " + i); + } + + // A non-frame element should not have these fields. + let button = document.getElementById("nonframe"); + ok(!(button instanceof XULFrameElement), "XULFrameElement non frame"); + is(button.docShell, undefined, "docShell non frame"); + is(button.webNavigation, undefined, "webNavigation non frame"); + is(button.contentWindow, undefined, "contentWindow non frame"); + is(button.contentDocument, undefined, "contentDocument non frame"); + + SimpleTest.finish(); +} +]]> +</script> + +<iframe id="frame1" src="data:text/html,<body id='thechildbody1'>"/> +<browser id="frame2" src="data:text/html,<body id='thechildbody2'>"/> +<editor id="frame3" src="data:text/html,<body id='thechildbody3'>"/> +<button id="nonframe"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"></div> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_hiddenitems.xhtml b/toolkit/content/tests/chrome/test_hiddenitems.xhtml new file mode 100644 index 0000000000..79f06c8890 --- /dev/null +++ b/toolkit/content/tests/chrome/test_hiddenitems.xhtml @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=317422 +--> +<window title="Mozilla Bug 317422" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=317422" + target="_blank">Mozilla Bug 317422</a> + </body> + + <richlistbox id="richlistbox" seltype="multiple"> + <richlistitem id="richlistbox_item1"><label value="Item 1"/></richlistitem> + <richlistitem id="richlistbox_item2"><label value="Item 2"/></richlistitem> + <richlistitem id="richlistbox_item3" hidden="true"><label value="Item 3"/></richlistitem> + <richlistitem id="richlistbox_item4"><label value="Item 4"/></richlistitem> + <richlistitem id="richlistbox_item5" collapsed="true"><label value="Item 5"/></richlistitem> + <richlistitem id="richlistbox_item6"><label value="Item 6"/></richlistitem> + <richlistitem id="richlistbox_item7" hidden="true"><label value="Item 7"/></richlistitem> + </richlistbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 317422 **/ +SimpleTest.waitForExplicitFinish(); + +function testListbox(id) +{ + var listbox = document.getElementById(id); + listbox.focus(); + is(listbox.getRowCount(), 7, id + ": Returned the wrong number of rows"); + is(listbox.getItemAtIndex(2).id, id + "_item3", id + ": Should still return hidden items"); + listbox.selectedIndex = 0; + is(listbox.selectedItem.id, id + "_item1", id + ": First item was not selected"); + sendKey("DOWN"); + is(listbox.selectedItem.id, id + "_item2", id + ": Down didn't move to second item"); + sendKey("DOWN"); + is(listbox.selectedItem.id, id + "_item4", id + ": Down didn't skip hidden item"); + sendKey("DOWN"); + is(listbox.selectedItem.id, id + "_item6", id + ": Down didn't skip collapsed item"); + sendKey("UP"); + is(listbox.selectedItem.id, id + "_item4", id + ": Up didn't skip collapsed item"); + sendKey("UP"); + is(listbox.selectedItem.id, id + "_item2", id + ": Up didn't skip hidden item"); + listbox.selectAll(); + is(listbox.selectedItems.length, 7, id + ": Should have still selected all items"); + listbox.selectedIndex = 2; + ok(listbox.selectedItem == listbox.getItemAtIndex(2), id + ": Should have selected the hidden item"); + listbox.selectedIndex = 0; + sendKey("END"); + is(listbox.selectedItem.id, id + "_item6", id + ": Should have moved to the last unhidden item"); + sendMouseEvent({type: 'click'}, id + "_item1"); + ok(listbox.selectedItem == listbox.getItemAtIndex(0), id + ": Should have selected the first item"); + is(listbox.selectedItems.length, 1, id + ": Should only be one selected item"); + sendMouseEvent({type: 'click', shiftKey: true}, id + "_item6"); + is(listbox.selectedItems.length, 4, id + ": Should have selected all visible items"); + listbox.selectedIndex = 0; + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item6", id + ": Page down should go to the last visible item"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item1", id + ": Page up should go to the first visible item"); +} + +window.onload = function runTests() { + testListbox("richlistbox"); + SimpleTest.finish(); +}; + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_hiddenpaging.xhtml b/toolkit/content/tests/chrome/test_hiddenpaging.xhtml new file mode 100644 index 0000000000..9b6f1cc8e5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_hiddenpaging.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=317422 +--> +<window title="Mozilla Bug 317422" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <style xmlns="http://www.w3.org/1999/xhtml"> + /* This makes the richlistbox about 4.5 rows high */ + richlistitem { + height: 30px; + } + richlistbox { + height: 135px; + } + </style> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=317422" + target="_blank">Mozilla Bug 317422</a> + </body> + + <richlistbox id="richlistbox" seltype="multiple"> + <richlistitem id="richlistbox_item1"><label value="Item 1"/></richlistitem> + <richlistitem id="richlistbox_item2"><label value="Item 2"/></richlistitem> + <richlistitem id="richlistbox_item3" hidden="true"><label value="Item 3"/></richlistitem> + <richlistitem id="richlistbox_item4"><label value="Item 4"/></richlistitem> + <richlistitem id="richlistbox_item5" collapsed="true"><label value="Item 5"/></richlistitem> + <richlistitem id="richlistbox_item6"><label value="Item 6"/></richlistitem> + <richlistitem id="richlistbox_item7"><label value="Item 7"/></richlistitem> + <richlistitem id="richlistbox_item8"><label value="Item 8"/></richlistitem> + <richlistitem id="richlistbox_item9"><label value="Item 9"/></richlistitem> + <richlistitem id="richlistbox_item10"><label value="Item 10"/></richlistitem> + <richlistitem id="richlistbox_item11"><label value="Item 11"/></richlistitem> + <richlistitem id="richlistbox_item12"><label value="Item 12"/></richlistitem> + <richlistitem id="richlistbox_item13"><label value="Item 13"/></richlistitem> + <richlistitem id="richlistbox_item14"><label value="Item 14"/></richlistitem> + <richlistitem id="richlistbox_item15" hidden="true"><label value="Item 15"/></richlistitem> + </richlistbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 317422 **/ +SimpleTest.waitForExplicitFinish(); + +function testRichlistbox() +{ + var id = "richlistbox"; + var listbox = document.getElementById(id); + listbox.focus(); + listbox.selectedIndex = 0; + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item7", id + ": Page down should go to the item one visible page away"); + is(listbox.getIndexOfFirstVisibleRow(), 6, id + ": Page down should have scrolled down a visible page"); + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item11", id + ": Second page down should go to the item two visible pages away"); + is(listbox.getIndexOfFirstVisibleRow(), 9, id + ": Second page down should not scroll beyond the end"); + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item14", id + ": Third page down should go to the last visible item"); + is(listbox.getIndexOfFirstVisibleRow(), 9, id + ": Third page down should not have scrolled at all"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item10", id + ": Page up should go to the item one visible page away"); + is(listbox.getIndexOfFirstVisibleRow(), 5, id + ": Page up should scroll up a visible page"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item6", id + ": Second page up should go to the item two visible pages away"); + is(listbox.getIndexOfFirstVisibleRow(), 0, id + ": Second page up should not scroll beyond the start"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item1", id + ": Third page up should return to the first visible item"); + is(listbox.getIndexOfFirstVisibleRow(), 0, id + ": Third page up should not have scrolled at all"); +} + +window.onload = function runTests() { + testRichlistbox(); + SimpleTest.finish(); +}; + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_keys.xhtml b/toolkit/content/tests/chrome/test_keys.xhtml new file mode 100644 index 0000000000..4d4c1ae8f0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_keys.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Keys Test" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_keys.xhtml", "_blank", "chrome,width=200,height=200,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_labelcontrol.xhtml b/toolkit/content/tests/chrome/test_labelcontrol.xhtml new file mode 100644 index 0000000000..06e7f96105 --- /dev/null +++ b/toolkit/content/tests/chrome/test_labelcontrol.xhtml @@ -0,0 +1,54 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for label control="value" + --> +<window title="tabindex" width="500" height="600" + onload="runTests()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<label id="textbox-label" control="textbox" /> +<html:input id="textbox" value="Test"/> +<label id="checkbox-label" control="checkbox" /> +<checkbox id="checkbox" value="Checkbox"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + let textbox = $("textbox"); + let textboxLabel = $("textbox-label"); + is(textboxLabel.control, "textbox", "textbox control"); + is(textboxLabel.labeledControlElement, textbox, "textbox labeledControlElement"); + + let checkbox = $("checkbox"); + let checkboxLabel = $("checkbox-label"); + is(checkboxLabel.control, "checkbox", "checkbox control"); + is(checkboxLabel.labeledControlElement, checkbox, "checkbox labeledControlElement"); + is(checkbox.accessKey, "", "checkbox accessKey not set"); + checkboxLabel.accessKey = "C"; + is(checkbox.accessKey, "C", "checkbox accessKey set"); + + SimpleTest.finish(); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_largemenu.html b/toolkit/content/tests/chrome/test_largemenu.html new file mode 100644 index 0000000000..489e7ca225 --- /dev/null +++ b/toolkit/content/tests/chrome/test_largemenu.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Large Menu Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + function runTest() { + window.openDialog("window_largemenu.xhtml", "_blank", "chrome,width=200,height=200,noopener", window); + } + </script> +</head> +<body onload="setTimeout(runTest, 0);"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_maximized_persist.xhtml b/toolkit/content/tests/chrome/test_maximized_persist.xhtml new file mode 100644 index 0000000000..c8558eb25c --- /dev/null +++ b/toolkit/content/tests/chrome/test_maximized_persist.xhtml @@ -0,0 +1,154 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Window Open Test" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +<script class="testbody" type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + const WIDTH = 300; + const HEIGHT = 300; + + function promiseMessage(msg) { + info(`wait for message "${msg}"`); + return new Promise(resolve => { + function listener(evt) { + info(`got message "${evt.data}"`); + if (evt.data == msg) { + window.removeEventListener("message", listener); + resolve(); + } + } + window.addEventListener("message", listener); + }); + } + + function openWindow(features = "") { + return window.browsingContext.topChromeWindow.openDialog( + "window_maximized_persist.xhtml", + "_blank", "chrome,dialog=no,all," + features, window); + } + + function checkWindow(msg, win, sizemode, width, height) { + is(win.windowState, sizemode, "sizemode should match " + msg); + if (sizemode == win.STATE_NORMAL) { + is(win.innerWidth, width, "width should match " + msg); + is(win.innerHeight, height, "height should match " + msg); + } + } + + function todoCheckWindow(msg, win, sizemode) { + todo_is(win.windowState, sizemode, "sizemode should match " + msg); + } + + // Persistence of "sizemode" is delayed to 500ms after it's changed. + // See SIZE_PERSISTENCE_TIMEOUT in nsWebShellWindow.cpp. + // We wait for 1000ms to ensure that it is actually persisted. + // We can also wait for condition that XULStore does have the value + // set, but that way we cannot test the cases where we don't expect + // persistence to happen. + function waitForSizeModePersisted() { + return new Promise(resolve => { + setTimeout(resolve, 1000); + }); + } + + async function changeSizeMode(func) { + let promiseSizeModeChange = promiseMessage("sizemodechange"); + func(); + await promiseSizeModeChange; + await waitForSizeModePersisted(); + } + + async function runTest() { + let win = openWindow(); + await SimpleTest.promiseFocus(win); + + // Check the default state. + const chrome_url = win.location.href; + checkWindow("when open initially", win, win.STATE_NORMAL, WIDTH, HEIGHT); + const widthDiff = win.outerWidth - win.innerWidth; + const heightDiff = win.outerHeight - win.innerHeight; + // Maximize the window. + await changeSizeMode(() => win.maximize()); + checkWindow("after maximize window", win, win.STATE_MAXIMIZED); + win.close(); + + // Open a new window to check persisted sizemode. + win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindow("when reopen to maximized", win, win.STATE_MAXIMIZED); + // Restore the window. + if (win.windowState == win.STATE_MAXIMIZED) { + await changeSizeMode(() => win.restore()); + } + checkWindow("after restore window", win, win.STATE_NORMAL, WIDTH, HEIGHT); + win.close(); + + // Open a new window again to check persisted sizemode. + win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindow("when reopen to normal", win, win.STATE_NORMAL, WIDTH, HEIGHT); + // And maximize the window again for next test. + await changeSizeMode(() => win.maximize()); + win.close(); + + // Open a new window again with centerscreen which shouldn't revert + // the persisted sizemode. + win = openWindow("centerscreen"); + await SimpleTest.promiseFocus(win); + checkWindow("when open with centerscreen", win, win.STATE_MAXIMIZED); + win.close(); + + // Linux doesn't seem to persist sizemode across opening window + // with specified size, so mark it expected fail for now. + let todo = navigator.platform.includes('Linux'); + let checkWindowMayFail = todo ? todoCheckWindow : checkWindow; + + // Open a new window with size specified. + win = openWindow("width=400,height=400"); + await SimpleTest.promiseFocus(win); + checkWindow("when reopen with size", win, win.STATE_NORMAL, 400, 400); + await waitForSizeModePersisted(); + win.close(); + + // Open a new window without size specified. + // The window opened before should not change persisted sizemode. + win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindowMayFail("when reopen without size", win, win.STATE_MAXIMIZED); + win.close(); + + // Open a new window with sizing synchronously. + win = openWindow(); + win.resizeTo(500 + widthDiff, 500 + heightDiff); + await SimpleTest.promiseFocus(win); + checkWindow("when sized synchronously", win, win.STATE_NORMAL, 500, 500); + await waitForSizeModePersisted(); + win.close(); + + // Open a new window without any sizing. + // The window opened before should not change persisted sizemode. + win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindowMayFail("when reopen without sizing", win, win.STATE_MAXIMIZED); + win.close(); + + // Clean up the XUL store for the given window. + let XULStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore); + XULStore.removeDocument(chrome_url); + + SimpleTest.finish(); + } +]]></script> +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</window> diff --git a/toolkit/content/tests/chrome/test_menu.xhtml b/toolkit/content/tests/chrome/test_menu.xhtml new file mode 100644 index 0000000000..8ef5c4ca15 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu.xhtml @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Destruction Test" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <menubar> + <menu label="top" id="top"> + <menupopup> + <menuitem label="top item"/> + + <menu label="hello" id="nested"> + <menupopup> + <menuitem label="item1"/> + <menuitem label="item2" id="item2"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menubar> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function runTests() + { + var menu = document.getElementById("nested"); + + // nsIDOMXULContainerElement::getIndexOfItem(); + var item = document.getElementById("item2"); + is(menu.getIndexOfItem(item), 1, + "nsIDOMXULContainerElement::getIndexOfItem() failed."); + + // nsIDOMXULContainerElement::getItemAtIndex(); + var itemAtIdx = menu.getItemAtIndex(1); + is(itemAtIdx, item, + "nsIDOMXULContainerElement::getItemAtIndex() failed."); + + // nsIDOMXULContainerElement::itemCount + is(menu.itemCount, 2, "nsIDOMXULContainerElement::itemCount failed."); + + // nsIDOMXULContainerElement::parentContainer + var topmenu = document.getElementById("top"); + is(menu.parentContainer, topmenu, + "nsIDOMXULContainerElement::parentContainer failed."); + + // nsIDOMXULContainerElement::appendItem(); + item = menu.appendItem("item3"); + is(menu.getIndexOfItem(item), 2, + "nsIDOMXULContainerElement::appendItem() failed."); + + SimpleTest.finish(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + +</window> + diff --git a/toolkit/content/tests/chrome/test_menu_hide.xhtml b/toolkit/content/tests/chrome/test_menu_hide.xhtml new file mode 100644 index 0000000000..cd9729151c --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu_hide.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Destruction Test" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<menu id="menu"> + <menupopup onpopuphidden="if (event.target == this) done()"> + <menu id="submenu" label="One"> + <menupopup onpopupshown="submenuOpened();"> + <menuitem label="Two"/> + </menupopup> + </menu> + </menupopup> +</menu> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + let menu = $("menu"); + let menuitemAddedWhileHidden = menu.appendItem("Added while hidden"); + ok(!menuitemAddedWhileHidden.querySelector(".menu-text"), "hidden menuitem hasn't rendered yet."); + + menu.menupopup.addEventListener("popupshown", () => { + is(menuitemAddedWhileHidden.querySelector(".menu-text").value, "Added while hidden", + "menuitemAddedWhileHidden item has rendered after shown."); + let menuitemAddedWhileShown = menu.appendItem("Added while shown"); + is(menuitemAddedWhileShown.querySelector(".menu-text").value, "Added while shown", + "menuitemAddedWhileShown item has eagerly rendered."); + + let submenu = $("submenu"); + is(submenu.querySelector(".menu-text").value, "One", "submenu has rendered."); + + let submenuDynamic = document.createXULElement("menu"); + submenuDynamic.setAttribute("label", "Dynamic"); + ok(!submenuDynamic.querySelector(".menu-text"), "dynamic submenu hasn't rendered yet."); + menu.menupopup.append(submenuDynamic); + is(submenuDynamic.querySelector(".menu-text").value, "Dynamic", "dynamic submenu has rendered."); + + menu.menupopup.firstElementChild.open = true; + }, { once: true }); + menu.open = true; +} + +function submenuOpened() +{ + let submenu = $("submenu"); + is(submenu.getAttribute('_moz-menuactive'), "true", "menu highlighted"); + submenu.hidden = true; + $("menu").open = false; +} + +function done() +{ + ok(!$("submenu").hasAttribute('_moz-menuactive'), "menu unhighlighted"); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menu_withcapture.xhtml b/toolkit/content/tests/chrome/test_menu_withcapture.xhtml new file mode 100644 index 0000000000..66f467e643 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu_withcapture.xhtml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu with Mouse Capture" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <menulist id="menulist"> + <menupopup onpopupshown="shown(this)" onpopuphidden="done();"> + <menuitem id="menuitem" label="Menu Item" + onmousemove="moveHappened = true;" + onmouseup="upHappened = true;"/> + </menupopup> + </menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(startTest); + +let moveHappened = false, upHappened = false; + +function startTest() { + disableNonTestMouseEvents(true); + document.getElementById("menulist"). open = true; +} + +function shown(popup) +{ + popup.setCaptureAlways(); + synthesizeMouseAtCenter(document.getElementById("menuitem"), { type: "mousemove" }); + synthesizeMouseAtCenter(document.getElementById("menuitem"), { type: "mouseup" }); +} + +function done() +{ + ok(moveHappened, "mousemove happened"); + ok(upHappened, "mouseup happened"); + disableNonTestMouseEvents(false); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menuchecks.xhtml b/toolkit/content/tests/chrome/test_menuchecks.xhtml new file mode 100644 index 0000000000..677fd7b075 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menuchecks.xhtml @@ -0,0 +1,147 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Checkbox and Radio Tests" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <hbox> + <button id="menu" type="menu" label="View"> + <menupopup id="popup" onpopupshown="popupShown()" onpopuphidden="popupHidden()"> + <menuitem id="toolbar" label="Show Toolbar" type="checkbox"/> + <menuitem id="statusbar" label="Show Status Bar" type="checkbox" checked="true"/> + <menuitem id="bookmarks" label="Show Bookmarks" type="checkbox" autocheck="false"/> + <menuitem id="history" label="Show History" type="checkbox" autocheck="false" checked="true"/> + <menuseparator/> + <menuitem id="byname" label="By Name" type="radio" name="sort"/> + <menuitem id="bydate" label="By Date" type="radio" name="sort" checked="true"/> + <menuseparator/> + <menuitem id="ascending" label="Ascending" type="radio" name="order" checked="true"/> + <menuitem id="descending" label="Descending" type="radio" name="order" autocheck="false"/> + <menuitem id="bysubject" label="By Subject" type="radio" name="sort"/> + </menupopup> + </button> + + </hbox> + + <!-- + This test checks that checkbox and radio menu items work properly + --> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + var gTestIndex = 0; + + // tests to perform + var tests = [ + { + testname: "select unchecked checkbox", + item: "toolbar", + checked: ["toolbar", "statusbar", "history", "bydate", "ascending"] + }, + { + testname: "select checked checkbox", + item: "statusbar", + checked: ["toolbar", "history", "bydate", "ascending"] + }, + { + testname: "select unchecked autocheck checkbox", + item: "bookmarks", + checked: ["toolbar", "history", "bydate", "ascending"] + }, + { + testname: "select checked autocheck checkbox", + item: "history", + checked: ["toolbar", "history", "bydate", "ascending"] + }, + { + testname: "select unchecked radio", + item: "byname", + checked: ["toolbar", "history", "byname", "ascending"] + }, + { + testname: "select checked radio", + item: "byname", + checked: ["toolbar", "history", "byname", "ascending"] + }, + { + testname: "select out of order checked radio", + item: "bysubject", + checked: ["toolbar", "history", "bysubject", "ascending"] + }, + { + testname: "select first radio again", + item: "byname", + checked: ["toolbar", "history", "byname", "ascending"] + }, + { + testname: "select autocheck radio", + item: "descending", + checked: ["toolbar", "history", "byname", "ascending"] + } + ]; + + function runTest() + { + checkMenus(["statusbar", "history", "bydate", "ascending"], "initial"); + document.getElementById("menu").open = true; + } + + function checkMenus(checkedItems, testname) + { + var isok = true; + var children = document.getElementById("popup").childNodes; + for (var c = 0; c < children.length; c++) { + var child = children[c]; + if ((checkedItems.includes(child.id) && child.getAttribute("checked") != "true") || + (!checkedItems.includes(child.id) && child.hasAttribute("checked"))) { + isok = false; + break; + } + } + + ok(isok, testname); + } + + function popupShown() + { + var test = tests[gTestIndex]; + synthesizeMouse(document.getElementById(test.item), 4, 4, { }); + } + + function popupHidden() + { + if (gTestIndex < tests.length) { + var test = tests[gTestIndex]; + checkMenus(test.checked, test.testname); + gTestIndex++; + if (gTestIndex < tests.length) { + document.getElementById("menu").open = true; + } + else { + // manually setting the checkbox should also update the radio state + document.getElementById("bydate").setAttribute("checked", "true"); + checkMenus(["toolbar", "history", "bydate", "ascending"], "set checked attribute on radio"); + SimpleTest.finish(); + } + } + } + + ]]> + </script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menuitem_blink.xhtml b/toolkit/content/tests/chrome/test_menuitem_blink.xhtml new file mode 100644 index 0000000000..3340f0b7b6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menuitem_blink.xhtml @@ -0,0 +1,106 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Blinking Context Menu Item Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <menulist id="menulist"> + <menupopup id="menupopup"> + <menuitem label="Menu Item" id="menuitem"/> + </menupopup> + </menulist> +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(startTest); + +function startTest() { + if (!/Mac/.test(navigator.platform)) { + ok(true, "Nothing to test on non-Mac."); + SimpleTest.finish(); + return; + } + // Destroy frame while removing the _moz-menuactive attribute. + test_crash("REMOVAL", test2); +} + +function test2() { + // Destroy frame while adding the _moz-menuactive attribute. + test_crash("ADDITION", test3); +} + +function test3() { + // Don't mess with the frame, just test whether we've blinked. + test_crash("", SimpleTest.finish); +} + +function test_crash(when, andThen) { + var menupopup = document.getElementById("menupopup"); + var menuitem = document.getElementById("menuitem"); + var attrChanges = { "REMOVAL": 0, "ADDITION": 0 }; + var storedEvent = null; + menupopup.addEventListener("popupshown", function () { + menuitem.addEventListener("mouseup", function (e) { + menuitem.addEventListener("DOMAttrModified", function onDOMAttrModified(e) { + if (e.target != e.currentTarget) { + return; + } + if (e.attrName == "_moz-menuactive") { + if (!attrChanges[e.attrChange]) + attrChanges[e.attrChange] = 1; + else + attrChanges[e.attrChange]++; + storedEvent = e; + if (e.attrChange == e[when]) { + menuitem.hidden = true; + menuitem.getBoundingClientRect(); + ok(true, "Didn't crash on _moz-menuactive " + when.toLowerCase() + " during blinking") + menuitem.hidden = false; + menuitem.removeEventListener("DOMAttrModified", onDOMAttrModified); + SimpleTest.executeSoon(function () { + menupopup.hidePopup(); + }); + } + } + }); + }, {once: true}); + menupopup.addEventListener("popuphidden", function() { + if (!when) { + // Test whether we've blinked at all. + var shouldBlink = navigator.platform.match(/Mac/); + var expectedNumRemoval = shouldBlink ? 2 : 1; + var expectedNumAddition = shouldBlink ? 1 : 0; + ok(storedEvent, "got DOMAttrModified events after clicking menuitem") + is(attrChanges[storedEvent.REMOVAL], expectedNumRemoval, "blinking unset attributes correctly"); + is(attrChanges[storedEvent.ADDITION], expectedNumAddition, "blinking set attributes correctly"); + } + SimpleTest.executeSoon(andThen); + }, {once: true}); + synthesizeMouse(menuitem, 10, 5, { type : "mousemove" }); + synthesizeMouse(menuitem, 10, 5, { type : "mousemove" }); + synthesizeMouse(menuitem, 10, 5, { type : "mousedown" }); + SimpleTest.executeSoon(function () { + synthesizeMouse(menuitem, 10, 5, { type : "mouseup" }); + }); + }, {once: true}); + document.getElementById("menulist").open = true; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menuitem_commands.xhtml b/toolkit/content/tests/chrome/test_menuitem_commands.xhtml new file mode 100644 index 0000000000..1e5f740909 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menuitem_commands.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menuitem Commands Test" + onload="runOrOpen()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +function checkAttributes(elem, label, accesskey, disabled, hidden, isAfter) +{ + var is = window.arguments[0].SimpleTest.is; + + is(elem.getAttribute("label"), label, elem.id + " label " + (isAfter ? "after" : "before") + " open"); + is(elem.getAttribute("accesskey"), accesskey, elem.id + " accesskey " + (isAfter ? "after" : "before") + " open"); + is(elem.getAttribute("disabled"), disabled, elem.id + " disabled " + (isAfter ? "after" : "before") + " open"); + is(elem.getAttribute("hidden"), hidden, elem.id + " hidden " + (isAfter ? "after" : "before") + " open"); +} + +function runOrOpen() +{ + if (window.arguments) { + SimpleTest.waitForFocus(runTest); + } + else { + window.openDialog("test_menuitem_commands.xhtml", "", "chrome,noopener", window); + } +} + +function runTest() +{ + runTestSet(""); + runTestSet("bar"); + window.close(); + window.arguments[0].SimpleTest.finish(); +} + +function runTestSet(suffix) +{ + var isMac = (navigator.platform.includes("Mac")); + + var one = $("one" + suffix); + var two = $("two" + suffix); + var three = $("three" + suffix); + var four = $("four" + suffix); + + checkAttributes(one, "One", "", "", "true", false); + checkAttributes(two, "", "", "false", "", false); + checkAttributes(three, "Three", "T", "true", "", false); + checkAttributes(four, "Four", "F", "", "", false); + + if (isMac && suffix) { + var utils = window.windowUtils; + utils.forceUpdateNativeMenuAt("0"); + } + else { + $("menu" + suffix).open = true; + } + + checkAttributes(one, "One", "", "", "false", true); + checkAttributes(two, "Cat", "C", "", "", true); + checkAttributes(three, "Dog", "D", "false", "true", true); + checkAttributes(four, "Four", "F", "true", "", true); + + $("menu" + suffix).open = false; +} +]]> +</script> + +<command id="cmd_one" hidden="false"/> +<command id="cmd_two" label="Cat" accesskey="C"/> +<command id="cmd_three" label="Dog" accesskey="D" disabled="false" hidden="true"/> +<command id="cmd_four" disabled="true"/> + +<button id="menu" type="menu"> + <menupopup> + <menuitem id="one" label="One" hidden="true" command="cmd_one"/> + <menuitem id="two" disabled="false" command="cmd_two"/> + <menuitem id="three" label="Three" accesskey="T" disabled="true" command="cmd_three"/> + <menuitem id="four" label="Four" accesskey="F" command="cmd_four"/> + </menupopup> +</button> + +<menubar> + <menu id="menubar" label="Sample"> + <menupopup> + <menuitem id="onebar" label="One" hidden="true" command="cmd_one"/> + <menuitem id="twobar" disabled="false" command="cmd_two"/> + <menuitem id="threebar" label="Three" accesskey="T" disabled="true" command="cmd_three"/> + <menuitem id="fourbar" label="Four" accesskey="F" command="cmd_four"/> + </menupopup> + </menu> +</menubar> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist.xhtml b/toolkit/content/tests/chrome/test_menulist.xhtml new file mode 100644 index 0000000000..4181d7f44f --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist.xhtml @@ -0,0 +1,353 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist Tests" + onload="setTimeout(testtag_menulists, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="xul_selectcontrol.js"></script> + +<vbox id="scroller" style="overflow: auto" height="60"> + <menulist id="menulist" onpopupshown="test_menulist_open(this, this.parentNode)" + onpopuphidden="$('menulist-in-listbox').open = true;"> + <menupopup id="menulist-popup"/> + </menulist> + <button label="Two"/> + <button label="Three"/> +</vbox> +<richlistbox id="scroller-in-listbox" style="overflow: auto" height="60"> + <richlistitem allowevents="true"> + <menulist id="menulist-in-listbox" onpopupshown="test_menulist_open(this, this.parentNode.parentNode)" + onpopuphidden="SimpleTest.executeSoon(() => checkScrollAndFinish().catch(ex => ok(false, ex)));"> + <menupopup id="menulist-in-listbox-popup"> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + </menupopup> + </menulist> + </richlistitem> + <richlistitem><label value="Two"/></richlistitem> + <richlistitem><label value="Three"/></richlistitem> + <richlistitem><label value="Four"/></richlistitem> + <richlistitem><label value="Five"/></richlistitem> + <richlistitem><label value="Six"/></richlistitem> +</richlistbox> + +<hbox> + <menulist id="menulist-size"> + <menupopup> + <menuitem label="Menuitem Label" width="200"/> + </menupopup> + </menulist> +</hbox> + +<menulist id="menulist-initwithvalue" value="two"> + <menupopup> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three"/> + </menupopup> +</menulist> +<menulist id="menulist-initwithselected" value="two"> + <menupopup> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three" selected="true"/> + </menupopup> +</menulist> + +<menulist id="menulist-clipped"> + <menupopup height="65"> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three"/> + <menuitem label="Four" value="four"/> + <menuitem label="Five" value="five" selected="true"/> + <menuitem label="Six" value="six"/> + <menuitem label="Seven" value="seven"/> + <menuitem label="Eight" value="eight"/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function waitForEvent(subject, eventName, checkFn) { + return new Promise(resolve => { + subject.addEventListener(eventName, function listener(event) { + if (checkFn && !checkFn(event)) { + return; + } + subject.removeEventListener(eventName, listener); + SimpleTest.executeSoon(() => resolve(event)); + }); + }); +} + +SimpleTest.waitForExplicitFinish(); + +function testtag_menulists() +{ + testtag_menulist_UI_start($("menulist"), false); +} + +function testtag_menulist_UI_start(element) +{ + // check the menupopup property + var popup = element.menupopup; + ok(popup && popup.localName == "menupopup" && + popup.parentNode == element, "menupopup"); + + // test the interfaces that menulist implements + test_nsIDOMXULMenuListElement(element); +} + +function testtag_menulist_UI_finish(element) +{ + element.value = ""; + + test_nsIDOMXULSelectControlElement(element, "menuitem", null); + + // bug 566154, the menulist width should account for vertical scrollbar + ok(document.getElementById("menulist-size").getBoundingClientRect().width >= 210, + "menulist popup width includes scrollbar width"); + + $("menulist").open = true; +} + +function test_nsIDOMXULMenuListElement(element) +{ + is(element.open, false, "open"); + + element.appendItem("Item One", "one"); + var seconditem = element.appendItem("Item Two", "two"); + var thirditem = element.appendItem("Item Three", "three"); + element.appendItem("Item Four", "four"); + + seconditem.image = "happy.png"; + seconditem.setAttribute("description", "This is the second description"); + thirditem.image = "happy.png"; + thirditem.setAttribute("description", "This is the third description"); + + // check the image and description properties + element.selectedIndex = 1; + is(element.image, "happy.png", "image set to selected"); + is(element.description, "This is the second description", "description set to selected"); + element.selectedIndex = -1; + is(element.image, "", "image set when none selected"); + is(element.description, "", "description set when none selected"); + element.selectedIndex = 2; + is(element.image, "happy.png", "image set to selected again"); + is(element.description, "This is the third description", "description set to selected again"); + + // check that changing the properties of the selected item changes the menulist's properties + let properties = [{attr: "label", value: "Item Number Three"}, + {attr: "value", value: "item-three"}, + {attr: "image", value: "smile.png"}, + {attr: "description", value: "Changed description"}]; + test_nsIDOMXULMenuListElement_properties(element, thirditem, properties); +} + +function test_nsIDOMXULMenuListElement_properties(element, thirditem, properties) +{ + let {attr, value} = properties.shift(); + let last = !properties.length; + + let mutObserver = new MutationObserver(() => { + is(element.getAttribute(attr), value, `${attr} modified`); + done(); + }); + mutObserver.observe(element, { attributeFilter: [attr] }); + + let failureTimeout = setTimeout(() => { + ok(false, `${attr} should have updated`); + done(); + }, 2000); + + function done() + { + clearTimeout(failureTimeout); + mutObserver.disconnect(); + if (!last) { + test_nsIDOMXULMenuListElement_properties(element, thirditem, properties); + } + else { + test_nsIDOMXULMenuListElement_unselected(element, thirditem); + } + } + + thirditem.setAttribute(attr, value) +} + +function test_nsIDOMXULMenuListElement_unselected(element, thirditem) +{ + let seconditem = thirditem.previousElementSibling; + seconditem.label = "Changed Label 2"; + is(element.label, "Item Number Three", "label of another item modified"); + + element.selectedIndex = 0; + is(element.image, "", "image set to selected with no image"); + is(element.description, "", "description set to selected with no description"); + test_nsIDOMXULMenuListElement_finish(element); +} + +function test_nsIDOMXULMenuListElement_finish(element) +{ + // check the removeAllItems method + element.appendItem("An Item", "anitem"); + element.appendItem("Another Item", "anotheritem"); + element.removeAllItems(); + is(element.itemCount, 0, "removeAllItems"); + + testtag_menulist_UI_finish(element); +} + +function test_menulist_open(element, scroller) +{ + element.appendItem("Scroll Item 1", "scrollitem1"); + element.appendItem("Scroll Item 2", "scrollitem2"); + element.focus(); + element.selectedIndex = 0; + +/* + // bug 530504, mousewheel while menulist is open should not scroll menulist + // items or parent + var scrolled = false; + var mouseScrolled = function (event) { scrolled = true; } + window.addEventListener("DOMMouseScroll", mouseScrolled, false); + synthesizeWheel(element, 2, 2, { deltaY: 10, + deltaMode: WheelEvent.DOM_DELTA_LINE }); + is(scrolled, true, "mousescroll " + element.id); + is(scroller.scrollTop, 0, "scroll position on mousescroll " + element.id); + window.removeEventListener("DOMMouseScroll", mouseScrolled, false); +*/ + + // bug 543065, hovering the mouse over an item should highlight it, not + // scroll the parent, and not change the selected index. + var item = element.menupopup.childNodes[1]; + + synthesizeMouse(element.menupopup.childNodes[1], 2, 2, { type: "mousemove" }); + synthesizeMouse(element.menupopup.childNodes[1], 6, 6, { type: "mousemove" }); + is(element.activeChild, item, "activeChild after menu highlight " + element.id); + is(element.selectedIndex, 0, "selectedIndex after menu highlight " + element.id); + is(scroller.scrollTop, 0, "scroll position after menu highlight " + element.id); + + element.open = false; +} + +async function checkScrollAndFinish() +{ + is($("scroller").scrollTop, 0, "mousewheel on menulist does not scroll vbox parent"); + is($("scroller-in-listbox").scrollTop, 0, "mousewheel on menulist does not scroll listbox parent"); + + let menulist = $("menulist-size"); + let shownPromise = waitForEvent(menulist, "popupshown"); + menulist.open = true; + await shownPromise; + + sendKey("ALT"); + is(menulist.menupopup.state, "open", "alt doesn't close menulist"); + menulist.open = false; + + await dragScroll(); +} + +async function dragScroll() +{ + let menulist = $("menulist-clipped"); + + let shownPromise = waitForEvent(menulist, "popupshown"); + menulist.open = true; + await shownPromise; + + let popup = menulist.menupopup; + let getScrollPos = () => popup.scrollBox.scrollbox.scrollTop; + let scrollPos = getScrollPos(); + let popupRect = popup.getBoundingClientRect(); + + // First, check that scrolling does not occur when the mouse is moved over the + // anchor button but not the popup yet. + synthesizeMouseAtPoint(popupRect.left + 5, popupRect.top - 10, { type: "mousemove" }); + is(getScrollPos(), scrollPos, "scroll position after mousemove over button should not change"); + + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.top + 10, { type: "mousemove" }); + + // Dragging above the popup scrolls it up. + let scrolledPromise = waitForEvent(popup, "scroll", false, + () => getScrollPos() < scrollPos - 5); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.top - 20, { type: "mousemove" }); + await scrolledPromise; + ok(true, "scroll position at drag up"); + + // Dragging below the popup scrolls it down. + scrollPos = getScrollPos(); + scrolledPromise = waitForEvent(popup, "scroll", false, + () => getScrollPos() > scrollPos + 5); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }); + await scrolledPromise; + ok(true, "scroll position at drag down"); + + // Releasing the mouse button and moving the mouse does not change the scroll position. + scrollPos = getScrollPos(); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 25, { type: "mouseup" }); + is(getScrollPos(), scrollPos, "scroll position at mouseup should not change"); + + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }); + is(getScrollPos(), scrollPos, "scroll position at mousemove after mouseup should not change"); + + // Now check dragging with a mousedown on an item. Make sure the element is + // visible, as the asynchronous scrolling may have moved it out of view. + popup.childNodes[4].scrollIntoView({ block: "nearest", behavior: "instant" }); + let menuRect = popup.childNodes[4].getBoundingClientRect(); + synthesizeMouseAtPoint(menuRect.left + 5, menuRect.top + 5, { type: "mousedown" }); + + // Dragging below the popup scrolls it down. + scrolledPromise = waitForEvent(popup, "scroll", false, + () => getScrollPos() > scrollPos + 5); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }); + await scrolledPromise; + ok(true, "scroll position at drag down from item"); + + // Dragging above the popup scrolls it up. + scrollPos = getScrollPos(); + scrolledPromise = waitForEvent(popup, "scroll", false, + () => getScrollPos() < scrollPos - 5); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.top - 20, { type: "mousemove" }); + await scrolledPromise; + ok(true, "scroll position at drag up from item"); + + scrollPos = getScrollPos(); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 25, { type: "mouseup" }); + is(getScrollPos(), scrollPos, "scroll position at mouseup should not change"); + + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }); + is(getScrollPos(), scrollPos, "scroll position at mousemove after mouseup should not change"); + + menulist.open = false; + + let mouseMoveTarget = null; + popup.childNodes[4].click(); + addEventListener("mousemove", function checkMouseMove(event) { + mouseMoveTarget = event.target; + }, {once: true}); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }); + isnot(mouseMoveTarget, popup, "clicking on item when popup closed doesn't start dragging"); + + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_keynav.xhtml b/toolkit/content/tests/chrome/test_menulist_keynav.xhtml new file mode 100644 index 0000000000..7a7bcd39cb --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_keynav.xhtml @@ -0,0 +1,312 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist Key Navigation Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<button id="button1" label="One"/> +<menulist id="list"> + <menupopup id="popup" onpopupshowing="return gShowPopup;"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> + <menuitem id="i2b" disabled="true" label="Two and a Half"/> + <menuitem id="i3" label="Three"/> + <menuitem id="i4" label="Four"/> + </menupopup> +</menulist> +<button id="button2" label="Two"/> +<menulist id="list2"> + <menupopup id="popup" onpopupshown="checkCursorNavigation();"> + <menuitem id="b1" label="One"/> + <menuitem id="b2" label="Two" selected="true"/> + <menuitem id="b3" label="Three"/> + <menuitem id="b4" label="Four"/> + </menupopup> +</menulist> +<menulist id="list3" sizetopopup="none"> + <menupopup> + <menuitem id="s1" label="One"/> + <menuitem id="s2" label="Two"/> + <menuitem id="s3" label="Three"/> + <menuitem id="s4" label="Four"/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gShowPopup = false; +var gModifiers = 0; +var gOpenPhase = false; + +var list = $("list"); +let expectCommandEvent; + +var iswin = (navigator.platform.indexOf("Win") == 0); +var ismac = (navigator.platform.indexOf("Mac") == 0); + +function runTests() +{ + list.focus(); + + // on Mac, up and cursor keys open the menu, but on other platforms, the + // cursor keys navigate between items without opening the menu + if (!ismac) { + expectCommandEvent = true; + keyCheck(list, "KEY_ArrowDown", 2, 1, "cursor down"); + keyCheck(list, "KEY_ArrowDown", 3, 1, "cursor down skip disabled"); + keyCheck(list, "KEY_ArrowUp", 2, 1, "cursor up skip disabled"); + keyCheck(list, "KEY_ArrowUp", 1, 1, "cursor up"); + + // On Windows, wrapping doesn't occur. + keyCheck(list, "KEY_ArrowUp", iswin ? 1 : 4, 1, "cursor up wrap"); + + list.selectedIndex = 4; + list.activeChild = list.selectedItem; + keyCheck(list, "KEY_ArrowDown", iswin ? 4 : 1, 4, "cursor down wrap"); + + list.selectedIndex = 0; + list.activeChild = list.selectedItem; + } + + // check that attempting to open the menulist does not change the selection + synthesizeKey("KEY_ArrowDown", {altKey: !ismac}); + is(list.selectedItem, $("i1"), "open menulist down selectedItem"); + synthesizeKey("KEY_ArrowUp", {altKey: !ismac}); + is(list.selectedItem, $("i1"), "open menulist up selectedItem"); + + list.selectedItem = $("i1"); + + pressLetter(); +} + +function pressLetter() +{ + // A command event should be fired only if the menulist is closed, or on Windows, + // where items are selected immediately. + expectCommandEvent = !gOpenPhase || iswin; + + sendString("G"); + is(list.selectedItem, $("i1"), "letter pressed not found selectedItem"); + + keyCheck(list, "T", 2, 1, "letter pressed"); + + if (!gOpenPhase) { + SpecialPowers.setIntPref("ui.menu.incremental_search.timeout", 0); // prevent to timeout + keyCheck(list, "T", 2, 1, "same letter pressed"); + SpecialPowers.clearUserPref("ui.menu.incremental_search.timeout"); + } + + setTimeout(pressedAgain, 1200); +} + +function pressedAgain() +{ + keyCheck(list, "T", 3, 1, "letter pressed again"); + SpecialPowers.setIntPref("ui.menu.incremental_search.timeout", 0); // prevent to timeout + keyCheck(list, "W", 2, 1, "second letter pressed"); + SpecialPowers.clearUserPref("ui.menu.incremental_search.timeout"); + setTimeout(differentPressed, 1200); +} + +function differentPressed() +{ + keyCheck(list, "O", 1, 1, "different letter pressed"); + + if (gOpenPhase) { + list.open = false; + tabAndScroll(); + } + else { + // Run the letter tests again with the popup open + info("list open phase"); + + list.selectedItem = $("i1"); + + // Hide and show the list to avoid using any existing incremental key state. + list.hidden = true; + list.clientWidth; + list.hidden = false; + + gShowPopup = true; + gOpenPhase = true; + + list.addEventListener("popupshown", function popupShownListener() { + pressLetter(); + }, { once: true}); + + list.open = true; + } +} + +function tabAndScroll() +{ + list = $("list"); + + if (!ismac) { + $("button1").focus(); + synthesizeKeyExpectEvent("KEY_Tab", {}, list, "focus", "focus to menulist"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("button2"), "focus", "focus to button"); + is(document.activeElement, $("button2"), "tab from menulist focused button"); + } + + // now make sure that using a key scrolls the menu correctly + + for (let i = 0; i < 65; i++) { + list.appendItem("Item" + i, "item" + i); + } + list.open = true; + is(list.getBoundingClientRect().width, list.menupopup.getBoundingClientRect().width, + "menu and popup width match"); + var minScrollbarWidth = window.matchMedia("(-moz-overlay-scrollbars)").matches ? 0 : 3; + ok(list.getBoundingClientRect().width >= list.getItemAtIndex(0).getBoundingClientRect().width + minScrollbarWidth, + "menuitem width accounts for scrollbar"); + list.open = false; + + list.menupopup.maxHeight = 100; + list.open = true; + + var rowdiff = list.getItemAtIndex(1).getBoundingClientRect().top - + list.getItemAtIndex(0).getBoundingClientRect().top; + + var item = list.getItemAtIndex(10); + var originalPosition = item.getBoundingClientRect().top; + + list.activeChild = item; + ok(item.getBoundingClientRect().top < originalPosition, + "position of item 1: " + item.getBoundingClientRect().top + " -> " + originalPosition); + + originalPosition = item.getBoundingClientRect().top; + + synthesizeKey("KEY_ArrowDown"); + is(item.getBoundingClientRect().top, originalPosition - rowdiff, "position of item 10"); + + list.open = false; + + checkEnter(); +} + +function keyCheck(list, key, index, defaultindex, testname) +{ + var item = $("i" + index); + var defaultitem = $("i" + defaultindex || 1); + + synthesizeKeyExpectEvent(key, { }, item, expectCommandEvent ? "command" : "!command", testname); + is(list.selectedItem, expectCommandEvent ? item : defaultitem, testname + " selectedItem----" + list.selectedItem.id); +} + +function checkModifiers(event) +{ + var expectedModifiers = (gModifiers == 1); + is(event.shiftKey, expectedModifiers, "shift key pressed"); + is(event.ctrlKey, expectedModifiers, "ctrl key pressed"); + is(event.altKey, expectedModifiers, "alt key pressed"); + is(event.metaKey, expectedModifiers, "meta key pressed"); + gModifiers++; +} + +function checkEnter() +{ + list.addEventListener("popuphidden", checkEnterWithModifiers); + list.addEventListener("command", checkModifiers); + list.open = true; + synthesizeKey("KEY_Enter"); +} + +function checkEnterWithModifiers() +{ + is(gModifiers, 1, "modifiers checked when not set"); + + ok(!list.open, "list closed on enter press"); + list.removeEventListener("popuphidden", checkEnterWithModifiers); + + list.addEventListener("popuphidden", verifyPopupOnClose); + list.open = true; + + synthesizeKey("KEY_Enter", {shiftKey: true, ctrlKey: true, altKey: true, metaKey: true}); +} + +function verifyPopupOnClose() +{ + is(gModifiers, 2, "modifiers checked when set"); + + ok(!list.open, "list closed on enter press with modifiers"); + list.removeEventListener("popuphidden", verifyPopupOnClose); + + list = $("list2"); + list.focus(); + list.open = true; +} + +function checkCursorNavigation() +{ + var commandEventsCount = 0; + list.addEventListener("command", event => { + is(event.target, list.selectedItem, "command event fired on selected item"); + commandEventsCount++; + }); + + is(list.selectedIndex, 1, "selectedIndex before cursor down"); + synthesizeKey("KEY_ArrowDown"); + is(list.selectedIndex, iswin ? 2 : 1, "selectedIndex after cursor down"); + is(commandEventsCount, iswin ? 1 : 0, "selectedIndex after cursor down command event"); + is(list.menupopup.state, "open", "cursor down popup state"); + synthesizeKey("KEY_PageDown"); + is(list.selectedIndex, iswin ? 3 : 1, "selectedIndex after page down"); + is(commandEventsCount, iswin ? 2 : 0, "selectedIndex after page down command event"); + is(list.menupopup.state, "open", "page down popup state"); + + // Check whether cursor up and down wraps. + list.selectedIndex = 0; + list.activeChild = list.selectedItem; + synthesizeKey("KEY_ArrowUp"); + is(list.activeChild, + document.getElementById(iswin || ismac ? "b1" : "b4"), "cursor up wrap while open"); + + list.selectedIndex = 3; + list.activeChild = list.selectedItem; + synthesizeKey("KEY_ArrowDown"); + is(list.activeChild, + document.getElementById(iswin || ismac ? "b4" : "b1"), "cursor down wrap while open"); + + synthesizeKey("KEY_ArrowUp", {altKey: true}); + is(list.open, ismac, "alt+up closes popup"); + + if (ismac) { + list.open = false; + } + + // Finally, test a menulist with sizetopopup="none" to ensure keyboard navigation + // still works when the popup has not been opened. + if (!ismac) { + let unsizedMenulist = document.getElementById("list3"); + unsizedMenulist.focus(); + synthesizeKey("KEY_ArrowDown"); + is(unsizedMenulist.selectedIndex, 1, "correct menulist index on keydown"); + is(unsizedMenulist.label, "Two", "correct menulist label on keydown"); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_null_value.xhtml b/toolkit/content/tests/chrome/test_menulist_null_value.xhtml new file mode 100644 index 0000000000..9312c236dc --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_null_value.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist value property" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<menulist id="list"> + <menupopup> + <menuitem id="i0" label="Zero" value="0"/> + <menuitem id="i1" label="One" value="item1"/> + <menuitem id="i2" label="Two" value="item2"/> + <menuitem id="ifalse" label="False" value="false"/> + <menuitem id="iempty" label="Empty" value=""/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + var list = document.getElementById("list"); + + list.value = "item2"; + is(list.value, "item2", "Check list value after setting value"); + is(list.getAttribute("label"), "Two", "Check list label after setting value"); + + list.selectedItem = null; + is(list.value, "", "Check list value after setting selectedItem to null"); + is(list.getAttribute("label"), "", "Check list label after setting selectedItem to null"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + is(list.value, "item1", "Check list value after setting selectedIndex"); + is(list.getAttribute("label"), "One", "Check list label after setting selectedIndex"); + + // check that an item can have the "false" value + list.value = false; + is(list.value, "false", "Check list value after setting it to false"); + is(list.getAttribute("label"), "False", "Check list labem after setting value to false"); + + // check that an item can have the "0" value + list.value = 0; + is(list.value, "0", "Check list value after setting it to 0"); + is(list.getAttribute("label"), "Zero", "Check list label after setting value to 0"); + + // check that an item can have the empty string value. + list.value = ""; + is(list.value, "", "Check list value after setting it to an empty string"); + is(list.getAttribute("label"), "Empty", "Check list label after setting value to an empty string"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + // set the value to null and test it (bug 408940) + list.value = null; + is(list.value, "", "Check list value after setting value to null"); + is(list.getAttribute("label"), "", "Check list label after setting value to null"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + // set the value to undefined and test it (bug 408940) + list.value = undefined; + is(list.value, "", "Check list value after setting value to undefined"); + is(list.getAttribute("label"), "", "Check list label after setting value to undefined"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + // set the value to something that does not exist in any menuitem of the list + // and make sure the previous label is removed + list.value = "this does not exist"; + is(list.value, "this does not exist", "Check the list value after setting it to something not associated witn an existing menuitem"); + is(list.getAttribute("label"), "", "Check that the list label is empty after selecting a nonexistent item"); + + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_paging.xhtml b/toolkit/content/tests/chrome/test_menulist_paging.xhtml new file mode 100644 index 0000000000..d36f0d48f0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_paging.xhtml @@ -0,0 +1,163 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist Tests" + onload="setTimeout(startTest, 0);" + onpopupshown="menulistShown()" onpopuphidden="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<menulist id="menulist1"> + <menupopup id="menulist-popup1"> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <menuitem label="Ten"/> + </menupopup> +</menulist> + +<menulist id="menulist2"> + <menupopup id="menulist-popup2"> + <menuitem label="One" disabled="true"/> + <menuitem label="Two" selected="true"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <menuitem label="Ten" disabled="true"/> + </menupopup> +</menulist> + +<menulist id="menulist3"> + <menupopup id="menulist-popup3"> + <label value="One"/> + <menuitem label="Two" selected="true"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five" disabled="true"/> + <menuitem label="Six" disabled="true"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <label value="Ten"/> + </menupopup> +</menulist> + +<menulist id="menulist4"> + <menupopup id="menulist-popup4"> + <label value="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six" selected="true"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <label value="Ten"/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +let test; + +// Fields: +// list - menulist id +// initial - initial selected index +// scroll - index of item at top of the visible scrolled area, -1 to skip this test +// downs - array of indicies that will be selected when pressing down in sequence +// ups - array of indicies that will be selected when pressing up in sequence +let tests = [ + { list: "menulist1", initial: 0, scroll: 0, downs: [3, 6, 9, 9], + ups: [6, 3, 0, 0] }, + { list: "menulist2", initial: 1, scroll: 0, downs: [4, 7, 8, 8], + ups: [5, 2, 1] }, + { list: "menulist3", initial: 1, scroll: -1, downs: [6, 8, 8], + ups: [3, 1, 1] }, + { list: "menulist4", initial: 5, scroll: 2, downs: [], ups: [] } +]; + +function startTest() +{ + let popup = document.getElementById("menulist-popup1"); + let menupopupHeight = popup.getBoundingClientRect().height; + let menuitemHeight = popup.firstChild.getBoundingClientRect().height; + + // First, set the height of each popup to the height of four menuitems plus + // any padding and border on the menupopup. + let height = menuitemHeight * 4 + (menupopupHeight - menuitemHeight * 10); + popup.height = height; + document.getElementById("menulist-popup2").height = height; + document.getElementById("menulist-popup3").height = height; + document.getElementById("menulist-popup4").height = height; + + runTest(); +} + +function runTest() +{ + if (!tests.length) { + SimpleTest.finish(); + return; + } + + test = tests.shift(); + document.getElementById(test.list).open = true; +} + +function menulistShown() +{ + let menulist = document.getElementById(test.list); + is(menulist.activeChild.label, menulist.getItemAtIndex(test.initial).label, test.list + " initial selection"); + + let cs = window.getComputedStyle(menulist.menupopup); + let bpTop = parseFloat(cs.paddingTop) + parseFloat(cs.borderTopWidth); + + // Skip menulist3 as it has a label that scrolling doesn't need normally deal with. + if (test.scroll >= 0) { + is(menulist.menupopup.childNodes[test.scroll].getBoundingClientRect().top, + menulist.menupopup.getBoundingClientRect().top + bpTop, + "Popup scroll at correct position"); + } + + for (let i = 0; i < test.downs.length; i++) { + sendKey("PAGE_DOWN"); + is(menulist.activeChild.label, menulist.getItemAtIndex(test.downs[i]).label, test.list + " page down " + i); + } + + for (let i = 0; i < test.ups.length; i++) { + sendKey("PAGE_UP"); + is(menulist.activeChild.label, menulist.getItemAtIndex(test.ups[i]).label, test.list + " page up " + i); + } + + menulist.open = false; +} +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_position.xhtml b/toolkit/content/tests/chrome/test_menulist_position.xhtml new file mode 100644 index 0000000000..1d0802cb37 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_position.xhtml @@ -0,0 +1,112 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist position Test" + onload="setTimeout(init, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This test checks the position of a menulist's popup. + --> + +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +var menulist; + +function init() +{ + menulist = document.getElementById("menulist"); + menulist.open = true; +} + +function isWithinHalfPixel(a, b) +{ + return Math.abs(a - b) <= 0.5; +} + +function popupShown() +{ + var menurect = menulist.getBoundingClientRect(); + var popuprect = menulist.menupopup.getBoundingClientRect(); + + let marginLeft = parseFloat(getComputedStyle(menulist.menupopup).marginLeft); + ok(isWithinHalfPixel(menurect.left + marginLeft, popuprect.left), "left position"); + ok(isWithinHalfPixel(menurect.right + marginLeft, popuprect.right), "right position"); + + let index = menulist.selectedIndex; + if (menulist.selectedItem && navigator.platform.includes("Mac")) { + let menulistlabel = menulist.shadowRoot.getElementById("label"); + let mitemlabel = menulist.selectedItem.querySelector(".menu-iconic-text"); + + ok(isWithinHalfPixel(menulistlabel.getBoundingClientRect().left, + mitemlabel.getBoundingClientRect().left), + "Labels horizontally aligned for index " + index); + ok(isWithinHalfPixel(menulistlabel.getBoundingClientRect().top, + mitemlabel.getBoundingClientRect().top), + "Labels vertically aligned for index " + index); + + // Store the current value and reset it afterwards. + let current = menulist.selectedIndex; + + // Cycle through the items to ensure that the popup doesn't move when the selection changes. + for (let i = 0; i < menulist.itemCount; i++) { + menulist.selectedIndex = i; + + let newpopuprect = menulist.menupopup.getBoundingClientRect(); + is(newpopuprect.x, popuprect.x, "Popup remained horizontally for index " + i + " starting at " + current); + is(newpopuprect.y, popuprect.y, "Popup remained vertically for index " + i + " starting at " + current); + } + menulist.selectedIndex = current; + } + else { + let marginTop = parseFloat(getComputedStyle(menulist.menupopup).marginTop); + ok(isWithinHalfPixel(menurect.bottom + marginTop, popuprect.top), + "Vertical alignment with no selection for index " + index); + } + + menulist.open = false; +} + +function popupHidden() +{ + if (!menulist.selectedItem) { + SimpleTest.finish(); + } + else { + menulist.selectedItem = menulist.selectedItem.nextSibling; + menulist.open = true; + } +} +]]> +</script> + +<hbox align="center" pack="center" style="margin-top: 140px;"> + <menulist id="menulist" onpopupshown="popupShown();" onpopuphidden="popupHidden();"> + <menupopup style="max-height: 90px;"> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six"/> + <menuitem label="Seven"/> + </menupopup> + </menulist> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_mousescroll.xhtml b/toolkit/content/tests/chrome/test_mousescroll.xhtml new file mode 100644 index 0000000000..5078000ae1 --- /dev/null +++ b/toolkit/content/tests/chrome/test_mousescroll.xhtml @@ -0,0 +1,274 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=378028 +--> +<window title="Mozilla Bug 378028" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=378028" + target="_blank">Mozilla Bug 378028</a> + </body> + + <!-- richlistbox currently has no way of giving us a defined number of + rows, so we just choose an arbitrary height limit that should give + us plenty of vertical scrollability --> + <richlistbox id="richlistbox" style="height:50px;"> + <richlistitem id="richlistbox_item0" hidden="true"><label value="Item 0"/></richlistitem> + <richlistitem id="richlistbox_item1"><label value="Item 1"/></richlistitem> + <richlistitem id="richlistbox_item2"><label value="Item 2"/></richlistitem> + <richlistitem id="richlistbox_item3"><label value="Item 3"/></richlistitem> + <richlistitem id="richlistbox_item4"><label value="Item 4"/></richlistitem> + <richlistitem id="richlistbox_item5"><label value="Item 5"/></richlistitem> + <richlistitem id="richlistbox_item6"><label value="Item 6"/></richlistitem> + <richlistitem id="richlistbox_item7"><label value="Item 7"/></richlistitem> + <richlistitem id="richlistbox_item8"><label value="Item 8"/></richlistitem> + </richlistbox> + + <box orient="horizontal"> + <arrowscrollbox id="hscrollbox" clicktoscroll="true" orient="horizontal" + smoothscroll="false" style="max-width:80px;" flex="1"> + <hbox style="width:40px; height:20px; background:black;" hidden="true"/> + <hbox style="width:40px; height:20px; background:white;"/> + <hbox style="width:40px; height:20px; background:black;"/> + <hbox style="width:40px; height:20px; background:white;"/> + <hbox style="width:40px; height:20px; background:black;"/> + <hbox style="width:40px; height:20px; background:white;"/> + <hbox style="width:40px; height:20px; background:black;"/> + <hbox style="width:40px; height:20px; background:white;"/> + <hbox style="width:40px; height:20px; background:black;"/> + </arrowscrollbox> + </box> + + <arrowscrollbox id="vscrollbox" clicktoscroll="true" orient="vertical" + smoothscroll="false" style="max-height:80px;" flex="1"> + <vbox style="width:100px; height:40px; background:black;" hidden="true"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + <vbox style="width:100px; height:40px; background:white;"/> + <vbox style="width:100px; height:40px; background:black;"/> + </arrowscrollbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 378028 **/ +/* and for Bug 350471 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(prepareRunningTests); + +// Some tests need to wait until stopping scroll completely. At this time, +// setTimeout() will retry to check up to MAX_RETRY_COUNT times. +const MAX_RETRY_COUNT = 5; + +const deltaModes = [ + WheelEvent.DOM_DELTA_PIXEL, // 0 + WheelEvent.DOM_DELTA_LINE, // 1 + WheelEvent.DOM_DELTA_PAGE // 2 +]; + +function sendWheelAndWait(aScrollTaget, aX, aY, aEvent, aChecker) +{ + function continueTestsIfScrolledAsExpected() { + if (!aChecker()) + SimpleTest.executeSoon(()=>{ continueTestsIfScrolledAsExpected(aChecker) }); + else + runTests(); + } + + sendWheelAndPaint(aScrollTaget, aX, aY, aEvent, ()=>{ + // sendWheelAndPaint may wait not enough for <scrollbox>. + // Let's check the position before using is() for avoiding random orange. + // So, this test may detect regressions with timeout. + continueTestsIfScrolledAsExpected(aChecker); + }); +} + +function* testRichListbox(id) +{ + var listbox = document.getElementById(id); + + function* helper(aStart, aDelta, aIntDelta, aDeltaMode) { + listbox.ensureElementIsVisible(listbox.getItemAtIndex(aStart),true); + + let event = { + deltaMode: aDeltaMode, + deltaY: aDelta, + lineOrPageDeltaY: aIntDelta + }; + // We don't need to wait for finishing the scroll in this test. + yield sendWheelAndWait(listbox, 10, 10, event, ()=>{ return true; }); + var change = listbox.getIndexOfFirstVisibleRow() - aStart; + var direction = (change > 0) - (change < 0); + var expected = (aDelta > 0) - (aDelta < 0); + is(direction, expected, + "testRichListbox(" + id + "): vertical, starting " + aStart + + " delta " + aDelta + " lineOrPageDeltaY " + aIntDelta + + " aDeltaMode " + aDeltaMode); + + // Check that horizontal scrolling has no effect + event = { + deltaMode: aDeltaMode, + deltaX: aDelta, + lineOrPageDeltaX: aIntDelta + }; + + listbox.ensureElementIsVisible(listbox.getItemAtIndex(aStart),true); + yield sendWheelAndWait(listbox, 10, 10, event, ()=>{ return true; }); + is(listbox.getIndexOfFirstVisibleRow(), aStart, + "testRichListbox(" + id + "): horizontal, starting " + aStart + + " delta " + aDelta + " lineOrPageDeltaX " + aIntDelta + + " aDeltaMode " + aDeltaMode); + } + + // richlistbox currently uses native XUL scrolling, so the "line" + // amounts don't necessarily correspond 1-to-1 with listbox items. So + // we just check that scrolling up/down scrolls in the right direction. + for (let i = 0; i < deltaModes.length; i++) { + let delta = (deltaModes[i] == WheelEvent.DOM_DELTA_PIXEL) ? 32.0 : 2.0; + yield* helper(5, -delta, -1, deltaModes[i]); + yield* helper(5, -delta, 0, deltaModes[i]); + yield* helper(5, delta, 1, deltaModes[i]); + yield* helper(5, delta, 0, deltaModes[i]); + } +} + +function* testArrowScrollbox(id) +{ + var scrollbox = document.getElementById(id).scrollbox; + var orient = scrollbox.getAttribute("orient"); + + function* helper(aStart, aDelta, aDeltaMode, aExpected) + { + var lineOrPageDelta = (aDeltaMode == WheelEvent.DOM_DELTA_PIXEL) ? aDelta / 10 : aDelta; + var orientIsHorizontal = (orient == "horizontal"); + + scrollbox.scrollTo(aStart, aStart); + for (let i = orientIsHorizontal ? 2 : 0; i >= 0; i--) { + // Note, vertical mouse scrolling is allowed to scroll horizontal + // arrowscrollboxes, because many users have no horizontal mouse scroll + // capability + let expected = !i ? aExpected : aStart; + let getPos = ()=>{ + return orientIsHorizontal ? scrollbox.scrollLeft : + scrollbox.scrollTop; + }; + let oldPos = -1; + let retry = 0; + yield sendWheelAndWait(scrollbox, 5, 5, + { deltaMode: aDeltaMode, deltaY: aDelta, + lineOrPageDeltaY: lineOrPageDelta }, + ()=>{ + if (getPos() == expected) { + return true; + } + if (oldPos == getPos()) { + // If scroll stopped completely, let's continue the test. + return ++retry == MAX_RETRY_COUNT; + } + oldPos = getPos(); + retry = 0; + return false; + }); + is(getPos(), expected, + "testArrowScrollbox(" + id + "): vertical, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + lineOrPageDelta + + " aDeltaMode " + aDeltaMode); + } + + scrollbox.scrollTo(aStart, aStart); + for (let i = orientIsHorizontal ? 2 : 0; i >= 0; i--) { + // horizontal mouse scrolling is never allowed to scroll vertical + // arrowscrollboxes + let expected = (!i && orientIsHorizontal) ? aExpected : aStart; + let getPos = ()=>{ + return orientIsHorizontal ? scrollbox.scrollLeft : + scrollbox.scrollTop; + }; + let oldPos = -1; + let retry = 0; + yield sendWheelAndWait(scrollbox, 5, 5, + { deltaMode: aDeltaMode, deltaX: aDelta, + lineOrPageDeltaX: lineOrPageDelta }, + ()=>{ + if (getPos() == expected) { + return true; + } + if (oldPos == getPos()) { + // If scroll stopped completely, let's continue the test. + return ++retry == MAX_RETRY_COUNT; + } + oldPos = getPos(); + retry = 0; + return false; + }); + is(getPos(), expected, + "testArrowScrollbox(" + id + "): horizontal, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + lineOrPageDelta + + " aDeltaMode " + aDeltaMode); + } + } + + var scrolledWidth = scrollbox.scrollWidth; + var scrolledHeight = scrollbox.scrollHeight; + var scrollMaxX = scrolledWidth - scrollbox.getBoundingClientRect().width; + var scrollMaxY = scrolledHeight - scrollbox.getBoundingClientRect().height; + var scrollMax = orient == "horizontal" ? scrollMaxX : scrollMaxY; + + for (let i = 0; i < deltaModes.length; i++) { + yield* helper(50, -1000, deltaModes[i], 0); + yield* helper(50, 1000, deltaModes[i], scrollMax); + } +} + +var gTestContinuation = null; + +function runTests() +{ + if (!gTestContinuation) { + gTestContinuation = testBody(); + } + var ret = gTestContinuation.next(); + if (ret.done) { + var winUtils = SpecialPowers.getDOMWindowUtils(window); + winUtils.restoreNormalRefresh(); + SimpleTest.finish(); + } +} + +async function prepareRunningTests() +{ + // Before actually running tests, we disable auto-dir scrolling, becasue the + // horizontal scrolling tests in this file are mostly meant to ensure that the + // tested controls in the default style should only have one scrollbar and it + // must always be in the block-flow direction so they are not really meant to + // test default actions for wheel events, so we simply disabled auto-dir + // scrolling, which are well tested in + // dom/events/test/window_wheel_default_action.html. + await SpecialPowers.pushPrefEnv({"set": [["mousewheel.autodir.enabled", + false]]}); + + runTests(); +} + +function* testBody() +{ + yield* testRichListbox("richlistbox"); + yield* testArrowScrollbox("hscrollbox"); + yield* testArrowScrollbox("vscrollbox"); +} + + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_mozinputbox_dictionary.xhtml b/toolkit/content/tests/chrome/test_mozinputbox_dictionary.xhtml new file mode 100644 index 0000000000..9c08be540d --- /dev/null +++ b/toolkit/content/tests/chrome/test_mozinputbox_dictionary.xhtml @@ -0,0 +1,100 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for textbox with Add and Undo Add to Dictionary + --> +<window title="Textbox Add and Undo Add to Dictionary Test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <hbox> + <moz-input-box id="t1" oncontextmenu="runContextMenuTest()" spellcheck="true"> + <html:input class="textbox-input" value="Hellop" spellcheck="true"/> + </moz-input-box> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var inputBox; +var testNum; + +function bringUpContextMenu(element) +{ + synthesizeMouseAtCenter(element, { type: "contextmenu", button: 2}); +} + +function leftClickElement(element) +{ + synthesizeMouseAtCenter(element, { button: 0 }); +} + +var onSpellCheck; +function startTests() +{ + inputBox = document.getElementById("t1"); + inputBox._input.focus(); + testNum = 0; + + ({onSpellCheck} = ChromeUtils.import( + "resource://testing-common/AsyncSpellCheckTestHelper.jsm")); + onSpellCheck(inputBox._input, function () { + bringUpContextMenu(inputBox); + }); +} + +function runContextMenuTest() +{ + SimpleTest.executeSoon(function() { + var contextMenu = inputBox.menupopup; + + switch(testNum) + { + case 0: // "Add to Dictionary" button + var addToDict = inputBox.getMenuItem("spell-add-to-dictionary"); + ok(!addToDict.hidden, "Is Add to Dictionary visible?"); + + var separator = inputBox.getMenuItem("spell-suggestions-separator"); + ok(!separator.hidden, "Is separator visible?"); + + addToDict.doCommand(); + + contextMenu.hidePopup(); + testNum++; + + onSpellCheck(inputBox._input, function () { + bringUpContextMenu(inputBox); + }); + break; + + case 1: // "Undo Add to Dictionary" button + var undoAddDict = inputBox.getMenuItem("spell-undo-add-to-dictionary"); + ok(!undoAddDict.hidden, "Is Undo Add to Dictioanry visible?"); + + separator = inputBox.getMenuItem("spell-suggestions-separator"); + ok(!separator.hidden, "Is separator hidden?"); + + undoAddDict.doCommand(); + + contextMenu.hidePopup(); + onSpellCheck(inputBox._input, function () { + SimpleTest.finish(); + }); + break; + } + }); +} + +SimpleTest.waitForFocus(startTests); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_navigate_persist.html b/toolkit/content/tests/chrome/test_navigate_persist.html new file mode 100644 index 0000000000..794422bfb7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_navigate_persist.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1460639 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1460639</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + SimpleTest.waitForExplicitFinish(); + + function navigateWindowTo(win, url) { + return new Promise(resolve => { + Services.obs.addObserver(function listener(document) { + Services.obs.removeObserver(listener, "document-element-inserted"); + document.addEventListener("DOMContentLoaded", () => { + resolve(); + }, { once: true } ); + }, "document-element-inserted"); + win.location = url; + }); + } + + function resize(win, size) { + let resizePromise = new Promise(resolve => { + if (win.outerWidth === size && win.outerHeight === size) { + resolve(); + } + win.addEventListener("resize", () => { + resolve(); + }, {once: true}); + }); + win.resizeTo(size, size); + return resizePromise; + } + + async function runTest() { + // Test that persisted window attributes are loaded when a top level + // window is navigated. This mimics the behavior of early first paint by + // first loading about:blank and then navigating to window_navigate_persist.html. + const PERSIST_SIZE = 200; + // First, load the document and resize it so the size is persisted. + let win = window.browsingContext.topChromeWindow + .openDialog("window_navigate_persist.html", "_blank", `chrome,all,dialog=no`); + await SimpleTest.promiseFocus(win); + await resize(win, PERSIST_SIZE); + is(win.outerWidth, PERSIST_SIZE, "Window is resized to desired width"); + is(win.outerHeight, PERSIST_SIZE, "Window is resized to desired height"); + win.close(); + + // Now mimic early first paint. + win = window.browsingContext.topChromeWindow + .openDialog("about:blank", "_blank", `chrome,all,dialog=no`); + await SimpleTest.promiseFocus(win, true); + isnot(win.outerWidth, PERSIST_SIZE, "Initial window width is not the persisted size"); + isnot(win.outerHeight, PERSIST_SIZE, "Initial window height is not the persisted size"); + + await navigateWindowTo(win, "window_navigate_persist.html"); + is(win.outerWidth, PERSIST_SIZE, "Window width is persisted"); + is(win.outerHeight, PERSIST_SIZE, "Window height is persisted"); + win.close(); + SimpleTest.finish(); + } + + </script> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1460639">Mozilla Bug 1460639</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_notificationbox.xhtml b/toolkit/content/tests/chrome/test_notificationbox.xhtml new file mode 100644 index 0000000000..bde2b1f63e --- /dev/null +++ b/toolkit/content/tests/chrome/test_notificationbox.xhtml @@ -0,0 +1,499 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for notificationbox + --> +<window title="Notification Box" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <vbox id="nb"/> + <menupopup id="menupopup" onpopupshown="this.hidePopup()" onpopuphidden="checkPopupClosed()"> + <menuitem label="One"/> + </menupopup> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ +SimpleTest.waitForExplicitFinish(); + +var testtag_notificationbox_buttons = [ + { + label: "Button 1", + accesskey: "u", + callback: testtag_notificationbox_buttonpressed, + popup: "menupopup" + } +]; + +function testtag_notificationbox_buttonpressed(event) +{ +} + +function testtag_notificationbox(nb) +{ + testtag_notificationbox_State(nb, "initial", null, 0); + + SimpleTest.is(nb.removeAllNotifications(false), undefined, "initial removeAllNotifications"); + testtag_notificationbox_State(nb, "initial removeAllNotifications", null, 0); + SimpleTest.is(nb.removeAllNotifications(true), undefined, "initial removeAllNotifications immediate"); + testtag_notificationbox_State(nb, "initial removeAllNotifications immediate", null, 0); + + runTimedTests(tests, -1, nb, null); +} + +var notification_last_events = []; +function notification_eventCallback(event) +{ + notification_last_events.push({ actualEvent: event , item: this }); +} + +/** + * For any notifications that have the notification_eventCallback on + * them, we will have recorded instances of those callbacks firing + * and stored them. This checks to see that the expected event types + * are being fired in order, and targeting the right item. + * + * @param {Array<string>} expectedEvents + * The list of event types, in order, that we expect to have been + * fired on the item. + * @param {<xul:notification>} ntf + * The notification we expect the callback to have been fired from. + * @param {string} testName + * The name of the current test, for logging. + */ +function testtag_notification_eventCallback(expectedEvents, ntf, testName) +{ + for (let i = 0; i < expectedEvents; ++i) { + let expected = expectedEvents[i]; + let { actualEvent, item } = notification_last_events[i]; + SimpleTest.is(actualEvent, expected, testName + ": event name"); + SimpleTest.is(item, ntf, testName + ": event item"); + } + notification_last_events = []; +} + +var tests = +[ + { + test(nb, ntf) { + // append a new notification + ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification"); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_LOW); + + // check the getNotificationWithValue method + var ntf_found = nb.getNotificationWithValue("note"); + SimpleTest.is(ntf, ntf_found, "getNotificationWithValue note"); + + var none_found = nb.getNotificationWithValue("notenone"); + SimpleTest.is(none_found, null, "getNotificationWithValue null"); + return ntf; + } + }, + { + test(nb, ntf) { + // check that notifications can be removed properly + nb.removeNotification(ntf); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "removeNotification", null, 0); + } + }, + { + test(nb, ntf) { + // append a new notification, but now with an event callback + ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, + testtag_notificationbox_buttons, + notification_eventCallback); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification with callback"); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append with callback", ntf, 1); + return ntf; + } + }, + { + test(nb, ntf) { + nb.removeNotification(ntf); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "removeNotification with callback", + null, 0); + + testtag_notification_eventCallback(["removed"], ntf, "removeNotification()"); + return ntf; + } + }, + { + test(nb, ntf) { + ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, + testtag_notificationbox_buttons, + notification_eventCallback); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification with callback"); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append with callback", ntf, 1); + return ntf; + } + }, + { + test(rb, ntf) { + // Dismissing the notification instead of removing it should + // fire a dismissed "event" on the callback, followed by + // a removed "event". + ntf.dismiss(); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "called dismiss()", null, 0); + testtag_notification_eventCallback(["dismissed", "removed"], ntf, + "dismiss()"); + return ntf; + } + }, + { + test(nb, ntf) { + ntf = nb.appendNotification( + "Notification", "note", "happy.png", + nb.PRIORITY_WARNING_LOW, + [{ + label: "Button", + }] + ); + + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_WARNING_LOW); + nb.removeNotification(ntf); + + return [1, null]; + } + }, + { + repeat: true, + test(nb, arr) { + var idx = arr[0]; + var ntf = arr[1]; + switch (idx) { + case 1: + // append a new notification + ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification"); + + // Test persistence + ntf.persistence++; + + return [idx, ntf]; + case 2: + case 3: + nb.removeTransientNotifications(); + + return [idx, ntf]; + } + return ntf; + }, + result(nb, arr) { + var idx = arr[0]; + var ntf = arr[1]; + switch (idx) { + case 1: + testtag_notificationbox_State(nb, "notification added", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_LOW); + SimpleTest.is(ntf.persistence, 1, "persistence is 1"); + + return [++idx, ntf]; + case 2: + testtag_notificationbox_State(nb, "first removeTransientNotifications", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_LOW); + SimpleTest.is(ntf.persistence, 0, "persistence is now 0"); + + return [++idx, ntf]; + case 3: + testtag_notificationbox_State(nb, "second removeTransientNotifications", null, 0); + + this.repeat = false; + } + return ntf; + } + }, + { + test(nb, ntf) { + // append another notification + ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_MEDIUM, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification again"); + return ntf; + }, + result(nb, ntf) { + // check that appending a second notification after removing the first one works + testtag_notificationbox_State(nb, "append again", ntf, 1); + testtag_notification_State(nb, ntf, "append again", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_MEDIUM); + return ntf; + } + }, + { + test(nb, ntf) { + // check the removeCurrentNotification method + nb.removeCurrentNotification(); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "removeCurrentNotification", null, 0); + } + }, + { + test(nb, ntf) { + ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_HIGH, testtag_notificationbox_buttons); + return ntf; + }, + result(nb, ntf) { + // test the removeAllNotifications method + testtag_notificationbox_State(nb, "append info_high", ntf, 1); + SimpleTest.is(ntf.priority, nb.PRIORITY_INFO_HIGH, + "notification.priority " + nb.PRIORITY_INFO_HIGH); + SimpleTest.is(nb.removeAllNotifications(false), undefined, "removeAllNotifications"); + } + }, + { + test(nb, unused) { + // add a number of notifications and check that they are added in order + nb.appendNotification("Four", "4", null, nb.PRIORITY_INFO_HIGH, testtag_notificationbox_buttons); + nb.appendNotification("Seven", "7", null, nb.PRIORITY_WARNING_HIGH, testtag_notificationbox_buttons); + nb.appendNotification("Two", "2", null, nb.PRIORITY_INFO_LOW, null); + nb.appendNotification("Eight", "8", null, nb.PRIORITY_CRITICAL_LOW, null); + nb.appendNotification("Five", "5", null, nb.PRIORITY_WARNING_LOW, null); + nb.appendNotification("Six", "6", null, nb.PRIORITY_WARNING_HIGH, null); + nb.appendNotification("One", "1", null, nb.PRIORITY_INFO_LOW, null); + nb.appendNotification("Nine", "9", null, nb.PRIORITY_CRITICAL_MEDIUM, null); + var ntf = nb.appendNotification("Ten", "10", null, nb.PRIORITY_CRITICAL_HIGH, null); + nb.appendNotification("Three", "3", null, nb.PRIORITY_INFO_MEDIUM, null); + return ntf; + }, + result(nb, ntf) { + is(nb.currentNotification, ntf, "appendNotification last notification"); + is(nb.currentNotification.getAttribute("value"), "10", "appendNotification order"); + return 1; + } + }, + { + // test closing notifications to make sure that the current notification is still set properly + repeat: true, + test(nb, testidx) { + switch (testidx) { + case 1: + nb.getNotificationWithValue("10").close(); + return [1, 9]; + case 2: + nb.removeNotification(nb.getNotificationWithValue("9")); + return [2, 8]; + case 3: + nb.removeCurrentNotification(); + return [3, 7]; + case 4: + nb.getNotificationWithValue("6").close(); + return [4, 7]; + case 5: + nb.removeNotification(nb.getNotificationWithValue("5")); + return [5, 7]; + case 6: + nb.removeCurrentNotification(); + return [6, 4]; + } + return testidx; + }, + result(nb, arr) { + // arr is [testindex, expectedvalue] + is(nb.currentNotification.getAttribute("value"), "" + arr[1], "close order " + arr[0]); + is(nb.allNotifications.length, 10 - arr[0], "close order " + arr[0] + " count"); + if (arr[0] == 6) + this.repeat = false; + return ++arr[0]; + } + }, + { + test(nb, ntf) { + var exh = false; + try { + nb.appendNotification("no", "no", "no", 0, null); + } catch (ex) { exh = true; } + SimpleTest.is(exh, true, "appendNotification priority too low"); + + exh = false; + try { + nb.appendNotification("no", "no", "no", 11, null); + } catch (ex) { exh = true; } + SimpleTest.is(exh, true, "appendNotification priority too high"); + + // check that the other priority types work properly + runTimedTests(appendPriorityTests, -1, nb, nb.PRIORITY_WARNING_LOW); + } + } +]; + +var appendPriorityTests = [ + { + test(nb, priority) { + var ntf = nb.appendNotification("Notification", "note", "happy.png", + priority, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification " + priority); + return [ntf, priority]; + }, + result(nb, obj) { + SimpleTest.is(obj[0].priority, obj[1], "notification.priority " + obj[1]); + return obj[1]; + } + }, + { + test(nb, priority) { + nb.removeCurrentNotification(); + return priority; + }, + result(nb, priority) { + if (priority == nb.PRIORITY_CRITICAL_HIGH) { + let ntf = nb.appendNotification("Notification", "note", "happy.png", + nb.PRIORITY_INFO_LOW, testtag_notificationbox_buttons); + setTimeout(checkPopupTest, 50, nb, ntf); + } + else { + runTimedTests(appendPriorityTests, -1, nb, ++priority); + } + } + } +]; + +function testtag_notificationbox_State(nb, testid, expecteditem, expectedcount) +{ + SimpleTest.is(nb.currentNotification, expecteditem, testid + " currentNotification"); + SimpleTest.is(nb.allNotifications ? nb.allNotifications.length : "no value", + expectedcount, testid + " allNotifications"); +} + +function testtag_notification_State(nb, ntf, testid, label, value, image, priority) +{ + is(ntf.messageText.textContent, label, testid + " notification label"); + is(ntf.getAttribute("value"), value, testid + " notification value"); + is(ntf.messageImage.getAttribute("src"), image, testid + " notification image"); + is(ntf.priority, priority, testid + " notification priority"); + + var type; + switch (priority) { + case nb.PRIORITY_INFO_LOW: + case nb.PRIORITY_INFO_MEDIUM: + case nb.PRIORITY_INFO_HIGH: + type = "info"; + break; + case nb.PRIORITY_WARNING_LOW: + case nb.PRIORITY_WARNING_MEDIUM: + case nb.PRIORITY_WARNING_HIGH: + type = "warning"; + break; + case nb.PRIORITY_CRITICAL_LOW: + case nb.PRIORITY_CRITICAL_MEDIUM: + case nb.PRIORITY_CRITICAL_HIGH: + type = "critical"; + break; + } + + is(ntf.getAttribute("type"), type, testid + " notification type"); +} + +function checkPopupTest(nb, ntf) +{ + if (nb._animating) { + setTimeout(checkPopupTest, ntf); + } else { + var evt = new Event(""); + ntf.dispatchEvent(evt); + evt.target.buttonInfo = testtag_notificationbox_buttons[0]; + ntf._doButtonCommand(evt); + } +} + +function checkPopupClosed() +{ + is(document.popupNode, null, "popupNode null after popup is closed"); + SimpleTest.finish(); +} + +/** + * run one or more tests which perform a test operation, wait for a delay, + * then perform a result operation. + * + * tests - array of objects where each object is : + * { + * test: test function, + * result: result function + * repeat: true to repeat the test + * } + * idx - starting index in tests + * element - element to run tests on + * arg - argument to pass between test functions + * + * If, after executing the result part, the repeat property of the test is + * true, then the test is repeated. If the repeat property is not true, + * continue on to the next test. + * + * The test and result functions take two arguments, the element and the arg. + * The test function may return a value which will passed to the result + * function as its arg. The result function may also return a value which + * will be passed to the next repetition or the next test in the array. + */ +function runTimedTests(tests, idx, element, arg) +{ + if (idx >= 0 && "result" in tests[idx]) + arg = tests[idx].result(element, arg); + + // if not repeating, move on to the next test + if (idx == -1 || !tests[idx].repeat) + idx++; + + if (idx < tests.length) { + var result = tests[idx].test(element, arg); + setTimeout(runTimedTestsWait, 50, tests, idx, element, result); + } +} + +function runTimedTestsWait(tests, idx, element, arg) +{ + // use this secret property to check if the animation is still running. If it + // is, then the notification hasn't fully opened or closed yet + if (element._animating) + setTimeout(runTimedTestsWait, 50, tests, idx, element, arg); + else + runTimedTests(tests, idx, element, arg); +} + +setTimeout(() => { + testtag_notificationbox(new MozElements.NotificationBox(e => { + document.getElementById("nb").appendChild(e); + })); +}, 0); +]]> +</script> + +</window> + diff --git a/toolkit/content/tests/chrome/test_panel.xhtml b/toolkit/content/tests/chrome/test_panel.xhtml new file mode 100644 index 0000000000..66cba3f232 --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Panel Tests" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_panel.xhtml", "_blank", "chrome,left=200,top=200,width=200,height=200,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_panel_anchoradjust.xhtml b/toolkit/content/tests/chrome/test_panel_anchoradjust.xhtml new file mode 100644 index 0000000000..4bcf3292e7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel_anchoradjust.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Test Panel Position When Anchor Changes" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_panel_anchoradjust.xhtml", "_blank", "chrome,left=200,top=200,width=200,height=200,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_panel_focus.xhtml b/toolkit/content/tests/chrome/test_panel_focus.xhtml new file mode 100644 index 0000000000..87dc6ec140 --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel_focus.xhtml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Panel Focus Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +// use a chrome window for this test as the focus in content windows can be +// adjusted by the current selection position + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + // move the mouse so any tooltips that might be open go away, otherwise this + // test can fail on Mac + synthesizeMouse(document.documentElement, 1, 1, { type: "mousemove" }); + + window.openDialog("window_panel_focus.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_panelfrommenu.xhtml b/toolkit/content/tests/chrome/test_panelfrommenu.xhtml new file mode 100644 index 0000000000..4a00dc58ab --- /dev/null +++ b/toolkit/content/tests/chrome/test_panelfrommenu.xhtml @@ -0,0 +1,118 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Open panel from menuitem" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This test does the following: + 1. Opens the menu, causing the popupshown event to fire, which will call menuOpened. + 2. Keyboard events are fired to cause the first item on the menu to be executed. + 3. The command event handler for the first menuitem opens the panel. + 4. As a menuitem was executed, the menu will roll up, hiding it. + 5. The popuphidden event for the menu calls menuClosed which tests the popup states. + 6. The panelOpened function tests the popup states again and hides the popup. + 7. Once the panel's popuphidden event fires, tests are performed to see if + panels inside buttons and toolbarbuttons work. Each is opened and the closed. + --> + +<menu id="menu" onpopupshown="menuOpened()" onpopuphidden="menuClosed();"> + <menupopup> + <menuitem id="i1" label="One" oncommand="$('panel').openPopup($('menu'), 'after_start');"/> + <menuitem id="i2" label="Two"/> + </menupopup> +</menu> + +<panel id="hiddenpanel" hidden="true"/> + +<panel id="panel" onpopupshown="panelOpened()" + onpopuphidden="$('button').focus(); $('button').open = true"> + <html:input/> +</panel> + +<button id="button" type="menu" label="Button"> + <panel onpopupshown="panelOnButtonOpened(this)" + onpopuphidden="$('tbutton').open = true;"> + <button label="OK" oncommand="this.parentNode.parentNode.open = false"/> + </panel> +</button> + +<toolbarbutton id="tbutton" type="menu" label="Toolbarbutton"> + <panel onpopupshown="panelOnToolbarbuttonOpened(this)" + onpopuphidden="SimpleTest.finish()"> + <html:input/> + </panel> +</toolbarbutton> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + is($("hiddenpanel").state, "closed", "hidden popup is closed"); + + var menu = $("menu"); + menu.open = true; +} + +function menuOpened() +{ + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); +} + +function menuClosed() +{ + // the panel will be open at this point, but the popupshown event + // still needs to fire + is($("panel").state, "showing", "panel is open after menu hide"); + is($("menu").menupopup.state, "closed", "menu is closed after menu hide"); +} + +function panelOpened() +{ + is($("panel").state, "open", "panel is open"); + is($("menu").menupopup.state, "closed", "menu is closed"); + $("panel").hidePopup(); +} + +function panelOnButtonOpened(panel) +{ + is(panel.state, 'open', 'button panel is open'); + is(document.activeElement, document.documentElement, "focus blurred on panel from button open"); + synthesizeKey("KEY_ArrowDown"); + is(document.activeElement, document.documentElement, "focus not modified on cursor down from button"); + panel.firstChild.doCommand() +} + +function panelOnToolbarbuttonOpened(panel) +{ + is(panel.state, 'open', 'toolbarbutton panel is open'); + is(document.activeElement, document.documentElement, "focus blurred on panel from toolbarbutton open"); + panel.firstChild.focus(); + synthesizeKey("KEY_ArrowDown"); + is(document.activeElement, panel.firstChild, "focus not modified on cursor down from toolbarbutton"); + panel.parentNode.open = false; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_anchor.xhtml b/toolkit/content/tests/chrome/test_popup_anchor.xhtml new file mode 100644 index 0000000000..8825e8fd14 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_anchor.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Anchor Tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_popup_anchor.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_anchoratrect.xhtml b/toolkit/content/tests/chrome/test_popup_anchoratrect.xhtml new file mode 100644 index 0000000000..ba37dfda3e --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_anchoratrect.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Button Popup Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_popup_anchoratrect.xhtml", "_blank", "chrome,width=200,height=200,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_attribute.xhtml b/toolkit/content/tests/chrome/test_popup_attribute.xhtml new file mode 100644 index 0000000000..e6b22ca7df --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_attribute.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Attribute Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_popup_attribute.xhtml", "_blank", "width=600,height=700"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_button.xhtml b/toolkit/content/tests/chrome/test_popup_button.xhtml new file mode 100644 index 0000000000..decc0f5d25 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_button.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Button Popup Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.open("window_popup_button.xhtml", "_blank", "width=700,height=700"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_coords.xhtml b/toolkit/content/tests/chrome/test_popup_coords.xhtml new file mode 100644 index 0000000000..86c3f6158a --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_coords.xhtml @@ -0,0 +1,91 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Coordinate Tests" + onload="setTimeout(openThePopup, 0, 'outer');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<deck style="margin-top: 5px; padding-top: 5px;"> + <label id="outer" popup="outerpopup" value="Popup"/> +</deck> + +<panel id="outerpopup" + onpopupshowing="popupShowingEventOccurred(event);" + onpopupshown="eventOccurred(event); openThePopup('inner')" + onpopuphiding="eventOccurred(event);" + onpopuphidden="eventOccurred(event); SimpleTest.finish();"> + <button id="item1" label="First"/> + <label id="inner" value="Second" popup="innerpopup"/> + <button id="item2" label="Third"/> +</panel> + +<menupopup id="innerpopup" + onpopupshowing="popupShowingEventOccurred(event);" + onpopupshown="eventOccurred(event); event.target.hidePopup();" + onpopuphiding="eventOccurred(event);" + onpopuphidden="eventOccurred(event); document.getElementById('outerpopup').hidePopup();"> + <menuitem id="inner1" label="Inner First"/> + <menuitem id="inner2" label="Inner Second"/> +</menupopup> + +<script> +SimpleTest.waitForExplicitFinish(); + +function openThePopup(id) +{ + if (id == "inner") + document.getElementById("item1").focus(); + + var trigger = document.getElementById(id); + synthesizeMouse(trigger, 4, 5, { }); +} + +function eventOccurred(event) +{ + var testname = event.type + " on " + event.target.id + " "; + ok(event instanceof MouseEvent, testname + "is a mouse event"); + is(event.clientX, 0, testname + "clientX"); + is(event.clientY, 0, testname + "clientY"); + is(event.rangeParent, null, testname + "rangeParent"); + is(event.rangeOffset, 0, testname + "rangeOffset"); +} + +function popupShowingEventOccurred(event) +{ + // the popupshowing event should have the event coordinates and + // range position filled in. + var testname = "popupshowing on " + event.target.id + " "; + ok(event instanceof MouseEvent, testname + "is a mouse event"); + + var trigger = document.getElementById(event.target.id == "outerpopup" ? "outer" : "inner"); + var rect = trigger.getBoundingClientRect(); + is(event.clientX, Math.round(rect.left + 4), testname + "clientX"); + is(event.clientY, Math.round(rect.top + 5), testname + "clientY"); + // rangeOffset should be just after the trigger element. As rangeOffset + // considers the zeroth position to be before the first element, the value + // should be one higher than its index within its parent. + is(event.rangeParent, trigger.parentNode, testname + "rangeParent"); + is(event.rangeOffset, Array.prototype.indexOf.call(trigger.parentNode.childNodes, trigger) + 1, testname + "rangeOffset"); + + var popuprect = event.target.getBoundingClientRect(); + is(Math.round(popuprect.left), Math.round(rect.left + 4), "popup left"); + is(Math.round(popuprect.top), Math.round(rect.top + 5), "popup top"); + ok(popuprect.width > 0, "popup width"); + ok(popuprect.height > 0, "popup height"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_keys.xhtml b/toolkit/content/tests/chrome/test_popup_keys.xhtml new file mode 100644 index 0000000000..09224f4582 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_keys.xhtml @@ -0,0 +1,143 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu ignorekeys Test" + onkeydown="keyDown()" onkeypress="gKeyPressCount++; event.stopPropagation(); event.preventDefault();" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This test checks that the ignorekeys attribute can be used on a menu to + disable key navigation. The test is performed twice by opening the menu, + simulating a cursor down key, and closing the popup. When keys are enabled, + the first item on the menu should be highlighted, otherwise the first item + should not be highlighted. + --> + +<menupopup id="popup"> + <menuitem id="i1" label="One" onDOMAttrModified="attrModified(event)"/> + <menuitem id="i2" label="Two"/> + <menuitem id="i3" label="Three"/> + <menuitem id="i4" label="Four"/> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gIgnoreKeys = false; +var gIgnoreAttrChange = false; +var gKeyPressCount = 0; + + +function waitForEvent(target, eventName) { + return new Promise(resolve => { + target.addEventListener(eventName, function eventOccurred(event) { + resolve(); + }, { once: true}); + }); +} + +function runTests() +{ + (async function() { + var popup = $("popup"); + is(popup.hasAttribute("ignorekeys"), false, "keys enabled"); + + let popupShownPromise = waitForEvent(popup, "popupshown"); + popup.openPopup(null, "after_start"); + await popupShownPromise; + + let popupHiddenPromise = waitForEvent(popup, "popuphidden"); + synthesizeKey("KEY_ArrowDown"); + await popupHiddenPromise; + + is(gKeyPressCount, 0, "keypresses with ignorekeys='false'"); + + gIgnoreKeys = true; + popup.setAttribute("ignorekeys", "true"); + // clear this first to avoid confusion + gIgnoreAttrChange = true; + $("i1").removeAttribute("_moz-menuactive") + gIgnoreAttrChange = false; + + popupShownPromise = waitForEvent(popup, "popupshown"); + popup.openPopup(null, "after_start"); + await popupShownPromise; + + synthesizeKey("KEY_ArrowDown"); + + await new Promise(resolve => setTimeout(() => resolve(), 1000)); + popupHiddenPromise = waitForEvent(popup, "popuphidden"); + popup.hidePopup(); + await popupHiddenPromise; + + is(gKeyPressCount, 1, "keypresses with ignorekeys='true'"); + + popup.setAttribute("ignorekeys", "shortcuts"); + // clear this first to avoid confusion + gIgnoreAttrChange = true; + $("i1").removeAttribute("_moz-menuactive") + gIgnoreAttrChange = false; + + popupShownPromise = waitForEvent(popup, "popupshown"); + popup.openPopup(null, "after_start"); + await popupShownPromise; + + // When ignorekeys="shortcuts", T should be handled but accel+T should propagate. + sendString("t"); + is(gKeyPressCount, 1, "keypresses after t pressed with ignorekeys='shortcuts'"); + + synthesizeKey("t", { accelKey: true }); + is(gKeyPressCount, 2, "keypresses after accel+t pressed with ignorekeys='shortcuts'"); + + popupHiddenPromise = waitForEvent(popup, "popuphidden"); + popup.hidePopup(); + await popupHiddenPromise; + + SimpleTest.finish(); + })(); +} + +function attrModified(event) +{ + if (gIgnoreAttrChange || event.attrName != "_moz-menuactive") + return; + + // the attribute should not be changed when ignorekeys is enabled + if (gIgnoreKeys) { + ok(false, "move key with keys disabled"); + } + else { + is($("i1").getAttribute("_moz-menuactive"), "true", "move key with keys enabled"); + $("popup").hidePopup(); + } +} + +function keyDown() +{ + // when keys are enabled, the menu should have stopped propagation of the + // event, so a bubbling listener for a keydown event should only occur + // when keys are disabled. + ok(gIgnoreKeys, "key listener fired with keys " + + (gIgnoreKeys ? "disabled" : "enabled")); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml new file mode 100644 index 0000000000..126ff7415f --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<vbox align="start"> + <button id="button1" label="Button 1" style="margin-top: 60px;"/> + <button id="button2" label="Button 2" style="margin-top: 70px;"/> +</vbox> + +<menupopup id="popup" onpopupshown="popupshown()" onpopuphidden="SimpleTest.finish()"> + <menuitem label="One"/> + <menuitem label="Two"/> +</menupopup> + +<script> +SimpleTest.waitForExplicitFinish(); + +function runTest(id) +{ + $("popup").openPopup($("button1"), "after_start"); +} + +function popupshown() +{ + var popup = $("popup"); + var popupheight = popup.getBoundingClientRect().height; + var button1rect = $("button1").getBoundingClientRect(); + var button2rect = $("button2").getBoundingClientRect(); + + checkCoords(popup, button1rect.left, button1rect.bottom, "initial"); + + popup.moveToAnchor($("button1"), "after_start", 0, 8); + checkCoords(popup, button1rect.left, button1rect.bottom + 8, "move anchor top + 8"); + + popup.moveToAnchor($("button1"), "after_start", 6, -10); + checkCoords(popup, button1rect.left + 6, button1rect.bottom - 10, "move anchor left + 6, top - 10"); + + popup.moveToAnchor($("button1"), "before_start", -2, 0); + checkCoords(popup, button1rect.left - 2, button1rect.top - popupheight, "move anchor before_start"); + + popup.moveToAnchor($("button2"), "before_start"); + checkCoords(popup, button2rect.left, button2rect.top - popupheight, "move button2"); + + popup.moveToAnchor($("button1"), "end_before"); + checkCoords(popup, button1rect.right, button1rect.top, "move anchor end_before"); + + popup.moveToAnchor($("button2"), "after_start", 5, 4); + checkCoords(popup, button2rect.left + 5, button2rect.bottom + 4, "move button2 left + 5, top + 4"); + + popup.moveTo($("button1").screenX + 10, $("button1").screenY + 12); + checkCoords(popup, button1rect.left + 10, button1rect.top + 12, "move to button1 screen with offset"); + + popup.moveToAnchor($("button1"), "after_start", 1, 2); + checkCoords(popup, button1rect.left + 1, button1rect.bottom + 2, "move button2 after screen"); + + popup.hidePopup(); +} + +function checkCoords(popup, expectedx, expectedy, testid) +{ + var rect = popup.getBoundingClientRect(); + is(Math.round(rect.left), Math.round(expectedx), testid + " left"); + is(Math.round(rect.top), Math.round(expectedy), testid + " top"); +} + +SimpleTest.waitForFocus(runTest); + +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_preventdefault.xhtml b/toolkit/content/tests/chrome/test_popup_preventdefault.xhtml new file mode 100644 index 0000000000..916fb7eb86 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_preventdefault.xhtml @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Prevent Default Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<!-- + This tests checks that preventDefault can be called on a popupshowing + event and that preventDefault has no effect for the popuphiding event. + --> + +<script> +SimpleTest.waitForExplicitFinish(); + +var gBlockShowing = true; +var gShownNotAllowed = true; + +function runTest() +{ + document.getElementById("menu").open = true; +} + +function popupShowing(event) +{ + if (gBlockShowing) { + event.preventDefault(); + gBlockShowing = false; + setTimeout(function() { + gShownNotAllowed = false; + document.getElementById("menu").open = true; + }, 3000, true); + } +} + +function popupShown() +{ + ok(!gShownNotAllowed, "popupshowing preventDefault"); + document.getElementById("menu").open = false; +} + +function popupHiding(event) +{ + // since this is a content test, preventDefault should have no effect + event.preventDefault(); +} + +function popupHidden() +{ + ok(true, "popuphiding preventDefault not allowed"); + SimpleTest.finish(); +} +</script> + +<button id="menu" type="menu" label="Menu"> + <menupopup onpopupshowing="popupShowing(event);" + onpopupshown="popupShown();" + onpopuphiding="popupHiding(event);" + onpopuphidden="popupHidden();"> + <menuitem label="Item"/> + </menupopup> +</button> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_preventdefault_chrome.xhtml b/toolkit/content/tests/chrome/test_popup_preventdefault_chrome.xhtml new file mode 100644 index 0000000000..d3e62d05c7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_preventdefault_chrome.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Attribute Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_popup_preventdefault_chrome.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_recreate.xhtml b/toolkit/content/tests/chrome/test_popup_recreate.xhtml new file mode 100644 index 0000000000..e82a8f07a4 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_recreate.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Recreate Test" + onload="setTimeout(init, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This is a test for bug 388361. + + This test checks that a menulist's popup is properly created and sized when + the popup node is removed and another added in its place. + + --> + +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +var gState = "before"; + +function init() +{ + document.getElementById("menulist").open = true; +} + +function isWithinHalfPixel(a, b) +{ + return Math.abs(a - b) <= 0.5; +} + +function recreate() +{ + if (gState == "before") { + var element = document.getElementById("menulist"); + while (element.hasChildNodes()) + element.firstChild.remove(); + element.appendItem("Cat"); + gState = "after"; + document.getElementById("menulist").open = true; + } + else { + SimpleTest.finish(); + } +} + +function checkSize() +{ + var menulist = document.getElementById("menulist"); + var menurect = menulist.getBoundingClientRect(); + var popuprect = menulist.menupopup.getBoundingClientRect(); + + let marginLeft = parseFloat(getComputedStyle(menulist.menupopup).marginLeft); + ok(isWithinHalfPixel(menurect.left + marginLeft, popuprect.left), "left position " + gState); + ok(isWithinHalfPixel(menurect.right + marginLeft, popuprect.right), "right position " + gState); + ok(Math.round(popuprect.right) - Math.round(popuprect.left) > 0, "height " + gState) + document.getElementById("menulist").open = false; +} +]]> +</script> + +<hbox align="center" pack="center"> + <menulist id="menulist" onpopupshown="checkSize();" onpopuphidden="recreate();"> + <menupopup position="after_start"> + <menuitem label="Cat"/> + </menupopup> + </menulist> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_scaled.xhtml b/toolkit/content/tests/chrome/test_popup_scaled.xhtml new file mode 100644 index 0000000000..fde0c24a41 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_scaled.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popups in Scaled Content" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- This test checks that the position is correct in two cases: + - a popup anchored at an element in a scaled document + - a popup opened at a screen coordinate in a scaled window + --> + +<iframe id="frame" width="60" height="140" + src="data:text/html,<html><body><input size='4' id='one'><input size='4' id='two'></body></html>"/> + +<menupopup id="popup" onpopupshown="shown()" onpopuphidden="nextTest()"> + <menuitem label="One"/> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var screenTest = false; +var screenx = -1, screeny = -1; + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + setScale($("frame").contentWindow, 2); + + var anchor = $("frame").contentDocument.getElementById("two"); + anchor.getBoundingClientRect(); // flush to update display after scale change + $("popup").openPopup(anchor, "after_start"); +} + +function setScale(win, scale) +{ + SpecialPowers.setFullZoom(win, scale); +} + +function shown() +{ + var popup = $("popup"); + if (screenTest) { + is(popup.screenX, screenx, "screen left position"); + is(popup.screenY, screeny, "screen top position"); + } + else { + var anchor = $("frame").contentDocument.getElementById("two"); + + is(Math.round(anchor.getBoundingClientRect().left * 2), + Math.round(popup.getBoundingClientRect().left), "anchored left position"); + is(Math.round(anchor.getBoundingClientRect().bottom * 2), + Math.round(popup.getBoundingClientRect().top), "anchored top position"); + } + + popup.hidePopup(); +} + +function nextTest() +{ + if (screenTest) { + setScale(window, 1); + SimpleTest.finish(); + } + else { + screenTest = true; + var rootElement = document.documentElement; + let x, y; + + // - the iframe is at 4×, but out here css pixels are only 2× device pixels + // - the popup manager rounds off (or truncates) the coordinates to + // integers, so ensure we pass in even numbers to openPopupAtScreen + screenx = (x = even(rootElement.screenX + 120))/2; + screeny = (y = even(rootElement.screenY + 120))/2; + setScale(window, 2); + $("popup").openPopupAtScreen(x, y); + } +} + +function even(n) +{ + return (n % 2) ? n+1 : n; +} +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_tree.xhtml b/toolkit/content/tests/chrome/test_popup_tree.xhtml new file mode 100644 index 0000000000..23a37e339d --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_tree.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tree in Popup Test" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<panel id="panel" onpopupshown="treeClick()" onpopuphidden="SimpleTest.finish()"> + <tree id="tree" width="350" rows="5"> + <treecols> + <treecol id="name" label="Name" flex="1"/> + <treecol id="address" label="Street" flex="1"/> + </treecols> + <treechildren id="treechildren"> + <treeitem> + <treerow> + <treecell label="Justin Thyme"/> + <treecell label="800 Bay Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary Goround"/> + <treecell label="47 University Avenue"/> + </treerow> + </treeitem> + </treechildren> + </tree> +</panel> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + $("panel").openPopup(null, "overlap", 2, 2); +} + +function treeClick() +{ + var tree = $("tree"); + is(tree.currentIndex, -1, "selectedIndex before click"); + synthesizeMouseExpectEvent($("treechildren"), 2, 2, { }, $("treechildren"), "click", ""); + is(tree.currentIndex, 0, "selectedIndex after click"); + + var rect = tree.getCoordsForCellItem(1, tree.columns.address, ""); + synthesizeMouseExpectEvent($("treechildren"), rect.x, rect.y + 2, + { }, $("treechildren"), "click", ""); + is(tree.currentIndex, 1, "selectedIndex after second click " + rect.x + "," + rect.y); + + $("panel").hidePopup(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popuphidden.xhtml b/toolkit/content/tests/chrome/test_popuphidden.xhtml new file mode 100644 index 0000000000..1240551807 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popuphidden.xhtml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Hidden Popup Test" + onload="setTimeout(runTests, 0, $('popup'));" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<menupopup id="popup" hidden="true" onpopupshown="ok(true, 'popupshown'); this.hidePopup()" + onpopuphidden="$('popup-hideonshow').openPopup(null, 'after_start')"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> +</menupopup> + +<menupopup id="popup-hideonshow" onpopupshowing="hidePopupWhileShowing(this)" + onpopupshown="ok(false, 'popupshown when hidden')"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> +</menupopup> + +<button id="button" type="menu" label="Menu" onDOMAttrModified="checkEndTest(event)"> + <menupopup id="popupinbutton" hidden="true" + onpopupshown="ok(true, 'popupshown'); ok($('button').open, 'open'); this.hidden = true;"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> + </menupopup> +</button> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests(popup) +{ + popup.hidden = false; + popup.openPopup(null, "after_start"); +} + +function hidePopupWhileShowing(popup) +{ + popup.hidden = true; + popup.clientWidth; // flush layout + is(popup.state, 'closed', 'popupshowing hidden'); + SimpleTest.executeSoon(() => runTests($('popupinbutton'))); +} + +function checkEndTest(event) +{ + var button = $("button"); + if (event.originalTarget != button || event.attrName != 'open' || event.attrChange != event.REMOVAL) + return; + + ok($("popupinbutton").hidden, "popup hidden"); + is($("popupinbutton").state, "closed", "popup state"); + ok(!button.open, "not open after hidden"); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popupincontent.xhtml b/toolkit/content/tests/chrome/test_popupincontent.xhtml new file mode 100644 index 0000000000..0994454cdd --- /dev/null +++ b/toolkit/content/tests/chrome/test_popupincontent.xhtml @@ -0,0 +1,137 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup in Content Positioning Tests" + onload="setTimeout(nextTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This test checks that popups in content areas don't extend past the content area. + --> + +<hbox> + <spacer width="100"/> + <menu id="menu" label="Menu"> + <menupopup style="margin:10px;" id="popup" onpopupshown="popupShown()" onpopuphidden="nextTest()"> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="A final longer label that is actually quite long. Very long indeed."/> + </menupopup> + </menu> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var step = ""; +var originalHeight = -1; + +function nextTest() +{ + // there are five tests here: + // openPopupAtScreen - checks that opening a popup using openPopupAtScreen + // constrains the popup to the content area + // left and top - check with the left and top attributes set + // open near bottom - open the menu near the bottom of the window + // large menu - try with a menu that is very large and should be scaled + // shorter menu again - try with a menu that is shorter again. It should have + // the same height as the 'left and top' test + var popup = $("popup"); + var menu = $("menu"); + switch (step) { + case "": + step = "openPopupAtScreen"; + popup.openPopupAtScreen(1000, 1200); + break; + case "openPopupAtScreen": + step = "left and top"; + popup.setAttribute("left", "800"); + popup.setAttribute("top", "2900"); + synthesizeMouse(menu, 2, 2, { }); + break; + case "left and top": + step = "open near bottom"; + // request that the menu be opened with a target point near the bottom of the window, + // so that the menu's top margin will push it completely outside the window. + popup.setAttribute("top", document.documentElement.screenY + window.innerHeight - 5); + synthesizeMouse(menu, 2, 2, { }); + break; + case "open near bottom": + step = "large menu"; + popup.removeAttribute("left"); + popup.removeAttribute("top"); + for (let i = 0; i < 80; i++) + menu.appendItem("Test", ""); + synthesizeMouse(menu, 2, 2, { }); + break; + case "large menu": + step = "shorter menu again"; + for (let i = 0; i < 80; i++) + popup.lastChild.remove(); + synthesizeMouse(menu, 2, 2, { }); + break; + case "shorter menu again": + SimpleTest.finish(); + break; + } +} + +async function popupShown() +{ + // Popup may have wrong initial size in non e10s mode tests, because + // layout is not yet ready for popup content lazy population on + // popupshowing event. + await new Promise(r => + requestAnimationFrame(() => requestAnimationFrame(r)) + ); + + var windowrect = document.documentElement.getBoundingClientRect(); + var popuprect = $("popup").getBoundingClientRect(); + + // subtract one off the edge due to a rounding issue + ok(popuprect.left >= windowrect.left, step + " left"); + ok(popuprect.right - 1 <= windowrect.right, step + " right"); + + if (step == "left and top") { + originalHeight = popuprect.bottom - popuprect.top; + } + else if (step == "open near bottom") { + // check that the menu flipped up so it's above our requested point + ok(popuprect.bottom - 1 <= windowrect.bottom - 5, step + " bottom"); + } + else if (step == "large menu") { + // add 10 to account for the margin + is(popuprect.top, $("menu").getBoundingClientRect().bottom + 10, step + " top"); + ok(popuprect.bottom == windowrect.bottom || + popuprect.bottom - 1 == windowrect.bottom, step + " bottom"); + } + else { + ok(popuprect.top >= windowrect.top, step + " top"); + ok(popuprect.bottom - 1 <= windowrect.bottom, step + " bottom"); + if (step == "shorter menu again") + is(popuprect.bottom - popuprect.top, originalHeight, step + " height shortened"); + } + + $("menu").open = false; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popupremoving.xhtml b/toolkit/content/tests/chrome/test_popupremoving.xhtml new file mode 100644 index 0000000000..0e5d3fc275 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popupremoving.xhtml @@ -0,0 +1,165 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Removing Tests" + onload="setTimeout(nextTest, 0)" + onDOMAttrModified="modified(event)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<!-- + This test checks that popup elements can be removed in various ways without + crashing. It tests two situations, one with menus that are 'separate', and + one with menus that are 'nested'. In each case, there are four levels of menu. + + The nextTest function starts the process by opening the first menu. A set of + popupshown event listeners are used to open the next menu until all four are + showing. This last one calls removePopup to remove the menu node from the + tree. This should hide the popups as they are no longer in a document. + + A mutation listener is triggered when the fourth menu closes by having its + open attribute cleared. This listener hides the third popup which causes + its frame to be removed. Naturally, we want to ensure that this doesn't + crash when the third menu is removed. + --> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<hbox> + +<menu id="nestedmenu1" label="1"> + <menupopup id="nestedpopup1" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu2" label="2"> + <menupopup id="nestedpopup2" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu3" label="3"> + <menupopup id="nestedpopup3" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu4" label="4" onpopupshown="removePopups()"> + <menupopup id="nestedpopup4"> + <menuitem label="Nested 1"/> + <menuitem label="Nested 2"/> + <menuitem label="Nested 3"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> +</menu> + +<menu id="separatemenu1" label="1"> + <menupopup id="separatepopup1" onpopupshown="$('separatemenu2').open = true"> + <menuitem label="L1 One"/> + <menuitem label="L1 Two"/> + <menuitem label="L1 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu2" label="2"> + <menupopup id="separatepopup2" onpopupshown="$('separatemenu3').open = true" + onpopuphidden="popup2Hidden()"> + <menuitem label="L2 One"/> + <menuitem label="L2 Two"/> + <menuitem label="L2 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu3" label="3" onpopupshown="$('separatemenu4').open = true"> + <menupopup id="separatepopup3"> + <menuitem label="L3 One"/> + <menuitem label="L3 Two"/> + <menuitem label="L3 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu4" label="4" onpopupshown="removePopups()" + onpopuphidden="$('separatemenu2').open = false"> + <menupopup id="separatepopup3"> + <menuitem label="L4 One"/> + <menuitem label="L4 Two"/> + <menuitem label="L4 Three"/> + </menupopup> +</menu> + +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gKey = ""; +let gTriggerMutation = null; +let gChangeMutation = null; + +function nextTest() +{ + if (gKey == "") { + gKey = "separate"; + } + else if (gKey == "separate") { + gKey = "nested"; + } + else { + SimpleTest.finish(); + return; + } + + $(gKey + "menu1").open = true; +} + +function modified(event) +{ + // use this mutation listener to hide the third popup, destroying its frame. + // It gets triggered when the open attribute is cleared on the fourth menu. + + if (event.target == gTriggerMutation && + event.attrName == "open") { + gChangeMutation.hidden = true; + // force a layout flush + document.documentElement.clientWidth; + gTriggerMutation = null; + gChangeMutation = null; + } +} + +function removePopups() +{ + var menu2 = $(gKey + "menu2"); + var menu3 = $(gKey + "menu3"); + is(menu2.getAttribute("open"), "true", gKey + " menu 2 open before"); + is(menu3.getAttribute("open"), "true", gKey + " menu 3 open before"); + + gTriggerMutation = menu3; + gChangeMutation = $(gKey + "menu4"); + var menu = $(gKey + "menu1"); + menu.remove(); + + if (gKey == "nested") { + // the 'separate' test checks this during the popup2 hidden event handler + is(menu2.hasAttribute("open"), false, gKey + " menu 2 open after"); + is(menu3.hasAttribute("open"), false, gKey + " menu 3 open after"); + nextTest(); + } +} + +function popup2Hidden() +{ + is($(gKey + "menu2").hasAttribute("open"), false, gKey + " menu 2 open after"); + nextTest(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popupremoving_frame.xhtml b/toolkit/content/tests/chrome/test_popupremoving_frame.xhtml new file mode 100644 index 0000000000..5c81f9f52e --- /dev/null +++ b/toolkit/content/tests/chrome/test_popupremoving_frame.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Unload Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<!-- + This test checks that popup elements are removed when the document is changed. + --> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<iframe id="frame" width="300" height="150" src="frame_popupremoving_frame.xhtml"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gMenus = []; + +function popupsOpened() +{ + var framedoc = $("frame").contentDocument; + framedoc.addEventListener("DOMAttrModified", modified); + + // this is the order in which the menus should be hidden (reverse of the + // order they were opened in). The second menu is removed during the + // mutation listener, so gets the event afterwards. + gMenus.push(framedoc.getElementById("nestedmenu4")); + gMenus.push(framedoc.getElementById("nestedmenu2")); + gMenus.push(framedoc.getElementById("nestedmenu3")); + gMenus.push(framedoc.getElementById("nestedmenu1")); + gMenus.push(framedoc.getElementById("separatemenu4")); + gMenus.push(framedoc.getElementById("separatemenu2")); + gMenus.push(framedoc.getElementById("separatemenu3")); + gMenus.push(framedoc.getElementById("separatemenu1")); + + framedoc.location = "about:blank"; +} + +function modified(event) +{ + if (event.attrName != "open") + return; + + var framedoc = $("frame").contentDocument; + + var tohide = null; + if (event.target.id == "separatemenu3") + tohide = framedoc.getElementById("separatemenu2"); + else if (event.target.id == "nestedmenu3") + tohide = framedoc.getElementById("nestedmenu2"); + + if (tohide) { + tohide.hidden = true; + // force a layout flush + $("frame").contentDocument.documentElement.clientWidth; + } + + is(event.target, gMenus.shift(), event.target.id + " hidden"); + if (!gMenus.length) + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_position.xhtml b/toolkit/content/tests/chrome/test_position.xhtml new file mode 100644 index 0000000000..3425bff18d --- /dev/null +++ b/toolkit/content/tests/chrome/test_position.xhtml @@ -0,0 +1,133 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for positioning + --> +<window title="position" width="500" height="600" + onload="setTimeout(runTest, 0);" + style="margin: 0; border: 0; padding; 0;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + +<hbox id="box1"> + <button label="0" width="100" height="40" style="margin: 3px;"/> +</hbox> +<scrollbox id="box2" orient="vertical" align="start" width="200" height="50" + style="overflow: hidden; margin-left: 2px; padding: 1px;"> + <deck> + <scrollbox id="box3" orient="vertical" align="start" height="100" + style="overflow: scroll; margin: 1px; padding: 0;"> + <vbox id="innerscroll" width="200" align="start"> + <button id="button1" label="1" width="90" maxwidth="100" + minheight="25" height="35" maxheight="50" + style="min-width: 80px; margin: 5px; border: 4px; padding: 7px; + -moz-appearance: none;"/> + <menu id="menu"> + <menupopup id="popup" style="-moz-appearance: none; margin:0; border: 0; padding: 0;" + onpopupshown="menuOpened()" + onpopuphidden="if (event.target == this) SimpleTest.finish()"> + <menuitem label="One"/> + <menu id="submenu" label="Three"> + <menupopup id="subpopup" style="-moz-appearance: none; margin:0; border: 0; padding: 0;" + onpopupshown="submenuOpened()"> + <menuitem label="Four"/> + </menupopup> + </menu> + </menupopup> + </menu> + <button label="2" maxwidth="100" maxheight="20" style="margin: 5px;"/> + <button label="3" maxwidth="100" maxheight="20" style="margin: 5px;"/> + <button label="4" maxwidth="100" maxheight="20" style="margin: 5px;"/> + </vbox> + <box height="200"/> + </scrollbox> + </deck> +</scrollbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTest() +{ + var winwidth = document.documentElement.getBoundingClientRect().width; + + var box1 = $("box1"); + checkPosition("box1", box1, 0, 0, winwidth, 46); + + var box2 = $("box2"); + checkPosition("box2", box2, 2, 46, winwidth, 96); + + // height is height(box1) = 46 + margin-top(box3) = 1 + margin-top(button1) = 5 + var button1 = $("button1"); + checkPosition("button1", button1, 9, 53, 99, 88); + + box2.scrollTo(7, 16); + + // clientRect height is offset from root so is 16 pixels vertically less + checkPosition("button1 scrolled", button1, 9, 37, 99, 72); + + var box3 = $("box3"); + box3.scrollTo(1, 2); + + checkPosition("button1 scrolled", button1, 9, 35, 99, 70); + + $("menu").open = true; +} + +function menuOpened() +{ + $("submenu").open = true; +} + +function submenuOpened() +{ + var menu = $("menu"); + var menuleft = Math.round(menu.getBoundingClientRect().left); + var menubottom = Math.round(menu.getBoundingClientRect().bottom); + + var submenu = $("submenu"); + var submenutop = Math.round(submenu.getBoundingClientRect().top); + var submenuright = Math.round(submenu.getBoundingClientRect().right); + + checkPosition("popup", $("popup"), menuleft, menubottom, -1, -1); + checkPosition("subpopup", $("subpopup"), submenuright, submenutop, -1, -1); + + menu.open = false; +} + +function checkPosition(testid, elem, cleft, ctop, cright, cbottom) +{ + // -1 for right or bottom means that the exact size should not be + // checked, just ensure it is larger then the left or top position + var rect = elem.getBoundingClientRect(); + is(Math.round(rect.left), cleft, testid + " client rect left"); + if (testid != "popup") + is(Math.round(rect.top), ctop, testid + " client rect top"); + if (cright >= 0) + is(Math.round(rect.right), cright, testid + " client rect right"); + else + ok(rect.right - rect.left > 20, testid + " client rect right"); + if (cbottom >= 0) + is(Math.round(rect.bottom), cbottom, testid + " client rect bottom"); + else + ok(rect.bottom - rect.top > 15, testid + " client rect bottom"); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_preferences.xhtml b/toolkit/content/tests/chrome/test_preferences.xhtml new file mode 100644 index 0000000000..ba5a9102f8 --- /dev/null +++ b/toolkit/content/tests/chrome/test_preferences.xhtml @@ -0,0 +1,539 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Preferences Window Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="RunTest();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + + const kPref = SpecialPowers.Services.prefs; + + // preference values, set 1 + const kPrefValueSet1 = + { + int: 23, + bool: true, + string: "rheeet!", + unichar: "äöüßÄÖÜ", + wstring_data: "日本語", + file_data: "/", + + wstring: Cc["@mozilla.org/pref-localizedstring;1"] + .createInstance(Ci.nsIPrefLocalizedString), + file: Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsIFile) + }; + kPrefValueSet1.wstring.data = kPrefValueSet1.wstring_data; + SafeFileInit(kPrefValueSet1.file, kPrefValueSet1.file_data); + + // preference values, set 2 + const kPrefValueSet2 = + { + int: 42, + bool: false, + string: "Mozilla", + unichar: "áôùšŽ", + wstring_data: "헤드라인A", + file_data: "/home", + + wstring: Cc["@mozilla.org/pref-localizedstring;1"] + .createInstance(Ci.nsIPrefLocalizedString), + file: Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsIFile) + }; + kPrefValueSet2.wstring.data = kPrefValueSet2.wstring_data; + SafeFileInit(kPrefValueSet2.file, kPrefValueSet2.file_data); + + + function SafeFileInit(aFile, aPath) + { + // set file path without dying for exceptions + try + { + aFile.initWithPath(aPath); + } + catch (ignored) {} + } + + function CreateEmptyPrefValueSet() + { + var result = + { + int: undefined, + bool: undefined, + string: undefined, + unichar: undefined, + wstring_data: undefined, + file_data: undefined, + wstring: undefined, + file: undefined + }; + return result; + } + + function WritePrefsToSystem(aPrefValueSet) + { + // write preference data via XPCOM + kPref.setIntPref ("tests.static_preference_int", aPrefValueSet.int); + kPref.setBoolPref("tests.static_preference_bool", aPrefValueSet.bool); + kPref.setCharPref("tests.static_preference_string", aPrefValueSet.string); + kPref.setStringPref("tests.static_preference_unichar", aPrefValueSet.unichar); + kPref.setComplexValue("tests.static_preference_wstring", + Ci.nsIPrefLocalizedString, + aPrefValueSet.wstring); + kPref.setComplexValue("tests.static_preference_file", + Ci.nsIFile, + aPrefValueSet.file); + } + + function ReadPrefsFromSystem() + { + // read preference data via XPCOM + var result = CreateEmptyPrefValueSet(); + // eslint-disable-next-line mozilla/use-default-preference-values + try {result.int = kPref.getIntPref ("tests.static_preference_int") } catch (ignored) {}; + // eslint-disable-next-line mozilla/use-default-preference-values + try {result.bool = kPref.getBoolPref("tests.static_preference_bool") } catch (ignored) {}; + // eslint-disable-next-line mozilla/use-default-preference-values + try {result.string = kPref.getCharPref("tests.static_preference_string")} catch (ignored) {}; + try + { + result.unichar = kPref.getStringPref("tests.static_preference_unichar"); + } + catch (ignored) {}; + try + { + result.wstring = kPref.getComplexValue("tests.static_preference_wstring", + Ci.nsIPrefLocalizedString); + result.wstring_data = result.wstring.data; + } + catch (ignored) {}; + try + { + result.file = kPref.getComplexValue("tests.static_preference_file", + Ci.nsIFile); + result.file_data = result.file.data; + } + catch (ignored) {}; + return result; + } + + function GetXULElement(aPrefWindow, aID) + { + return aPrefWindow.document.getElementById(aID); + } + + function GetPreference(aPrefWindow, aID) + { + return aPrefWindow.Preferences.get(aID); + } + + function WritePrefsToPreferences(aPrefWindow, aPrefValueSet) + { + // write preference data into Preference instances + GetPreference(aPrefWindow, "tests.static_preference_int" ).value = aPrefValueSet.int; + GetPreference(aPrefWindow, "tests.static_preference_bool" ).value = aPrefValueSet.bool; + GetPreference(aPrefWindow, "tests.static_preference_string" ).value = aPrefValueSet.string; + GetPreference(aPrefWindow, "tests.static_preference_unichar").value = aPrefValueSet.unichar; + GetPreference(aPrefWindow, "tests.static_preference_wstring").value = aPrefValueSet.wstring_data; + GetPreference(aPrefWindow, "tests.static_preference_file" ).value = aPrefValueSet.file_data; + } + + function ReadPrefsFromPreferences(aPrefWindow) + { + // read preference data from Preference instances + var result = + { + int: GetPreference(aPrefWindow, "tests.static_preference_int" ).value, + bool: GetPreference(aPrefWindow, "tests.static_preference_bool" ).value, + string: GetPreference(aPrefWindow, "tests.static_preference_string" ).value, + unichar: GetPreference(aPrefWindow, "tests.static_preference_unichar").value, + wstring_data: GetPreference(aPrefWindow, "tests.static_preference_wstring").value, + file_data: GetPreference(aPrefWindow, "tests.static_preference_file" ).value, + wstring: Cc["@mozilla.org/pref-localizedstring;1"] + .createInstance(Ci.nsIPrefLocalizedString), + file: Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsIFile) + } + result.wstring.data = result.wstring_data; + SafeFileInit(result.file, result.file_data); + return result; + } + + function WritePrefsToUI(aPrefWindow, aPrefValueSet) + { + // write preference data into UI elements + GetXULElement(aPrefWindow, "static_element_int" ).value = aPrefValueSet.int; + GetXULElement(aPrefWindow, "static_element_bool" ).checked = aPrefValueSet.bool; + GetXULElement(aPrefWindow, "static_element_string" ).value = aPrefValueSet.string; + GetXULElement(aPrefWindow, "static_element_unichar").value = aPrefValueSet.unichar; + GetXULElement(aPrefWindow, "static_element_wstring").value = aPrefValueSet.wstring_data; + GetXULElement(aPrefWindow, "static_element_file" ).value = aPrefValueSet.file_data; + } + + function ReadPrefsFromUI(aPrefWindow) + { + // read preference data from Preference instances + var result = + { + int: GetXULElement(aPrefWindow, "static_element_int" ).value, + bool: GetXULElement(aPrefWindow, "static_element_bool" ).checked, + string: GetXULElement(aPrefWindow, "static_element_string" ).value, + unichar: GetXULElement(aPrefWindow, "static_element_unichar").value, + wstring_data: GetXULElement(aPrefWindow, "static_element_wstring").value, + file_data: GetXULElement(aPrefWindow, "static_element_file" ).value, + wstring: Cc["@mozilla.org/pref-localizedstring;1"] + .createInstance(Ci.nsIPrefLocalizedString), + file: Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsIFile) + } + result.wstring.data = result.wstring_data; + SafeFileInit(result.file, result.file_data); + return result; + } + + + function RunInstantPrefTest(aPrefWindow) + { + // remark: there's currently no UI element binding for files + + // were all Preference instances correctly initialized? + var expected = kPrefValueSet1; + var found = ReadPrefsFromPreferences(aPrefWindow); + ok(found.int === expected.int, "instant pref init int" ); + ok(found.bool === expected.bool, "instant pref init bool" ); + ok(found.string === expected.string, "instant pref init string" ); + ok(found.unichar === expected.unichar, "instant pref init unichar"); + ok(found.wstring_data === expected.wstring_data, "instant pref init wstring"); + todo(found.file_data === expected.file_data, "instant pref init file" ); + + // were all elements correctly initialized? (loose check) + found = ReadPrefsFromUI(aPrefWindow); + ok(found.int == expected.int, "instant element init int" ); + ok(found.bool == expected.bool, "instant element init bool" ); + ok(found.string == expected.string, "instant element init string" ); + ok(found.unichar == expected.unichar, "instant element init unichar"); + ok(found.wstring_data == expected.wstring_data, "instant element init wstring"); + todo(found.file_data == expected.file_data, "instant element init file" ); + + // do some changes in the UI + expected = kPrefValueSet2; + WritePrefsToUI(aPrefWindow, expected); + + // UI changes should get passed to the Preference instances, + // but currently they aren't if the changes are made programmatically + // (the handlers preference.change/prefpane.input and prefpane.change + // are called for manual changes, though). + found = ReadPrefsFromPreferences(aPrefWindow); + todo(found.int === expected.int, "instant change pref int" ); + todo(found.bool === expected.bool, "instant change pref bool" ); + todo(found.string === expected.string, "instant change pref string" ); + todo(found.unichar === expected.unichar, "instant change pref unichar"); + todo(found.wstring_data === expected.wstring_data, "instant change pref wstring"); + todo(found.file_data === expected.file_data, "instant change pref file" ); + + // and these changes should get passed to the system instantly + // (which obviously can't pass with the above failing) + found = ReadPrefsFromSystem(); + todo(found.int === expected.int, "instant change element int" ); + todo(found.bool === expected.bool, "instant change element bool" ); + todo(found.string === expected.string, "instant change element string" ); + todo(found.unichar === expected.unichar, "instant change element unichar"); + todo(found.wstring_data === expected.wstring_data, "instant change element wstring"); + todo(found.file_data === expected.file_data, "instant change element file" ); + + // try resetting the prefs to default values (which should be empty here) + GetPreference(aPrefWindow, "tests.static_preference_int" ).reset(); + GetPreference(aPrefWindow, "tests.static_preference_bool" ).reset(); + GetPreference(aPrefWindow, "tests.static_preference_string" ).reset(); + GetPreference(aPrefWindow, "tests.static_preference_unichar").reset(); + GetPreference(aPrefWindow, "tests.static_preference_wstring").reset(); + GetPreference(aPrefWindow, "tests.static_preference_file" ).reset(); + + // check system + expected = CreateEmptyPrefValueSet(); + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "instant reset system int" ); + ok(found.bool === expected.bool, "instant reset system bool" ); + ok(found.string === expected.string, "instant reset system string" ); + ok(found.unichar === expected.unichar, "instant reset system unichar"); + ok(found.wstring_data === expected.wstring_data, "instant reset system wstring"); + ok(found.file_data === expected.file_data, "instant reset system file" ); + + // check UI + expected = + { + // alas, we don't have XUL elements with typeof(value) == int :( + // int: 0, + int: "", + bool: false, + string: "", + unichar: "", + wstring_data: "", + file_data: "", + wstring: {}, + file: {} + }; + found = ReadPrefsFromUI(aPrefWindow); + ok(found.int === expected.int, "instant reset element int" ); + ok(found.bool === expected.bool, "instant reset element bool" ); + ok(found.string === expected.string, "instant reset element string" ); + ok(found.unichar === expected.unichar, "instant reset element unichar"); + ok(found.wstring_data === expected.wstring_data, "instant reset element wstring"); +// ok(found.file_data === expected.file_data, "instant reset element file" ); + + // check hasUserValue + ok(GetPreference(aPrefWindow, "tests.static_preference_int" ).hasUserValue === false, "instant reset hasUserValue int" ); + ok(GetPreference(aPrefWindow, "tests.static_preference_bool" ).hasUserValue === false, "instant reset hasUserValue bool" ); + ok(GetPreference(aPrefWindow, "tests.static_preference_string" ).hasUserValue === false, "instant reset hasUserValue string" ); + ok(GetPreference(aPrefWindow, "tests.static_preference_unichar").hasUserValue === false, "instant reset hasUserValue unichar"); + ok(GetPreference(aPrefWindow, "tests.static_preference_wstring").hasUserValue === false, "instant reset hasUserValue wstring"); + ok(GetPreference(aPrefWindow, "tests.static_preference_file" ).hasUserValue === false, "instant reset hasUserValue file" ); + + // done with instant apply checks + } + + function RunNonInstantPrefTestGeneral(aPrefWindow) + { + // Non-instant apply tests are harder: not only do we need to check that + // fiddling with the values does *not* change the system settings, but + // also that they *are* (not) set after closing (cancelling) the dialog... + + // remark: there's currently no UI element binding for files + + // were all Preference instances correctly initialized? + var expected = kPrefValueSet1; + var found = ReadPrefsFromPreferences(aPrefWindow); + ok(found.int === expected.int, "non-instant pref init int" ); + ok(found.bool === expected.bool, "non-instant pref init bool" ); + ok(found.string === expected.string, "non-instant pref init string" ); + ok(found.unichar === expected.unichar, "non-instant pref init unichar"); + ok(found.wstring_data === expected.wstring_data, "non-instant pref init wstring"); + todo(found.file_data === expected.file_data, "non-instant pref init file" ); + + // were all elements correctly initialized? (loose check) + found = ReadPrefsFromUI(aPrefWindow); + ok(found.int == expected.int, "non-instant element init int" ); + ok(found.bool == expected.bool, "non-instant element init bool" ); + ok(found.string == expected.string, "non-instant element init string" ); + ok(found.unichar == expected.unichar, "non-instant element init unichar"); + ok(found.wstring_data == expected.wstring_data, "non-instant element init wstring"); + todo(found.file_data == expected.file_data, "non-instant element init file" ); + + // do some changes in the UI + expected = kPrefValueSet2; + WritePrefsToUI(aPrefWindow, expected); + + // UI changes should get passed to the Preference instances, + // but currently they aren't if the changes are made programmatically + // (the handlers preference.change/prefpane.input and prefpane.change + // are called for manual changes, though). + found = ReadPrefsFromPreferences(aPrefWindow); + todo(found.int === expected.int, "non-instant change pref int" ); + todo(found.bool === expected.bool, "non-instant change pref bool" ); + todo(found.string === expected.string, "non-instant change pref string" ); + todo(found.unichar === expected.unichar, "non-instant change pref unichar"); + todo(found.wstring_data === expected.wstring_data, "non-instant change pref wstring"); + todo(found.file_data === expected.file_data, "non-instant change pref file" ); + + // and these changes should *NOT* get passed to the system + // (which obviously always passes with the above failing) + expected = kPrefValueSet1; + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant change element int" ); + ok(found.bool === expected.bool, "non-instant change element bool" ); + ok(found.string === expected.string, "non-instant change element string" ); + ok(found.unichar === expected.unichar, "non-instant change element unichar"); + ok(found.wstring_data === expected.wstring_data, "non-instant change element wstring"); + todo(found.file_data === expected.file_data, "non-instant change element file" ); + + // try resetting the prefs to default values (which should be empty here) + GetPreference(aPrefWindow, "tests.static_preference_int" ).reset(); + GetPreference(aPrefWindow, "tests.static_preference_bool" ).reset(); + GetPreference(aPrefWindow, "tests.static_preference_string" ).reset(); + GetPreference(aPrefWindow, "tests.static_preference_unichar").reset(); + GetPreference(aPrefWindow, "tests.static_preference_wstring").reset(); + GetPreference(aPrefWindow, "tests.static_preference_file" ).reset(); + + // check system: the current values *MUST NOT* change + expected = kPrefValueSet1; + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant reset system int" ); + ok(found.bool === expected.bool, "non-instant reset system bool" ); + ok(found.string === expected.string, "non-instant reset system string" ); + ok(found.unichar === expected.unichar, "non-instant reset system unichar"); + ok(found.wstring_data === expected.wstring_data, "non-instant reset system wstring"); + todo(found.file_data === expected.file_data, "non-instant reset system file" ); + + // check UI: these values should be reset + expected = + { + // alas, we don't have XUL elements with typeof(value) == int :( + // int: 0, + int: "", + bool: false, + string: "", + unichar: "", + wstring_data: "", + file_data: "", + wstring: {}, + file: {} + }; + found = ReadPrefsFromUI(aPrefWindow); + ok(found.int === expected.int, "non-instant reset element int" ); + ok(found.bool === expected.bool, "non-instant reset element bool" ); + ok(found.string === expected.string, "non-instant reset element string" ); + ok(found.unichar === expected.unichar, "non-instant reset element unichar"); + ok(found.wstring_data === expected.wstring_data, "non-instant reset element wstring"); +// ok(found.file_data === expected.file_data, "non-instant reset element file" ); + + // check hasUserValue + ok(GetPreference(aPrefWindow, "tests.static_preference_int" ).hasUserValue === false, "non-instant reset hasUserValue int" ); + ok(GetPreference(aPrefWindow, "tests.static_preference_bool" ).hasUserValue === false, "non-instant reset hasUserValue bool" ); + ok(GetPreference(aPrefWindow, "tests.static_preference_string" ).hasUserValue === false, "non-instant reset hasUserValue string" ); + ok(GetPreference(aPrefWindow, "tests.static_preference_unichar").hasUserValue === false, "non-instant reset hasUserValue unichar"); + ok(GetPreference(aPrefWindow, "tests.static_preference_wstring").hasUserValue === false, "non-instant reset hasUserValue wstring"); + ok(GetPreference(aPrefWindow, "tests.static_preference_file" ).hasUserValue === false, "non-instant reset hasUserValue file" ); + } + + function RunNonInstantPrefTestClose(aPrefWindow) + { + WritePrefsToPreferences(aPrefWindow, kPrefValueSet2); + } + + function RunCheckCommandRedirect(aPrefWindow) + { + GetXULElement(aPrefWindow, "checkbox").click(); + ok(GetPreference(aPrefWindow, "tests.static_preference_bool").value, "redirected command bool"); + GetXULElement(aPrefWindow, "checkbox").click(); + ok(!GetPreference(aPrefWindow, "tests.static_preference_bool").value, "redirected command bool"); + } + + function RunCheckDisabled(aPrefWindow) + { + ok(!GetXULElement(aPrefWindow, "disabled_checkbox").disabled, "Checkbox should be enabled"); + GetPreference(aPrefWindow, "tests.disabled_preference_bool").updateControlDisabledState(true); + ok(GetXULElement(aPrefWindow, "disabled_checkbox").disabled, "Checkbox should be disabled"); + GetPreference(aPrefWindow, "tests.locked_preference_bool").updateControlDisabledState(false);; + ok(GetXULElement(aPrefWindow, "locked_checkbox").disabled, "Locked checkbox should stay disabled"); + } + + function RunResetPrefTest(aPrefWindow) + { + // try resetting the prefs to default values + GetPreference(aPrefWindow, "tests.static_preference_int" ).reset(); + GetPreference(aPrefWindow, "tests.static_preference_bool" ).reset(); + GetPreference(aPrefWindow, "tests.static_preference_string" ).reset(); + GetPreference(aPrefWindow, "tests.static_preference_unichar").reset(); + GetPreference(aPrefWindow, "tests.static_preference_wstring").reset(); + GetPreference(aPrefWindow, "tests.static_preference_file" ).reset(); + } + + function InitTestPrefs(aInstantApply) + { + // set instant apply mode and init prefs to set 1 + kPref.setBoolPref("browser.preferences.instantApply", aInstantApply); + WritePrefsToSystem(kPrefValueSet1); + } + + function RunTestInstant() + { + // test with instantApply + InitTestPrefs(true); + window.browsingContext.topChromeWindow.openDialog("window_preferences.xhtml", "", "modal", RunInstantPrefTest, false); + + // - test deferred reset in child window + InitTestPrefs(true); + window.browsingContext.topChromeWindow.openDialog("window_preferences2.xhtml", "", "modal", RunResetPrefTest, false); + let expected = kPrefValueSet1; + let found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "instant reset deferred int" ); + ok(found.bool === expected.bool, "instant reset deferred bool" ); + ok(found.string === expected.string, "instant reset deferred string" ); + ok(found.unichar === expected.unichar, "instant reset deferred unichar"); + ok(found.wstring_data === expected.wstring_data, "instant reset deferred wstring"); + todo(found.file_data === expected.file_data, "instant reset deferred file" ); + } + + function RunTestNonInstant() + { + // test without instantApply + // - general tests, similar to instant apply + InitTestPrefs(false); + window.browsingContext.topChromeWindow.openDialog("window_preferences.xhtml", "", "modal", RunNonInstantPrefTestGeneral, false); + + // - test Cancel + InitTestPrefs(false); + window.browsingContext.topChromeWindow.openDialog("window_preferences.xhtml", "", "modal", RunNonInstantPrefTestClose, false); + var expected = kPrefValueSet1; + var found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant cancel system int" ); + ok(found.bool === expected.bool, "non-instant cancel system bool" ); + ok(found.string === expected.string, "non-instant cancel system string" ); + ok(found.unichar === expected.unichar, "non-instant cancel system unichar"); + ok(found.wstring_data === expected.wstring_data, "non-instant cancel system wstring"); + todo(found.file_data === expected.file_data, "non-instant cancel system file" ); + + // - test Accept + InitTestPrefs(false); + window.browsingContext.topChromeWindow.openDialog("window_preferences.xhtml", "", "modal", RunNonInstantPrefTestClose, true); + expected = kPrefValueSet2; + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant accept system int" ); + ok(found.bool === expected.bool, "non-instant accept system bool" ); + ok(found.string === expected.string, "non-instant accept system string" ); + ok(found.unichar === expected.unichar, "non-instant accept system unichar"); + ok(found.wstring_data === expected.wstring_data, "non-instant accept system wstring"); + todo(found.file_data === expected.file_data, "non-instant accept system file" ); + + // - test deferred reset in child window + InitTestPrefs(false); + window.browsingContext.topChromeWindow.openDialog("window_preferences2.xhtml", "", "modal", RunResetPrefTest, true); + expected = CreateEmptyPrefValueSet(); + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant reset deferred int" ); + ok(found.bool === expected.bool, "non-instant reset deferred bool" ); + ok(found.string === expected.string, "non-instant reset deferred string" ); + ok(found.unichar === expected.unichar, "non-instant reset deferred unichar"); + ok(found.wstring_data === expected.wstring_data, "non-instant reset deferred wstring"); + ok(found.file_data === expected.file_data, "non-instant reset deferred file" ); + } + + function RunTestCommandRedirect() + { + window.browsingContext.topChromeWindow.openDialog("window_preferences_commandretarget.xhtml", "", "modal", RunCheckCommandRedirect, true); + } + + function RunTestDisabled() + { + // Because this pref is on the default branch and locked, we need to set it before opening the dialog. + const defaultBranch = kPref.getDefaultBranch(""); + defaultBranch.setBoolPref("tests.locked_preference_bool", true); + defaultBranch.lockPref("tests.locked_preference_bool"); + window.browsingContext.topChromeWindow.openDialog("window_preferences_disabled.xhtml", "", "modal", RunCheckDisabled, true); + } + + function RunTest() + { + RunTestInstant(); + RunTestNonInstant(); + RunTestCommandRedirect(); + RunTestDisabled(); + SimpleTest.finish(); + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_preferences_beforeaccept.xhtml b/toolkit/content/tests/chrome/test_preferences_beforeaccept.xhtml new file mode 100644 index 0000000000..cbb5d059f3 --- /dev/null +++ b/toolkit/content/tests/chrome/test_preferences_beforeaccept.xhtml @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Preferences Window beforeaccept Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set":[["browser.preferences.instantApply", false]]}, function() { + + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("tests.beforeaccept.dialogShown"); + SpecialPowers.clearUserPref("tests.beforeaccept.called"); + }); + + // No instant-apply for this test + var prefWindow = window.browsingContext.topChromeWindow.openDialog("window_preferences_beforeaccept.xhtml", "", "", windowOnload); + + function windowOnload() { + var dialogShown = prefWindow.Preferences.get("tests.beforeaccept.dialogShown"); + var called = prefWindow.Preferences.get("tests.beforeaccept.called"); + is(dialogShown.value, true, "dialog opened, shown pref set"); + is(dialogShown.valueFromPreferences, null, "shown pref not committed"); + is(called.value, null, "beforeaccept not yet called"); + is(called.valueFromPreferences, null, "beforeaccept not yet called, pref not committed"); + + // try to accept the dialog, should fail the first time + prefWindow.document.getElementById("beforeaccept_dialog").acceptDialog(); + is(prefWindow.closed, false, "window not closed"); + is(dialogShown.value, true, "shown pref still set"); + is(dialogShown.valueFromPreferences, null, "shown pref still not committed"); + is(called.value, true, "beforeaccept called"); + is(called.valueFromPreferences, null, "called pref not committed"); + + // try again, this one should succeed + prefWindow.document.getElementById("beforeaccept_dialog").acceptDialog(); + is(prefWindow.closed, true, "window now closed"); + is(dialogShown.valueFromPreferences, true, "shown pref committed"); + is(called.valueFromPreferences, true, "called pref committed"); + + SimpleTest.finish(); + } +}); + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_preferences_onsyncfrompreference.xhtml b/toolkit/content/tests/chrome/test_preferences_onsyncfrompreference.xhtml new file mode 100644 index 0000000000..3f4d2037c5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_preferences_onsyncfrompreference.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- 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/. --> +<window title="Preferences Window beforeaccept Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + const PREFS = ['tests.onsyncfrompreference.pref1', + 'tests.onsyncfrompreference.pref2', + 'tests.onsyncfrompreference.pref3']; + + SimpleTest.waitForExplicitFinish(); + + for (let pref of PREFS) { + Services.prefs.setIntPref(pref, 1); + } + + let counter = 0; + let prefWindow = window.browsingContext.topChromeWindow.openDialog("window_preferences_onsyncfrompreference.xhtml", "", "", onSync); + + SimpleTest.registerCleanupFunction(() => { + for (let pref of PREFS) { + Services.prefs.clearUserPref(pref); + } + prefWindow.close(); + }); + + // Onsyncfrompreference handler for the prefs + function onSync() { + for (let pref of PREFS) { + // The `value` field of each <preference> element should be initialized by now. + + is(Services.prefs.getIntPref(pref), prefWindow.Preferences.get(pref).value, + "Pref constructor was called correctly") + } + + counter++; + + if (counter == PREFS.length) { + SimpleTest.finish(); + } + return true; + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_props.xhtml b/toolkit/content/tests/chrome/test_props.xhtml new file mode 100644 index 0000000000..e370bf6735 --- /dev/null +++ b/toolkit/content/tests/chrome/test_props.xhtml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for basic properties - this test checks that the basic + properties defined in general.js and inherited by a number of elements + work properly. + --> +<window title="Basic Properties Test" + onload="setTimeout(test_props, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<command id="cmd_nothing"/> +<command id="cmd_action"/> + +<button id="button" label="Button" accesskey="B" + crop="end" image="happy.png" command="cmd_nothing"/> +<checkbox id="checkbox" label="Checkbox" accesskey="B" + crop="end" image="happy.png" command="cmd_nothing"/> +<radiogroup> + <radio id="radio" label="Radio Button" value="rb1" accesskey="B" + crop="end" image="happy.png" command="cmd_nothing"/> +</radiogroup> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_props() +{ + test_props_forelement($("button"), "Button", null); + test_props_forelement($("checkbox"), "Checkbox", null); + test_props_forelement($("radio"), "Radio Button", "rb1"); + + SimpleTest.finish(); +} + +function test_props_forelement(element, label, value) +{ + // check the initial values + is(element.label, label, "element label"); + if (value) + is(element.value, value, "element value"); + is(element.accessKey, "B", "element accessKey"); + is(element.image, "happy.png", "element image"); + is(element.command, "cmd_nothing", "element command"); + ok(element.tabIndex === 0, "element tabIndex"); + + synthesizeMouseExpectEvent(element, 4, 4, { }, $("cmd_nothing"), "command", "element"); + + // make sure that setters return the value + is(element.label = "Label", "Label", "element label setter return"); + if (value) + is(element.value = "lb", "lb", "element value setter return"); + is(element.accessKey = "L", "L", "element accessKey setter return"); + is(element.image = "sad.png", "sad.png", "element image setter return"); + is(element.command = "cmd_action", "cmd_action", "element command setter return"); + + // check the value after it was changed + is(element.label, "Label", "element label after set"); + if (value) + is(element.value, "lb", "element value after set"); + is(element.accessKey, "L", "element accessKey after set"); + is(element.image, "sad.png", "element image after set"); + is(element.command, "cmd_action", "element command after set"); + + synthesizeMouseExpectEvent(element, 4, 4, { }, $("cmd_action"), "command", "element"); + + // check that clicks on disabled items don't fire a command event + ok((element.disabled = true) === true, "element disabled setter return"); + ok(element.disabled === true, "element disabled after set"); + synthesizeMouseExpectEvent(element, 4, 4, { }, $("cmd_action"), "!command", "element"); + + element.disabled = false; +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_radio.xhtml b/toolkit/content/tests/chrome/test_radio.xhtml new file mode 100644 index 0000000000..3f222a8daa --- /dev/null +++ b/toolkit/content/tests/chrome/test_radio.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for radio buttons + --> +<window title="Radio Buttons" width="500" height="600" + onload="setTimeout(test_radio, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="xul_selectcontrol.js"/> + +<radiogroup id="radiogroup"/> + +<radiogroup id="radiogroup-initwithvalue" value="two"> + <radio label="One" value="one"/> + <radio label="Two" value="two"/> + <radio label="Three" value="three"/> +</radiogroup> +<radiogroup id="radiogroup-initwithselected" value="two"> + <radio id="one" label="One" value="one" accesskey="o"/> + <radio id="two" label="Two" value="two" accesskey="t"/> + <radio label="Three" value="three" selected="true"/> +</radiogroup> + +<radiogroup id="radio-creation" hidden="true" /> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +async function test_radio() +{ + var element = document.getElementById("radiogroup"); + test_nsIDOMXULSelectControlElement(element, "radio", null); + test_nsIDOMXULSelectControlElement_UI(element, null); + + window.blur(); + + var accessKeyDetails = (navigator.platform.includes("Mac")) ? + { altKey : true, ctrlKey : true } : + { altKey : true, shiftKey: true }; + synthesizeKey("t", accessKeyDetails); + + var radiogroup = $("radiogroup-initwithselected"); + is(document.activeElement, radiogroup, "accesskey focuses radiogroup"); + is(radiogroup.selectedItem, $("two"), "accesskey selects radio"); + + $("radiogroup-initwithvalue").focus(); + + $("one").disabled = true; + synthesizeKey("o", accessKeyDetails); + + is(document.activeElement, $("radiogroup-initwithvalue"), "accesskey on disabled radio doesn't focus"); + is(radiogroup.selectedItem, $("two"), "accesskey on disabled radio doesn't change selection"); + + info("Testing appending child"); + var dynamicRadiogroup = document.querySelector("#radio-creation"); + var radio = document.createXULElement("radio"); + radio.setAttribute("selected", "true"); + radio.setAttribute("label", "one"); + radio.setAttribute("value", "one"); + dynamicRadiogroup.appendChild(radio); + dynamicRadiogroup.appendChild(document.createXULElement("radio")); + + dynamicRadiogroup.hidden = false; + info("Waiting for condition"); + await SimpleTest.promiseWaitForCondition(() => dynamicRadiogroup.value == "one", + "Value gets set once child is constructed"); + is(dynamicRadiogroup._radioChildren.length, 2, "Correct number of children"); + + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_richlistbox.xhtml b/toolkit/content/tests/chrome/test_richlistbox.xhtml new file mode 100644 index 0000000000..0d9ef04ea6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_richlistbox.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for listbox direction + --> +<window title="Listbox direction test" + onload="test_richlistbox()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <richlistbox seltype="multiple" id="richlistbox" flex="1" minheight="80" maxheight="80" height="80" /> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var richListBox = document.getElementById("richlistbox"); + +function getScrollIndexAmount(aDirection) { + return (4 * aDirection + richListBox.currentIndex); +} + +function test_richlistbox() +{ + var height = richListBox.clientHeight; + var item; + do { + item = richListBox.appendItem("Test", ""); + item.height = item.minHeight = item.maxHeight = Math.floor(height / 4); + } while (item.getBoundingClientRect().bottom < (height * 2)) + richListBox.appendItem("Test", ""); + richListBox.firstChild.nextSibling.id = "list-box-first"; + richListBox.lastChild.previousSibling.id = "list-box-last"; + + var count = richListBox.itemCount; + richListBox.focus(); + + // Test that dir="reverse" is ignored and behaves the same as dir="normal". + for (let dir of ["reverse", "normal"]) { + richListBox.style.MozBoxDirection = dir; + richListBox.selectedIndex = 0; + sendKey("DOWN"); + is(richListBox.currentIndex, 1, "Selection should move to the next item"); + sendKey("UP"); + is(richListBox.currentIndex, 0, "Selection should move to the previous item"); + sendKey("END"); + is(richListBox.currentIndex, count - 1, "Selection should move to the last item"); + sendKey("HOME"); + is(richListBox.currentIndex, 0, "Selection should move to the first item"); + var currentIndex = richListBox.currentIndex; + var index = richListBox.scrollOnePage(1); + sendKey("PAGE_DOWN"); + is(richListBox.currentIndex, index, "Selection should move to one page down"); + ok(richListBox.currentIndex > currentIndex, "Selection should move downwards"); + sendKey("END"); + currentIndex = richListBox.currentIndex; + index = richListBox.scrollOnePage(-1) + richListBox.currentIndex; + sendKey("PAGE_UP"); + is(richListBox.currentIndex, index, "Selection should move to one page up"); + ok(richListBox.currentIndex < currentIndex, "Selection should move upwards"); + richListBox.selectedItem = richListBox.firstChild; + richListBox.focus(); + synthesizeKey("KEY_ArrowDown", {shiftKey: true}, window); + let items = [richListBox.selectedItems[0], + richListBox.selectedItems[1]]; + is(items[0], richListBox.firstChild, "The last element should still be selected"); + is(items[1], richListBox.firstChild.nextSibling, "Both elements should now be selected"); + richListBox.clearSelection(); + richListBox.selectedItem = richListBox.firstChild; + sendMouseEvent({type: "click", shiftKey: true, clickCount: 1}, + "list-box-first", + window); + items = [richListBox.selectedItems[0], + richListBox.selectedItems[1]]; + is(items[0], richListBox.firstChild, "The last element should still be selected"); + is(items[1], richListBox.firstChild.nextSibling, "Both elements should now be selected"); + richListBox.addEventListener("keypress", function(aEvent) { + aEvent.preventDefault(); + }, { useCapture: true, once: true }); + richListBox.selectedIndex = 1; + sendKey("HOME"); + is(richListBox.selectedIndex, 1, "A stopped event should return indexing to normal"); + } + + // Test attempting to select a disabled item. + richListBox.clearSelection(); + richListBox.selectedItem = richListBox.firstChild; + richListBox.firstChild.nextSibling.setAttribute("disabled", true); + richListBox.focus(); + synthesizeKey("KEY_ArrowDown", {}, window); + is(richListBox.selectedItems.length, 1, "one item selected"); + is(richListBox.selectedItems[0], richListBox.firstChild, "first item selected"); + + // Selected item re-insertion should keep the item selected. + richListBox.clearSelection(); + item = richListBox.firstElementChild; + richListBox.selectedItem = item; + is(richListBox.selectedItems.length, 1, "one item selected"); + is(richListBox.selectedItems[0], item, "first item selected"); + item.remove(); + richListBox.append(item); + is(richListBox.selectedItems.length, 1, "one item selected"); + is(richListBox.selectedItems[0], item, "last (previosly first) item selected"); + + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_screenPersistence.xhtml b/toolkit/content/tests/chrome/test_screenPersistence.xhtml new file mode 100644 index 0000000000..667dcf9590 --- /dev/null +++ b/toolkit/content/tests/chrome/test_screenPersistence.xhtml @@ -0,0 +1,62 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Window Open Test" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script class="testbody" type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + let win; + var left = 60 + screen.availLeft; + var upper = 60 + screen.availTop; + + function runTest() { + win = window.browsingContext.topChromeWindow + .openDialog("window_screenPosSize.xhtml", + "_blank", + "chrome,dialog=no,all,screenX=" + left + ",screenY=" + upper + ",outerHeight=200,outerWidth=200"); + SimpleTest.waitForFocus(checkTest, win); + } + function checkTest() { + is(win.screenX, left, "The window should be placed now at x=" + left + "px"); + is(win.screenY, upper, "The window should be placed now at y=" + upper + "px"); + is(win.outerHeight, 200, "The window size should be height=200px"); + is(win.outerWidth, 200, "The window size should be width=200px"); + runTest2(); + } + function runTest2() { + win.close(); + win = window.browsingContext.topChromeWindow + .openDialog("window_screenPosSize.xhtml", + "_blank", + "chrome,dialog=no,all"); + SimpleTest.waitForFocus(checkTest2, win); + } + function checkTest2() { + let runTime = SpecialPowers.Services.appinfo; + if (runTime.OS != "Linux") { + is(win.screenX, 80, "The window should be placed now at x=80px"); + is(win.screenY, 80, "The window should be placed now at y=80px"); + } + is(win.innerHeight, 300, "The window size should be height=300px"); + is(win.innerWidth, 300, "The window size should be width=300px"); + win.close(); + SimpleTest.finish(); + } +]]></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_scrollbar.xhtml b/toolkit/content/tests/chrome/test_scrollbar.xhtml new file mode 100644 index 0000000000..b1295b5a72 --- /dev/null +++ b/toolkit/content/tests/chrome/test_scrollbar.xhtml @@ -0,0 +1,132 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for scrollbars + --> +<window title="Scrollbar" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"/> + + <hbox> + <scrollbar orient="horizontal" + id="scroller" + curpos="0" + maxpos="600" + pageincrement="400" + width="500" + style="margin:0"/> + </hbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Scrollbar **/ +var scrollbarTester = { + scrollbar: null, + middlePref: false, + startTest() { + this.scrollbar = $("scroller"); + this.middlePref = this.getMiddlePref(); + var self = this; + [0, 1, 2].map(function(button) { + [false, true].map(function(alt) { + [false, true].map(function(shift) { + self.testThumbDragging(button, alt, shift); + }) + }) + }); + SimpleTest.finish(); + }, + testThumbDragging(button, withAlt, withShift) { + this.reset(); + var x = 160; // on the right half of the thumb + var y = 5; + + var isMac = navigator.platform.includes("Mac"); + let runtime = SpecialPowers.Services.appinfo; + var isGtk = runtime.widgetToolkit.includes("gtk"); + + // Start the drag. + this.mousedown(x, y, button, withAlt, withShift); + var newPos = this.getPos(); + var scrollToClick = (newPos != 0); + if (isMac || isGtk) { + ok(!scrollToClick, "On Linux and Mac OS X, clicking the scrollbar thumb "+ + "should never move it."); + } else if (button == 0 && withShift) { + ok(scrollToClick, "On platforms other than Linux and Mac OS X, holding "+ + "shift should enable scroll-to-click on the scrollbar thumb."); + } else if (button == 1 && this.middlePref) { + ok(scrollToClick, "When middlemouse.scrollbarPosition is on, clicking the "+ + "thumb with the middle mouse button should center it "+ + "around the cursor.") + } + + // Move one pixel to the right. + this.mousemove(x+1, y, button, withAlt, withShift); + var newPos2 = this.getPos(); + if (newPos2 != newPos) { + ok(newPos2 > newPos, "Scrollbar thumb should follow the mouse when dragged."); + ok(newPos2 - newPos < 3, "Scrollbar shouldn't move further than the mouse when dragged."); + ok(button == 0 || (button == 1 && this.middlePref) || (button == 2 && isGtk), + "Dragging the scrollbar should only be possible with the left mouse button."); + } else if (button == 0) { + // Dragging had no effect. + ok(false, "Dragging the scrollbar thumb should work."); + } else if (button == 1 && this.middlePref && (!isGtk && !isMac)) { + ok(false, "When middlemouse.scrollbarPosition is on, dragging the "+ + "scrollbar thumb should be possible using the middle mouse button."); + } else { + ok(true, "Dragging works correctly."); + } + + // Release the mouse button. + this.mouseup(x+1, y, button, withAlt, withShift); + var newPos3 = this.getPos(); + ok(newPos3 == newPos2, + "Releasing the mouse button after dragging the thumb shouldn't move it."); + }, + getMiddlePref() { + // It would be better to test with different middlePref settings, + // but the setting is only queried once, at browser startup, so + // changing it here wouldn't have any effect + var mouseBranch = SpecialPowers.Services.prefs.getBranch("middlemouse."); + return mouseBranch.getBoolPref("scrollbarPosition"); + }, + setPos(pos) { + this.scrollbar.setAttribute("curpos", pos); + }, + getPos() { + return this.scrollbar.getAttribute("curpos"); + }, + reset() { + this.setPos(0); + }, + mousedown(x, y, button, alt, shift) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousedown", 'button': button, + altKey: alt, shiftKey: shift }); + }, + mousemove(x, y, button, alt, shift) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousemove", 'button': button, + altKey: alt, shiftKey: shift }); + }, + mouseup(x, y, button, alt, shift) { + synthesizeMouse(this.scrollbar, x, y, { type: "mouseup", 'button': button, + altKey: alt, shiftKey: shift }); + } +} + +function doTest() { + setTimeout(function() { scrollbarTester.startTest(); }, 0); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_showcaret.xhtml b/toolkit/content/tests/chrome/test_showcaret.xhtml new file mode 100644 index 0000000000..eb344589fc --- /dev/null +++ b/toolkit/content/tests/chrome/test_showcaret.xhtml @@ -0,0 +1,99 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Show Caret Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<iframe id="f1" width="100" height="100" onload="frameLoaded()" + src="data:text/html,%3Cbody%20style='height:%208000px'%3E%3Cp%3EHello%3C/p%3EGoodbye%3C/body%3E"/> +<!-- <body style='height: 8000px'><p>Hello</p><span id='s'>Goodbye<span></body> --> +<iframe id="f2" type="content" showcaret="true" width="100" height="100" onload="frameLoaded()" + src="data:text/html,%3Cbody%20style%3D%27height%3A%208000px%27%3E%3Cp%3EHello%3C%2Fp%3E%3Cspan%20id%3D%27s%27%3EGoodbye%3Cspan%3E%3C%2Fbody%3E"/> + +<script> +<![CDATA[ + +var framesLoaded = 0; +var otherWindow = null; + +function frameLoaded() { if (++framesLoaded == 2) SimpleTest.waitForFocus(runTest); } + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + var sel1 = frames[0].getSelection(); + sel1.collapse(frames[0].document.body, 0); + + var sel2 = frames[1].getSelection(); + sel2.collapse(frames[1].document.body, 0); + window.frames[0].focus(); + document.commandDispatcher.getControllerForCommand("cmd_moveBottom").doCommand("cmd_moveBottom"); + + var listener = function() { + if (!(frames[0].scrollY > 0)) { + window.content.removeEventListener("scroll", listener); + } + } + window.frames[0].addEventListener("scroll", listener); + + sel1 = frames[0].getSelection(); + sel1.collapse(frames[0].document.body, 0); + + sel2 = frames[1].getSelection(); + sel2.collapse(frames[1].document.body, 0); + + window.frames[0].focus(); + document.commandDispatcher.getControllerForCommand("cmd_moveBottom").doCommand("cmd_moveBottom"); + is(sel1.focusNode, frames[0].document.body, "focusNode for non-showcaret"); + is(sel1.focusOffset, 0, "focusOffset for non-showcaret"); + + window.frames[1].focus(); + document.commandDispatcher.getControllerForCommand("cmd_moveBottom").doCommand("cmd_moveBottom"); + + ok(frames[1].scrollY < + frames[1].document.getElementById('s').getBoundingClientRect().top, + "scrollY for showcaret"); + isnot(sel2.focusNode, frames[1].document.body, "focusNode for showcaret"); + ok(sel2.anchorOffset > 0, "focusOffset for showcaret"); + + otherWindow = window.browsingContext.topChromeWindow.open("window_showcaret.xhtml", "_blank", "chrome,width=400,height=200"); + otherWindow.addEventListener("focus", otherWindowFocused); +} + +function otherWindowFocused() +{ + otherWindow.removeEventListener("focus", otherWindowFocused); + + // enable caret browsing temporarily to test caret movement + let prefs = SpecialPowers.Services.prefs; + prefs.setBoolPref("accessibility.browsewithcaret", true); + + var hbox = otherWindow.document.documentElement.firstChild; + hbox.focus(); + is(otherWindow.document.activeElement, hbox, "hbox in other window is focused"); + + document.commandDispatcher.getControllerForCommand("cmd_lineNext").doCommand("cmd_lineNext"); + is(otherWindow.document.activeElement, hbox, "hbox still focused in other window after down movement"); + + prefs.setBoolPref("accessibility.browsewithcaret", false); + + otherWindow.close(); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_subframe_origin.xhtml b/toolkit/content/tests/chrome/test_subframe_origin.xhtml new file mode 100644 index 0000000000..65b7190baf --- /dev/null +++ b/toolkit/content/tests/chrome/test_subframe_origin.xhtml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Subframe Event Tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> + +// Added after content child widgets were removed from ui windows. Tests sub frame +// event client coordinate offsets. + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_subframe_origin.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_tabbox.xhtml b/toolkit/content/tests/chrome/test_tabbox.xhtml new file mode 100644 index 0000000000..bca919f8c5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tabbox.xhtml @@ -0,0 +1,223 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tabboxes + --> +<window title="Tabbox Test" width="500" height="600" + onload="setTimeout(test_tabbox, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="xul_selectcontrol.js"/> + +<vbox id="tabboxes"> + +<tabbox id="tabbox"> + <tabs id="tabs"> + <tab id="tab1" label="Tab 1"/> + <tab id="tab2" label="Tab 2"/> + </tabs> + <tabpanels id="tabpanels"> + <button id="panel1" label="Panel 1"/> + <button id="panel2" label="Panel 2"/> + </tabpanels> +</tabbox> + +<tabbox id="tabbox-initwithvalue"> + <tabs id="tabs-initwithvalue" value="two"> + <tab label="Tab 1" value="one"/> + <tab label="Tab 2" value="two"/> + <tab label="Tab 3" value="three"/> + </tabs> + <tabpanels id="tabpanels-initwithvalue"> + <button label="Panel 1"/> + <button label="Panel 2"/> + <button label="Panel 3"/> + </tabpanels> +</tabbox> + +<tabbox id="tabbox-initwithselected"> + <tabs id="tabs-initwithselected" value="two"> + <tab label="Tab 1" value="one"/> + <tab label="Tab 2" value="two"/> + <tab label="Tab 3" value="three" selected="true"/> + </tabs> + <tabpanels id="tabpanels-initwithselected"> + <button label="Panel 1"/> + <button label="Panel 2"/> + <button label="Panel 3"/> + </tabpanels> +</tabbox> + +</vbox> + +<tabbox id="tabbox-nofocus"> + <html:input id="textbox-extra" hidden="true"/> + <tabs> + <tab label="Tab 1" value="one"/> + <tab id="tab-nofocus" label="Tab 2" value="two"/> + </tabs> + <tabpanels> + <tabpanel> + <button id="tab-nofocus-button" label="Label"/> + </tabpanel> + <tabpanel id="tabpanel-nofocusinpaneltab"> + <label id="tablabel" value="Label"/> + </tabpanel> + </tabpanels> +</tabbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_tabbox() +{ + var tabbox = document.getElementById("tabbox"); + var tabs = document.getElementById("tabs"); + var tabpanels = document.getElementById("tabpanels"); + + test_tabbox_State(tabbox, "tabbox initial", 0, tabs.allTabs[0], tabpanels.firstChild); + + // check the selectedIndex property + tabbox.selectedIndex = 1; + test_tabbox_State(tabbox, "tabbox selectedIndex 1", 1, tabs.allTabs[tabs.allTabs.length - 1], tabpanels.lastChild); + + tabbox.selectedIndex = 2; + test_tabbox_State(tabbox, "tabbox selectedIndex 2", 1, tabs.allTabs[tabs.allTabs.length - 1], tabpanels.lastChild); + + // tabbox must have a selection, so setting to -1 should do nothing + tabbox.selectedIndex = -1; + test_tabbox_State(tabbox, "tabbox selectedIndex -1", 1, tabs.allTabs[tabs.allTabs.length - 1], tabpanels.lastChild); + + // check the selectedTab property + tabbox.selectedTab = tabs.allTabs[0]; + test_tabbox_State(tabbox, "tabbox selected", 0, tabs.allTabs[0], tabpanels.firstChild); + + // setting selectedTab to null should not do anything + tabbox.selectedTab = null; + test_tabbox_State(tabbox, "tabbox selectedTab null", 0, tabs.allTabs[0], tabpanels.firstChild); + + // check the selectedPanel property + tabbox.selectedPanel = tabpanels.lastChild; + test_tabbox_State(tabbox, "tabbox selectedPanel", 0, tabs.allTabs[0], tabpanels.lastChild); + + // setting selectedPanel to null should not do anything + tabbox.selectedPanel = null; + test_tabbox_State(tabbox, "tabbox selectedPanel null", 0, tabs.allTabs[0], tabpanels.lastChild); + + tabbox.selectedIndex = 0; + test_tabpanels(tabpanels, tabbox); + + tabs.firstChild.remove(); + tabs.firstChild.remove(); + + test_tabs(tabs); + + test_tabbox_focus(); +} + +function test_tabpanels(tabpanels, tabbox) +{ + var tab = tabbox.selectedTab; + + // changing the selection on the tabpanels should not affect the tabbox + // or tabs within + // check the selectedIndex property + tabpanels.selectedIndex = 1; + test_tabbox_State(tabbox, "tabpanels tabbox selectedIndex 1", 0, tab, tabpanels.lastChild); + test_tabpanels_State(tabpanels, "tabpanels selectedIndex 1", 1, tabpanels.lastChild); + + tabpanels.selectedIndex = 0; + test_tabbox_State(tabbox, "tabpanels tabbox selectedIndex 2", 0, tab, tabpanels.firstChild); + test_tabpanels_State(tabpanels, "tabpanels selectedIndex 2", 0, tabpanels.firstChild); + + // setting selectedIndex to -1 should do nothing + tabpanels.selectedIndex = 1; + tabpanels.selectedIndex = -1; + test_tabbox_State(tabbox, "tabpanels tabbox selectedIndex -1", 0, tab, tabpanels.lastChild); + test_tabpanels_State(tabpanels, "tabpanels selectedIndex -1", 1, tabpanels.lastChild); + + // check the tabpanels.selectedPanel property + tabpanels.selectedPanel = tabpanels.lastChild; + test_tabbox_State(tabbox, "tabpanels tabbox selectedPanel", 0, tab, tabpanels.lastChild); + test_tabpanels_State(tabpanels, "tabpanels selectedPanel", 1, tabpanels.lastChild); + + // check setting the tabpanels.selectedPanel property to null + tabpanels.selectedPanel = null; + test_tabbox_State(tabbox, "tabpanels selectedPanel null", 0, tab, tabpanels.lastChild); +} + +function test_tabs(tabs) +{ + test_nsIDOMXULSelectControlElement(tabs, "tab", "tabs"); + // XXXndeakin would test the UI aspect of tabs, but the mouse + // events on tabs are fired in a timeout causing the generic + // test_nsIDOMXULSelectControlElement_UI method not to work + // test_nsIDOMXULSelectControlElement_UI(tabs, null); +} + +function test_tabbox_State(tabbox, testid, index, tab, panel) +{ + is(tabbox.selectedIndex, index, testid + " selectedIndex"); + is(tabbox.selectedTab, tab, testid + " selectedTab"); + is(tabbox.selectedPanel, panel, testid + " selectedPanel"); +} + +function test_tabpanels_State(tabpanels, testid, index, panel) +{ + is(tabpanels.selectedIndex, index, testid + " selectedIndex"); + is(tabpanels.selectedPanel, panel, testid + " selectedPanel"); +} + +function test_tabbox_focus() +{ + $("tabboxes").hidden = true; + $(document.activeElement).blur(); + + var tabbox = $("tabbox-nofocus"); + var tab = $("tab-nofocus"); + + when_tab_focused(tab, function () { + is(document.activeElement, tab, "focus in tab with no focusable elements"); + + tabbox.selectedIndex = 0; + $("tab-nofocus-button").focus(); + + when_tab_focused(tab, function () { + is(document.activeElement, tab, "focus in tab with no focusable elements, but with something in another tab focused"); + + var textboxExtra = $("textbox-extra"); + textboxExtra.addEventListener("focus", function () { + is(document.activeElement, textboxExtra, "focus in tab with focus currently in textbox that is sibling of tabs"); + + SimpleTest.finish(); + }, {once: true}); + + tabbox.selectedIndex = 0; + textboxExtra.hidden = false; + synthesizeMouseAtCenter(tab, { }); + }); + + synthesizeMouseAtCenter(tab, { }); + }); + + synthesizeMouseAtCenter(tab, { }); +} + +function when_tab_focused(tab, callback) { + tab.addEventListener("focus", function onFocused() { + SimpleTest.executeSoon(callback); + }, {once: true}); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tabindex.xhtml b/toolkit/content/tests/chrome/test_tabindex.xhtml new file mode 100644 index 0000000000..9828728c63 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tabindex.xhtml @@ -0,0 +1,136 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tabindex + --> +<window title="tabindex" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + Elements are navigated in the following order: + 1. tabindex > 0 in tree order + 2. tabindex = 0 in tree order + Elements with tabindex = -1 are focusable, but not in the tab order + --> +<hbox> + <button id="t7" label="One"/> + <checkbox id="f1" label="Two" tabindex="-1"/> + <button id="t8" label="Three" tabindex="0"/> + <checkbox id="t1" label="Four" tabindex="1"/> +</hbox> +<hbox> + <html:input id="t9" idmod="t5" size="3"/> + <html:input id="f2" size="3" tabindex="-1"/> + <html:input id="t10" idmod="t6" size="3" tabindex="0"/> + <html:input id="t2" idmod="t1" size="3" tabindex="1"/> +</hbox> +<hbox> + <button id="n1" style="-moz-user-focus: ignore;" label="One"/> + <checkbox id="f3" style="-moz-user-focus: ignore;" label="Two" tabindex="-1"/> + <button id="t11" style="-moz-user-focus: ignore;" label="Three" tabindex="0"/> + <checkbox id="t3" style="-moz-user-focus: ignore;" label="Four" tabindex="1"/> +</hbox> +<hbox> + <html:input id="t12" idmod="t7" style="-moz-user-focus: ignore;" size="3"/> + <html:input id="f4" style="-moz-user-focus: ignore;" size="3" tabindex="-1"/> + <html:input id="t13" idmod="t8" style="-moz-user-focus: ignore;" size="3" tabindex="0"/> + <html:input id="t4" idmod="t2" style="-moz-user-focus: ignore;" size="3" tabindex="1"/> +</hbox> +<richlistbox id="t14" idmod="t9"> + <richlistitem><label value="Item One"/></richlistitem> +</richlistbox> + +<hbox> + <!-- the tabindex attribute applies to non-controls as well. They are not + focusable unless tabindex is explicitly specified. + --> + <dropmarker id="n2"/> + <dropmarker id="f5" tabindex="-1"/> + <dropmarker id="t15" tabindex="0"/> + <dropmarker id="t5" idmod="t3" tabindex="1"/> + <dropmarker id="t16" style="-moz-user-focus: normal;"/> + <dropmarker id="f6" style="-moz-user-focus: normal;" tabindex="-1"/> + <dropmarker id="t17" style="-moz-user-focus: normal;" tabindex="0"/> + <dropmarker id="t6" idmod="t4" style="-moz-user-focus: normal;" tabindex="1"/> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function checkFocusability(aId, aFocusable) +{ + document.activeElement.blur(); + let testNode = document.getElementById(aId); + testNode.focus(); + let newFocus = document.activeElement; + let check = aFocusable ? is : isnot; + let focusableText = aFocusable ? "focusable " : "unfocusable "; + check(newFocus, testNode, + ".focus() call on " + focusableText + aId); +} + +var gAdjustedTabFocusModel = false; +var gTestCount = 17; +var gTestsOccurred = 0; +let gFocusableNotTabbableCount = 6; +let gNotFocusableCount = 2; + +function runTests() +{ + var t; + function onFocus(event) { + if (t == 1 && event.target.id == "t2") { + // looks to be using the MacOSX Full Keyboard Access set to Textboxes + // and lists only so use the idmod attribute instead + gAdjustedTabFocusModel = true; + gTestCount = 9; + } + + var attrcompare = gAdjustedTabFocusModel ? "idmod" : "id"; + + // check for the last test which should wrap aorund to the first item + // consider the focus event on the inner input of textboxes instead + is(event.target.getAttribute(attrcompare), "t" + t, + "tab " + t + " to " + event.target.localName); + gTestsOccurred++; + } + window.addEventListener("focus", onFocus, true); + + for (t = 1; t <= gTestCount; t++) + synthesizeKey("KEY_Tab"); + + is(gTestsOccurred, gTestCount, "test count"); + window.removeEventListener("focus", onFocus, true); + + for (let i = 1; i <= gFocusableNotTabbableCount; ++i) { + checkFocusability("f" + i, true); + } + + for (let i = 1; i <= gNotFocusableCount; ++i) { + checkFocusability("n" + i, false); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_textbox_search.xhtml b/toolkit/content/tests/chrome/test_textbox_search.xhtml new file mode 100644 index 0000000000..78523e53a4 --- /dev/null +++ b/toolkit/content/tests/chrome/test_textbox_search.xhtml @@ -0,0 +1,174 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for search textbox + --> +<window title="Search textbox test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <hbox id="searchbox-container"> + <search-textbox id="searchbox" + oncommand="doSearch(this.value);" + placeholder="random placeholder" + timeout="1"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gExpectedValue; +var gLastTest; + +function doTests() { + var textbox = $("searchbox"); + + // Reconnect the searchbox to ensure connectedCallback only runs once + // (bug 1650486). + textbox.remove(); + $("searchbox-container").append(textbox); + is(textbox.shadowRoot.querySelectorAll(".textbox-search-sign").length, 1, + "only one search icon in the search box"); + + var icons = textbox._searchIcons; + var searchIcon = icons.querySelector(".textbox-search-icon"); + var clearIcon = icons.querySelector(".textbox-search-clear"); + + ok(icons, "icon deck found"); + ok(searchIcon, "search icon found"); + ok(clearIcon, "clear icon found"); + is(icons.selectedPanel, searchIcon, "search icon is displayed"); + + is(textbox.placeholder, "random placeholder", "search textbox supports placeholder"); + is(textbox.value, "", "placeholder doesn't interfere with the real value"); + + function iconClick(aIcon) { + is(icons.selectedPanel, aIcon, aIcon.className + " icon must be displayed in order to be clickable"); + + //XXX synthesizeMouse worked on Linux but failed on Windows an Mac + // for unknown reasons. Manually dispatch the event for now. + //synthesizeMouse(aIcon, 0, 0, {}); + + var event = document.createEvent("MouseEvent"); + event.initMouseEvent("click", true, true, window, 1, + 0, 0, 0, 0, + false, false, false, false, + 0, null); + aIcon.dispatchEvent(event); + } + + iconClick(searchIcon); + is(textbox.getAttribute("focused"), "true", "clicking the search icon focuses the textbox"); + + textbox.value = "foo"; + is(icons.selectedPanel, clearIcon, "clear icon is displayed when setting a value"); + + textbox.value = ""; + is(textbox.defaultValue, "", "defaultValue is empty"); + is(textbox.value, "", "reset method clears the textbox"); + is(icons.selectedPanel, searchIcon, "search icon is displayed after clearing value"); + + textbox.value = "foo"; + gExpectedValue = ""; + iconClick(clearIcon); + is(textbox.value, "", "clicking the clear icon clears the textbox"); + ok(gExpectedValue == null, "search triggered when clearing the textbox with the clear icon"); + + textbox.value = "foo"; + gExpectedValue = ""; + synthesizeKey("KEY_Escape"); + is(textbox.value, "", "escape key clears the textbox"); + ok(gExpectedValue == null, "search triggered when clearing the textbox with the escape key"); + + textbox.value = "bar"; + gExpectedValue = "bar"; + textbox.doCommand(); + ok(gExpectedValue == null, "search triggered with doCommand"); + + gExpectedValue = "bar"; + synthesizeKey("KEY_Enter"); + ok(gExpectedValue == null, "search triggered with enter key"); + + textbox.value = ""; + textbox.searchButton = true; + is(textbox.getAttribute("searchbutton"), "true", "searchbutton attribute set on the textbox"); + + textbox.value = "foo"; + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode if there's a value"); + + gExpectedValue = "foo"; + iconClick(searchIcon); + ok(gExpectedValue == null, "search triggered when clicking the search icon in search button mode"); + is(icons.selectedPanel, clearIcon, "clear icon displayed in search button mode after submitting"); + + sendString("o"); + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode when typing a key"); + + gExpectedValue = "fooo"; + iconClick(searchIcon); // display the clear icon (tested above) + + textbox.value = "foo"; + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode when the value is changed"); + + gExpectedValue = "foo"; + synthesizeKey("KEY_Enter"); + ok(gExpectedValue == null, "search triggered with enter key in search button mode"); + is(icons.selectedPanel, clearIcon, "clear icon displayed in search button mode after submitting with enter key"); + + textbox.value = "x"; + synthesizeKey("KEY_Backspace"); + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode when deleting the value with the backspace key"); + + gExpectedValue = ""; + synthesizeKey("KEY_Enter"); + ok(gExpectedValue == null, "search triggered with enter key in search button mode"); + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode after submitting an empty string"); + + textbox.readOnly = true; + gExpectedValue = "foo"; + textbox.value = "foo"; + iconClick(searchIcon); + ok(gExpectedValue == null, "search triggered when clicking the search icon in search button mode while the textbox is read-only"); + is(icons.selectedPanel, searchIcon, "search icon persists in search button mode after submitting while the textbox is read-only"); + textbox.readOnly = false; + + textbox.disabled = true; + is(searchIcon.getAttribute("disabled"), "true", "disabled attribute inherited to the search icon"); + is(clearIcon.getAttribute("disabled"), "true", "disabled attribute inherited to the clear icon"); + gExpectedValue = false; + textbox.value = "foo"; + iconClick(searchIcon); + ok(!gExpectedValue, "search *not* triggered when clicking the search icon in search button mode while the textbox is disabled"); + is(icons.selectedPanel, searchIcon, "search icon persists in search button mode when trying to submit while the textbox is disabled"); + textbox.disabled = false; + ok(!searchIcon.hasAttribute("disabled"), "disabled attribute removed from the search icon"); + ok(!clearIcon.hasAttribute("disabled"), "disabled attribute removed from the clear icon"); + + textbox.searchButton = false; + ok(!textbox.hasAttribute("searchbutton"), "searchbutton attribute removed from the textbox"); + + gLastTest = true; + gExpectedValue = "123"; + textbox.value = "1"; + sendString("23"); +} + +function doSearch(aValue) { + is(aValue, gExpectedValue, "search triggered with expected value"); + gExpectedValue = null; + if (gLastTest) + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTests); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_titlebar.xhtml b/toolkit/content/tests/chrome/test_titlebar.xhtml new file mode 100644 index 0000000000..0ead954426 --- /dev/null +++ b/toolkit/content/tests/chrome/test_titlebar.xhtml @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for the titlebar element and window dragging + --> +<window title="Titlebar" width="200" height="200" + onload="setTimeout(test_titlebar, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_titlebar() +{ + window.openDialog("window_titlebar.xhtml", "_blank", "chrome,left=200,top=200,noopener", window); +} + +function done(testWindow) +{ + testWindow.close(); + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tooltip.xhtml b/toolkit/content/tests/chrome/test_tooltip.xhtml new file mode 100644 index 0000000000..ec4855131d --- /dev/null +++ b/toolkit/content/tests/chrome/test_tooltip.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tooltip Tests" + onload="setTimeout(runTest, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_tooltip.xhtml", "_blank", "chrome,width=700,height=700,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_tooltip_noautohide.xhtml b/toolkit/content/tests/chrome/test_tooltip_noautohide.xhtml new file mode 100644 index 0000000000..99216809bf --- /dev/null +++ b/toolkit/content/tests/chrome/test_tooltip_noautohide.xhtml @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tooltip Noautohide Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<tooltip id="thetooltip" noautohide="true" + onpopupshown="setTimeout(tooltipStillShown, 6000)" + onpopuphidden="ok(gChecked, 'tooltip did not hide'); SimpleTest.finish()"> + <label id="label" value="This is a tooltip"/> +</tooltip> + +<button id="button" label="Tooltip Text" tooltip="thetooltip"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var gChecked = false; + +function runTests() +{ + var button = document.getElementById("button"); + var windowUtils = window.windowUtils; + windowUtils.disableNonTestMouseEvents(true); + synthesizeMouse(button, 2, 2, { type: "mouseover" }); + synthesizeMouse(button, 4, 4, { type: "mousemove" }); + synthesizeMouse(button, 6, 6, { type: "mousemove" }); + windowUtils.disableNonTestMouseEvents(false); +} + +function tooltipStillShown() +{ + gChecked = true; + document.getElementById("thetooltip").hidePopup(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree.xhtml b/toolkit/content/tests/chrome/test_tree.xhtml new file mode 100644 index 0000000000..d35d6354ab --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tree using multiple row selection + --> +<window title="Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-simple', 'treechildren-simple', 'multiple', 'simple', 'tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<script src="tree_shared.js"/> + +<tree id="tree-simple" rows="4"> + <treecols> + <treecol id="name" label="Name" sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" flex="1"/> + </treecols> + <treechildren id="treechildren-simple"> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> + diff --git a/toolkit/content/tests/chrome/test_tree_hier.xhtml b/toolkit/content/tests/chrome/test_tree_hier.xhtml new file mode 100644 index 0000000000..81d0567691 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_hier.xhtml @@ -0,0 +1,136 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for hierarchical tree + --> +<window title="Hierarchical Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-hier', 'treechildren-hier', 'multiple', '', 'hierarchical tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<script src="tree_shared.js"/> + +<tree id="tree-hier" rows="4"> + <treecols> + <treecol id="name" label="Name" primary="true" + sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" flex="2"/> + <treecol id="planet" label="Planet" flex="1"/> + <treecol id="gender" label="Gender" flex="1" cycler="true"/> + </treecols> + <treechildren id="treechildren-hier"> + <treeitem> + <treerow properties="firstrow"> + <treecell label="Mary" value="mary" properties="firstname"/> + <treecell label="206 Garden Avenue" value="206ga"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell/> + <treecell value="19ms"/> + <treecell label="Earth"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem container="true"> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + <treecell label="Saturn"/> + <treecell label="Female" value="f"/> + </treerow> + <treechildren> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + <treecell label="Female" value="f"/> + <treecell label="Neptune"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + <treecell label="Omicron Persei 8"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell label="Neptune"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue" selectable="false"/> + <treecell label=""/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + <treecell label="Neptune"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell label="Mars"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree_single.xhtml b/toolkit/content/tests/chrome/test_tree_single.xhtml new file mode 100644 index 0000000000..d5bae73ccb --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_single.xhtml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for single selection tree + --> +<window title="Single Selection Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-single', 'treechildren-single', + 'single', 'simple', 'single selection tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<script src="tree_shared.js"/> + +<tree id="tree-single" rows="4" seltype="single"> + <treecols> + <treecol id="name" label="Name" sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" flex="1"/> + </treecols> + <treechildren id="treechildren-single"> + <treeitem> + <treerow properties="firstrow"> + <treecell label="Mary" value="mary" properties="firstname"/> + <treecell label="206 Garden Avenue" value="206ga"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell/> + <treecell value="19ms"/> + </treerow> + </treeitem> + <treeitem container="true"> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + </treerow> + <treechildren> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue" selectable="false"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree_view.xhtml b/toolkit/content/tests/chrome/test_tree_view.xhtml new file mode 100644 index 0000000000..bf20a2c8b6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_view.xhtml @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tree using a custom nsITreeView + --> +<window title="Tree" onload="init()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<script src="tree_shared.js"/> + +<script> +<![CDATA[ +/* import-globals-from ../widgets/tree_shared.js */ +// This is our custom view, based on the treeview interface +var view = +{ + treeData: [["Mary", "206 Garden Avenue"], + ["Chris", "19 Marion Street"], + ["Sarah", "702 Fern Avenue"], + ["John", "99 Westminster Avenue"]], + value: "", + rowCount: 8, + getCellText(row, column) { return this.treeData[row % 4][column.index]; }, + getCellValue(row, column) { return this.value; }, + setCellText(row, column, val) { this.treeData[row % 4][column.index] = val; }, + setCellValue(row, column, val) { this.value = val; }, + setTree(tree) { this.tree = tree; }, + isContainer(row) { return false; }, + isContainerOpen(row) { return false; }, + isContainerEmpty(row) { return false; }, + isSeparator(row) { return false; }, + isSorted(row) { return false; }, + isEditable(row, column) { return row != 2 || column.index != 1; }, + getParentIndex(row, column) { return -1; }, + getLevel(row) { return 0; }, + hasNextSibling(row, column) { return row != this.rowCount - 1; }, + getImageSrc(row, column) { return ""; }, + cycleHeader(column) { }, + getRowProperties(row) { return ""; }, + getCellProperties(row, column) { return ""; }, + getColumnProperties(column) + { + if (!column.index) { + return "one two"; + } + + return ""; + } +} + +function getCustomTreeViewCellInfo() +{ + var obj = { rows: [] }; + + for (var row = 0; row < view.rowCount; row++) { + var cellInfo = [ ]; + for (var column = 0; column < 1; column++) { + cellInfo.push({ label: "" + view.treeData[row % 4][column], + value: "", + properties: "", + editable: row != 2 || column.index != 1, + selectable: true, + image: "" }); + } + + obj.rows.push({ cells: cellInfo, + properties: "", + container: false, + separator: false, + children: null, + level: 0, + parent: -1 }); + } + + return obj; +} + +function init() +{ + var tree = document.getElementById("tree-view"); + tree.view = view; + tree.ensureRowIsVisible(0); + is(tree.getFirstVisibleRow(), 0, "first visible after ensureRowIsVisible on load"); + tree.setAttribute("rows", "4"); + + setTimeout(testtag_tree, 0, "tree-view", "treechildren-view", "multiple", "simple", "tree view"); +} + +]]> +</script> + +<tree id="tree-view"> + <treecols> + <treecol id="name" label="Name" sort="label" flex="1"/> + <treecol id="address" label="Address" flex="1"/> + </treecols> + <treechildren id="treechildren-view"/> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_window_intrinsic_size.xhtml b/toolkit/content/tests/chrome/test_window_intrinsic_size.xhtml new file mode 100644 index 0000000000..451d2f0cce --- /dev/null +++ b/toolkit/content/tests/chrome/test_window_intrinsic_size.xhtml @@ -0,0 +1,43 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Window Open Test" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +<script class="testbody" type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + function openWindow(features = "") { + return window.browsingContext.topChromeWindow + .openDialog("window_intrinsic_size.xhtml", + "", "chrome,dialog=no,all," + features); + } + + function checkWindowSize(win, width, height, msg) { + is(win.innerWidth, width, "width should match " + msg); + is(win.innerHeight, height, "height should match " + msg); + } + + async function runTest() { + let win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindowSize(win, 300, 500, "with width attribute specified"); + + win = openWindow("width=400"); + await SimpleTest.promiseFocus(win); + checkWindowSize(win, 400, 500, "with width feature specified"); + + SimpleTest.finish(); + } +]]></script> +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</window> diff --git a/toolkit/content/tests/chrome/window_browser_drop.xhtml b/toolkit/content/tests/chrome/window_browser_drop.xhtml new file mode 100644 index 0000000000..2d55ec2357 --- /dev/null +++ b/toolkit/content/tests/chrome/window_browser_drop.xhtml @@ -0,0 +1,250 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Browser Drop Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +<![CDATA[ + +const { ContentTask } = ChromeUtils.import("resource://testing-common/ContentTask.jsm"); + +function dropOnRemoteBrowserAsync(browser, data, shouldExpectStateChange) { + ContentTask.setTestScope(window); // Need this so is/isnot/ok are available inside the contenttask + return ContentTask.spawn(browser, {data, shouldExpectStateChange}, async function({data, shouldExpectStateChange}) { + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + if (!content.document.documentElement) { + // Wait until the testing document gets loaded. + await new Promise(resolve => { + let onload = function() { + content.window.removeEventListener("load", onload); + resolve(); + }; + content.window.addEventListener("load", onload); + }); + } + + let dataTransfer = new content.DataTransfer(); + for (let i = 0; i < data.length; i++) { + let types = data[i]; + for (let j = 0; j < types.length; j++) { + dataTransfer.mozSetDataAt(types[j].type, types[j].data, i); + } + } + let event = content.document.createEvent("DragEvent"); + event.initDragEvent("drop", true, true, content, 0, 0, 0, 0, 0, + false, false, false, false, 0, null, dataTransfer); + content.document.body.dispatchEvent(event); + + let links = []; + try { + links = Services.droppedLinkHandler.dropLinks(event, true); + } catch (ex) { + if (shouldExpectStateChange) { + ok(false, "Should not have gotten an exception from the dropped link handler, but got: " + ex); + Cu.reportError(ex); + } + } + + return links; + }); +} + +async function expectLink(browser, expectedLinks, data, testid, onbody=false) { + let lastLinks = []; + let lastLinksPromise = new Promise(resolve => { + browser.droppedLinkHandler = function(event, links) { + info(`droppedLinkHandler called, received links ${JSON.stringify(links)}`); + if (!expectedLinks.length) { + ok(false, `droppedLinkHandler called for ${JSON.stringify(links)} which we didn't expect.`); + } + lastLinks = links; + resolve(links); + }; + }); + + function dropOnBrowserSync() { + let dropEl = onbody ? browser.contentDocument.body : browser; + synthesizeDrop(dropEl, dropEl, data, null, dropEl.ownerGlobal); + } + let links; + if (browser.isRemoteBrowser) { + let remoteLinks = await dropOnRemoteBrowserAsync(browser, data, expectedLinks.length); + is(remoteLinks.length, expectedLinks.length, testid + " remote links length"); + for (let i = 0, length = remoteLinks.length; i < length; i++) { + is(remoteLinks[i].url, expectedLinks[i].url, testid + "[" + i + "] remote link"); + is(remoteLinks[i].name, expectedLinks[i].name, testid + "[" + i + "] remote name"); + } + + if (!expectedLinks.length) { + // There is no way to check if nothing happens asynchronously. + return; + } + + links = await lastLinksPromise; + } else { + dropOnBrowserSync(); + links = lastLinks; + } + + is(links.length, expectedLinks.length, testid + " links length"); + for (let i = 0, length = links.length; i < length; i++) { + is(links[i].url, expectedLinks[i].url, testid + "[" + i + "] link"); + is(links[i].name, expectedLinks[i].name, testid + "[" + i + "] name"); + } +}; + +async function dropLinksOnBrowser(browser, type) { + // Dropping single text/plain item with single link should open single + // page. + await expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" } ], + [ [ { type: "text/plain", + data: "http://www.mozilla.org/" } ] ], + "text/plain drop on browser " + type); + + // Dropping single text/plain item with multiple links should open + // multiple pages. + await expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" }, + { url: "http://www.example.com/", + name: "http://www.example.com/" } ], + [ [ { type: "text/plain", + data: "http://www.mozilla.org/\nhttp://www.example.com/" } ] ], + "text/plain with 2 URLs drop on browser " + type); + + // Dropping sinlge unsupported type item should not open anything. + await expectLink(browser, + [], + [ [ { type: "text/link", + data: "http://www.mozilla.org/" } ] ], + "text/link drop on browser " + type); + + // Dropping single text/uri-list item with single link should open single + // page. + await expectLink(browser, + [ { url: "http://www.example.com/", + name: "http://www.example.com/" } ], + [ [ { type: "text/uri-list", + data: "http://www.example.com/" } ] ], + "text/uri-list drop on browser " + type); + + // Dropping single text/uri-list item with multiple links should open + // multiple pages. + await expectLink(browser, + [ { url: "http://www.example.com/", + name: "http://www.example.com/" }, + { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" }], + [ [ { type: "text/uri-list", + data: "http://www.example.com/\nhttp://www.mozilla.org/" } ] ], + "text/uri-list with 2 URLs drop on browser " + type); + + // Name in text/x-moz-url should be handled. + await expectLink(browser, + [ { url: "http://www.example.com/", + name: "Example.com" } ], + [ [ { type: "text/x-moz-url", + data: "http://www.example.com/\nExample.com" } ] ], + "text/x-moz-url drop on browser " + type); + + await expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "Mozilla.org" }, + { url: "http://www.example.com/", + name: "Example.com" } ], + [ [ { type: "text/x-moz-url", + data: "http://www.mozilla.org/\nMozilla.org\nhttp://www.example.com/\nExample.com" } ] ], + "text/x-moz-url with 2 URLs drop on browser " + type); + + // Dropping single item with multiple types should open single page. + await expectLink(browser, + [ { url: "http://www.example.org/", + name: "Example.com" } ], + [ [ { type: "text/plain", + data: "http://www.mozilla.org/" }, + { type: "text/x-moz-url", + data: "http://www.example.org/\nExample.com" } ] ], + "text/plain and text/x-moz-url drop on browser " + type); + + // Dropping javascript or data: URLs should fail: + await expectLink(browser, + [], + [ [ { type: "text/plain", + data: "javascript:'bad'" } ] ], + "text/plain javascript url drop on browser " + type); + await expectLink(browser, + [], + [ [ { type: "text/plain", + data: "jAvascript:'also bad'" } ] ], + "text/plain mixed-case javascript url drop on browser " + type); + await expectLink(browser, + [], + [ [ { type: "text/plain", + data: "data:text/html,bad" } ] ], + "text/plain data url drop on browser " + type); + + // Dropping a chrome url should fail as we don't have a source node set, + // defaulting to a source of file:/// + await expectLink(browser, + [], + [ [ { type: "text/x-moz-url", + data: "chrome://browser/content/browser.xhtml" } ] ], + "text/x-moz-url chrome url drop on browser " + type); + + if (browser.type == "content") { + await SpecialPowers.spawn(browser, [], function() { + content.window.stopMode = true; + }); + + // stopPropagation should not prevent the browser link handling from occuring + await expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" } ], + [ [ { type: "text/uri-list", + data: "http://www.mozilla.org/" } ] ], + "text/x-moz-url drop on browser with stopPropagation drop event", true); + + await SpecialPowers.spawn(browser, [], function() { + content.window.cancelMode = true; + }); + + // Canceling the event, however, should prevent the link from being handled. + await expectLink(browser, + [], + [ [ { type: "text/uri-list", data: "http://www.mozilla.org/" } ] ], + "text/x-moz-url drop on browser with cancelled drop event", true); + } +} + +function info(msg) { window.arguments[0].SimpleTest.info(msg); } +function is(l, r, n) { window.arguments[0].SimpleTest.is(l,r,n); } +function ok(v, n) { window.arguments[0].SimpleTest.ok(v,n); } + +]]> +</script> + +<html:style> + /* FIXME: This is just preserving behavior from before bug 1656081. + * Without this there's one subtest that fails, but the browser + * elements were zero-sized before so... */ + browser { + width: 0; + height: 0; + } +</html:style> + +<browser id="chromechild" src="about:blank"/> +<browser id="contentchild" type="content" width="100" height="100" + src="data:text/html,<html draggable='true'><body draggable='true' style='width: 100px; height: 100px;' ondragover='event.preventDefault()' ondrop='if (window.stopMode) event.stopPropagation(); if (window.cancelMode) event.preventDefault();'></body></html>"/> + +<browser id="remote-contentchild" type="content" width="100" height="100" remote="true" + src="data:text/html,<html draggable='true'><body draggable='true' style='width: 100px; height: 100px;' ondragover='event.preventDefault()' ondrop='if (window.stopMode) event.stopPropagation(); if (window.cancelMode) event.preventDefault();'></body></html>"/> +</window> diff --git a/toolkit/content/tests/chrome/window_chromemargin.xhtml b/toolkit/content/tests/chrome/window_chromemargin.xhtml new file mode 100644 index 0000000000..d5fe4dde37 --- /dev/null +++ b/toolkit/content/tests/chrome/window_chromemargin.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="window" title="Subframe Origin Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +chrome margins rock! +<script> + +// Tests parsing of the chrome margin attrib on a window. + +function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); +} + +function doSingleTest(param, shouldSucceed) +{ + var exception = null; + try { + document.documentElement.removeAttribute("chromemargin"); + document.documentElement.setAttribute("chromemargin", param); + ok(document. + documentElement. + getAttribute("chromemargin") == param, "couldn't set/get chromemargin?"); + } catch (ex) { + exception = ex; + } + if (shouldSucceed) + ok(!exception, "failed for param:'" + param + "'"); + else + ok(exception, "did not fail for invalid param:'" + param + "'"); + return true; +} + +function runTests() +{ + var doc = document.documentElement; + + // make sure we can set and get + doc.setAttribute("chromemargin", "0,0,0,0"); + ok(doc.getAttribute("chromemargin") == "0,0,0,0", "couldn't set/get chromemargin?"); + doc.setAttribute("chromemargin", "-1,-1,-1,-1"); + ok(doc.getAttribute("chromemargin") == "-1,-1,-1,-1", "couldn't set/get chromemargin?"); + + // test remove + doc.removeAttribute("chromemargin"); + ok(doc.getAttribute("chromemargin") == "", "couldn't remove chromemargin?"); + + // we already test these really well in a c++ test in widget + doSingleTest("1,2,3,4", true); + doSingleTest("-2,-2,-2,-2", true); + doSingleTest("1,1,1,1", true); + doSingleTest("", false); + doSingleTest("12123123", false); + doSingleTest("0,-1,-1,-1", true); + doSingleTest("-1,0,-1,-1", true); + doSingleTest("-1,-1,0,-1", true); + doSingleTest("-1,-1,-1,0", true); + doSingleTest("1234567890,1234567890,1234567890,1234567890", true); + doSingleTest("-1,-1,-1,-1", true); + + window.arguments[0].SimpleTest.finish(); + window.close(); +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml b/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml new file mode 100644 index 0000000000..d5c0e2753e --- /dev/null +++ b/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Cursor snapping test" + width="600" height="600" + onload="onload();" + onunload="onunload();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<dialog id="dialog" + buttons="accept,cancel"> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.arguments[0].SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function canRetryTest() +{ + return window.arguments[0].canRetryTest(); +} + +function getTimeoutTime() +{ + return window.arguments[0].getTimeoutTime(); +} + +var gTimer; +var gRetry; + +function finishByTimeout() +{ + var button = document.getElementById("dialog").getButton("accept"); + if (button.disabled) { + ok(true, "cursor is NOT snapped to the disabled button (dialog)"); + } else if (button.hidden) { + ok(true, "cursor is NOT snapped to the hidden button (dialog)"); + } else if (!canRetryTest()) { + ok(false, "cursor is NOT snapped to the default button (dialog)"); + } else { + // otherwise, this may be unexpected timeout, we should retry the test. + gRetry = true; + } + finish(); +} + +function finish() +{ + window.close(); +} + +function onMouseMove(aEvent) +{ + var button = document.getElementById("dialog").getButton("accept"); + if (button.disabled) + ok(false, "cursor IS snapped to the disabled button (dialog)"); + else if (button.hidden) + ok(false, "cursor IS snapped to the hidden button (dialog)"); + else + ok(true, "cursor IS snapped to the default button (dialog)"); + clearTimeout(gTimer); + finish(); +} + +function onload() +{ + var button = document.getElementById("dialog").getButton("accept"); + button.addEventListener("mousemove", onMouseMove); + + if (window.arguments[0].gDisable) { + button.disabled = true; + } + if (window.arguments[0].gHidden) { + button.hidden = true; + } + gRetry = false; + gTimer = setTimeout(finishByTimeout, getTimeoutTime()); +} + +function onunload() +{ + if (gRetry) { + window.arguments[0].retryCurrentTest(); + } else { + window.arguments[0].runNextTest(); + } +} + +]]> +</script> + +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_cursorsnap_wizard.xhtml b/toolkit/content/tests/chrome/window_cursorsnap_wizard.xhtml new file mode 100644 index 0000000000..661e9c4e65 --- /dev/null +++ b/toolkit/content/tests/chrome/window_cursorsnap_wizard.xhtml @@ -0,0 +1,109 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<wizard title="Cursor snapping test" id="wizard" + width="600" height="600" + onload="onload();" + onunload="onunload();" + buttons="accept,cancel" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <wizardpage> + <label value="first page"/> + </wizardpage> + + <wizardpage> + <label value="second page"/> + </wizardpage> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function canRetryTest() +{ + return window.opener.wrappedJSObject.canRetryTest(); +} + +function getTimeoutTime() +{ + return window.opener.wrappedJSObject.getTimeoutTime(); +} + +var gTimer; +var gRetry = false; + +function finishByTimeout() +{ + var button = document.getElementById("wizard").getButton("next"); + if (button.disabled) { + ok(true, "cursor is NOT snapped to the disabled button (wizard)"); + } else if (button.hidden) { + ok(true, "cursor is NOT snapped to the hidden button (wizard)"); + } else if (!canRetryTest()) { + ok(false, "cursor is NOT snapped to the default button (wizard)"); + } else { + // otherwise, this may be unexpected timeout, we should retry the test. + gRetry = true; + } + finish(); +} + +function finish() +{ + window.close(); +} + +function onMouseMove() +{ + var button = document.getElementById("wizard").getButton("next"); + if (button.disabled) + ok(false, "cursor IS snapped to the disabled button (wizard)"); + else if (button.hidden) + ok(false, "cursor IS snapped to the hidden button (wizard)"); + else + ok(true, "cursor IS snapped to the default button (wizard)"); + clearTimeout(gTimer); + finish(); +} + +function onload() +{ + var button = document.getElementById("wizard").getButton("next"); + button.addEventListener("mousemove", onMouseMove); + + if (window.opener.wrappedJSObject.gDisable) { + button.disabled = true; + } + if (window.opener.wrappedJSObject.gHidden) { + button.hidden = true; + } + gTimer = setTimeout(finishByTimeout, getTimeoutTime()); +} + +function onunload() +{ + if (gRetry) { + window.opener.wrappedJSObject.retryCurrentTest(); + } else { + window.opener.wrappedJSObject.runNextTest(); + } +} + +]]> +</script> + +</wizard> diff --git a/toolkit/content/tests/chrome/window_intrinsic_size.xhtml b/toolkit/content/tests/chrome/window_intrinsic_size.xhtml new file mode 100644 index 0000000000..08bad28e9b --- /dev/null +++ b/toolkit/content/tests/chrome/window_intrinsic_size.xhtml @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Window Open Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="300"> +<div xmlns="http://www.w3.org/1999/xhtml" style="height: 500px"></div> +</window> diff --git a/toolkit/content/tests/chrome/window_keys.xhtml b/toolkit/content/tests/chrome/window_keys.xhtml new file mode 100644 index 0000000000..e72c790947 --- /dev/null +++ b/toolkit/content/tests/chrome/window_keys.xhtml @@ -0,0 +1,200 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Key Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gExpected = null; + +const kIsWin = navigator.platform.includes("Win"); + +// Only on Windows, osKey state is ignored when there is no shortcut key handler +// which exactly matches with osKey state. +var keysToTest = [ + ["k-v", "V", { } ], + ["", "V", { shiftKey: true } ], + ["k-v-scy", "V", { ctrlKey: true } ], + ["", "V", { altKey: true } ], + ["", "V", { metaKey: true } ], + [kIsWin ? "k-v" : "", "V", { osKey: true } ], + ["k-v-scy", "V", { shiftKey: true, ctrlKey: true } ], + ["", "V", { shiftKey: true, ctrlKey: true, altKey: true } ], + ["k-e-y", "E", { } ], + ["", "E", { shiftKey: true } ], + ["", "E", { ctrlKey: true } ], + ["", "E", { altKey: true } ], + ["", "E", { metaKey: true } ], + [kIsWin ? "k-e-y" : "", "E", { osKey: true } ], + ["k-d-a", "D", { altKey: true } ], + ["k-8-m", "8", { metaKey: true } ], + [kIsWin ? "k-8-m" : "", "8", { metaKey: true, osKey: true } ], + ["k-a-o", "A", { osKey: true } ], + ["", "A", { osKey: true, metaKey: true } ], + ["", "B", {} ], + ["k-b-myo", "B", { osKey: true } ], + ["k-b-myo", "B", { osKey: true, metaKey: true } ], + ["k-f-oym", "F", { metaKey: true } ], + ["k-f-oym", "F", { metaKey: true, osKey: true } ], + ["k-c-scaym", "C", { metaKey: true } ], + ["k-c-scaym", "C", { shiftKey: true, ctrlKey: true, altKey: true, metaKey: true } ], + [kIsWin ? "k-c-scaym" : "", "C", { shiftKey: true, ctrlKey: true, altKey: true, metaKey: true, osKey: true } ], + ["", "V", { shiftKey: true, ctrlKey: true, altKey: true } ], + ["k-h-l", "H", { accelKey: true } ], +// ["k-j-s", "J", { accessKey: true } ], + ["", "T", { } ], + ["k-g-c", "G", { ctrlKey: true } ], + ["k-g-co", "G", { ctrlKey: true, osKey: true } ], + ["scommand", "Y", { } ], + ["", "U", { } ], +]; + +function runTest() +{ + iterateKeys(true, "normal"); + + var keyset = document.getElementById("keyset"); + keyset.setAttribute("disabled", "true"); + iterateKeys(false, "disabled"); + + keyset = document.getElementById("keyset"); + keyset.removeAttribute("disabled"); + iterateKeys(true, "reenabled"); + + keyset.remove(); + iterateKeys(false, "removed"); + + document.documentElement.appendChild(keyset); + iterateKeys(true, "appended"); + + var accelText = menuitem => menuitem.getAttribute("acceltext").toLowerCase(); + + $("menubutton").open = true; + + // now check if a menu updates its accelerator text when a key attribute is changed + var menuitem1 = $("menuitem1"); + ok(accelText(menuitem1).includes("d"), "menuitem1 accelText before"); + if (kIsWin) { + ok(accelText(menuitem1).includes("alt"), "menuitem1 accelText modifier before"); + } + + menuitem1.setAttribute("key", "k-s-c"); + ok(accelText(menuitem1).includes("s"), "menuitem1 accelText after"); + if (kIsWin) { + ok(accelText(menuitem1).includes("ctrl"), "menuitem1 accelText modifier after"); + } + + menuitem1.setAttribute("acceltext", "custom"); + is(accelText(menuitem1), "custom", "menuitem1 accelText set custom"); + menuitem1.removeAttribute("acceltext"); + ok(accelText(menuitem1).includes("s"), "menuitem1 accelText remove"); + if (kIsWin) { + ok(accelText(menuitem1).includes("ctrl"), "menuitem1 accelText modifier remove"); + } + + var menuitem2 = $("menuitem2"); + is(accelText(menuitem2), "", "menuitem2 accelText before"); + menuitem2.setAttribute("key", "k-s-c"); + ok(accelText(menuitem2).includes("s"), "menuitem2 accelText before"); + if (kIsWin) { + ok(accelText(menuitem2).includes("ctrl"), "menuitem2 accelText modifier before"); + } + + menuitem2.setAttribute("key", "k-h-l"); + ok(accelText(menuitem2).includes("h"), "menuitem2 accelText after"); + if (kIsWin) { + ok(accelText(menuitem2).includes("ctrl"), "menuitem2 accelText modifier after"); + } + + menuitem2.removeAttribute("key"); + is(accelText(menuitem2), "", "menuitem2 accelText after remove"); + + $("menubutton").open = false; + + window.close(); + window.arguments[0].SimpleTest.finish(); +} + +function iterateKeys(enabled, testid) +{ + for (var k = 0; k < keysToTest.length; k++) { + gExpected = keysToTest[k]; + var expectedKey = gExpected[0]; + if (!gExpected[2].accessKey || !navigator.platform.includes("Mac")) { + synthesizeKey(gExpected[1], gExpected[2]); + ok((enabled && expectedKey) || expectedKey == "k-d-a" ? + !gExpected : gExpected, testid + " key step " + (k + 1)); + } + } +} + +function checkKey(event) +{ + // the first element of the gExpected array holds the id of the <key> element + // that was expected. If this is empty, a handler wasn't expected to be called + if (gExpected[0]) + is(event.originalTarget.id, gExpected[0], "key " + gExpected[1]); + else + is("key " + event.originalTarget.id + " was activated", "", "key " + gExpected[1]); + gExpected = null; +} + +function is(l, r, n) { window.arguments[0].SimpleTest.is(l,r,n); } +function ok(v, n) { window.arguments[0].SimpleTest.ok(v,n); } + +SimpleTest.waitForFocus(runTest); + +]]> +</script> + +<command id="scommand" oncommand="checkKey(event)"/> +<command id="scommand-disabled" disabled="true"/> + +<keyset id="keyset"> + <key id="k-v" key="v" oncommand="checkKey(event)"/> + <key id="k-v-scy" key="v" modifiers="shift any control" oncommand="checkKey(event)"/> + <key id="k-e-y" key="e" modifiers="any" oncommand="checkKey(event)"/> + <key id="k-8-m" key="8" modifiers="meta" oncommand="checkKey(event)"/> + <key id="k-a-o" key="a" modifiers="os" oncommand="checkKey(event)"/> + <key id="k-b-myo" key="b" modifiers="meta any os" oncommand="checkKey(event)"/> + <key id="k-f-oym" key="f" modifiers="os any meta" oncommand="checkKey(event)"/> + <key id="k-c-scaym" key="c" modifiers="shift control alt any meta" oncommand="checkKey(event)"/> + <key id="k-h-l" key="h" modifiers="accel" oncommand="checkKey(event)"/> + <key id="k-j-s" key="j" modifiers="access" oncommand="checkKey(event)"/> + <key id="k-t-y" disabled="true" key="t" oncommand="checkKey(event)"/> + <key id="k-g-c" key="g" modifiers="control" oncommand="checkKey(event)"/> + <key id="k-g-co" key="g" modifiers="control os" oncommand="checkKey(event)"/> + <key id="k-y" key="y" command="scommand"/> + <key id="k-u" key="u" command="scommand-disabled"/> +</keyset> + +<keyset id="keyset2"> + <key id="k-d-a" key="d" modifiers="alt" oncommand="checkKey(event)"/> + <key id="k-s-c" key="s" modifiers="control" oncommand="checkKey(event)"/> +</keyset> + +<button id="menubutton" label="Menu" type="menu"> + <menupopup> + <menuitem id="menuitem1" label="Item 1" key="k-d-a"/> + <menuitem id="menuitem2" label="Item 2"/> + </menupopup> +</button> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_largemenu.xhtml b/toolkit/content/tests/chrome/window_largemenu.xhtml new file mode 100644 index 0000000000..35f7c80d74 --- /dev/null +++ b/toolkit/content/tests/chrome/window_largemenu.xhtml @@ -0,0 +1,424 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Large Menu Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<!-- + This test checks that a large menu is displayed with arrow buttons + and is on the screen. + --> + +<script> +<![CDATA[ + +var gOverflowed = false, gUnderflowed = false; +var gContextMenuTests = false; +var gScreenY = -1; +var gTestIndex = 0; +var gTests = ["open normal", "open when bottom would overlap", "open with scrolling", + "open after scrolling", "open small again", + "menu movement", "panel movement", + "context menu enough space below", + "context menu more space above", + "context menu too big either side", + "context menu larger than screen", + "context menu flips horizontally on osx"]; +function getScreenXY(element) +{ + var screenX, screenY; + var mouseFn = function(event) { + screenX = event.screenX - 1; + screenY = event.screenY - 1; + } + + // a hacky way to get the screen position of an element without using the box object + window.addEventListener("mousedown", mouseFn); + synthesizeMouse(element, 1, 1, { }); + window.removeEventListener("mousedown", mouseFn); + + return [screenX, screenY]; +} + +function hidePopup() { + window.requestAnimationFrame( + function() { + setTimeout( + function() { + document.getElementById("popup").hidePopup(); + }, 0); + }); +} + +function runTests() +{ + [, gScreenY] = getScreenXY(document.documentElement); + nextTest(); +} + +function nextTest() +{ + gOverflowed = false; gUnderflowed = false; + + var y = screen.height; + if (gTestIndex == 1) // open with bottom overlap test: + y -= 100; + else + y /= 2; + + var popup = document.getElementById("popup"); + if (gTestIndex == 2) { + // add some more menuitems so that scrolling will be necessary + var moreItemCount = Math.round(screen.height / popup.firstChild.getBoundingClientRect().height); + for (var t = 1; t <= moreItemCount; t++) { + var menu = document.createXULElement("menuitem"); + menu.setAttribute("label", "More" + t); + popup.appendChild(menu); + } + } + else if (gTestIndex == 4) { + // remove the items added in test 2 above + while (popup.childNodes.length > 15) + popup.removeChild(popup.lastChild); + } + + window.requestAnimationFrame(function() { + setTimeout( + function() { + popup.openPopupAtScreen(100, y, false); + }, 0); + }); +} + +function popupShown() +{ + if (gTests[gTestIndex] == "menu movement") + return testPopupMovement(); + + if (gContextMenuTests) + return contextMenuPopupShown(); + + var popup = document.getElementById("popup"); + var rect = popup.getBoundingClientRect(); + var scrollbox = popup.scrollBox.scrollbox; + var expectedScrollPos = 0; + + if (gTestIndex == 0) { + // the popup should be in the center of the screen + // note that if the height is odd, the y-offset will have been rounded + // down when we pass the fractional value to openPopupAtScreen above. + is(Math.round(rect.top) + gScreenY, Math.floor(screen.height / 2), + gTests[gTestIndex] + " top"); + ok(Math.round(rect.bottom) + gScreenY < screen.height, + gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow") + } + else if (gTestIndex == 1) { + // the popup was supposed to open 100 pixels from the bottom, but that + // would put it off screen so ... + if (platformIsMac()) { + // On OSX the popup is constrained so it remains within the + // bounds of the screen + ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top"); + is(Math.round(rect.bottom) + gScreenY, screen.availTop + screen.availHeight, gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow"); + } + else { + // On other platforms the menu should be flipped to have its bottom + // edge 100 pixels from the bottom + ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top"); + is(Math.round(rect.bottom) + gScreenY, screen.height - 100, + gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow"); + } + } + else if (gTestIndex == 2) { + // the popup is too large so ensure that it is on screen + ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top"); + ok(Math.round(rect.bottom) + gScreenY <= screen.height, gTests[gTestIndex] + " bottom"); + ok(gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow") + + scrollbox.scrollTo(0, 40); + expectedScrollPos = 40; + } + else if (gTestIndex == 3) { + expectedScrollPos = 40; + } + else if (gTestIndex == 4) { + // note that if the height is odd, the y-offset will have been rounded + // down when we pass the fractional value to openPopupAtScreen above. + is(Math.round(rect.top) + gScreenY, Math.floor(screen.height / 2), + gTests[gTestIndex] + " top"); + ok(Math.round(rect.bottom) + gScreenY < screen.height, + gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && gUnderflowed, gTests[gTestIndex] + " overflow"); + } + + is(scrollbox.scrollTop, expectedScrollPos, "menu scroll position") + + return hidePopup(); +} + +function is(l, r, n) { window.arguments[0].SimpleTest.is(l,r,n); } +function ok(v, n) { window.arguments[0].SimpleTest.ok(v,n); } + +var oldx, oldy, waitSteps = 0; +function moveWindowTo(x, y, callback, arg) +{ + if (!waitSteps) { + oldx = window.screenX; + oldy = window.screenY; + window.moveTo(x, y); + + waitSteps++; + setTimeout(moveWindowTo, 100, x, y, callback, arg); + return; + } + + if (window.screenX == oldx && window.screenY == oldy) { + if (waitSteps++ > 10) { + ok(false, "Window never moved properly to " + x + "," + y); + window.arguments[0].SimpleTest.finish(); + window.close(); + } + + setTimeout(moveWindowTo, 100, x, y, callback, arg); + } + else { + waitSteps = 0; + callback(arg); + } +} + +function popupHidden() +{ + gTestIndex++; + if (gTestIndex == gTests.length) { + window.arguments[0].SimpleTest.finish(); + window.close(); + } + else if (gTests[gTestIndex] == "context menu enough space below") { + gContextMenuTests = true; + moveWindowTo(window.screenX, screen.availTop + 10, + () => synthesizeMouse(document.getElementById("label"), 4, 4, { type: "contextmenu", button: 2 })); + } + else if (gTests[gTestIndex] == "menu movement") { + document.getElementById("popup").openPopup( + document.getElementById("label"), "after_start", 0, 0, false, false); + } + else if (gTests[gTestIndex] == "panel movement") { + document.getElementById("panel").openPopup( + document.getElementById("label"), "after_start", 0, 0, false, false); + } + else if (gContextMenuTests) { + contextMenuPopupHidden(); + } + else { + nextTest(); + } +} + +function contextMenuPopupShown() +{ + var popup = document.getElementById("popup"); + var rect = popup.getBoundingClientRect(); + var labelrect = document.getElementById("label").getBoundingClientRect(); + + // Click to open popup in popupHidden() occurs at (4,4) in label's coordinate space + var clickX = 4; + var clickY = 4; + + var testPopupAppearedRightOfCursor = true; + switch (gTests[gTestIndex]) { + case "context menu enough space below": + is(rect.top, labelrect.top + clickY + (platformIsMac() ? -6 : 2), gTests[gTestIndex] + " top"); + break; + case "context menu more space above": + if (platformIsMac()) { + let screenY; + [, screenY] = getScreenXY(popup); + // Macs constrain their popup menus vertically rather than flip them. + is(screenY, screen.availTop + screen.availHeight - rect.height, gTests[gTestIndex] + " top"); + } else { + is(rect.top, labelrect.top + clickY - rect.height - 2, gTests[gTestIndex] + " top"); + } + + break; + case "context menu too big either side": + [, gScreenY] = getScreenXY(document.documentElement); + // compare against the available size as well as the total size, as some + // platforms allow the menu to overlap os chrome and others do not + var pos = (screen.availTop + screen.availHeight - rect.height) - gScreenY; + var availPos = (screen.top + screen.height - rect.height) - gScreenY; + ok(rect.top == pos || rect.top == availPos, + gTests[gTestIndex] + " top"); + break; + case "context menu larger than screen": + ok(rect.top == -(gScreenY - screen.availTop) || rect.top == -(gScreenY - screen.top), gTests[gTestIndex] + " top"); + break; + case "context menu flips horizontally on osx": + testPopupAppearedRightOfCursor = false; + if (platformIsMac()) { + is(Math.round(rect.right), labelrect.left + clickX - 1, gTests[gTestIndex] + " right"); + } + break; + } + + if (testPopupAppearedRightOfCursor) { + is(rect.left, labelrect.left + clickX + (platformIsMac() ? 1 : 2), gTests[gTestIndex] + " left"); + } + + hidePopup(); +} + +function contextMenuPopupHidden() +{ + var screenAvailBottom = screen.availTop + screen.availHeight; + + if (gTests[gTestIndex] == "context menu more space above") { + moveWindowTo(window.screenX, screenAvailBottom - 80, nextContextMenuTest, -1); + } + else if (gTests[gTestIndex] == "context menu too big either side") { + moveWindowTo(window.screenX, screenAvailBottom / 2 - 80, nextContextMenuTest, screenAvailBottom / 2 + 120); + } + else if (gTests[gTestIndex] == "context menu larger than screen") { + nextContextMenuTest(screen.availHeight + 80); + } + else if (gTests[gTestIndex] == "context menu flips horizontally on osx") { + var popup = document.getElementById("popup"); + var popupWidth = popup.getBoundingClientRect().width; + moveWindowTo(screen.availLeft + screen.availWidth - popupWidth, 100, nextContextMenuTest, -1); + } +} + +function nextContextMenuTest(desiredHeight) +{ + if (desiredHeight >= 0) { + var popup = document.getElementById("popup"); + var height = popup.getBoundingClientRect().height; + var itemheight = document.getElementById("firstitem").getBoundingClientRect().height; + while (height < desiredHeight) { + var menu = document.createXULElement("menuitem"); + menu.setAttribute("label", "Item"); + popup.appendChild(menu); + height += itemheight; + } + } + + synthesizeMouse(document.getElementById("label"), 4, 4, { type: "contextmenu", button: 2 }); +} + +function testPopupMovement() +{ + var isPanelTest = (gTests[gTestIndex] == "panel movement"); + var popup = document.getElementById(isPanelTest ? "panel" : "popup"); + + var screenX, screenY; + var rect = popup.getBoundingClientRect(); + + var overlapOSChrome = !platformIsMac(); + popup.moveTo(1, 1); + [screenX, screenY] = getScreenXY(popup); + + var expectedx = 1, expectedy = 1; + if (!overlapOSChrome) { + if (screen.availLeft >= 1) expectedx = screen.availLeft; + if (screen.availTop >= 1) expectedy = screen.availTop; + } + is(screenX, expectedx, gTests[gTestIndex] + " (1, 1) x"); + is(screenY, expectedy, gTests[gTestIndex] + " (1, 1) y"); + + popup.moveTo(100, 8000); + if (isPanelTest) { + expectedy = 8000; + } + else { + expectedy = (overlapOSChrome ? screen.height + screen.top : screen.availHeight + screen.availTop) - + Math.round(rect.height); + } + + [screenX, screenY] = getScreenXY(popup); + is(screenX, 100, gTests[gTestIndex] + " (100, 8000) x"); + is(screenY, expectedy, gTests[gTestIndex] + " (100, 8000) y"); + + popup.moveTo(6000, 100); + + if (isPanelTest) { + expectedx = 6000; + } + else { + expectedx = (overlapOSChrome ? screen.width + screen.left : screen.availWidth + screen.availLeft) - + Math.round(rect.width); + } + + [screenX, screenY] = getScreenXY(popup); + is(screenX, expectedx, gTests[gTestIndex] + " (6000, 100) x"); + is(screenY, 100, gTests[gTestIndex] + " (6000, 100) y"); + + is(popup.getAttribute("left"), "", gTests[gTestIndex] + " left is empty after moving"); + is(popup.getAttribute("top"), "", gTests[gTestIndex] + " top is empty after moving"); + popup.setAttribute("left", "80"); + popup.setAttribute("top", "82"); + [screenX, screenY] = getScreenXY(popup); + is(screenX, 80, gTests[gTestIndex] + " set left and top x"); + is(screenY, 82, gTests[gTestIndex] + " set left and top y"); + popup.moveTo(95, 98); + [screenX, screenY] = getScreenXY(popup); + is(screenX, 95, gTests[gTestIndex] + " move after set left and top x"); + is(screenY, 98, gTests[gTestIndex] + " move after set left and top y"); + is(popup.getAttribute("left"), "95", gTests[gTestIndex] + " left is set after moving"); + is(popup.getAttribute("top"), "98", gTests[gTestIndex] + " top is set after moving"); + popup.removeAttribute("left"); + popup.removeAttribute("top"); + + popup.moveTo(-1, -1); + [screenX, screenY] = getScreenXY(popup); + + expectedx = (overlapOSChrome ? screen.left : screen.availLeft); + expectedy = (overlapOSChrome ? screen.top : screen.availTop); + + is(screenX, expectedx, gTests[gTestIndex] + " move after set left and top x to -1"); + is(screenY, expectedy, gTests[gTestIndex] + " move after set left and top y to -1"); + is(popup.getAttribute("left"), "", gTests[gTestIndex] + " left is not set after moving to -1"); + is(popup.getAttribute("top"), "", gTests[gTestIndex] + " top is not set after moving to -1"); + + popup.hidePopup(); +} + +function platformIsMac() +{ + return navigator.platform.indexOf("Mac") > -1; +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); + +]]> +</script> + +<button id="label" label="OK" context="popup"/> +<menupopup id="popup" onpopupshown="popupShown();" onpopuphidden="popupHidden();" + onoverflow="gOverflowed = true" onunderflow="gUnderflowed = true;"> + <menuitem id="firstitem" label="1"/> + <menuitem label="2"/> + <menuitem label="3"/> + <menuitem label="4"/> + <menuitem label="5"/> + <menuitem label="6"/> + <menuitem label="7"/> + <menuitem label="8"/> + <menuitem label="9"/> + <menuitem label="10"/> + <menuitem label="11"/> + <menuitem label="12"/> + <menuitem label="13"/> + <menuitem label="14"/> + <menuitem label="15"/> +</menupopup> + +<panel id="panel" onpopupshown="testPopupMovement();" onpopuphidden="popupHidden();" style="margin: 0"> + <button label="OK"/> +</panel> + +</window> diff --git a/toolkit/content/tests/chrome/window_maximized_persist.xhtml b/toolkit/content/tests/chrome/window_maximized_persist.xhtml new file mode 100644 index 0000000000..f7eb695f0f --- /dev/null +++ b/toolkit/content/tests/chrome/window_maximized_persist.xhtml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Window Open Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + height="300" + width="300" + sizemode="normal" + id="window" + persist="height width sizemode"> +<script type="application/javascript"><![CDATA[ + window.addEventListener("sizemodechange", evt => { + window.arguments[0].postMessage("sizemodechange", "*"); + }); +]]></script> +</window> diff --git a/toolkit/content/tests/chrome/window_navigate_persist.html b/toolkit/content/tests/chrome/window_navigate_persist.html new file mode 100644 index 0000000000..8b45212bed --- /dev/null +++ b/toolkit/content/tests/chrome/window_navigate_persist.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html dir="" + id="persist-window" + width="300" height="300" + persist="screenX screenY width height sizemode"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + </head> + <body> + </body> +</html> diff --git a/toolkit/content/tests/chrome/window_panel.xhtml b/toolkit/content/tests/chrome/window_panel.xhtml new file mode 100644 index 0000000000..c72b555753 --- /dev/null +++ b/toolkit/content/tests/chrome/window_panel.xhtml @@ -0,0 +1,285 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for panels + --> +<window title="Titlebar" width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<tree id="tree" seltype="single" width="100" height="100"> + <treecols> + <treecol flex="1"/> + <treecol flex="1"/> + </treecols> + <treechildren id="treechildren"> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + </treechildren> +</tree> + + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var currentTest = null; + +function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); +} + +function is(left, right, message) { + window.arguments[0].SimpleTest.is(left, right, message); +} + +function test_panels() +{ + checkTreeCoords(); + + addEventListener("popupshowing", popupShowing, false); + addEventListener("popupshown", popupShown, false); + addEventListener("popuphidden", nextTest, false); + nextTest(); +} + +function nextTest() +{ + if (!tests.length) { + window.close(); + window.arguments[0].SimpleTest.finish(); + return; + } + + currentTest = tests.shift(); + var panel = createPanel(currentTest.attrs); + currentTest.test(panel); +} + +function popupShowing(event) +{ + var rect = event.target.getOuterScreenRect(); + ok(!rect.left && !rect.top && !rect.width && !rect.height, + currentTest.testname + " empty rectangle during popupshowing"); +} + +var waitSteps = 0; +function popupShown(event) +{ + var panel = event.target; + + if (waitSteps > 0 && navigator.platform.includes("Linux") && + panel.screenY == 210) { + waitSteps--; + setTimeout(popupShown, 10, event); + return; + } + + currentTest.result(currentTest.testname + " ", panel); + panel.hidePopup(); +} + +function createPanel(attrs) +{ + var panel = document.createXULElement("panel"); + for (var a in attrs) { + panel.setAttribute(a, attrs[a]); + } + + var button = document.createXULElement("button"); + panel.appendChild(button); + button.label = "OK"; + button.width = 120; + button.height = 40; + button.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;"); + panel.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;"); + return document.documentElement.appendChild(panel); +} + +function checkTreeCoords() +{ + var tree = $("tree"); + var treechildren = $("treechildren"); + tree.currentIndex = 0; + tree.scrollToRow(0); + synthesizeMouse(treechildren, 10, tree.rowHeight + 2, { }); + is(tree.currentIndex, 1, "tree selection"); + + tree.scrollToRow(2); + synthesizeMouse(treechildren, 10, tree.rowHeight + 2, { }); + is(tree.currentIndex, 3, "tree selection after scroll"); +} + +var tests = [ + { + testname: "normal panel", + attrs: { }, + test(panel) { + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 0, this.testname + " screen left before open"); + is(screenRect.top, 0, this.testname + " screen top before open"); + is(screenRect.width, 0, this.testname + " screen width before open"); + is(screenRect.height, 0, this.testname + " screen height before open"); + + panel.openPopupAtScreen(200, 210); + }, + result(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, 200 - window.mozInnerScreenX, testname + "left"); + is(panelrect.top, 210 - window.mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 200, testname + " screen left"); + is(screenRect.top, 210, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, + { + // only noautohide panels support titlebars, so one shouldn't be shown here + testname: "autohide panel with titlebar", + attrs: { titlebar: "normal" }, + test(panel) { + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 0, this.testname + " screen left before open"); + is(screenRect.top, 0, this.testname + " screen top before open"); + is(screenRect.width, 0, this.testname + " screen width before open"); + is(screenRect.height, 0, this.testname + " screen height before open"); + + panel.openPopupAtScreen(200, 210); + }, + result(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, 200 - window.mozInnerScreenX, testname + "left"); + is(panelrect.top, 210 - window.mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 200, testname + " screen left"); + is(screenRect.top, 210, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, + { + testname: "noautohide panel with titlebar", + attrs: { noautohide: true, titlebar: "normal" }, + test(panel) { + waitSteps = 25; + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 0, this.testname + " screen left before open"); + is(screenRect.top, 0, this.testname + " screen top before open"); + is(screenRect.width, 0, this.testname + " screen width before open"); + is(screenRect.height, 0, this.testname + " screen height before open"); + + panel.openPopupAtScreen(200, 210); + }, + result(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + ok(panelrect.left >= 200 - window.mozInnerScreenX, testname + "left"); + if (!navigator.platform.includes("Linux")) { + ok(panelrect.top >= 210 - window.mozInnerScreenY + 10, testname + "top greater"); + } + ok(panelrect.top <= 210 - window.mozInnerScreenY + 36, testname + "top less"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + if (!navigator.platform.includes("Linux")) { + is(screenRect.left, 200, testname + " screen left"); + is(screenRect.top, 210, testname + " screen top"); + } + ok(screenRect.width >= 120 && screenRect.width <= 140, testname + " screen width"); + ok(screenRect.height >= 40 && screenRect.height <= 80, testname + " screen height"); + + var gotMouseEvent = false; + function mouseMoved(event) + { + is(event.clientY, panelrect.top + 10, + "popup clientY"); + is(event.screenY, panel.screenY + 10, + "popup screenY"); + is(event.originalTarget, panel.firstChild, "popup target"); + gotMouseEvent = true; + } + + panel.addEventListener("mousemove", mouseMoved, true); + synthesizeMouse(panel, 10, 10, { type: "mousemove" }); + ok(gotMouseEvent, "mouse event on panel"); + panel.removeEventListener("mousemove", mouseMoved, true); + + var tree = $("tree"); + tree.currentIndex = 0; + panel.appendChild(tree); + checkTreeCoords(); + } + }, + { + // The panel should be allowed to appear and remain offscreen + testname: "normal panel with flip='none' off-screen", + attrs: { "flip": "none" }, + test(panel) { + panel.openPopup(document.documentElement, "", -100 - window.mozInnerScreenX, -100 - window.mozInnerScreenY, false, false, null); + }, + result(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, -100 - window.mozInnerScreenX, testname + "left"); + is(panelrect.top, -100 - window.mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, -100, testname + " screen left"); + is(screenRect.top, -100, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, + { + // The panel should be allowed to remain offscreen after moving and it should follow the anchor + testname: "normal panel with flip='none' moved off-screen", + attrs: { "flip": "none" }, + test(panel) { + panel.openPopup(document.documentElement, "", -100 - window.mozInnerScreenX, -100 - window.mozInnerScreenY, false, false, null); + window.moveBy(-50, -50); + }, + result(testname, panel) { + if (navigator.platform.includes("Linux")) { + // The window position doesn't get updated immediately on Linux. + return; + } + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, -150 - window.mozInnerScreenX, testname + "left"); + is(panelrect.top, -150 - window.mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, -150, testname + " screen left"); + is(screenRect.top, -150, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, +]; + +window.arguments[0].SimpleTest.waitForFocus(test_panels, window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/window_panel_anchoradjust.xhtml b/toolkit/content/tests/chrome/window_panel_anchoradjust.xhtml new file mode 100644 index 0000000000..3fe0ef2183 --- /dev/null +++ b/toolkit/content/tests/chrome/window_panel_anchoradjust.xhtml @@ -0,0 +1,185 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<deck id="deck"> + <hbox id="container"> + <button id="anchor" label="Anchor"/> + </hbox> + <button id="anchor3" label="Anchor3"/> +</deck> + +<hbox id="container2"> + <button id="anchor2" label="Anchor2"/> +</hbox> + +<button id="anchor4" label="Anchor4"/> + +<panel id="panel" type="arrow"> + <button label="OK"/> +</panel> + +<menupopup id="menupopup"> + <menuitem label="One"/> + <menuitem id="menuanchor" label="Two"/> + <menuitem label="Three"/> +</menupopup> + +<script type="application/javascript"><![CDATA[ + + +SimpleTest.waitForExplicitFinish(); + +function next() +{ + return new Promise(r => { + requestAnimationFrame(() => requestAnimationFrame(r)); + }) +} + +function waitForPanel(panel, event) +{ + return new Promise(resolve => { + panel.addEventListener(event, () => { resolve(); }, { once: true }); + }); +} + +function isWithinHalfPixel(a, b) +{ + return Math.abs(a - b) <= 0.5; +} + +async function runTests() { + let panel = document.getElementById("panel"); + let anchor = document.getElementById("anchor"); + + let popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(anchor, "after_start"); + info("popupshown"); + await popupshown; + + let anchorrect = anchor.getBoundingClientRect(); + let panelrect = panel.getBoundingClientRect(); + let xarrowdiff = panelrect.left - anchorrect.left; + + // When the anchor is moved in some manner, the panel should be adjusted + let popuppositioned = waitForPanel(panel, "popuppositioned"); + document.getElementById("anchor").style.marginLeft = "50px" + info("before popuppositioned"); + await popuppositioned; + info("after popuppositioned"); + + anchorrect = anchor.getBoundingClientRect(); + panelrect = panel.getBoundingClientRect(); + ok(isWithinHalfPixel(anchorrect.left, panelrect.left - xarrowdiff), "anchor moved x"); + ok(isWithinHalfPixel(anchorrect.bottom, panelrect.top), "anchor moved y"); + + // moveToAnchor is used to change the anchor + let anchor2 = document.getElementById("anchor2"); + popuppositioned = waitForPanel(panel, "popuppositioned"); + panel.moveToAnchor(anchor2, "after_end"); + info("before popuppositioned 2"); + await popuppositioned; + info("after popuppositioned 2"); + + let anchor2rect = anchor2.getBoundingClientRect(); + panelrect = panel.getBoundingClientRect(); + ok(isWithinHalfPixel(anchor2rect.right, panelrect.right + xarrowdiff), "new anchor x"); + ok(isWithinHalfPixel(anchor2rect.bottom, panelrect.top), "new anchor y"); + + // moveToAnchor is used to change the anchor with an x and y offset + popuppositioned = waitForPanel(panel, "popuppositioned"); + panel.moveToAnchor(anchor2, "after_end", 7, 9); + await popuppositioned; + + anchor2rect = anchor2.getBoundingClientRect(); + panelrect = panel.getBoundingClientRect(); + ok(isWithinHalfPixel(anchor2rect.right + 7, panelrect.right + xarrowdiff), "new anchor with offset x"); + ok(isWithinHalfPixel(anchor2rect.bottom + 9, panelrect.top), "new anchor with offset y"); + + // When the container of the anchor is collapsed, the panel should be hidden. + let popuphidden = waitForPanel(panel, "popuphidden"); + anchor2.parentNode.collapsed = true; + await popuphidden; + + popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(anchor, "after_start"); + await popupshown; + + // When the deck containing the anchor changes to a different page, the panel should be hidden. + popuphidden = waitForPanel(panel, "popuphidden"); + document.getElementById("deck").selectedIndex = 1; + await popuphidden; + + let anchor3 = document.getElementById("anchor3"); + popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(anchor3, "after_start"); + await popupshown; + + // When the anchor is hidden; the panel should be hidden. + popuphidden = waitForPanel(panel, "popuphidden"); + anchor3.parentNode.hidden = true; + await popuphidden; + + // When the panel is anchored to an element in a popup, the panel should + // also be hidden when that popup is hidden. + let menupopup = document.getElementById("menupopup"); + popupshown = waitForPanel(menupopup, "popupshown"); + menupopup.openPopupAtScreen(200, 200); + await popupshown; + + popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(document.getElementById("menuanchor"), "after_start"); + await popupshown; + + popuphidden = waitForPanel(panel, "popuphidden"); + let menupopuphidden = waitForPanel(menupopup, "popuphidden"); + menupopup.hidePopup(); + await popuphidden; + await menupopuphidden; + + // The panel should no longer follow anchors. + panel.setAttribute("followanchor", "false"); + + let anchor4 = document.getElementById("anchor4"); + popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(anchor4, "after_start"); + await popupshown; + + let anchor4rect = anchor4.getBoundingClientRect(); + panelrect = panel.getBoundingClientRect(); + + anchor4.style.marginLeft = "50px" + await next(); + + panelrect = panel.getBoundingClientRect(); + ok(isWithinHalfPixel(anchor4rect.left, panelrect.left - xarrowdiff), "no follow anchor x"); + ok(isWithinHalfPixel(anchor4rect.bottom, panelrect.top), "no follow anchor y"); + + popuphidden = waitForPanel(panel, "popuphidden"); + panel.hidePopup(); + await popuphidden; + + window.close(); + window.arguments[0].SimpleTest.finish(); +} + +function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); +} + +function is(left, right, message) { + window.arguments[0].SimpleTest.is(left, right, message); +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/window_panel_focus.xhtml b/toolkit/content/tests/chrome/window_panel_focus.xhtml new file mode 100644 index 0000000000..962e43db58 --- /dev/null +++ b/toolkit/content/tests/chrome/window_panel_focus.xhtml @@ -0,0 +1,132 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Panel Focus Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<checkbox id="b1" label="Item 1"/> + +<!-- Focus should be in this order: 2 6 3 8 1 4 5 7 9 --> +<panel id="panel" norestorefocus="true" onpopupshown="panelShown()" onpopuphidden="panelHidden()"> + <button id="t1" label="Button One"/> + <button id="t2" tabindex="1" label="Button Two" onblur="gButtonBlur++;"/> + <button id="t3" tabindex="2" label="Button Three"/> + <button id="t4" tabindex="0" label="Button Four"/> + <button id="t5" label="Button Five"/> + <button id="t6" tabindex="1" label="Button Six"/> + <button id="t7" label="Button Seven"/> + <button id="t8" tabindex="4" label="Button Eight"/> + <button id="t9" label="Button Nine"/> +</panel> + +<panel id="noautofocusPanel" noautofocus="true" + onpopupshown="noautofocusPanelShown()" onpopuphidden="noautofocusPanelHidden()"> + <html:input id="tb3"/> +</panel> + +<checkbox id="b2" label="Item 2" popup="panel" onblur="gButtonBlur++;"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var gButtonBlur = 0; + +function showPanel() +{ + // click on the document so that the window has focus + synthesizeMouse(document.documentElement, 1, 1, { }); + + // focus the button + synthesizeKeyExpectEvent("KEY_Tab", {}, $("b1"), "focus", "button focus"); + // tabbing again should skip the popup + synthesizeKeyExpectEvent("KEY_Tab", {}, $("b2"), "focus", "popup skipped in focus navigation"); + + $("panel").openPopup(null, "", 10, 10, false, false); +} + +function panelShown() +{ + // the focus on the button should have been removed when the popup was opened + is(gButtonBlur, 1, "focus removed when popup opened"); + + // press tab numerous times to cycle through the buttons. The t2 button will + // be blurred twice, so gButtonBlur will be 3 afterwards. + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t2"), "focus", "tabindex 1"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t6"), "focus", "tabindex 2"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t3"), "focus", "tabindex 3"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t8"), "focus", "tabindex 4"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t1"), "focus", "tabindex 5"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t4"), "focus", "tabindex 6"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t5"), "focus", "tabindex 7"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t7"), "focus", "tabindex 8"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t9"), "focus", "tabindex 9"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t2"), "focus", "tabindex 10"); + + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t9"), "focus", "back tabindex 1"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t7"), "focus", "back tabindex 2"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t5"), "focus", "back tabindex 3"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t4"), "focus", "back tabindex 4"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t1"), "focus", "back tabindex 5"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t8"), "focus", "back tabindex 6"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t3"), "focus", "back tabindex 7"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t6"), "focus", "back tabindex 8"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t2"), "focus", "back tabindex 9"); + + is(gButtonBlur, 3, "blur events fired within popup"); + + synthesizeKey("KEY_Escape"); +} + +function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); +} + +function is(left, right, message) { + window.arguments[0].SimpleTest.is(left, right, message); +} + +function panelHidden() +{ + // closing the popup should have blurred the focused element + is(gButtonBlur, 4, "focus removed when popup closed"); + + // now that the panel is hidden, pressing tab should focus the elements in + // the main window again + synthesizeKeyExpectEvent("KEY_Tab", {}, $("b1"), "focus", "focus after popup closed"); + + $("noautofocusPanel").openPopup(null, "", 10, 10, false, false); +} + +function noautofocusPanelShown() +{ + // with noautofocus="true", the focus should not be removed when the panel is + // opened, so key events should still be fired at the checkbox. + synthesizeKeyExpectEvent(" ", {}, $("b1"), "command", "noautofocus"); + $("noautofocusPanel").hidePopup(); +} + +function noautofocusPanelHidden() +{ + window.close(); + window.arguments[0].SimpleTest.finish(); +} + +window.arguments[0].SimpleTest.waitForFocus(showPanel, window); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_anchor.xhtml b/toolkit/content/tests/chrome/window_popup_anchor.xhtml new file mode 100644 index 0000000000..803253e21a --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_anchor.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Anchor Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script> +function runTests() +{ + frames[0].openPopup(); +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); +</script> + +<spacer height="13"/> +<button id="outerbutton" label="Button One" style="margin-left: 6px; -moz-appearance: none;"/> +<hbox> + <spacer width="20"/> + <deck> + <vbox> + <iframe id="frame" style="margin-left: 60px; margin-top: 10px; border-left: 17px solid red; padding-left: 0 !important; padding-top: 3px;" + width="250" height="80" src="frame_popup_anchor.xhtml"/> + </vbox> + </deck> +</hbox> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml b/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml new file mode 100644 index 0000000000..6088f20a11 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onpopupshown="popupshown(event.target)" onpopuphidden="nextTest()"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<label value="Popup Test"/> + +<menupopup id="popup"> + <menuitem label="One"/> + <menuitem label="Two"/> +</menupopup> + +<panel id="panel" noautohide="true" height="20"> + <label value="OK"/> +</panel> + +<script> +<![CDATA[ + +let menupopup; + +let tests = [ + { + test: () => menupopup.openPopupAtScreenRect("after_start", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 150, "popup at screen position x"); + is(rect.top, 290, "popup at screen position y"); + } + }, + { + test: () => menupopup.openPopupAtScreenRect("after_start", 150, 350, 30, 9000), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 150, "flipped popup at screen position x"); + is(rect.bottom, 350, "flipped popup at screen position y"); + } + }, + { + test: () => menupopup.openPopupAtScreenRect("end_before", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 180, "popup at end_before screen position x"); + is(rect.top, 250, "popup at end_before screen position y"); + } + }, + { + test: () => $("panel").openPopupAtScreenRect("after_start", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 150, "panel at screen position x"); + is(rect.top, 290, "panel at screen position y"); + } + }, + { + test: () => $("panel").openPopupAtScreenRect("before_start", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + is(rect.left, 150, "panel at before_start screen position x"); + is(rect.bottom, 250, "panel at before_start screen position y"); + } + }, +]; + +function runTest(id) +{ + menupopup = $("popup"); + nextTest(); +} + +function nextTest() +{ + if (!tests.length) { + window.close(); + window.arguments[0].SimpleTest.finish(); + return; + } + + tests[0].test(); +} + +function popupshown(popup) +{ + tests[0].verify(popup); + tests.shift(); + popup.hidePopup(); +} + +function is(left, right, message) +{ + window.arguments[0].SimpleTest.is(left, right, message); +} + +function ok(value, message) +{ + window.arguments[0].SimpleTest.ok(value, message); +} + +window.arguments[0].SimpleTest.waitForFocus(runTest, window); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_attribute.xhtml b/toolkit/content/tests/chrome/window_popup_attribute.xhtml new file mode 100644 index 0000000000..7014eee4c0 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_attribute.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Attribute Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + <script type="application/javascript" src="popup_trigger.js"></script> + +<script> +window.opener.SimpleTest.waitForFocus(runTests, window); +</script> + +<hbox style="margin-left: 200px; margin-top: 290px;"> + <label id="trigger" popup="thepopup" value="Popup" height="60"/> +</hbox> +<!-- this frame is used to check that document.popupNode + is inaccessible from different sources --> +<iframe id="childframe" type="content" width="10" height="10" + src="http://sectest2.example.org:80/chrome/toolkit/content/tests/chrome/popup_childframe_node.xhtml"/> + +<menupopup id="thepopup"> + <menuitem id="item1" label="First"/> + <menuitem id="item2" label="Main Item"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem id="item3" label="Third"/> + <menuitem id="one" label="One"/> + <menuitem id="fancier" label="Fancier Menu"/> + <menu id="submenu" label="Only Menu"> + <menupopup id="submenupopup"> + <menuitem id="submenuitem" label="Test Submenu"/> + </menupopup> + </menu> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="secondlast" label="Second Last Menu" accesskey="T"/> + <menuitem id="last" label="One Other Menu"/> +</menupopup> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_button.xhtml b/toolkit/content/tests/chrome/window_popup_button.xhtml new file mode 100644 index 0000000000..4a53bbe0b2 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_button.xhtml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + <script type="application/javascript" src="popup_trigger.js"></script> + +<script> +window.opener.SimpleTest.waitForFocus(runTests, window); +</script> + +<hbox style="margin-left: 200px; margin-top: 290px;"> + <button id="trigger" type="menu" label="Popup" width="100" height="50"> + <menupopup id="thepopup"> + <menuitem id="item1" label="First"/> + <menuitem id="item2" label="Main Item"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem id="item3" label="Third"/> + <menuitem id="one" label="One"/> + <menuitem id="fancier" label="Fancier Menu"/> + <menu id="submenu" label="Only Menu"> + <menupopup id="submenupopup"> + <menuitem id="submenuitem" label="Test Submenu"/> + </menupopup> + </menu> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="secondlast" label="Second Last Menu" accesskey="T"/> + <menuitem id="last" label="One Other Menu"/> + </menupopup> + </button> +</hbox> + +<!-- this frame is used to check that document.popupNode + is inaccessible from different sources --> +<iframe id="childframe" type="content" width="10" height="10" + src="http://sectest2.example.org:80/chrome/toolkit/content/tests/chrome/popup_childframe_node.xhtml"/> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_preventdefault_chrome.xhtml b/toolkit/content/tests/chrome/window_popup_preventdefault_chrome.xhtml new file mode 100644 index 0000000000..3889a917f9 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_preventdefault_chrome.xhtml @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Prevent Default Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<!-- + This tests checks that preventDefault can be called on a popupshowing + event or popuphiding event to prevent the default behaviour. + --> + +<script> + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var gBlockShowing = true; +var gBlockHiding = true; +var gShownNotAllowed = true; +var gHiddenNotAllowed = true; + +var fm = Services.focus; + +var is = function(l, r, v) { window.arguments[0].SimpleTest.is(l, r, v); } +var isnot = function(l, r, v) { window.arguments[0].SimpleTest.isnot(l, r, v); } + +function runTest() +{ + var menu = document.getElementById("menu"); + + is(fm.activeWindow, window, "active window at start"); + is(fm.focusedWindow, window, "focused window at start"); + + is(window.windowState, window.STATE_NORMAL, "window is normal"); + // the minimizing test sometimes fails on Linux so don't test it there + if (navigator.platform.indexOf("Lin") == 0) { + menu.open = true; + return; + } + window.minimize(); + is(window.windowState, window.STATE_MINIMIZED, "window is minimized"); + + isnot(fm.activeWindow, window, "active window after minimize"); + isnot(fm.focusedWindow, window, "focused window after minimize"); + + menu.open = true; + + setTimeout(runTestAfterMinimize, 0); +} + +function runTestAfterMinimize() +{ + var menu = document.getElementById("menu"); + is(menu.firstChild.state, "closed", "popup not opened when window minimized"); + + window.restore(); + is(window.windowState, window.STATE_NORMAL, "window is restored"); + + is(fm.activeWindow, window, "active window after restore"); + is(fm.focusedWindow, window, "focused window after restore"); + + menu.open = true; +} + +function popupShowing(event) +{ + if (gBlockShowing) { + event.preventDefault(); + gBlockShowing = false; + setTimeout(function() { + gShownNotAllowed = false; + document.getElementById("menu").open = true; + }, 3000, true); + } +} + +function popupShown() +{ + window.arguments[0].SimpleTest.ok(!gShownNotAllowed, "popupshowing preventDefault"); + document.getElementById("menu").open = false; +} + +function popupHiding(event) +{ + if (gBlockHiding) { + event.preventDefault(); + gBlockHiding = false; + setTimeout(function() { + gHiddenNotAllowed = false; + document.getElementById("menu").open = false; + }, 3000, true); + } +} + +function popupHidden() +{ + window.arguments[0].SimpleTest.ok(!gHiddenNotAllowed, "popuphiding preventDefault"); + window.arguments[0].SimpleTest.finish(); + window.close(); +} + +window.arguments[0].SimpleTest.waitForFocus(runTest, window); +</script> + +<button id="menu" type="menu" label="Menu"> + <menupopup onpopupshowing="popupShowing(event);" + onpopupshown="popupShown();" + onpopuphiding="popupHiding(event);" + onpopuphidden="popupHidden();"> + <menuitem label="Item"/> + </menupopup> +</button> + + +</window> diff --git a/toolkit/content/tests/chrome/window_preferences.xhtml b/toolkit/content/tests/chrome/window_preferences.xhtml new file mode 100644 index 0000000000..34849c5c17 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences.xhtml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + class="prefwindow" + title="preferences window" + windowtype="test:preferences" + onload="RunTest(window.arguments)"> +<dialog id="window_preferences_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function RunTest(aArgs) + { + // run test + aArgs[0](this); + // close dialog + let dialog = document.getElementById("window_preferences_dialog"); + dialog[aArgs[1] ? "acceptDialog" : "cancelDialog"](); + } + + Preferences.addAll([ + // one of each type known to Preference.valueFromPreferences + { id: "tests.static_preference_int", type: "int" }, + { id: "tests.static_preference_bool", type: "bool" }, + { id: "tests.static_preference_string", type: "string" }, + { id: "tests.static_preference_wstring", type: "wstring" }, + { id: "tests.static_preference_unichar", type: "unichar" }, + { id: "tests.static_preference_file", type: "file" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + + <!-- one element for each preference type above --> + <hbox> + <label flex="1" value="int"/> + <html:input id="static_element_int" preference="tests.static_preference_int"/> + </hbox> + <hbox> + <label flex="1" value="bool"/> + <checkbox id="static_element_bool" preference="tests.static_preference_bool"/> + </hbox> + <hbox> + <label flex="1" value="string"/> + <html:input id="static_element_string" preference="tests.static_preference_string"/> + </hbox> + <hbox> + <label flex="1" value="wstring"/> + <html:input id="static_element_wstring" preference="tests.static_preference_wstring"/> + </hbox> + <hbox> + <label flex="1" value="unichar"/> + <html:input id="static_element_unichar" preference="tests.static_preference_unichar"/> + </hbox> + <hbox> + <label flex="1" value="file"/> + <html:input id="static_element_file" preference="tests.static_preference_file"/> + </hbox> + </vbox> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences2.xhtml b/toolkit/content/tests/chrome/window_preferences2.xhtml new file mode 100644 index 0000000000..0ef9d8f588 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences2.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="preferences window" + windowtype="test:preferences2" + onload="RunTest(window.arguments)"> +<dialog id="window_preferences2_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + function RunTest(aArgs) + { + // open child + openDialog("window_preferences3.xhtml", "", "modal,centerscreen,resizable=no", {test: aArgs[0], accept: aArgs[1]}); + // close dialog + let dialog = document.getElementById("window_preferences2_dialog"); + dialog[aArgs[1] ? "acceptDialog" : "cancelDialog"](); + } + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"/> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences3.xhtml b/toolkit/content/tests/chrome/window_preferences3.xhtml new file mode 100644 index 0000000000..38f31c0a11 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences3.xhtml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + class="prefwindow" + title="preferences window" + windowtype="test:preferences3" + onload="RunTest(window.arguments)" + type="child"> +<dialog id="window_preferences3_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function RunTest(aArgs) + { + // run test + aArgs[0].test(this); + // close dialog + let dialog = document.getElementById("window_preferences3_dialog"); + dialog[aArgs[0].accept ? "acceptDialog" : "cancelDialog"](); + } + + Preferences.addAll([ + // one of each type known to Preference.valueFromPreferences + { id: "tests.static_preference_int", type: "int" }, + { id: "tests.static_preference_bool", type: "bool" }, + { id: "tests.static_preference_string", type: "string" }, + { id: "tests.static_preference_wstring", type: "wstring" }, + { id: "tests.static_preference_unichar", type: "unichar" }, + { id: "tests.static_preference_file", type: "file" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + <!-- one element for each preference type above --> + <hbox> + <label flex="1" value="int"/> + <html:input id="static_element_int" preference="tests.static_preference_int"/> + </hbox> + <hbox> + <label flex="1" value="bool"/> + <checkbox id="static_element_bool" preference="tests.static_preference_bool"/> + </hbox> + <hbox> + <label flex="1" value="string"/> + <html:input id="static_element_string" preference="tests.static_preference_string"/> + </hbox> + <hbox> + <label flex="1" value="wstring"/> + <html:input id="static_element_wstring" preference="tests.static_preference_wstring"/> + </hbox> + <hbox> + <label flex="1" value="unichar"/> + <html:input id="static_element_unichar" preference="tests.static_preference_unichar"/> + </hbox> + <hbox> + <label flex="1" value="file"/> + <html:input id="static_element_file" preference="tests.static_preference_file"/> + </hbox> + </vbox> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences_beforeaccept.xhtml b/toolkit/content/tests/chrome/window_preferences_beforeaccept.xhtml new file mode 100644 index 0000000000..519f65bcaa --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_beforeaccept.xhtml @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window with beforeaccept +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="preferences window" + width="300" height="300" + windowtype="test:preferences" + onload="onDialogLoad();"> +<dialog id="beforeaccept_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function onDialogLoad() { + document.addEventListener("beforeaccept", beforeAccept); + var pref = Preferences.get("tests.beforeaccept.dialogShown"); + pref.value = true; + + // call the onload handler we were passed + window.arguments[0](); + } + + function beforeAccept(event) { + var beforeAcceptPref = window.Preferences.get("tests.beforeaccept.called"); + var oldValue = beforeAcceptPref.value; + beforeAcceptPref.value = true; + + if (!oldValue) { + event.preventDefault(); + } + } + + Preferences.addAll([ + { id: "tests.beforeaccept.called", type: "bool" }, + { id: "tests.beforeaccept.dialogShown", type: "bool" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + </vbox> + <label>Test Prefpane</label> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences_commandretarget.xhtml b/toolkit/content/tests/chrome/window_preferences_commandretarget.xhtml new file mode 100644 index 0000000000..7a0d5c0399 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_commandretarget.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window. This particular test ensures that + a checkbox with a command attribute properly updates even though the command + event gets retargeted. +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="preferences window" + windowtype="test:preferences" + onload="RunTest(window.arguments)"> +<dialog id="commandretarget_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function RunTest(aArgs) + { + aArgs[0](this); + let dialog = document.getElementById("commandretarget_dialog"); + dialog.cancelDialog(); + } + + Preferences.addAll([ + { id: "tests.static_preference_bool", type: "bool" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + <commandset> + <command id="cmd_test" preference="tests.static_preference_bool"/> + </commandset> + + <checkbox id="checkbox" label="Enable Option" preference="tests.static_preference_bool" command="cmd_test"/> + </vbox> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences_disabled.xhtml b/toolkit/content/tests/chrome/window_preferences_disabled.xhtml new file mode 100644 index 0000000000..1666f0dfea --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_disabled.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window. This particular test ensures that + when a preference is disabled, the checkbox disabled and when a preference + is locked, the checkbox can't be enabled. +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="preferences window" + windowtype="test:preferences" + onload="RunTest(window.arguments)"> +<dialog id="disabled_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function RunTest(aArgs) + { + aArgs[0](this); + let dialog = document.getElementById("disabled_dialog"); + dialog.cancelDialog(); + } + + Preferences.addAll([ + { id: "tests.disabled_preference_bool", type: "bool" }, + { id: "tests.locked_preference_bool", type: "bool" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + <checkbox id="disabled_checkbox" label="Disabled" preference="tests.disabled_preference_bool"/> + <checkbox id="locked_checkbox" label="Locked" preference="tests.locked_preference_bool"/> + </vbox> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences_onsyncfrompreference.xhtml b/toolkit/content/tests/chrome/window_preferences_onsyncfrompreference.xhtml new file mode 100644 index 0000000000..3218344ddd --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_onsyncfrompreference.xhtml @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- 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/. --> +<!-- + XUL Widget Test for preferences window with onsyncfrompreference + This test ensures that onsyncfrompreference handlers are called after all the + values of the corresponding preference element have been set correctly +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="preferences window" + width="300" height="300" + windowtype="test:preferences" + onload="onLoad()"> +<dialog> + + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + Preferences.addAll([ + { id: "tests.onsyncfrompreference.pref1", type: "int" }, + { id: "tests.onsyncfrompreference.pref2", type: "int" }, + { id: "tests.onsyncfrompreference.pref3", type: "int" }, + ]); + + function onLoad() { + Preferences.addSyncFromPrefListener(document.getElementById("check1"), + () => window.arguments[0]()); + Preferences.addSyncFromPrefListener(document.getElementById("check2"), + () => window.arguments[0]()); + Preferences.addSyncFromPrefListener(document.getElementById("check3"), + () => window.arguments[0]()); + Preferences.addSyncToPrefListener(document.getElementById("check1"), + () => 1); + Preferences.addSyncToPrefListener(document.getElementById("check2"), + () => 1); + Preferences.addSyncToPrefListener(document.getElementById("check3"), + () => 1); + } + ]]> + </script> + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + </vbox> + <label>Test Prefpane</label> + <checkbox id="check1" label="Label1" + preference="tests.onsyncfrompreference.pref1"/> + <checkbox id="check2" label="Label2" + preference="tests.onsyncfrompreference.pref2"/> + <checkbox id="check3" label="Label3" + preference="tests.onsyncfrompreference.pref3"/> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_screenPosSize.xhtml b/toolkit/content/tests/chrome/window_screenPosSize.xhtml new file mode 100644 index 0000000000..accc10d8f1 --- /dev/null +++ b/toolkit/content/tests/chrome/window_screenPosSize.xhtml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Window Open Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + screenX="80" + screenY="80" + height="300" + width="300" + persist="screenX screenY height width"> + +<body xmlns="http://www.w3.org/1999/xhtml"> + +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_showcaret.xhtml b/toolkit/content/tests/chrome/window_showcaret.xhtml new file mode 100644 index 0000000000..4c508d52a0 --- /dev/null +++ b/toolkit/content/tests/chrome/window_showcaret.xhtml @@ -0,0 +1,11 @@ +<?xml version='1.0'?> + +<?xml-stylesheet href='chrome://global/skin' type='text/css'?> + +<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' + xmlns:html="http://www.w3.org/1999/xhtml"> + +<hbox style='-moz-user-focus: normal;' width='20' height='20'/> +<html:input/> + +</window> diff --git a/toolkit/content/tests/chrome/window_subframe_origin.xhtml b/toolkit/content/tests/chrome/window_subframe_origin.xhtml new file mode 100644 index 0000000000..65d0a043fd --- /dev/null +++ b/toolkit/content/tests/chrome/window_subframe_origin.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="window" title="Subframe Origin Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<iframe + style="margin-left:20px; margin-top:20px; min-height:300px; max-width:300px; max-height:300px; border:solid 1px black;" + src="frame_subframe_origin_subframe1.xhtml"></iframe> +<caption id="parentcap" label=""/> + +<script> + +// Fire a mouse move event aimed at this window, and check to be +// sure the client coords translate from widget to the dom correctly. + +function runTests() +{ + synthesizeMouse(document.getElementById("window"), 1, 2, { type: "mousemove" }); +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); + +function mouseMove(e) { + var el = document.getElementById("parentcap"); + el.label = "client: (" + e.clientX + "," + e.clientY + ")"; + window.arguments[0].SimpleTest.is(e.clientX, 1, "mouse event clientX"); + window.arguments[0].SimpleTest.is(e.clientY, 2, "mouse event clientY"); + // fire the next test on the sub frame + frames[0].runTests(); +} + +window.addEventListener("mousemove",mouseMove); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/window_titlebar.xhtml b/toolkit/content/tests/chrome/window_titlebar.xhtml new file mode 100644 index 0000000000..886d2cf5f6 --- /dev/null +++ b/toolkit/content/tests/chrome/window_titlebar.xhtml @@ -0,0 +1,221 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for the titlebar element and window dragging + --> +<window title="Titlebar" width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <titlebar id="titlebar"> + <label id="label" value="Titlebar"/> + </titlebar> + + <!-- a non-noautohide panel is treated as anchored --> + <panel id="panel" onpopupshown="popupshown(this, false)" onpopuphidden="popuphidden('panelnoautohide')"> + <titlebar> + <label id="panellabel" value="Titlebar"/> + </titlebar> + </panel> + + <panel id="panelnoautohide" noautohide="true" + onpopupshown="popupshown(this, false)" onpopuphidden="popuphidden('panelanchored')"> + <titlebar> + <label id="panellabelnoautohide" value="Titlebar"/> + </titlebar> + </panel> + + <panel id="panelanchored" noautohide="true" + onpopupshown="popupshown(this, true)" onpopuphidden="popuphidden('paneltop')"> + <titlebar> + <label id="panellabelanchored" value="Titlebar"/> + </titlebar> + </panel> + + <panel id="paneltop" noautohide="true" level="top" + onpopupshown="popupshown(this, false)" onpopuphidden="popuphidden('panelfloating')"> + <titlebar> + <label id="panellabeltop" value="Titlebar"/> + </titlebar> + </panel> + + <panel id="panelfloating" noautohide="true" level="floating" + onpopupshown="popupshown(this, false)" onpopuphidden="popuphidden('')"> + <titlebar> + <label id="panellabelfloating" value="Titlebar"/> + </titlebar> + </panel> + + <button id="button" label="OK"/> + +<script> +<![CDATA[ + +var SimpleTest = window.arguments[0].SimpleTest; + +SimpleTest.waitForFocus(test_titlebar, window); + +var mouseDownTarget; +var origoldx, origoldy, oldx, oldy, waitSteps = 0; +function waitForWindowMove(element, x, y, callback, arg, panel, anchored) +{ + var isPanelMove = (element.id != "label"); + + if (!waitSteps) { + oldx = isPanelMove ? panel.getBoundingClientRect().left : window.screenX; + oldy = isPanelMove ? panel.getBoundingClientRect().top : window.screenY; + synthesizeMouse(element, x, y, { type: "mousemove" }); + } + + var newx = isPanelMove ? panel.getBoundingClientRect().left : window.screenX; + var newy = isPanelMove ? panel.getBoundingClientRect().top : window.screenY; + if (newx == oldx && newy == oldy) { + if (waitSteps++ > 10) { + SimpleTest.is(window.screenX + "," + window.screenY, oldx + "," + oldy + " ", + "Window never moved properly to " + x + "," + y + (panel ? " " + panel.id : "")); + window.arguments[0].SimpleTest.finish(); + window.close(); + return; + } + + setTimeout(waitForWindowMove, 100, element, x, y, callback, arg, panel, anchored); + } + else { + waitSteps = 0; + + // on Linux, we need to wait a bit for the popup to be moved as well + if (navigator.platform.includes("Linux")) { + setTimeout(callback, 0, arg, panel, anchored); + } + else { + callback(arg, panel, anchored); + } + } +} + +function test_titlebar() +{ + var titlebar = document.getElementById("titlebar"); + var label = document.getElementById("label"); + + origoldx = window.screenX; + origoldy = window.screenY; + + var mousedownListener = event => mouseDownTarget = event.originalTarget; + window.addEventListener("mousedown", mousedownListener); + synthesizeMouse(label, 2, 2, { type: "mousedown" }); + SimpleTest.is(mouseDownTarget, titlebar, "movedown on titlebar"); + waitForWindowMove(label, 22, 22, test_titlebar_step2, mousedownListener); +} + +function test_titlebar_step2(mousedownListener) +{ + var titlebar = document.getElementById("titlebar"); + var label = document.getElementById("label"); + + SimpleTest.is(window.screenX, origoldx + 20, "move window horizontal"); + SimpleTest.is(window.screenY, origoldy + 20, "move window vertical"); + synthesizeMouse(label, 22, 22, { type: "mouseup" }); + + // with allowEvents set to true, the mouse should target the label instead + // and not move the window + titlebar.allowEvents = true; + + synthesizeMouse(label, 2, 2, { type: "mousedown" }); + SimpleTest.is(mouseDownTarget, label, "movedown on titlebar with allowevents"); + synthesizeMouse(label, 22, 22, { type: "mousemove" }); + SimpleTest.is(window.screenX, origoldx + 20, "mouse on label move window horizontal"); + SimpleTest.is(window.screenY, origoldy + 20, "mouse on label move window vertical"); + synthesizeMouse(label, 22, 22, { type: "mouseup" }); + + window.removeEventListener("mousedown", mousedownListener); + + document.getElementById("panel").openPopupAtScreen(window.screenX + 50, window.screenY + 60, false); +} + +function popupshown(panel, anchored) +{ + var rect = panel.getBoundingClientRect(); + + // skip this check for non-noautohide panels + if (panel.id == "panel") { + var panellabel = panel.firstChild.firstChild; + synthesizeMouse(panellabel, 2, 2, { type: "mousedown" }); + waitForWindowMove(panellabel, 22, 22, popupshown_step3, rect, panel, anchored); + return; + } + + // now, try moving the window. If anchored, the popup should move with the + // window. If not anchored, the popup should remain at its current screen location. + window.moveBy(10, 10); + waitSteps = 1; + waitForWindowMove(document.getElementById("label"), 1, 1, popupshown_step2, rect, panel, anchored); +} + +function popupshown_step2(oldrect, panel, anchored) +{ + var newrect = panel.getBoundingClientRect(); + + // The window movement that occured long ago at the beginning of the test + // on Linux is delayed and there isn't any way to tell when the move + // actually happened. This causes the checks here to fail. Instead, just + // wait a bit for the test to be ready. + if (navigator.platform.includes("Linux") && + newrect.left != oldrect.left - (anchored ? 0 : 10)) { + setTimeout(popupshown_step2, 10, oldrect, panel, anchored); + return; + } + + // anchored popups should still be at the same offset. Non-anchored popups will + // now be offset by 10 pixels less. + SimpleTest.is(newrect.left, oldrect.left - (anchored ? 0 : 10), + panel.id + " horizontal after window move"); + SimpleTest.is(newrect.top, oldrect.top - (anchored ? 0 : 10), + panel.id + " vertical after window move"); + + var panellabel = panel.firstChild.firstChild; + synthesizeMouse(panellabel, 2, 2, { type: "mousedown" }); + waitForWindowMove(panellabel, 22, 22, popupshown_step3, newrect, panel, anchored); +} + +function popupshown_step3(oldrect, panel, anchored) +{ + // skip this check on Linux for the same window positioning reasons as above + if (!navigator.platform.includes("Linux") || (panel.id != "panelanchored" && panel.id != "paneltop")) { + // next, drag the titlebar in the panel + var newrect = panel.getBoundingClientRect(); + SimpleTest.is(newrect.left, oldrect.left + 20, panel.id + " move popup horizontal"); + SimpleTest.is(newrect.top, oldrect.top + 20, panel.id + " move popup vertical"); + synthesizeMouse(document.getElementById("panellabel"), 22, 22, { type: "mouseup" }); + + synthesizeMouse(document.getElementById("button"), 5, 5, { type: "mousemove" }); + newrect = panel.getBoundingClientRect(); + SimpleTest.is(newrect.left, oldrect.left + 20, panel.id + " horizontal after mouse on button"); + SimpleTest.is(newrect.top, oldrect.top + 20, panel.id + " vertical after mouse on button"); + } + else { + synthesizeMouse(document.getElementById("panellabel"), 22, 22, { type: "mouseup" }); + } + + panel.hidePopup(); +} + +function popuphidden(nextPopup) +{ + if (nextPopup) { + var panel = document.getElementById(nextPopup); + if (panel.id == "panelnoautohide") { + panel.openPopupAtScreen(window.screenX + 50, window.screenY + 60, false); + } else { + panel.openPopup(document.getElementById("button"), "after_start"); + } + } else { + window.arguments[0].done(window); + } +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/window_tooltip.xhtml b/toolkit/content/tests/chrome/window_tooltip.xhtml new file mode 100644 index 0000000000..fd34be9ce8 --- /dev/null +++ b/toolkit/content/tests/chrome/window_tooltip.xhtml @@ -0,0 +1,360 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tooltip Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<tooltip id="thetooltip"> + <label id="label" value="This is a tooltip"/> +</tooltip> + +<box id="parent" tooltiptext="Box Tooltip" style="margin: 10px"> + <button id="withtext" label="Tooltip Text" tooltiptext="Button Tooltip" + style="-moz-appearance: none; padding: 0;"/> + <button id="without" label="No Tooltip" style="-moz-appearance: none; padding: 0;"/> + <!-- remove the native theme and borders to avoid some platform + specific sizing differences --> + <button id="withtooltip" label="Tooltip Element" tooltip="thetooltip" + class="plain" style="-moz-appearance: none; padding: 0;"/> + <iframe id="childframe" type="content" width="10" height="10" + src="http://sectest2.example.org:80/chrome/toolkit/content/tests/chrome/popup_childframe_node.xhtml"/> +</box> + +<script class="testbody" type="application/javascript"> +<![CDATA[ +/* import-globals-from ../widgets/popup_shared.js */ +var gOriginalWidth = -1; +var gOriginalHeight = -1; +var gButton = null; + +function runTest() +{ + startPopupTests(popupTests); +} + +function checkCoords(event) +{ + // all but one test open the tooltip at the button location offset by 6 + // in each direction. Test 5 opens it at 4 in each direction. + var mod = (gTestIndex == 5) ? 4 : 6; + + var rect = gButton.getBoundingClientRect(); + is(event.clientX, Math.round(rect.left + mod), + "step " + (gTestIndex + 1) + " clientX"); + is(event.clientY, Math.round(rect.top + mod), + "step " + (gTestIndex + 1) + " clientY"); + ok(event.screenX > 0, "step " + (gTestIndex + 1) + " screenX"); + ok(event.screenY > 0, "step " + (gTestIndex + 1) + " screenY"); +} + +var popupTests = [ +{ + testname: "hover tooltiptext attribute", + events: [ "popupshowing #tooltip", "popupshown #tooltip" ], + test() { + gButton = document.getElementById("withtext"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + } +}, +{ + testname: "close tooltip", + events: [ "popuphiding #tooltip", "popuphidden #tooltip", + "DOMMenuInactive #tooltip" ], + test() { + disableNonTestMouse(true); + synthesizeMouse(document.documentElement, 2, 2, { type: "mousemove" }); + disableNonTestMouse(false); + } +}, +{ + testname: "hover inherited tooltip", + events: [ "popupshowing #tooltip", "popupshown #tooltip" ], + test() { + gButton = document.getElementById("without"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + } +}, +{ + testname: "hover tooltip attribute", + events: [ "popuphiding #tooltip", "popuphidden #tooltip", + "DOMMenuInactive #tooltip", + "popupshowing thetooltip", "popupshown thetooltip" ], + test() { + gButton = document.getElementById("withtooltip"); + gExpectedTriggerNode = gButton; + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result(testname) { + var tooltip = document.getElementById("thetooltip"); + gExpectedTriggerNode = null; + is(tooltip.triggerNode, gButton, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + is(document.tooltipNode, gButton, testname + " document.tooltipNode"); + + var child = $("childframe").contentDocument; + var evt = child.createEvent("Event"); + evt.initEvent("click", true, true); + child.documentElement.dispatchEvent(evt); + is(child.documentElement.getAttribute("data"), "xundefined", + "cannot get tooltipNode from other document"); + + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = tooltip.getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip")); + + is(Math.round(rect.left), + Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 6), + testname + " left position of tooltip"); + is(Math.round(rect.top), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6), + testname + " top position of tooltip"); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + gOriginalWidth = rect.right - rect.left; + gOriginalHeight = rect.bottom - rect.top; + } +}, +{ + testname: "click to close tooltip", + events: [ "popuphiding thetooltip", "popuphidden thetooltip", + "command withtooltip", "DOMMenuInactive thetooltip" ], + test() { + gButton = document.getElementById("withtooltip"); + synthesizeMouse(gButton, 2, 2, { }); + }, + result(testname) { + var tooltip = document.getElementById("thetooltip"); + is(tooltip.triggerNode, null, testname + " triggerNode"); + is(document.popupNode, null, testname + " document.popupNode"); + is(document.tooltipNode, null, testname + " document.tooltipNode"); + } +}, +{ + testname: "hover tooltip after size increased", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + test() { + var label = document.getElementById("label"); + label.removeAttribute("value"); + label.textContent = "This is a longer tooltip than before\nIt has multiple lines\nIt is testing tooltip sizing\n"; + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result(testname) { + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip")); + + is(Math.round(rect.left), + Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 4), + testname + " left position of tooltip"); + is(Math.round(rect.top), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 4), + testname + " top position of tooltip"); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + // make sure that the tooltip is larger than it was before by just + // checking against the original height plus an arbitrary 15 pixels + ok(gOriginalWidth + 15 < rect.right - rect.left, testname + " tooltip is wider"); + ok(gOriginalHeight + 15 < rect.bottom - rect.top, testname + " tooltip is taller"); + } +}, +{ + testname: "close tooltip with hidePopup", + events: [ "popuphiding thetooltip", "popuphidden thetooltip", + "DOMMenuInactive thetooltip" ], + test() { + document.getElementById("thetooltip").hidePopup(); + }, +}, +{ + testname: "hover tooltip after size decreased", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + autohide: "thetooltip", + test() { + var label = document.getElementById("label"); + label.value = "This is a tooltip"; + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result(testname) { + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip")); + + is(Math.round(rect.left), + Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 6), + testname + " left position of tooltip"); + is(Math.round(rect.top), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6), + testname + " top position of tooltip"); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + is(gOriginalWidth, rect.right - rect.left, testname + " tooltip is original width"); + is(gOriginalHeight, rect.bottom - rect.top, testname + " tooltip is original height"); + } +}, +{ + testname: "hover tooltip at bottom edge of screen", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + autohide: "thetooltip", + condition() { + // Only checking OSX here because on other platforms popups and tooltips behave the same way + // when there's not enough space to show them below (by flipping vertically) + // However, on OSX most popups are not flipped but tooltips are. + return navigator.platform.indexOf("Mac") > -1; + }, + test() { + var buttonRect = document.getElementById("withtext").getBoundingClientRect(); + var windowY = screen.height - + (window.mozInnerScreenY - window.screenY ) - buttonRect.bottom; + + moveWindowTo(window.screenX, windowY, function() { + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + disableNonTestMouse(false); + }); + }, + result(testname) { + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip")); + + is(Math.round(rect.y + rect.height), + Math.round(buttonrect.top + 4 - parseFloat(popupstyle.marginTop)), + testname + " position of tooltip above button"); + } +}, +{ + testname: "open tooltip for keyclose", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + test() { + gButton = document.getElementById("withtooltip"); + gExpectedTriggerNode = gButton; + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, +}, +{ + testname: "close tooltip with modifiers", + test() { + // Press all of the modifiers; the tooltip should remain open on all platforms. + synthesizeKey("KEY_Shift"); + synthesizeKey("KEY_Control"); + synthesizeKey("KEY_Alt"); + synthesizeKey("KEY_Alt"); + }, + result() { + is(document.getElementById("thetooltip").state, "open", "tooltip still open after modifiers pressed") + } +}, +{ + testname: "close tooltip with key", + events() { + if (navigator.platform.indexOf("Win") > -1) { + return []; + } + return [ "popuphiding thetooltip", "popuphidden thetooltip", + "DOMMenuInactive thetooltip" ]; + }, + test() { + sendString("a"); + }, + result() { + let expectedState = (navigator.platform.indexOf("Win") > -1) ? "open" : "closed"; + is(document.getElementById("thetooltip").state, expectedState, "tooltip closed after key pressed") + } +}, +{ + testname: "close tooltip with hidePopup again", + condition() { return navigator.platform.indexOf("Win") > -1; }, + events: [ "popuphiding thetooltip", "popuphidden thetooltip", + "DOMMenuInactive thetooltip" ], + test() { + document.getElementById("thetooltip").hidePopup(); + }, +} +]; + +var waitSteps = 0; +var oldx, oldy; +function moveWindowTo(x, y, callback, arg) +{ + if (!waitSteps) { + oldx = window.screenX; + oldy = window.screenY; + window.moveTo(x, y); + + waitSteps++; + setTimeout(moveWindowTo, 100, x, y, callback, arg); + return; + } + + if (window.screenX == oldx && window.screenY == oldy) { + if (waitSteps++ > 10) { + ok(false, "Window never moved properly to " + x + "," + y); + window.arguments[0].SimpleTest.finish(); + window.close(); + } + + setTimeout(moveWindowTo, 100, x, y, callback, arg); + } + else { + waitSteps = 0; + callback(arg); + } +} + +window.arguments[0].SimpleTest.waitForFocus(runTest, window); +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/xul_selectcontrol.js b/toolkit/content/tests/chrome/xul_selectcontrol.js new file mode 100644 index 0000000000..7e6b06bf83 --- /dev/null +++ b/toolkit/content/tests/chrome/xul_selectcontrol.js @@ -0,0 +1,691 @@ +// This script is used to test elements that implement +// nsIDOMXULSelectControlElement. This currently is the following elements: +// listbox, menulist, radiogroup, richlistbox, tabs +// +// flag behaviours that differ for certain elements +// allow-other-value - alternate values for the value property may be used +// besides those in the list +// other-value-clears-selection - alternative values for the value property +// clears the selected item +// selection-required - an item must be selected in the list, unless there +// aren't any to select +// activate-disabled-menuitem - disabled menuitems can be highlighted +// select-keynav-wraps - key navigation over a selectable list wraps +// select-extended-keynav - home, end, page up and page down keys work to +// navigate over a selectable list +// keynav-leftright - key navigation is left/right rather than up/down +// The win:, mac: and gtk: or other prefixes may be used for platform specific behaviour +var behaviours = { + menu: + "win:activate-disabled-menuitem activate-disabled-menuitem-mousemove select-keynav-wraps select-extended-keynav", + menulist: "allow-other-value other-value-clears-selection", + listbox: "select-extended-keynav", + richlistbox: "select-extended-keynav", + radiogroup: "select-keynav-wraps dont-select-disabled allow-other-value", + tabs: + "select-extended-keynav mac:select-keynav-wraps allow-other-value selection-required keynav-leftright", +}; + +function behaviourContains(tag, behaviour) { + var platform = "none:"; + if (navigator.platform.includes("Mac")) { + platform = "mac:"; + } else if (navigator.platform.includes("Win")) { + platform = "win:"; + } else if (navigator.platform.includes("X")) { + platform = "gtk:"; + } + + var re = new RegExp( + "\\s" + platform + behaviour + "\\s|\\s" + behaviour + "\\s" + ); + return re.test(" " + behaviours[tag] + " "); +} + +function test_nsIDOMXULSelectControlElement(element, childtag, testprefix) { + var testid = testprefix ? testprefix + " " : ""; + testid += element.localName + " nsIDOMXULSelectControlElement "; + + // 'initial' - check if the initial state of the element is correct + test_nsIDOMXULSelectControlElement_States( + element, + testid + "initial", + 0, + null, + -1, + "" + ); + + test_nsIDOMXULSelectControlElement_init(element, testid); + + // 'appendItem' - check if appendItem works to add a new item + var firstitem = element.appendItem("First Item", "first"); + is( + firstitem.localName, + childtag, + testid + "appendItem - first item is " + childtag + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "appendItem", + 1, + null, + -1, + "" + ); + + is(firstitem.control, element, testid + "control"); + + // 'selectedIndex' - check if an item may be selected + element.selectedIndex = 0; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedIndex", + 1, + firstitem, + 0, + "first" + ); + + // 'appendItem 2' - check if a second item may be added + var seconditem = element.appendItem("Second Item", "second"); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "appendItem 2", + 2, + firstitem, + 0, + "first" + ); + + // 'selectedItem' - check if the second item may be selected + element.selectedItem = seconditem; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedItem", + 2, + seconditem, + 1, + "second" + ); + + // 'selectedIndex 2' - check if selectedIndex may be set to -1 to deselect items + var selectionRequired = behaviourContains( + element.localName, + "selection-required" + ); + element.selectedIndex = -1; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedIndex 2", + 2, + selectionRequired ? seconditem : null, + selectionRequired ? 1 : -1, + selectionRequired ? "second" : "" + ); + + // 'selectedItem 2' - check if the selectedItem property may be set to null + element.selectedIndex = 1; + element.selectedItem = null; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedItem 2", + 2, + selectionRequired ? seconditem : null, + selectionRequired ? 1 : -1, + selectionRequired ? "second" : "" + ); + + // 'getIndexOfItem' - check if getIndexOfItem returns the right index + is( + element.getIndexOfItem(firstitem), + 0, + testid + "getIndexOfItem - first item at index 0" + ); + is( + element.getIndexOfItem(seconditem), + 1, + testid + "getIndexOfItem - second item at index 1" + ); + + var otheritem = element.ownerDocument.createXULElement(childtag); + is( + element.getIndexOfItem(otheritem), + -1, + testid + "getIndexOfItem - other item not found" + ); + + // 'getItemAtIndex' - check if getItemAtIndex returns the right item + is( + element.getItemAtIndex(0), + firstitem, + testid + "getItemAtIndex - index 0 is first item" + ); + is( + element.getItemAtIndex(1), + seconditem, + testid + "getItemAtIndex - index 0 is second item" + ); + is( + element.getItemAtIndex(-1), + null, + testid + "getItemAtIndex - index -1 is null" + ); + is( + element.getItemAtIndex(2), + null, + testid + "getItemAtIndex - index 2 is null" + ); + + // check if setting the value changes the selection + element.value = "first"; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "set value 1", + 2, + firstitem, + 0, + "first" + ); + element.value = "second"; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "set value 2", + 2, + seconditem, + 1, + "second" + ); + // setting the value attribute to one not in the list doesn't change the selection. + // The value is only changed for elements which support having a value other than the + // selection. + element.value = "other"; + var allowOtherValue = behaviourContains( + element.localName, + "allow-other-value" + ); + var otherValueClearsSelection = behaviourContains( + element.localName, + "other-value-clears-selection" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "set value other", + 2, + otherValueClearsSelection ? null : seconditem, + otherValueClearsSelection ? -1 : 1, + allowOtherValue ? "other" : "second" + ); + if (allowOtherValue) { + element.value = ""; + } + + var fourthitem = element.appendItem("Fourth Item", "fourth"); + element.selectedIndex = 0; + fourthitem.disabled = true; + element.selectedIndex = 2; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedIndex disabled", + 3, + fourthitem, + 2, + "fourth" + ); + + element.selectedIndex = 0; + element.selectedItem = fourthitem; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedItem disabled", + 3, + fourthitem, + 2, + "fourth" + ); + + if (element.menupopup) { + element.menupopup.textContent = ""; + } else { + element.textContent = ""; + } +} + +function test_nsIDOMXULSelectControlElement_init(element, testprefix) { + var id = element.id; + element = document.getElementById(id + "-initwithvalue"); + if (element) { + var seconditem = element.getItemAtIndex(1); + test_nsIDOMXULSelectControlElement_States( + element, + testprefix + " value initialization", + 3, + seconditem, + 1, + seconditem.value + ); + } + + element = document.getElementById(id + "-initwithselected"); + if (element) { + var thirditem = element.getItemAtIndex(2); + test_nsIDOMXULSelectControlElement_States( + element, + testprefix + " selected initialization", + 3, + thirditem, + 2, + thirditem.value + ); + } +} + +function test_nsIDOMXULSelectControlElement_States( + element, + testid, + expectedcount, + expecteditem, + expectedindex, + expectedvalue +) { + // need an itemCount property here + var count = element.itemCount; + is(count, expectedcount, testid + " item count"); + is(element.selectedItem, expecteditem, testid + " selectedItem"); + is(element.selectedIndex, expectedindex, testid + " selectedIndex"); + is(element.value, expectedvalue, testid + " value"); + if (element.selectedItem) { + is( + element.selectedItem.selected, + true, + testid + " selectedItem marked as selected" + ); + } +} + +/** test_nsIDOMXULSelectControlElement_UI + * + * Test the UI aspects of an element which implements nsIDOMXULSelectControlElement + * + * Parameters: + * element - element to test + */ +function test_nsIDOMXULSelectControlElement_UI(element, testprefix) { + var testid = testprefix ? testprefix + " " : ""; + testid += element.localName + " nsIDOMXULSelectControlElement UI "; + + if (element.menupopup) { + element.menupopup.textContent = ""; + } else { + element.textContent = ""; + } + + var firstitem = element.appendItem("First Item", "first"); + var seconditem = element.appendItem("Second Item", "second"); + + // 'mouse select' - check if clicking an item selects it + synthesizeMouseExpectEvent( + firstitem, + 2, + 2, + {}, + element, + "select", + testid + "mouse select" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "mouse select", + 2, + firstitem, + 0, + "first" + ); + + synthesizeMouseExpectEvent( + seconditem, + 2, + 2, + {}, + element, + "select", + testid + "mouse select 2" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "mouse select 2", + 2, + seconditem, + 1, + "second" + ); + + // make sure the element is focused so keyboard navigation will apply + element.selectedIndex = 1; + element.focus(); + + var navLeftRight = behaviourContains(element.localName, "keynav-leftright"); + var backKey = navLeftRight ? "VK_LEFT" : "VK_UP"; + var forwardKey = navLeftRight ? "VK_RIGHT" : "VK_DOWN"; + + // 'key select' - check if keypresses move between items + synthesizeKeyExpectEvent(backKey, {}, element, "select", testid + "key up"); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up", + 2, + firstitem, + 0, + "first" + ); + + var keyWrap = behaviourContains(element.localName, "select-keynav-wraps"); + + var expectedItem = keyWrap ? seconditem : firstitem; + var expectedIndex = keyWrap ? 1 : 0; + var expectedValue = keyWrap ? "second" : "first"; + synthesizeKeyExpectEvent( + backKey, + {}, + keyWrap ? element : null, + "select", + testid + "key up 2" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up 2", + 2, + expectedItem, + expectedIndex, + expectedValue + ); + + element.selectedIndex = 0; + synthesizeKeyExpectEvent( + forwardKey, + {}, + element, + "select", + testid + "key down" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down", + 2, + seconditem, + 1, + "second" + ); + + expectedItem = keyWrap ? firstitem : seconditem; + expectedIndex = keyWrap ? 0 : 1; + expectedValue = keyWrap ? "first" : "second"; + synthesizeKeyExpectEvent( + forwardKey, + {}, + keyWrap ? element : null, + "select", + testid + "key down 2" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down 2", + 2, + expectedItem, + expectedIndex, + expectedValue + ); + + var thirditem = element.appendItem("Third Item", "third"); + var fourthitem = element.appendItem("Fourth Item", "fourth"); + if (behaviourContains(element.localName, "select-extended-keynav")) { + element.appendItem("Fifth Item", "fifth"); + var sixthitem = element.appendItem("Sixth Item", "sixth"); + + synthesizeKeyExpectEvent( + "VK_END", + {}, + element, + "select", + testid + "key end" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key end", + 6, + sixthitem, + 5, + "sixth" + ); + + synthesizeKeyExpectEvent( + "VK_HOME", + {}, + element, + "select", + testid + "key home" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key home", + 6, + firstitem, + 0, + "first" + ); + + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + {}, + element, + "select", + testid + "key page down" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key page down", + 6, + fourthitem, + 3, + "fourth" + ); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + {}, + element, + "select", + testid + "key page down to end" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key page down to end", + 6, + sixthitem, + 5, + "sixth" + ); + + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + {}, + element, + "select", + testid + "key page up" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key page up", + 6, + thirditem, + 2, + "third" + ); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + {}, + element, + "select", + testid + "key page up to start" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key page up to start", + 6, + firstitem, + 0, + "first" + ); + + element.getItemAtIndex(5).remove(); + element.getItemAtIndex(4).remove(); + } + + // now test whether a disabled item works. + element.selectedIndex = 0; + seconditem.disabled = true; + + var dontSelectDisabled = behaviourContains( + element.localName, + "dont-select-disabled" + ); + + // 'mouse select' - check if clicking an item selects it + synthesizeMouseExpectEvent( + seconditem, + 2, + 2, + {}, + element, + dontSelectDisabled ? "!select" : "select", + testid + "mouse select disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "mouse select disabled", + 4, + dontSelectDisabled ? firstitem : seconditem, + dontSelectDisabled ? 0 : 1, + dontSelectDisabled ? "first" : "second" + ); + + if (dontSelectDisabled) { + // test whether disabling an item won't allow it to be selected + synthesizeKeyExpectEvent( + forwardKey, + {}, + element, + "select", + testid + "key down disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down disabled", + 4, + thirditem, + 2, + "third" + ); + + synthesizeKeyExpectEvent( + backKey, + {}, + element, + "select", + testid + "key up disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up disabled", + 4, + firstitem, + 0, + "first" + ); + + element.selectedIndex = 2; + firstitem.disabled = true; + + synthesizeKeyExpectEvent( + backKey, + {}, + keyWrap ? element : null, + "select", + testid + "key up disabled 2" + ); + expectedItem = keyWrap ? fourthitem : thirditem; + expectedIndex = keyWrap ? 3 : 2; + expectedValue = keyWrap ? "fourth" : "third"; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up disabled 2", + 4, + expectedItem, + expectedIndex, + expectedValue + ); + } else { + // in this case, disabled items should behave the same as non-disabled items. + element.selectedIndex = 0; + synthesizeKeyExpectEvent( + forwardKey, + {}, + element, + "select", + testid + "key down disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down disabled", + 4, + seconditem, + 1, + "second" + ); + synthesizeKeyExpectEvent( + forwardKey, + {}, + element, + "select", + testid + "key down disabled again" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down disabled again", + 4, + thirditem, + 2, + "third" + ); + + synthesizeKeyExpectEvent( + backKey, + {}, + element, + "select", + testid + "key up disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up disabled", + 4, + seconditem, + 1, + "second" + ); + synthesizeKeyExpectEvent( + backKey, + {}, + element, + "select", + testid + "key up disabled again" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up disabled again", + 4, + firstitem, + 0, + "first" + ); + } +} diff --git a/toolkit/content/tests/mochitest/file_mousecapture.html b/toolkit/content/tests/mochitest/file_mousecapture.html new file mode 100644 index 0000000000..bc3d1869f6 --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture.html @@ -0,0 +1 @@ +<html><p>One</p><p style='margin-top: 200px;'>Two</p><p style='margin-top: 4000px'>This is some text</p></html> diff --git a/toolkit/content/tests/mochitest/file_mousecapture2.html b/toolkit/content/tests/mochitest/file_mousecapture2.html new file mode 100644 index 0000000000..e746cf7807 --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture2.html @@ -0,0 +1,8 @@ +<html><p>One</p> +<p id="myStyle">Two</p> +<p style='margin-top: 4000px'>This is some text</p> +</html> + +<script type="application/javascript"> +document.getElementById("myStyle").style.marginTop = new URL(location.href).searchParams.get("topPos"); +</script> diff --git a/toolkit/content/tests/mochitest/file_mousecapture3.html b/toolkit/content/tests/mochitest/file_mousecapture3.html new file mode 100644 index 0000000000..ee3fad455a --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture3.html @@ -0,0 +1 @@ +<body style='font-size: 40pt;'>.<b id='b'>This</b> is some text<div id='fixed' style='position: fixed; left: 55px; top: 5px; width: 10px; height: 10px'>.</div></body> diff --git a/toolkit/content/tests/mochitest/file_mousecapture4.html b/toolkit/content/tests/mochitest/file_mousecapture4.html new file mode 100644 index 0000000000..fbf30589c1 --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture4.html @@ -0,0 +1 @@ +<frameset cols='50%, 50%'><frame src='about:blank'><frame src='about:blank'></frameset> diff --git a/toolkit/content/tests/mochitest/file_mousecapture5.html b/toolkit/content/tests/mochitest/file_mousecapture5.html new file mode 100644 index 0000000000..bc632b6156 --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture5.html @@ -0,0 +1 @@ +<input id='input' onfocus='this.style.display = "none"' style='float: left;'> diff --git a/toolkit/content/tests/mochitest/gizmo.mp4 b/toolkit/content/tests/mochitest/gizmo.mp4 Binary files differnew file mode 100644 index 0000000000..87efad5ade --- /dev/null +++ b/toolkit/content/tests/mochitest/gizmo.mp4 diff --git a/toolkit/content/tests/mochitest/mochitest.ini b/toolkit/content/tests/mochitest/mochitest.ini new file mode 100644 index 0000000000..a2128a5e0d --- /dev/null +++ b/toolkit/content/tests/mochitest/mochitest.ini @@ -0,0 +1,20 @@ +[test_bug1407085.html] + +[test_autocomplete_change_after_focus.html] +skip-if = toolkit == "android" +[test_mousecapture.xhtml] +support-files = + file_mousecapture.html + file_mousecapture2.html + file_mousecapture3.html + file_mousecapture4.html + file_mousecapture5.html +skip-if = + toolkit == "android" + xorigin # SecurityError: Permission denied to access property "scrollX" on cross-origin object at runTests@http://mochi.test:8888/tests/toolkit/content/tests/mochitest/test_mousecapture.xhtml:170:17, inconsistent fail/pass + os == "mac" && webrender && debug # Bug 1664730 + os == "linux" && !asan && bits == 64 # Bug 1664730 +[test_video_control_no_control_overlay.html] +support-files = + gizmo.mp4 +skip-if = toolkit != "android" diff --git a/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html b/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html new file mode 100644 index 0000000000..04d487b0a3 --- /dev/null +++ b/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=998893 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 998893 - Ensure that input.value changes affect autocomplete</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + /* eslint-env mozilla/frame-script */ + + /** Test for Bug 998893 **/ + add_task(async function waitForFocus() { + await new Promise(resolve => SimpleTest.waitForFocus(resolve)); + }); + + add_task(async function setup() { + await new Promise(resolve => { + let chromeScript = SpecialPowers.loadChromeScript(function() { + const {FormHistory} = ChromeUtils.import("resource://gre/modules/FormHistory.jsm"); + FormHistory.update([ + { op: "bump", fieldname: "field1", value: "Default text option" }, + { op: "bump", fieldname: "field1", value: "New value option" }, + ], { + handleCompletion() { + sendAsyncMessage("Test:Resume"); + }, + }); + }); + + chromeScript.addMessageListener("Test:Resume", function resumeListener() { + chromeScript.removeMessageListener("Test:Resume", resumeListener); + chromeScript.destroy(); + resolve(); + }); + }); + }); + + add_task(async function runTest() { + let promisePopupShown = new Promise(resolve => { + let chromeScript = SpecialPowers.loadChromeScript(function() { + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + let window = Services.wm.getMostRecentWindow("navigator:browser"); + let popup = window.document.getElementById("PopupAutoComplete"); + popup.addEventListener("popupshown", function() { + sendAsyncMessage("Test:Resume"); + }, {once: true}); + }); + + chromeScript.addMessageListener("Test:Resume", function resumeListener() { + chromeScript.removeMessageListener("Test:Resume", resumeListener); + chromeScript.destroy(); + resolve(); + }); + }); + + let field = document.getElementById("field1"); + + let promiseFieldFocus = new Promise(resolve => { + field.addEventListener("focus", function onFocus() { + info("field focused"); + field.value = "New value"; + sendKey("DOWN"); + resolve(); + }); + }); + + let handleEnterPromise = new Promise(resolve => { + function handleEnter(evt) { + if (evt.keyCode != KeyEvent.DOM_VK_RETURN) { + return; + } + info("RETURN received for phase: " + evt.eventPhase); + is(evt.target.value, "New value option", "Check that the correct autocomplete entry was used"); + resolve(); + } + + SpecialPowers.addSystemEventListener(field, "keypress", handleEnter, true); + }); + + field.focus(); + + await promiseFieldFocus; + + await promisePopupShown; + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Enter"); + + await handleEnterPromise; + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998893">Mozilla Bug 998893</a> +<p id="display"><input id="field1" value="Default text"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/mochitest/test_bug1407085.html b/toolkit/content/tests/mochitest/test_bug1407085.html new file mode 100644 index 0000000000..17fc2f8a3b --- /dev/null +++ b/toolkit/content/tests/mochitest/test_bug1407085.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 1407085</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <input id="input" value="original value"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + let input = document.getElementById("input"); + input.focus(); + input.addEventListener("keydown", () => { + input.value = "new value"; + }, { once: true }); + synthesizeKey("KEY_Escape"); + is(input.value, "new value", + "New <input> value changed by an Escape key event listener shouldn't be " + + "overwritten by original value even if Escape key is pressed"); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/mochitest/test_mousecapture.xhtml b/toolkit/content/tests/mochitest/test_mousecapture.xhtml new file mode 100644 index 0000000000..96f4a2af47 --- /dev/null +++ b/toolkit/content/tests/mochitest/test_mousecapture.xhtml @@ -0,0 +1,329 @@ +<?xml version="1.0"?> +<!DOCTYPE HTML> +<html xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Mouse Capture Tests</title> + <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body id="body" xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"/><div id="content" style="display: none"/><pre id="test"/> + +<script><![CDATA[ + +SimpleTest.expectAssertions(6, 12); + +SimpleTest.waitForExplicitFinish(); + +const TEST_PATH = location.href.replace("test_mousecapture.xhtml", ""); + +var captureRetargetMode = false; +var cachedMouseDown = null; +var previousWidth = 0, originalWidth = 0; +var loadInWindow = false; + +function splitterCallback(adjustment) { + var newWidth = Number($("leftbox").width); // getBoundingClientRect().width; + var expectedWidth = previousWidth + adjustment; + if (expectedWidth > $("splitterbox").getBoundingClientRect().width) + expectedWidth = $("splitterbox").getBoundingClientRect().width - $("splitter").getBoundingClientRect().width; + is(newWidth, expectedWidth, "splitter left box size (" + adjustment + ")"); + previousWidth = newWidth; +} + +function selectionCallback(adjustment) { + if (adjustment == 4000) { + is(frames[0].getSelection().toString(), "This is some text", "selection after drag (" + adjustment + ")"); + ok(frames[0].scrollY > 40, "selection caused scroll down (" + adjustment + ")"); + } else { + if (adjustment == 0) { + is(frames[0].getSelection().toString(), ".", "selection after drag (" + adjustment + ")"); + } + is(frames[0].scrollY, 0, "selection scrollY (" + adjustment + ")"); + } +} + +function framesetCallback(adjustment) { + var newWidth = frames[1].frames[0].document.documentElement.clientWidth; + var expectedWidth = originalWidth + adjustment; + if (adjustment == 0) + expectedWidth = originalWidth - 12; + else if (expectedWidth >= 4000) + expectedWidth = originalWidth * 2 - 2; + + ok(Math.abs(newWidth - expectedWidth) <= 1, "frameset after drag (" + adjustment + "), new width " + newWidth + ", expected " + expectedWidth); +} + +var otherWindow = null; + +function selectionScrollCheck() { + var element = otherWindow.document.documentElement; + + var count = 0; + function selectionScrollDone() { + // wait for 6 scroll events to occur + if (count++ < 6) + return; + + otherWindow.removeEventListener("scroll", selectionScrollDone); + + var selectedText = otherWindow.getSelection().toString().replace(/\r/g, ""); + // We trim the end of the string because, per the below comment, we might + // select additional newlines, and that's OK. + is(selectedText.trimEnd(), "One\n\nTwo", "text is selected"); + + // should have scrolled 20 pixels from the mousemove above and at least 6 + // extra 20-pixel increments from the selection scroll timer. "At least 6" + // because we waited for 6 scroll events but multiple scrolls could get + // coalesced into a single scroll event, and paints could be delayed when + // the window loads when the compositor is busy. As a result, we have no + // real guarantees about the upper bound here, and as the upper bound is + // not important for what we're testing here, we don't check it. + var scrollY = otherWindow.scrollY; + info(`Scrolled ${scrollY} pixels`); + ok(scrollY >= 140, "selection scroll position after timer is at least 140"); + ok((scrollY % 20) == 0, "selection scroll position after timer is multiple of 20"); + + synthesizeMouse(element, 4, otherWindow.innerHeight + 25, { type: "mouseup" }, otherWindow); + disableNonTestMouseEvents(false); + otherWindow.close(); + + if (loadInWindow) { + SimpleTest.finish(); + } else { + // now try again, but open the page in a new window + loadInWindow = true; + synthesizeMouse(document.getElementById("custom"), 2, 2, { type: "mousedown" }); + + // check to ensure that selection dragging scrolls the right scrollable area + otherWindow = window.open(TEST_PATH + "file_mousecapture.html", "_blank", "width=200,height=200,scrollbars=yes"); + SimpleTest.waitForFocus(selectionScrollCheck, otherWindow); + } + } + + SimpleTest.executeSoon(function() { + disableNonTestMouseEvents(true); + synthesizeMouse(element, 2, 2, { type: "mousedown" }, otherWindow); + synthesizeMouse(element, 100, otherWindow.innerHeight + 20, { type: "mousemove" }, otherWindow); + otherWindow.addEventListener("scroll", selectionScrollDone); + }); +} + +function runTests() { + previousWidth = $("leftbox").getBoundingClientRect().width; + runCaptureTest($("splitter"), splitterCallback); + + var custom = document.getElementById("custom"); + runCaptureTest(custom); + + synthesizeMouseExpectEvent($("rightbox"), 2, 2, { type: "mousemove" }, + $("rightbox"), "mousemove", "setCapture and releaseCapture"); + + custom.setCapture(); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "setCapture fails on non mousedown"); + + var custom2 = document.getElementById("custom2"); + synthesizeMouse(custom2, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "document.releaseCapture releases capture"); + + var custom3 = document.getElementById("custom3"); + synthesizeMouse(custom3, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "element.releaseCapture releases capture"); + + var custom4 = document.getElementById("custom4"); + synthesizeMouse(custom4, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + custom4, "mousemove", "element.releaseCapture during mousemove before releaseCapture"); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "element.releaseCapture during mousemove after releaseCapture"); + + var custom5 = document.getElementById("custom5"); + runCaptureTest(custom5); + captureRetargetMode = true; + runCaptureTest(custom5); + captureRetargetMode = false; + + var custom6 = document.getElementById("custom6"); + synthesizeMouse(custom6, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "setCapture only works on elements in documents"); + synthesizeMouse(custom6, 2, 2, { type: "mouseup" }); + + // test that mousedown on an image with setCapture followed by a big enough + // mouse move does not start a drag (bug 517737) + var image = document.getElementById("image"); + image.scrollIntoView(); + synthesizeMouse(image, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + image, "mousemove", "setCapture works on images"); + synthesizeMouse(image, 2, 2, { type: "mouseup" }); + + window.scroll(0, 0); + + // save scroll + var scrollX = parent ? parent.scrollX : 0; + var scrollY = parent ? parent.scrollY : 0; + + // restore scroll + if (parent) parent.scroll(scrollX, scrollY); + +// frames[0].getSelection().collapseToStart(); + + var body = frames[0].document.body; + var fixed = frames[0].document.getElementById("fixed"); + function captureOnBody() { body.setCapture(); } + body.addEventListener("mousedown", captureOnBody, true); + synthesizeMouse(body, 8, 8, { type: "mousedown" }, frames[0]); + body.removeEventListener("mousedown", captureOnBody, true); + synthesizeMouseExpectEvent(fixed, 2, 2, { type: "mousemove" }, + fixed, "mousemove", "setCapture on body retargets to root node", frames[0]); + synthesizeMouse(body, 8, 8, { type: "mouseup" }, frames[0]); + + previousWidth = frames[1].frames[0].document.documentElement.clientWidth; + originalWidth = previousWidth; + runCaptureTest(frames[1].document.documentElement.lastElementChild, framesetCallback); + + // ensure that clicking on an element where the frame disappears doesn't crash + synthesizeMouse(frames[2].document.getElementById("input"), 8, 8, { type: "mousedown" }, frames[2]); + synthesizeMouse(frames[2].document.getElementById("input"), 8, 8, { type: "mouseup" }, frames[2]); + + var select = document.getElementById("select"); + select.scrollIntoView(); + + synthesizeMouse(document.getElementById("option3"), 2, 2, { type: "mousedown" }); + synthesizeMouse(document.getElementById("option3"), 2, 1000, { type: "mousemove" }); + is(select.selectedIndex, 2, "scroll select"); + synthesizeMouse(document.getElementById("select"), 2, 2, { type: "mouseup" }); + window.scroll(0, 0); + + synthesizeMouse(custom, 2, 2, { type: "mousedown" }); + + // check to ensure that selection dragging scrolls the right scrollable area. + // This should open the page in a new tab. + + var topPos = window.innerHeight; + otherWindow = window.open(TEST_PATH + "file_mousecapture2.html?topPos=" + topPos, "_blank"); + SimpleTest.waitForFocus(selectionScrollCheck, otherWindow); +} + +function runCaptureTest(element, callback) { + var expectedTarget = null; + + // ownerGlobal doesn't exist in content privileged windows. + // eslint-disable-next-line mozilla/use-ownerGlobal + var win = element.ownerDocument.defaultView; + + function mouseMoved(event) { + is(event.originalTarget, expectedTarget, + expectedTarget.id + " target for point " + event.clientX + "," + event.clientY); + } + win.addEventListener("mousemove", mouseMoved); + + expectedTarget = element; + + var basepoint = element.localName == "frameset" ? 50 : 2; + synthesizeMouse(element, basepoint, basepoint, { type: "mousedown" }, win); + + // in setCapture(true) mode, all events should fire on custom5. In + // setCapture(false) mode, events can fire at a descendant + if (expectedTarget == $("custom5") && !captureRetargetMode) + expectedTarget = $("custom5spacer"); + + // releaseCapture should do nothing for an element which isn't capturing + $("splitterbox").releaseCapture(); + + synthesizeMouse(element, basepoint + 2, basepoint + 2, { type: "mousemove" }, win); + if (callback) + callback(2); + + if (expectedTarget == $("custom5spacer") && !captureRetargetMode) + expectedTarget = $("custom5inner"); + + if (element.id == "b") { + var tooltip = document.getElementById("tooltip"); + tooltip.openPopup(); + tooltip.hidePopup(); + } + + synthesizeMouse(element, basepoint + 25, basepoint + 25, { type: "mousemove" }, win); + if (callback) + callback(25); + + expectedTarget = element.localName == "b" ? win.document.documentElement : element; + synthesizeMouse(element, basepoint + 4000, basepoint + 4000, { type: "mousemove" }, win); + if (callback) + callback(4000); + synthesizeMouse(element, basepoint - 12, basepoint - 12, { type: "mousemove" }, win); + if (callback) + callback(-12); + + expectedTarget = element.localName == "frameset" ? element : win.document.documentElement; + synthesizeMouse(element, basepoint + 30, basepoint + 30, { type: "mouseup" }, win); + synthesizeMouse(win.document.documentElement, 2, 2, { type: "mousemove" }, win); + if (callback) + callback(0); + + win.removeEventListener("mousemove", mouseMoved); +} + +SimpleTest.waitForFocus(runTests); + +]]> +</script> + +<xul:vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" align="start"> + <tooltip id="tooltip"> + <label value="Test"/> + </tooltip> + + <hbox id="splitterbox" style="margin-top: 5px;" onmousedown="this.setCapture()"> + <hbox id="leftbox" width="100" flex="1"/> + <splitter id="splitter" width="5" height="5"/> + <hbox id="rightbox" width="100" flex="1"/> + </hbox> + + <vbox id="custom" width="10" height="10" onmousedown="this.setCapture(); cachedMouseDown = event;"/> + <vbox id="custom2" width="10" height="10" onmousedown="this.setCapture(); document.releaseCapture();"/> + <vbox id="custom3" width="10" height="10" onmousedown="this.setCapture(); this.releaseCapture();"/> + <vbox id="custom4" width="10" height="10" onmousedown="this.setCapture();" + onmousemove="this.releaseCapture();"/> + <hbox id="custom5" width="40" height="40" + onmousedown="this.setCapture(captureRetargetMode);"> + <spacer id="custom5spacer" width="5"/> + <hbox id="custom5inner" width="35" height="35"/> + </hbox> + <vbox id="custom6" width="10" height="10" + onmousedown="document.createElement('hbox').setCapture();"/> +</xul:vbox> + + <iframe width="100" height="100" src="file_mousecapture3.html"/> + <iframe width="100" height="100" src="file_mousecapture4.html"/> + <iframe width="100" height="100" src="file_mousecapture5.html"/> + + <select id="select" xmlns="http://www.w3.org/1999/xhtml" size="4"> + <option id="option1">One</option> + <option id="option2">Two</option> + <option id="option3">Three</option> + <option id="option4">Four</option> + <option id="option5">Five</option> + <option id="option6">Six</option> + <option id="option7">Seven</option> + <option id="option8">Eight</option> + <option id="option9">Nine</option> + <option id="option10">Ten</option> + </select> + + <img id="image" xmlns="http://www.w3.org/1999/xhtml" + onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + ondragstart="ok(false, 'should not get a drag when a setCapture is active');" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC"/> + +</body> + +</html> diff --git a/toolkit/content/tests/mochitest/test_video_control_no_control_overlay.html b/toolkit/content/tests/mochitest/test_video_control_no_control_overlay.html new file mode 100644 index 0000000000..6a079d77dc --- /dev/null +++ b/toolkit/content/tests/mochitest/test_video_control_no_control_overlay.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Show 'click-to-play' icon on blocked autoplay media</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> +/** + * This test is used to check whether 'click-to-play' icon would be showed + * correctly when autoplay media is blocked. + */ +add_task(async function testShowClickToPlayWhenAutoplayMediaGetsBlocked() { + info(`setting testing pref`); + await SpecialPowers.pushPrefEnv( + {"set": [["media.autoplay.default", 1 /* BLOCKED */]]} + ); + + info(`create video and load resource`); + let video = document.createElement('video'); + video.src = "gizmo.mp4"; + document.body.appendChild(video); + + info(`blocking autoplay would reject media to play`); + ok(await video.play().then(_ => false, _ => true), "Play got rejected"); + + info(`'click-to-play' should display when autoplay media is blocked`); + const button = SpecialPowers.wrap(video).openOrClosedShadowRoot.querySelector(".clickToPlay"); + ok(!button.hidden, "Click-to-play button is not hidden"); +}); + +</script> +</head> +<body> +</body> +</html> diff --git a/toolkit/content/tests/moz.build b/toolkit/content/tests/moz.build new file mode 100644 index 0000000000..f3000bd55c --- /dev/null +++ b/toolkit/content/tests/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"] + +BROWSER_CHROME_MANIFESTS += [ + "browser/browser.ini", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "chrome/chrome.ini", + "widgets/chrome.ini", +] + +MOCHITEST_MANIFESTS += [ + "mochitest/mochitest.ini", + "widgets/mochitest.ini", +] diff --git a/toolkit/content/tests/reftests/audio-dynamically-change-small-width-ref.html b/toolkit/content/tests/reftests/audio-dynamically-change-small-width-ref.html new file mode 100644 index 0000000000..4bcac35d84 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-dynamically-change-small-width-ref.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } +</style> +</head> +<body> + <audio controls></audio> +</body> diff --git a/toolkit/content/tests/reftests/audio-dynamically-change-small-width.html b/toolkit/content/tests/reftests/audio-dynamically-change-small-width.html new file mode 100644 index 0000000000..0e9059541a --- /dev/null +++ b/toolkit/content/tests/reftests/audio-dynamically-change-small-width.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } +</style> +</head> +<body> + <audio id="tweakme" controls></audio> + + <script> + function doTest() { + setTimeout(() => { + let tweakme = document.getElementById("tweakme"); + + // Make the audio element extremely skinny, flush layout, and then revert + // that change: + tweakme.style.width = "1px"; + tweakme.offsetHeight; // flush layout + tweakme.style.width = ""; + tweakme.offsetHeight; // flush layout + + document.documentElement.removeAttribute("class"); + }, 300); + } + + window.addEventListener("MozReftestInvalidate", doTest); + </script> +</body> diff --git a/toolkit/content/tests/reftests/audio-with-bogus-url-ref.html b/toolkit/content/tests/reftests/audio-with-bogus-url-ref.html new file mode 100644 index 0000000000..7731e5ced6 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-with-bogus-url-ref.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } +</style> +</head> +<body> + <audio controls></audio> +</body> +</html> diff --git a/toolkit/content/tests/reftests/audio-with-bogus-url.html b/toolkit/content/tests/reftests/audio-with-bogus-url.html new file mode 100644 index 0000000000..d6bf7ff861 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-with-bogus-url.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } +</style> +</head> +<body> + <audio src="bogus.mp3" controls></audio> +</body> +</html> diff --git a/toolkit/content/tests/reftests/audio-with-padding-ref.html b/toolkit/content/tests/reftests/audio-with-padding-ref.html new file mode 100644 index 0000000000..b0fe3fa6e5 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-with-padding-ref.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } + + #wrapper { + padding: 20px; + } +</style> +</head> +<body> + <div id="wrapper"> + <audio controls></audio> + <div> +</body> diff --git a/toolkit/content/tests/reftests/audio-with-padding.html b/toolkit/content/tests/reftests/audio-with-padding.html new file mode 100644 index 0000000000..c18b746374 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-with-padding.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } + + audio { + padding: 20px; + } +</style> +</head> +<body> + <audio controls></audio> +</body> diff --git a/toolkit/content/tests/reftests/foo.vtt b/toolkit/content/tests/reftests/foo.vtt new file mode 100644 index 0000000000..b533895c60 --- /dev/null +++ b/toolkit/content/tests/reftests/foo.vtt @@ -0,0 +1,4 @@ +WEBVTT + +00:00:00.000 --> 00:00:05.000 +Foo diff --git a/toolkit/content/tests/reftests/reftest.list b/toolkit/content/tests/reftests/reftest.list new file mode 100644 index 0000000000..d1e8ce3dbd --- /dev/null +++ b/toolkit/content/tests/reftests/reftest.list @@ -0,0 +1,4 @@ +== videocontrols-dynamically-add-cc.html videocontrols-dynamically-add-cc-ref.html +== audio-with-bogus-url.html audio-with-bogus-url-ref.html +== audio-dynamically-change-small-width.html audio-dynamically-change-small-width-ref.html +== audio-with-padding.html audio-with-padding-ref.html diff --git a/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc-ref.html b/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc-ref.html new file mode 100644 index 0000000000..1dcf4949a6 --- /dev/null +++ b/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc-ref.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } + + video { + width: 320px; + height: 240px; + } + + #mask { + position: absolute; + z-index: 3; + width: 320px; + height: 200px; + background-color: green; + top: 0; + left: 0; + } +</style> +</head> +<body> + <video id="vid" controls> + <track kind="subtitles" src="foo.vtt" srclang="en"> + </video> + <div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc.html b/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc.html new file mode 100644 index 0000000000..e3d0912a51 --- /dev/null +++ b/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } + + video { + width: 320px; + height:240px; + } + + #mask { + position: absolute; + z-index: 3; + width: 320px; + height: 200px; + background-color: green; + top: 0; + left: 0; + } + + #vid-preload-image { + visibility: hidden; + } +</style> +<script> + function addCCToVid(videoElem) { + videoElem.addTextTrack("subtitles", "English", "en"); + } +</script> +</head> +<body> + <video id="vid" controls></video> + <div id="mask"></div> + <!-- Create a hidden video with CC button displayed upfront to decode image + earlier, so that the CC image will be ready to paint once the track added. --> + <video id="vid-preload-image" controls> + <track kind="subtitles" src="foo.vtt" srclang="en"> + </video> + + <script> + function doTest() { + var vid = document.getElementById("vid"); + + // Videocontrols binding's "addtrack" handler synchronously fires + // "adjustControlSize()" first, and then the layout is ready for + // the reftest snapshot. + vid.textTracks.addEventListener("addtrack", function() { + document.documentElement.removeAttribute("class"); + }); + + addCCToVid(vid); + } + + window.addEventListener("MozReftestInvalidate", doTest); + </script> +</body> +</html> diff --git a/toolkit/content/tests/unit/test_contentAreaUtils.js b/toolkit/content/tests/unit/test_contentAreaUtils.js new file mode 100644 index 0000000000..b06432fa5a --- /dev/null +++ b/toolkit/content/tests/unit/test_contentAreaUtils.js @@ -0,0 +1,82 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +function loadUtilsScript() { + /* import-globals-from ../../contentAreaUtils.js */ + Services.scriptloader.loadSubScript( + "chrome://global/content/contentAreaUtils.js" + ); +} + +function test_urlSecurityCheck() { + var nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({}); + + const HTTP_URI = "http://www.mozilla.org/"; + const CHROME_URI = "chrome://browser/content/browser.xhtml"; + const DISALLOW_INHERIT_PRINCIPAL = + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; + + try { + urlSecurityCheck( + makeURI(HTTP_URI), + nullPrincipal, + DISALLOW_INHERIT_PRINCIPAL + ); + } catch (ex) { + do_throw( + "urlSecurityCheck should not throw when linking to a http uri with a null principal" + ); + } + + // urlSecurityCheck also supports passing the url as a string + try { + urlSecurityCheck(HTTP_URI, nullPrincipal, DISALLOW_INHERIT_PRINCIPAL); + } catch (ex) { + do_throw( + "urlSecurityCheck failed to handle the http URI as a string (uri spec)" + ); + } + + let shouldThrow = true; + try { + urlSecurityCheck(CHROME_URI, nullPrincipal, DISALLOW_INHERIT_PRINCIPAL); + } catch (ex) { + shouldThrow = false; + } + if (shouldThrow) { + do_throw( + "urlSecurityCheck should throw when linking to a chrome uri with a null principal" + ); + } +} + +function test_stringBundle() { + // This test verifies that the elements that can be used as file picker title + // keys in the save* functions are actually present in the string bundle. + // These keys are part of the contentAreaUtils.js public API. + var validFilePickerTitleKeys = [ + "SaveImageTitle", + "SaveVideoTitle", + "SaveAudioTitle", + "SaveLinkTitle", + ]; + + for (let filePickerTitleKey of validFilePickerTitleKeys) { + // Just check that the string exists + try { + ContentAreaUtils.stringBundle.GetStringFromName(filePickerTitleKey); + } catch (e) { + do_throw("Error accessing file picker title key: " + filePickerTitleKey); + } + } +} + +function run_test() { + loadUtilsScript(); + test_urlSecurityCheck(); + test_stringBundle(); +} diff --git a/toolkit/content/tests/unit/xpcshell.ini b/toolkit/content/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..52d5df56f9 --- /dev/null +++ b/toolkit/content/tests/unit/xpcshell.ini @@ -0,0 +1,4 @@ +[DEFAULT] +head = + +[test_contentAreaUtils.js] diff --git a/toolkit/content/tests/widgets/.eslintrc.js b/toolkit/content/tests/widgets/.eslintrc.js new file mode 100644 index 0000000000..721e0938af --- /dev/null +++ b/toolkit/content/tests/widgets/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/mochitest-test", "plugin:mozilla/chrome-test"], +}; diff --git a/toolkit/content/tests/widgets/audio.ogg b/toolkit/content/tests/widgets/audio.ogg Binary files differnew file mode 100644 index 0000000000..a553c23e73 --- /dev/null +++ b/toolkit/content/tests/widgets/audio.ogg diff --git a/toolkit/content/tests/widgets/audio.wav b/toolkit/content/tests/widgets/audio.wav Binary files differnew file mode 100644 index 0000000000..c6fd5cb869 --- /dev/null +++ b/toolkit/content/tests/widgets/audio.wav diff --git a/toolkit/content/tests/widgets/chrome.ini b/toolkit/content/tests/widgets/chrome.ini new file mode 100644 index 0000000000..655c41e3e1 --- /dev/null +++ b/toolkit/content/tests/widgets/chrome.ini @@ -0,0 +1,27 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + tree_shared.js + popup_shared.js + window_label_checkbox.xhtml + window_menubar.xhtml + seek_with_sound.ogg + +[test_contextmenu_nested.xhtml] +skip-if = os == 'linux' # Bug 1116215 +[test_contextmenu_menugroup.xhtml] +skip-if = os == 'linux' # Bug 1115088 +[test_editor_currentURI.xhtml] +[test_label_checkbox.xhtml] +[test_menubar.xhtml] +skip-if = os == 'mac' +[test_popupanchor.xhtml] +skip-if = os == 'linux' || (verify && (os == 'win')) # Bug 1335894 perma-fail on linux 16.04 +[test_popupreflows.xhtml] +[test_tree_column_reorder.xhtml] +[test_videocontrols_onclickplay.html] +[test_videocontrols_focus.html] +support-files = + head.js + video.ogg +skip-if = toolkit == 'android' diff --git a/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html b/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html new file mode 100644 index 0000000000..56917b69ac --- /dev/null +++ b/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html @@ -0,0 +1,2 @@ +<video src="seek_with_sound.ogg" controls autoplay=true></video> +<script>window.testExpando = true;</script> diff --git a/toolkit/content/tests/widgets/head.js b/toolkit/content/tests/widgets/head.js new file mode 100644 index 0000000000..49838fc118 --- /dev/null +++ b/toolkit/content/tests/widgets/head.js @@ -0,0 +1,57 @@ +"use strict"; + +const InspectorUtils = SpecialPowers.InspectorUtils; + +var tests = []; + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { + clearInterval(interval); + nextTest(); + }; +} + +function getElementWithinVideo(video, aValue) { + const shadowRoot = SpecialPowers.wrap(video).openOrClosedShadowRoot; + return shadowRoot.getElementById(aValue); +} + +function executeTests() { + return tests + .map(fn => () => new Promise(fn)) + .reduce((promise, task) => promise.then(task), Promise.resolve()); +} + +function once(target, name, cb) { + let p = new Promise(function(resolve, reject) { + target.addEventListener( + name, + function() { + resolve(); + }, + { once: true } + ); + }); + if (cb) { + p.then(cb); + } + return p; +} diff --git a/toolkit/content/tests/widgets/mochitest.ini b/toolkit/content/tests/widgets/mochitest.ini new file mode 100644 index 0000000000..985f96d04b --- /dev/null +++ b/toolkit/content/tests/widgets/mochitest.ini @@ -0,0 +1,55 @@ +[DEFAULT] +support-files = + audio.wav + audio.ogg + file_videocontrols_jsdisabled.html + seek_with_sound.ogg + video.ogg + head.js + tree_shared.js + videocontrols_direction-1-ref.html + videocontrols_direction-1a.html + videocontrols_direction-1b.html + videocontrols_direction-1c.html + videocontrols_direction-1d.html + videocontrols_direction-1e.html + videocontrols_direction-2-ref.html + videocontrols_direction-2a.html + videocontrols_direction-2b.html + videocontrols_direction-2c.html + videocontrols_direction-2d.html + videocontrols_direction-2e.html + videocontrols_direction_test.js + videomask.css + +[test_audiocontrols_dimensions.html] +[test_mousecapture_area.html] +skip-if = (verify && debug) +[test_ua_widget_sandbox.html] +[test_ua_widget_unbind.html] +[test_videocontrols.html] +tags = fullscreen +skip-if = toolkit == 'android' || (verify && debug && (os == 'linux')) || (webrender && (os == 'linux')) #TIMED_OUT #Bug 1484210 #Bug 1511256 +[test_videocontrols_keyhandler.html] +skip-if = (toolkit == 'android') || (os == 'linux') #Bug 1366957 +[test_videocontrols_vtt.html] +[test_videocontrols_iframe_fullscreen.html] +[test_videocontrols_size.html] +[test_videocontrols_audio.html] +[test_videocontrols_audio_direction.html] +skip-if = xorigin # Rendering of reftest videocontrols_direction-2a.html should not be different to the reference, fails/passes inconsistently +[test_videocontrols_jsdisabled.html] +skip-if = toolkit == 'android' # bug 1272646 +[test_videocontrols_standalone.html] +skip-if = toolkit == 'android' # bug 1075573 +[test_videocontrols_video_direction.html] +skip-if = + os == 'win' + xorigin # Rendering of reftest videocontrols_direction-2a.html should not be different to the reference, fails/passes inconsistently +[test_videocontrols_video_noaudio.html] +[test_bug898940.html] +skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536347 +[test_videocontrols_error.html] +[test_videocontrols_orientation.html] +skip-if = true # Bug 1483656 +[test_bug1654500.html] diff --git a/toolkit/content/tests/widgets/popup_shared.js b/toolkit/content/tests/widgets/popup_shared.js new file mode 100644 index 0000000000..e4cefb238d --- /dev/null +++ b/toolkit/content/tests/widgets/popup_shared.js @@ -0,0 +1,548 @@ +/* + * This script is used for menu and popup tests. Call startPopupTests to start + * the tests, passing an array of tests as an argument. Each test is an object + * with the following properties: + * testname - name of the test + * test - function to call to perform the test + * events - a list of events that are expected to be fired in sequence + * as a result of calling the 'test' function. This list should be + * an array of strings of the form "eventtype targetid" where + * 'eventtype' is the event type and 'targetid' is the id of + * target of the event. This function will be passed two + * arguments, the testname and the step argument. + * Alternatively, events may be a function which returns the array + * of events. This can be used when the events vary per platform. + * result - function to call after all the events have fired to check + * for additional results. May be null. This function will be + * passed two arguments, the testname and the step argument. + * steps - optional array of values. The test will be repeated for + * each step, passing each successive value within the array to + * the test and result functions + * autohide - if set, should be set to the id of a popup to hide after + * the test is complete. This is a convenience for some tests. + * condition - an optional function which, if it returns false, causes the + * test to be skipped. + * end - used for debugging. Set to true to stop the tests after running + * this one. + */ + +const menuactiveAttribute = "_moz-menuactive"; + +var gPopupTests = null; +var gTestIndex = -1; +var gTestStepIndex = 0; +var gTestEventIndex = 0; +var gAutoHide = false; +var gExpectedEventDetails = null; +var gExpectedTriggerNode = null; +var gWindowUtils; +var gPopupWidth = -1, + gPopupHeight = -1; + +function startPopupTests(tests) { + document.addEventListener("popupshowing", eventOccurred); + document.addEventListener("popupshown", eventOccurred); + document.addEventListener("popuphiding", eventOccurred); + document.addEventListener("popuphidden", eventOccurred); + document.addEventListener("command", eventOccurred); + document.addEventListener("DOMMenuItemActive", eventOccurred); + document.addEventListener("DOMMenuItemInactive", eventOccurred); + document.addEventListener("DOMMenuInactive", eventOccurred); + document.addEventListener("DOMMenuBarActive", eventOccurred); + document.addEventListener("DOMMenuBarInactive", eventOccurred); + + gPopupTests = tests; + gWindowUtils = SpecialPowers.getDOMWindowUtils(window); + + goNext(); +} + +if (!window.opener && window.arguments) { + window.opener = window.arguments[0]; +} + +function finish() { + if (window.opener) { + window.close(); + window.opener.SimpleTest.finish(); + return; + } + SimpleTest.finish(); +} + +function ok(condition, message) { + if (window.opener) { + window.opener.SimpleTest.ok(condition, message); + } else { + SimpleTest.ok(condition, message); + } +} + +function is(left, right, message) { + if (window.opener) { + window.opener.SimpleTest.is(left, right, message); + } else { + SimpleTest.is(left, right, message); + } +} + +function disableNonTestMouse(aDisable) { + gWindowUtils.disableNonTestMouseEvents(aDisable); +} + +function eventOccurred(event) { + if (gPopupTests.length <= gTestIndex) { + ok(false, "Extra " + event.type + " event fired"); + return; + } + + var test = gPopupTests[gTestIndex]; + if ("autohide" in test && gAutoHide) { + if (event.type == "DOMMenuInactive") { + gAutoHide = false; + setTimeout(goNextStep, 0); + } + return; + } + + var events = test.events; + if (typeof events == "function") { + events = events(); + } + if (events) { + if (events.length <= gTestEventIndex) { + ok( + false, + "Extra " + + event.type + + " event fired for " + + event.target.id + + " " + + gPopupTests[gTestIndex].testname + ); + return; + } + + var eventitem = events[gTestEventIndex].split(" "); + var matches; + if (eventitem[1] == "#tooltip") { + is( + event.originalTarget.localName, + "tooltip", + test.testname + " event.originalTarget.localName is 'tooltip'" + ); + is( + event.originalTarget.getAttribute("default"), + "true", + test.testname + " event.originalTarget default attribute is 'true'" + ); + matches = + event.originalTarget.localName == "tooltip" && + event.originalTarget.getAttribute("default") == "true"; + } else { + is( + event.type, + eventitem[0], + test.testname + " event type " + event.type + " fired" + ); + is( + event.target.id, + eventitem[1], + test.testname + " event target ID " + event.target.id + ); + matches = eventitem[0] == event.type && eventitem[1] == event.target.id; + } + + var modifiersMask = eventitem[2]; + if (modifiersMask) { + var m = ""; + m += event.altKey ? "1" : "0"; + m += event.ctrlKey ? "1" : "0"; + m += event.shiftKey ? "1" : "0"; + m += event.metaKey ? "1" : "0"; + is(m, modifiersMask, test.testname + " modifiers mask matches"); + } + + var expectedState; + switch (event.type) { + case "popupshowing": + expectedState = "showing"; + break; + case "popupshown": + expectedState = "open"; + break; + case "popuphiding": + expectedState = "hiding"; + break; + case "popuphidden": + expectedState = "closed"; + break; + } + + if (gExpectedTriggerNode && event.type == "popupshowing") { + if (gExpectedTriggerNode == "notset") { + // check against null instead + gExpectedTriggerNode = null; + } + + is( + event.originalTarget.triggerNode, + gExpectedTriggerNode, + test.testname + " popupshowing triggerNode" + ); + var isTooltip = event.target.localName == "tooltip"; + is( + document.popupNode, + isTooltip ? null : gExpectedTriggerNode, + test.testname + " popupshowing document.popupNode" + ); + is( + document.tooltipNode, + isTooltip ? gExpectedTriggerNode : null, + test.testname + " popupshowing document.tooltipNode" + ); + } + + if (expectedState) { + is( + event.originalTarget.state, + expectedState, + test.testname + " " + event.type + " state" + ); + } + + if (matches) { + gTestEventIndex++; + if (events.length <= gTestEventIndex) { + setTimeout(checkResult, 0); + } + } + } +} + +async function checkResult() { + var step = null; + var test = gPopupTests[gTestIndex]; + if ("steps" in test) { + step = test.steps[gTestStepIndex]; + } + + if ("result" in test) { + await test.result(test.testname, step); + } + + if ("autohide" in test) { + gAutoHide = true; + document.getElementById(test.autohide).hidePopup(); + return; + } + + goNextStep(); +} + +function goNextStep() { + gTestEventIndex = 0; + + var step = null; + var test = gPopupTests[gTestIndex]; + if ("steps" in test) { + gTestStepIndex++; + step = test.steps[gTestStepIndex]; + if (gTestStepIndex < test.steps.length) { + test.test(test.testname, step); + return; + } + } + + goNext(); +} + +function goNext() { + // We want to continue after the next animation frame so that + // we're in a stable state and don't get spurious mouse events at unexpected targets. + window.requestAnimationFrame(function() { + setTimeout(goNextStepSync, 0); + }); +} + +function goNextStepSync() { + if ( + gTestIndex >= 0 && + "end" in gPopupTests[gTestIndex] && + gPopupTests[gTestIndex].end + ) { + finish(); + return; + } + + gTestIndex++; + gTestStepIndex = 0; + if (gTestIndex < gPopupTests.length) { + var test = gPopupTests[gTestIndex]; + // Set the location hash so it's easy to see which test is running + document.location.hash = test.testname; + + // skip the test if the condition returns false + if ("condition" in test && !test.condition()) { + goNext(); + return; + } + + // start with the first step if there are any + var step = null; + if ("steps" in test) { + step = test.steps[gTestStepIndex]; + } + + test.test(test.testname, step); + + // no events to check for so just check the result + if (!("events" in test)) { + checkResult(); + } else if (typeof test.events == "function" && !test.events().length) { + checkResult(); + } + } else { + finish(); + } +} + +function openMenu(menu) { + if ("open" in menu) { + menu.open = true; + } else if (menu.hasMenu()) { + menu.openMenu(true); + } else { + synthesizeMouse(menu, 4, 4, {}); + } +} + +function closeMenu(menu, popup) { + if ("open" in menu) { + menu.open = false; + } else if (menu.hasMenu()) { + menu.openMenu(false); + } else { + popup.hidePopup(); + } +} + +function checkActive(popup, id, testname) { + var activeok = true; + var children = popup.childNodes; + for (var c = 0; c < children.length; c++) { + var child = children[c]; + if ( + (id == child.id && child.getAttribute(menuactiveAttribute) != "true") || + (id != child.id && child.hasAttribute(menuactiveAttribute) != "") + ) { + activeok = false; + break; + } + } + ok(activeok, testname + " item " + (id ? id : "none") + " active"); +} + +function checkOpen(menuid, testname) { + var menu = document.getElementById(menuid); + if ("open" in menu) { + ok(menu.open, testname + " " + menuid + " menu is open"); + } else if (menu.hasMenu()) { + ok( + menu.getAttribute("open") == "true", + testname + " " + menuid + " menu is open" + ); + } +} + +function checkClosed(menuid, testname) { + var menu = document.getElementById(menuid); + if ("open" in menu) { + ok(!menu.open, testname + " " + menuid + " menu is open"); + } else if (menu.hasMenu()) { + ok(!menu.hasAttribute("open"), testname + " " + menuid + " menu is closed"); + } +} + +function convertPosition(anchor, align) { + if (anchor == "topleft" && align == "topleft") { + return "overlap"; + } + if (anchor == "topleft" && align == "topright") { + return "start_before"; + } + if (anchor == "topleft" && align == "bottomleft") { + return "before_start"; + } + if (anchor == "topright" && align == "topleft") { + return "end_before"; + } + if (anchor == "topright" && align == "bottomright") { + return "before_end"; + } + if (anchor == "bottomleft" && align == "bottomright") { + return "start_after"; + } + if (anchor == "bottomleft" && align == "topleft") { + return "after_start"; + } + if (anchor == "bottomright" && align == "bottomleft") { + return "end_after"; + } + if (anchor == "bottomright" && align == "topright") { + return "after_end"; + } + return ""; +} + +/* + * When checking position of the bottom or right edge of the popup's rect, + * use this instead of strict equality check of rounded values, + * because we snap the top/left edges to pixel boundaries, + * which can shift the bottom/right up to 0.5px from its "ideal" location, + * and could cause it to round differently. (See bug 622507.) + */ +function isWithinHalfPixel(a, b) { + return Math.abs(a - b) <= 0.5; +} + +function compareEdge(anchor, popup, edge, offsetX, offsetY, testname) { + testname += " " + edge; + + checkOpen(anchor.id, testname); + + var anchorrect = anchor.getBoundingClientRect(); + var popuprect = popup.getBoundingClientRect(); + var check1 = false, + check2 = false; + + if (gPopupWidth == -1) { + ok( + Math.round(popuprect.right) - Math.round(popuprect.left) && + Math.round(popuprect.bottom) - Math.round(popuprect.top), + testname + " size" + ); + } else { + is(Math.round(popuprect.width), gPopupWidth, testname + " width"); + is(Math.round(popuprect.height), gPopupHeight, testname + " height"); + } + + var spaceIdx = edge.indexOf(" "); + if (spaceIdx > 0) { + let cornerX, cornerY; + let [position, align] = edge.split(" "); + switch (position) { + case "topleft": + cornerX = anchorrect.left; + cornerY = anchorrect.top; + break; + case "topcenter": + cornerX = anchorrect.left + anchorrect.width / 2; + cornerY = anchorrect.top; + break; + case "topright": + cornerX = anchorrect.right; + cornerY = anchorrect.top; + break; + case "leftcenter": + cornerX = anchorrect.left; + cornerY = anchorrect.top + anchorrect.height / 2; + break; + case "rightcenter": + cornerX = anchorrect.right; + cornerY = anchorrect.top + anchorrect.height / 2; + break; + case "bottomleft": + cornerX = anchorrect.left; + cornerY = anchorrect.bottom; + break; + case "bottomcenter": + cornerX = anchorrect.left + anchorrect.width / 2; + cornerY = anchorrect.bottom; + break; + case "bottomright": + cornerX = anchorrect.right; + cornerY = anchorrect.bottom; + break; + } + + switch (align) { + case "topleft": + cornerX += offsetX; + cornerY += offsetY; + break; + case "topright": + cornerX += -popuprect.width + offsetX; + cornerY += offsetY; + break; + case "bottomleft": + cornerX += offsetX; + cornerY += -popuprect.height + offsetY; + break; + case "bottomright": + cornerX += -popuprect.width + offsetX; + cornerY += -popuprect.height + offsetY; + break; + } + + is( + Math.round(popuprect.left), + Math.round(cornerX), + testname + " x position" + ); + is( + Math.round(popuprect.top), + Math.round(cornerY), + testname + " y position" + ); + return; + } + + if (edge == "after_pointer") { + is( + Math.round(popuprect.left), + Math.round(anchorrect.left) + offsetX, + testname + " x position" + ); + is( + Math.round(popuprect.top), + Math.round(anchorrect.top) + offsetY + 21, + testname + " y position" + ); + return; + } + + if (edge == "overlap") { + ok( + Math.round(anchorrect.left) + offsetY == Math.round(popuprect.left) && + Math.round(anchorrect.top) + offsetY == Math.round(popuprect.top), + testname + " position" + ); + return; + } + + if (edge.indexOf("before") == 0) { + check1 = isWithinHalfPixel(anchorrect.top + offsetY, popuprect.bottom); + } else if (edge.indexOf("after") == 0) { + check1 = + Math.round(anchorrect.bottom) + offsetY == Math.round(popuprect.top); + } else if (edge.indexOf("start") == 0) { + check1 = isWithinHalfPixel(anchorrect.left + offsetX, popuprect.right); + } else if (edge.indexOf("end") == 0) { + check1 = + Math.round(anchorrect.right) + offsetX == Math.round(popuprect.left); + } + + if (0 < edge.indexOf("before")) { + check2 = Math.round(anchorrect.top) + offsetY == Math.round(popuprect.top); + } else if (0 < edge.indexOf("after")) { + check2 = isWithinHalfPixel(anchorrect.bottom + offsetY, popuprect.bottom); + } else if (0 < edge.indexOf("start")) { + check2 = + Math.round(anchorrect.left) + offsetX == Math.round(popuprect.left); + } else if (0 < edge.indexOf("end")) { + check2 = isWithinHalfPixel(anchorrect.right + offsetX, popuprect.right); + } + + ok(check1 && check2, testname + " position"); +} diff --git a/toolkit/content/tests/widgets/seek_with_sound.ogg b/toolkit/content/tests/widgets/seek_with_sound.ogg Binary files differnew file mode 100644 index 0000000000..c86d9946bd --- /dev/null +++ b/toolkit/content/tests/widgets/seek_with_sound.ogg diff --git a/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html b/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html new file mode 100644 index 0000000000..2d29fe9be4 --- /dev/null +++ b/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Audio controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls preload="auto"></audio> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + const audio = document.getElementById("audio"); + const controlBar = getElementWithinVideo(audio, "controlBar"); + + add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + await new Promise(resolve => { + audio.addEventListener("loadedmetadata", resolve, {once: true}); + audio.src = "audio.wav"; + }); + }); + + add_task(async function check_audio_height() { + is(audio.clientHeight, 40, "checking height of audio element"); + }); + + add_task(async function check_controlbar_width() { + const originalControlBarWidth = controlBar.clientWidth; + + isnot(originalControlBarWidth, 400, "the default audio width is not 400px"); + + audio.style.width = "400px"; + audio.offsetWidth; // force reflow + + isnot(controlBar.clientWidth, originalControlBarWidth, "new width should differ from the origianl width"); + is(controlBar.clientWidth, 400, "controlbar's width should grow with audio width"); + }); + + add_task(function check_audio_height_construction_sync() { + let el = new Audio(); + el.src = "audio.wav"; + el.controls = true; + document.body.appendChild(el); + is(el.clientHeight, 40, "Height of audio element with controls"); + document.body.removeChild(el); + }); + + add_task(function check_audio_height_add_control_sync() { + let el = new Audio(); + el.src = "audio.wav"; + document.body.appendChild(el); + is(el.clientHeight, 0, "Height of audio element without controls"); + el.controls = true; + is(el.clientHeight, 40, "Height of audio element with controls"); + document.body.removeChild(el); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_bug1654500.html b/toolkit/content/tests/widgets/test_bug1654500.html new file mode 100644 index 0000000000..9bb05ba9c8 --- /dev/null +++ b/toolkit/content/tests/widgets/test_bug1654500.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Clear disabled/readonly datetime inputs</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<p>Disabled inputs should be able to be cleared just as they can be set with real values</p> +<input type="date" id="date1" value="2020-08-11" disabled> +<input type="date" id="date2" value="2020-08-11" readonly> +<input type="time" id="time1" value="07:01" disabled> +<input type="time" id="time2" value="07:01" readonly> +<script> +/* global date1, date2, time1, time2 */ +function querySelectorAllShadow(parent, selector) { + const shadowRoot = SpecialPowers.wrap(parent).openOrClosedShadowRoot; + return shadowRoot.querySelectorAll(selector); +} +date1.value = date2.value = ""; +time1.value = time2.value = ""; +for (const date of [date1, date2]) { + const fields = [...querySelectorAllShadow(date, ".datetime-edit-field")]; + is(fields.length, 3, "Three numeric fields are expected"); + for (const field of fields) { + is(field.getAttribute("value"), "", "All fields should be cleared"); + } +} +for (const time of [time1, time2]) { + const fields = [...querySelectorAllShadow(time, ".datetime-edit-field")]; + ok(fields.length >= 2, "At least two numeric fields are expected"); + for (const field of fields) { + is(field.getAttribute("value"), "", "All fields should be cleared"); + } +} +</script> diff --git a/toolkit/content/tests/widgets/test_bug898940.html b/toolkit/content/tests/widgets/test_bug898940.html new file mode 100644 index 0000000000..f2349c9a4c --- /dev/null +++ b/toolkit/content/tests/widgets/test_bug898940.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that an audio element that's already playing when controls are attached displays the controls</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls src="audio.ogg"></audio> +</div> + +<pre id="test"> +<script class="testbody"> + var audio = document.getElementById("audio"); + audio.play(); + audio.ontimeupdate = function doTest() { + ok(audio.getBoundingClientRect().height > 0, + "checking audio element height is greater than zero"); + audio.ontimeupdate = null; + SimpleTest.finish(); + }; + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml b/toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml new file mode 100644 index 0000000000..edc7dc54b1 --- /dev/null +++ b/toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Context menugroup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<menupopup id="context"> + <menugroup> + <menuitem id="a"/> + <menuitem id="b"/> + </menugroup> + <menuitem id="c" label="c"/> + <menugroup/> +</menupopup> + +<button label="Check"/> + +<vbox id="popuparea" popup="context" width="20" height="20"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gMenuPopup = $("context"); +ok(gMenuPopup, "Got the reference to the context menu"); + +var popupTests = [ +{ + testname: "one-down-key", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "popupshowing context", "popupshown context", "DOMMenuItemActive a" ], + test() { + synthesizeMouse($("popuparea"), 4, 4, {}); + synthesizeKey("KEY_ArrowDown"); + }, + result(testname) { + checkActive(gMenuPopup, "a", testname); + } +}, +{ + testname: "two-down-keys", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive a", "DOMMenuItemActive b" ], + test: () => synthesizeKey("KEY_ArrowDown"), + result(testname) { + checkActive(gMenuPopup, "b", testname); + } +}, +{ + testname: "three-down-keys", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive b", "DOMMenuItemActive c" ], + test: () => synthesizeKey("KEY_ArrowDown"), + result(testname) { + checkActive(gMenuPopup, "c", testname); + } +}, +{ + testname: "three-down-keys-one-up-key", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive c", "DOMMenuItemActive b" ], + test: () => synthesizeKey("KEY_ArrowUp"), + result (testname) { + checkActive(gMenuPopup, "b", testname); + } +}, +{ + testname: "three-down-keys-two-up-keys", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive b", "DOMMenuItemActive a" ], + test: () => synthesizeKey("KEY_ArrowUp"), + result(testname) { + checkActive(gMenuPopup, "a", testname); + } +}, +{ + testname: "three-down-keys-three-up-key", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive a", "DOMMenuItemActive c" ], + test: () => synthesizeKey("KEY_ArrowUp"), + result(testname) { + checkActive(gMenuPopup, "c", testname); + } +}, +]; + +SimpleTest.waitForFocus(function runTest() { + startPopupTests(popupTests); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/widgets/test_contextmenu_nested.xhtml b/toolkit/content/tests/widgets/test_contextmenu_nested.xhtml new file mode 100644 index 0000000000..84258a0dd6 --- /dev/null +++ b/toolkit/content/tests/widgets/test_contextmenu_nested.xhtml @@ -0,0 +1,138 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Nested Context Menu Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<menupopup id="outercontext"> + <menuitem label="Context One"/> + <menu id="outercontextmenu" label="Sub"> + <menupopup id="innercontext"> + <menuitem id="innercontextmenu" label="Sub Context One"/> + </menupopup> + </menu> +</menupopup> + +<menupopup id="outermain"> + <menuitem label="One"/> + <menu id="outermenu" label="Sub"> + <menupopup id="innermain"> + <menuitem id="innermenu" label="Sub One" context="outercontext"/> + </menupopup> + </menu> +</menupopup> + +<button label="Check"/> + +<vbox id="popuparea" popup="outermain" width="20" height="20"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var popupTests = [ +{ + testname: "open outer popup", + events: [ "popupshowing outermain", "popupshown outermain" ], + test: () => synthesizeMouse($("popuparea"), 4, 4, {}), + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname); + is(document.popupNode, $("popuparea"), testname + " document.popupNode"); + } +}, +{ + testname: "open inner popup", + events: [ "DOMMenuItemActive outermenu", "popupshowing innermain", "popupshown innermain" ], + test () { + synthesizeMouse($("outermenu"), 4, 4, { type: "mousemove" }); + synthesizeMouse($("outermenu"), 2, 2, { type: "mousemove" }); + }, + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is(document.popupNode, $("popuparea"), testname + " document.popupNode"); + } +}, +{ + testname: "open outer context", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "popupshowing outercontext", "popupshown outercontext" ], + test: () => synthesizeMouse($("innermenu"), 4, 4, { type: "contextmenu", button: 2 }), + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, $("innermenu"), testname + " outer context"); + is(document.popupNode, $("innermenu"), testname + " document.popupNode"); + } +}, +{ + testname: "open inner context", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemActive outercontextmenu", "popupshowing innercontext", "popupshown innercontext" ], + test () { + synthesizeMouse($("outercontextmenu"), 4, 4, { type: "mousemove" }); + setTimeout(function() { + synthesizeMouse($("outercontextmenu"), 2, 2, { type: "mousemove" }); + }, 1000); + }, + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, $("innermenu"), testname + " outer context"); + is($("innercontext").triggerNode, $("innermenu"), testname + " inner context"); + is(document.popupNode, $("innermenu"), testname + " document.popupNode"); + } +}, +{ + testname: "close context", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "popuphiding innercontext", "popuphidden innercontext", + "popuphiding outercontext", "popuphidden outercontext", + "DOMMenuInactive innercontext", + "DOMMenuItemInactive outercontextmenu", "DOMMenuItemInactive outercontextmenu", + "DOMMenuInactive outercontext" ], + test: () => $("outercontext").hidePopup(), + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is($("innercontext").triggerNode, null, testname + " inner context"); + is(document.popupNode, $("popuparea"), testname + " document.popupNode"); + } +}, +{ + testname: "hide menus", + events: [ "popuphiding innermain", "popuphidden innermain", + "popuphiding outermain", "popuphidden outermain", + "DOMMenuInactive innermain", + "DOMMenuItemInactive outermenu", "DOMMenuItemInactive outermenu", + "DOMMenuInactive outermain" ], + + test: () => $("outermain").hidePopup(), + result (testname) { + is($("outermain").triggerNode, null, testname + " outer"); + is($("innermain").triggerNode, null, testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is($("innercontext").triggerNode, null, testname + " inner context"); + is(document.popupNode, null, testname + " document.popupNode"); + } +} +]; + +SimpleTest.waitForFocus(function runTest() { + return startPopupTests(popupTests); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/widgets/test_editor_currentURI.xhtml b/toolkit/content/tests/widgets/test_editor_currentURI.xhtml new file mode 100644 index 0000000000..bbb1f33623 --- /dev/null +++ b/toolkit/content/tests/widgets/test_editor_currentURI.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Editor currentURI Tests" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + editortype="html" + style="width: 400px; height: 100px;"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + var editor = document.getElementById("editor"); + // Check that currentURI is a property of editor. + var result = "currentURI" in editor; + is(result, true, "currentURI is a property of editor"); + is(editor.currentURI.spec, "about:blank", "currentURI.spec is about:blank"); + SimpleTest.finish(); + } +]]> +</script> +</window> diff --git a/toolkit/content/tests/widgets/test_label_checkbox.xhtml b/toolkit/content/tests/widgets/test_label_checkbox.xhtml new file mode 100644 index 0000000000..87918b0278 --- /dev/null +++ b/toolkit/content/tests/widgets/test_label_checkbox.xhtml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Label Checkbox Tests" + onload="onLoad()" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <title>Label Checkbox Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + +SimpleTest.waitForExplicitFinish(); +function onLoad() +{ + runTest(); +} + +function runTest() +{ + window.open("window_label_checkbox.xhtml", "_blank", "width=600,height=600"); +} + +onmessage = function onMessage() +{ + SimpleTest.finish(); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_menubar.xhtml b/toolkit/content/tests/widgets/test_menubar.xhtml new file mode 100644 index 0000000000..16fae52b03 --- /dev/null +++ b/toolkit/content/tests/widgets/test_menubar.xhtml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menubar Popup Tests" + onload="setTimeout(onLoad, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <title>Menubar Popup Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function onLoad() +{ + window.open("window_menubar.xhtml", "_blank", "width=600,height=600"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_mousecapture_area.html b/toolkit/content/tests/widgets/test_mousecapture_area.html new file mode 100644 index 0000000000..b0c1a40add --- /dev/null +++ b/toolkit/content/tests/widgets/test_mousecapture_area.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Mouse capture on area elements tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <!-- The border="0" on the images is needed so that when we use + synthesizeMouse we don't accidentally target the border of the image and + miss the area because synthesizeMouse gets the rect of the primary frame + of the target (the area), which is the image due to bug 135040, which + includes the border, but the events targetted at the border aren't + targeted at the area. --> + + <!-- 20x20 of red --> + <img id="image" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#Map"/> + + <map name="Map"> + <!-- area over the whole image --> + <area id="area" onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + shape="poly" coords="0,0, 0,20, 20,20, 20,0" href="javascript:void(0);"/> + </map> + + + <!-- 20x20 of red --> + <img id="img1" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#sharedMap"/> + + <!-- 20x20 of red --> + <img id="img2" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#sharedMap"/> + + <map name="sharedMap"> + <!-- area over the whole image --> + <area id="sharedarea" onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + shape="poly" coords="0,0, 0,20, 20,20, 20,0" href="javascript:void(0);"/> + </map> + + + <div id="otherelement" style="width: 100px; height: 100px;"></div> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.expectAssertions(1); + +SimpleTest.waitForExplicitFinish(); + +function runTests() { + // XXX We send a useless click to each image to force it to setup its image + // map, because flushing layout won't do it. Hopefully bug 135040 will make + // this not suck. + synthesizeMouse($("image"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("image"), 5, 5, { type: "mouseup" }); + synthesizeMouse($("img1"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("img1"), 5, 5, { type: "mouseup" }); + synthesizeMouse($("img2"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("img2"), 5, 5, { type: "mouseup" }); + + + // test that setCapture works on an area element (bug 517737) + var area = document.getElementById("area"); + synthesizeMouse(area, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + area, "mousemove", "setCapture works on areas"); + synthesizeMouse(area, 5, 5, { type: "mouseup" }); + + // test that setCapture works on an area element when it is part of an image + // map that is used by two images + + var img1 = document.getElementById("img1"); + var sharedarea = document.getElementById("sharedarea"); + // synthesizeMouse just sends the event by coordinates, so this is really a click on the area + synthesizeMouse(img1, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + sharedarea, "mousemove", "setCapture works on areas with multiple images"); + synthesizeMouse(img1, 5, 5, { type: "mouseup" }); + + var img2 = document.getElementById("img2"); + // synthesizeMouse just sends the event by coordinates, so this is really a click on the area + synthesizeMouse(img2, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + sharedarea, "mousemove", "setCapture works on areas with multiple images"); + synthesizeMouse(img2, 5, 5, { type: "mouseup" }); + + // Bug 862673 - nuke all content so assertions in this test are attributed to + // this test rather than the one which happens to follow. + var content = document.getElementById("content"); + content.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_popupanchor.xhtml b/toolkit/content/tests/widgets/test_popupanchor.xhtml new file mode 100644 index 0000000000..c4f4596eca --- /dev/null +++ b/toolkit/content/tests/widgets/test_popupanchor.xhtml @@ -0,0 +1,474 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Anchor Tests" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <panel id="testPanel" + type="arrow" + animate="false" + noautohide="true"> + </panel> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +<![CDATA[ +var anchor, panel, arrow; + +function is_close(got, exp, msg) { + // on some platforms we see differences of a fraction of a pixel - so + // allow any difference of < 1 pixels as being OK. + ok(Math.abs(got - exp) < 1, msg + ": " + got + " should be equal(-ish) to " + exp); +} + +function isArrowPositionedOn(side, offset) { + var arrowRect = arrow.getBoundingClientRect(); + var arrowMidX = (arrowRect.left + arrowRect.right) / 2; + var arrowMidY = (arrowRect.top + arrowRect.bottom) / 2; + var panelRect = panel.getBoundingClientRect(); + var panelMidX = (panelRect.left + panelRect.right) / 2; + var panelMidY = (panelRect.top + panelRect.bottom) / 2; + // First check the "flip" of the panel is correct. If we are expecting the + // arrow to be pointing to the left side of the anchor, the arrow must + // also be on the left side of the panel (and vice-versa) + // XXX - on OSX, the arrow seems to always be exactly in the center, hence + // the 'equals' sign in the "<=" and ">=" comparisons. NFI why though... + switch (side) { + case "left": + ok(arrowMidX <= panelMidX, "arrow should be on the left of the panel"); + break; + case "right": + ok(arrowMidX >= panelMidX, "arrow should be on the right of the panel"); + break; + case "top": + ok(arrowMidY <= panelMidY, "arrow should be on the top of the panel"); + break; + case "bottom": + ok(arrowMidY >= panelMidY, "arrow should be on the bottom of the panel"); + break; + default: + ok(false, "invalid position " + side); + break; + } + // Now check the arrow really is pointing where we expect. The middle of + // the arrow should be pointing exactly to the left (or right) side of the + // anchor rect, +- any offsets. + if (offset === null) // special case - explicit 'null' means 'don't check offset' + return; + offset = offset || 0; // no param means no offset expected. + var anchorRect = anchor.getBoundingClientRect(); + var anchorPos = anchorRect[side]; + switch (side) { + case "left": + case "right": + is_close(arrowMidX - anchorPos, offset, "arrow should be " + offset + "px from " + side + " side of anchor"); + is_close(panelRect.top, anchorRect.bottom, "top of panel should be at bottom of anchor"); + break; + case "top": + case "bottom": + is_close(arrowMidY - anchorPos, offset, "arrow should be " + offset + "px from " + side + " side of anchor"); + is_close(panelRect.right, anchorRect.left, "right of panel should be left of anchor"); + break; + default: + ok(false, "unknown side " + side); + break; + } +} + +function openSlidingPopup(position, callback) { + panel.setAttribute("flip", "slide"); + _openPopup(position, callback); +} + +function openPopup(position, callback) { + panel.setAttribute("flip", "both"); + _openPopup(position, callback); +} + +async function waitForPopupPositioned(actionFn, callback) +{ + info("waitForPopupPositioned"); + let a = new Promise(resolve => { + panel.addEventListener("popuppositioned", () => resolve(true), { once: true }); + }); + + actionFn(); + + // Ensure we get at least one event, but we might get more than one, so wait for them as needed. + // + // The double-raf ensures layout runs once. setTimeout is needed because that's how popuppositioned is scheduled. + let b = new Promise(resolve => { + requestAnimationFrame(() => requestAnimationFrame(() => { + setTimeout(() => resolve(false), 0); + })); + }); + + let gotEvent = await Promise.race([a, b]); + info("got event: " + gotEvent); + if (gotEvent) { + waitForPopupPositioned(() => {}, callback); + } else { + SimpleTest.executeSoon(callback); + } +} + +function _openPopup(position, callback) { + // this is very ugly: the panel CSS sets the arrow's list-style-image based + // on the 'side' attribute. If the setting of the 'side' attribute causes + // the image to change, we may get the popupshown event before the new + // image has loaded - which causes the size of the arrow to be incorrect + // for a brief moment - right when we are measuring it! + // So we work around this in 2 steps: + // * Force the 'side' attribute to a value which causes the CSS to not + // specify an image - then when the popup gets shown, the correct image + // is set, causing a load() event on the image element. + // * Listen to *both* popupshown and the image load event. When both have + // fired (the order is indeterminate) we start the test. + panel.setAttribute("side", "noside"); + var numEvents = 0; + function onEvent() { + if (++numEvents == 2) // after both panel 'popupshown' and image 'load' + callback(); + }; + panel.addEventListener("popupshown", function popupshown() { + onEvent(); + }, {once: true}); + arrow.addEventListener("load", function imageload() { + onEvent(); + }, {once: true}); + panel.openPopup(anchor, position); +} + +var tests = [ + // A panel with the anchor after_end - the anchor should not move on resize + ['simpleResizeHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + isArrowPositionedOn("right"); + var origPanelRect = panel.getBoundingClientRect(); + panel.sizeTo(100, 100); + isArrowPositionedOn("right"); // should not have flipped, so still "right" + panel.sizeTo(origPanelRect.width, origPanelRect.height); + isArrowPositionedOn("right"); // should not have flipped, so still "right" + next(); + }); + }], + + ['simpleResizeVertical', 'middle', function(next) { + openPopup("start_after", function() { + isArrowPositionedOn("bottom"); + var origPanelRect = panel.getBoundingClientRect(); + panel.sizeTo(100, 100); + isArrowPositionedOn("bottom"); // should not have flipped + panel.sizeTo(origPanelRect.width, origPanelRect.height); + isArrowPositionedOn("bottom"); // should not have flipped + next(); + }); + }], + ['flippingResizeHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + isArrowPositionedOn("right"); + waitForPopupPositioned( + () => { panel.sizeTo(anchor.getBoundingClientRect().left + 50, 50); }, + () => { + isArrowPositionedOn("left"); // check it flipped and has zero offset. + next(); + }); + }); + }], + ['flippingResizeVertical', 'middle', function(next) { + openPopup("start_after", function() { + isArrowPositionedOn("bottom"); + waitForPopupPositioned( + () => { panel.sizeTo(50, anchor.getBoundingClientRect().top + 50); }, + () => { + isArrowPositionedOn("top"); // check it flipped and has zero offset. + next(); + }); + }); + }], + + ['simpleMoveToAnchorHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + isArrowPositionedOn("right"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "after_end", 20, 0); }, + () => { + // the anchor and the panel should have moved 20px right without flipping. + isArrowPositionedOn("right", 20); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "after_end", -20, 0); }, + () => { + // the anchor and the panel should have moved 20px left without flipping. + isArrowPositionedOn("right", -20); + next(); + }); + }); + }); + }], + + ['simpleMoveToAnchorVertical', 'middle', function(next) { + openPopup("start_after", function() { + isArrowPositionedOn("bottom"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "start_after", 0, 20); }, + () => { + // the anchor and the panel should have moved 20px down without flipping. + isArrowPositionedOn("bottom", 20); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "start_after", 0, -20) }, + () => { + // the anchor and the panel should have moved 20px up without flipping. + isArrowPositionedOn("bottom", -20); + next(); + }); + }); + }); + }], + + // Do a moveToAnchor that causes the panel to flip horizontally + ['flippingMoveToAnchorHorizontal', 'middle', function(next) { + var anchorRight = anchor.getBoundingClientRect().right; + // Size the panel such that it only just fits from the left-hand side of + // the window to the right of the anchor - thus, it will fit when + // anchored to the right-hand side of the anchor. + panel.sizeTo(anchorRight - 10, 100); + openPopup("after_end", function() { + isArrowPositionedOn("right"); + // Ask for it to be anchored 1/2 way between the left edge of the window + // and the anchor right - it can't fit with the panel on the left/arrow + // on the right, so it must flip (arrow on the left, panel on the right) + var offset = Math.floor(-anchorRight / 2); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "after_end", offset, 0), + () => { + isArrowPositionedOn("left", offset); // should have flipped and have the offset. + // resize back to original and move to a zero offset - it should flip back. + + panel.sizeTo(anchorRight - 10, 100); + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "after_end", 0, 0), + () => { + isArrowPositionedOn("right"); // should have flipped back and no offset + next(); + }); + }); + }); + }], + + // Do a moveToAnchor that causes the panel to flip vertically + ['flippingMoveToAnchorVertical', 'middle', function(next) { + var anchorBottom = anchor.getBoundingClientRect().bottom; + // See comments above in flippingMoveToAnchorHorizontal, but read + // "top/bottom" instead of "left/right" + panel.sizeTo(100, anchorBottom - 10); + openPopup("start_after", function() { + isArrowPositionedOn("bottom"); + var offset = Math.floor(-anchorBottom / 2); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "start_after", 0, offset), + () => { + isArrowPositionedOn("top", offset); + panel.sizeTo(100, anchorBottom - 10); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "start_after", 0, 0), + () => { + isArrowPositionedOn("bottom"); + next(); + }); + }); + }); + }], + + ['veryWidePanel-after_end', 'middle', function(next) { + openSlidingPopup("after_end", function() { + var origArrowRect = arrow.getBoundingClientRect(); + // Now move it such that the arrow can't be at either end of the panel but + // instead somewhere in the middle as that is the only way things fit, + // meaning the arrow should "slide" down the panel. + waitForPopupPositioned( + () => { panel.sizeTo(window.innerWidth - 10, 60); }, + () => { + is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested.") + // the arrow should not have moved. + var curArrowRect = arrow.getBoundingClientRect(); + is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved"); + is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down"); + next(); + }); + }); + }], + + ['veryWidePanel-before_start', 'middle', function(next) { + openSlidingPopup("before_start", function() { + var origArrowRect = arrow.getBoundingClientRect(); + // Now size it such that the arrow can't be at either end of the panel but + // instead somewhere in the middle as that is the only way things fit. + waitForPopupPositioned( + () => { panel.sizeTo(window.innerWidth - 10, 60); }, + () => { + is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested") + // the arrow should not have moved. + var curArrowRect = arrow.getBoundingClientRect(); + is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved"); + is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down"); + next(); + }); + }); + }], + + ['veryTallPanel-start_after', 'middle', function(next) { + openSlidingPopup("start_after", function() { + var origArrowRect = arrow.getBoundingClientRect(); + // Now move it such that the arrow can't be at either end of the panel but + // instead somewhere in the middle as that is the only way things fit, + // meaning the arrow should "slide" down the panel. + waitForPopupPositioned( + () => { panel.sizeTo(100, window.innerHeight - 10); }, + () => { + is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested.") + // the arrow should not have moved. + var curArrowRect = arrow.getBoundingClientRect(); + is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved"); + is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down"); + next(); + }); + }); + }], + + ['veryTallPanel-start_before', 'middle', function(next) { + openSlidingPopup("start_before", function() { + var origArrowRect = arrow.getBoundingClientRect(); + // Now size it such that the arrow can't be at either end of the panel but + // instead somewhere in the middle as that is the only way things fit. + waitForPopupPositioned( + () => { panel.sizeTo(100, window.innerHeight - 10); }, + () => { + is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested") + // the arrow should not have moved. + var curArrowRect = arrow.getBoundingClientRect(); + is_close(curArrowRect.left, origArrowRect.left, "arrow should not have moved"); + is_close(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down"); + next(); + }); + }); + }], + + // Tests against the anchor at the right-hand side of the window + ['afterend', 'right', function(next) { + openPopup("after_end", function() { + // when we request too far to the right/bottom, the panel gets shrunk + // and moved. The amount it is shrunk by is how far it is moved. + var panelRect = panel.getBoundingClientRect(); + // panel was requested 100px wide - calc offset based on actual width. + var offset = panelRect.width - 100; + isArrowPositionedOn("right", offset); + next(); + }); + }], + + ['after_start', 'right', function(next) { + openPopup("after_start", function() { + // See above - we are still too far to the right, but the anchor is + // on the other side. + var panelRect = panel.getBoundingClientRect(); + var offset = panelRect.width - 100; + isArrowPositionedOn("right", offset); + next(); + }); + }], + + // Tests against the anchor at the left-hand side of the window + ['after_start', 'left', function(next) { + openPopup("after_start", function() { + var panelRect = panel.getBoundingClientRect(); + is(panelRect.left, 0, "panel remains within the screen"); + // not sure how to determine the offset here, so given we have checked + // the panel is as left as possible while still being inside the window, + // we just don't check the offset. + isArrowPositionedOn("left", null); + next(); + }); + }], +] + +function runTests() { + function runNextTest() { + let result = tests.shift(); + if (!result) { + // out of tests + panel.hidePopup(); + SimpleTest.finish(); + return; + } + let [name, anchorPos, test] = result; + SimpleTest.info("sub-test " + anchorPos + "." + name + " starting"); + // first arrange for the anchor to be where the test requires it. + panel.hidePopup(); + panel.sizeTo(100, 50); + // hide all the anchors here, then later we make one of them visible. + document.getElementById("anchor-left-wrapper").style.display = "none"; + document.getElementById("anchor-middle-wrapper").style.display = "none"; + document.getElementById("anchor-right-wrapper").style.display = "none"; + switch(anchorPos) { + case 'middle': + anchor = document.getElementById("anchor-middle"); + document.getElementById("anchor-middle-wrapper").style.display = "block"; + break; + case 'left': + anchor = document.getElementById("anchor-left"); + document.getElementById("anchor-left-wrapper").style.display = "block"; + break; + case 'right': + anchor = document.getElementById("anchor-right"); + document.getElementById("anchor-right-wrapper").style.display = "block"; + break; + default: + SimpleTest.ok(false, "Bad anchorPos: " + anchorPos); + runNextTest(); + return; + } + try { + test(runNextTest); + } catch (ex) { + SimpleTest.ok(false, "sub-test " + anchorPos + "." + name + " failed: " + ex.toString() + "\n" + ex.stack); + runNextTest(); + } + } + runNextTest(); +} + +SimpleTest.waitForExplicitFinish(); + +addEventListener("load", function() { + // anchor is set by the test runner above + panel = document.getElementById("testPanel"); + + arrow = panel.shadowRoot.querySelector(".panel-arrow"); + runTests(); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<!-- Our tests assume at least 100px around the anchor on all sides, else the + panel may flip when we don't expect it to +--> +<div id="anchor-middle-wrapper" style="margin: 100px 100px 100px 100px;"> + <p>The anchor --> <span id="anchor-middle">v</span> <--</p> +</div> +<div id="anchor-left-wrapper" style="text-align: left; display: none;"> + <p><span id="anchor-left">v</span> <-- The anchor;</p> +</div> +<div id="anchor-right-wrapper" style="text-align: right; display: none;"> + <p>The anchor --> <span id="anchor-right">v</span></p> +</div> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_popupreflows.xhtml b/toolkit/content/tests/widgets/test_popupreflows.xhtml new file mode 100644 index 0000000000..30aff07d31 --- /dev/null +++ b/toolkit/content/tests/widgets/test_popupreflows.xhtml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Reflow Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <panel id="testPanel" + type="arrow" + noautohide="true"> + </panel> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +<![CDATA[ +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +let panel, anchor, arrow; + +// A reflow observer - it just remembers the stack trace of all sync reflows +// done by the panel. +let observer = { + reflows: [], + reflow (start, end) { + // Ignore reflows triggered by native code + // (Reflows from native code only have an empty stack after the first frame) + var path = (new Error().stack).split("\n").slice(1).join(""); + if (path === "") { + return; + } + + this.reflows.push(new Error().stack); + }, + + reflowInterruptible (start, end) { + // We're not interested in interruptible reflows. Why, you ask? Because + // we've simply cargo-culted this test from browser_tabopen_reflows.js! + }, + + QueryInterface: ChromeUtils.generateQI(["nsIReflowObserver", + "nsISupportsWeakReference"]) +}; + +// A test utility that counts the reflows caused by a test function. If the +// count of reflows isn't what is expected, it causes a test failure and logs +// the stack trace of all seen reflows. +function countReflows(testfn, expected) { + return new Promise(resolve => { + observer.reflows = []; + let docShell = panel.ownerGlobal.docShell; + docShell.addWeakReflowObserver(observer); + testfn().then(() => { + docShell.removeWeakReflowObserver(observer); + SimpleTest.is(observer.reflows.length, expected, "correct number of reflows"); + if (observer.reflows.length != expected) { + SimpleTest.info("stack traces of reflows:\n" + observer.reflows.join("\n") + "\n"); + } + resolve(); + }); + }); +} + +function openPopup() { + return new Promise(resolve => { + panel.addEventListener("popupshown", function popupshown() { + resolve(); + }, {once: true}); + panel.openPopup(anchor, "before_start"); + }); +} + +// ******************** +// The actual tests... +// We only have one atm - simply open a popup. +// +function testSimplePanel() { + return openPopup(); +} + +// ******************** +// The test harness... +// +SimpleTest.waitForExplicitFinish(); + +addEventListener("load", function() { + anchor = document.getElementById("anchor"); + panel = document.getElementById("testPanel"); + arrow = panel.shadowRoot.querySelector(".panel-arrow"); + + // Cancel the arrow panel slide-in transition (bug 767133) - we are only + // testing reflows in the core panel implementation and not reflows that may + // or may not be caused by transitioning.... + arrow.style.transition = "none"; + + // and off we go... + countReflows(testSimplePanel, 0).then(SimpleTest.finish); +}); +]]> +</script> +<body xmlns="http://www.w3.org/1999/xhtml"> + <p>The anchor --> <span id="anchor">v</span> <--</p> +</body> +</window> diff --git a/toolkit/content/tests/widgets/test_tree_column_reorder.xhtml b/toolkit/content/tests/widgets/test_tree_column_reorder.xhtml new file mode 100644 index 0000000000..d1dc9c1c20 --- /dev/null +++ b/toolkit/content/tests/widgets/test_tree_column_reorder.xhtml @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- + XUL Widget Test for reordering tree columns + --> +<window title="Tree" width="500" height="600" + onload="setTimeout(testtag_tree_column_reorder, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script src="tree_shared.js"/> + +<tree id="tree-column-reorder" rows="1" enableColumnDrag="true"> + <treecols> + <treecol id="col_0" label="col_0" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_1" label="col_1" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_2" label="col_2" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_3" label="col_3" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_4" label="col_4" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_5" label="col_5" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_6" label="col_6" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_7" label="col_7" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_8" label="col_8" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_9" label="col_9" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_10" label="col_10" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_11" label="col_11" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_12" label="col_12" flex="1"/> + </treecols> + <treechildren id="treechildren-column-reorder"> + <treeitem> + <treerow> + <treecell label="col_0"/> + <treecell label="col_1"/> + <treecell label="col_2"/> + <treecell label="col_3"/> + <treecell label="col_4"/> + <treecell label="col_5"/> + <treecell label="col_6"/> + <treecell label="col_7"/> + <treecell label="col_8"/> + <treecell label="col_9"/> + <treecell label="col_10"/> + <treecell label="col_11"/> + <treecell label="col_12"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/widgets/test_ua_widget_sandbox.html b/toolkit/content/tests/widgets/test_ua_widget_sandbox.html new file mode 100644 index 0000000000..134892619b --- /dev/null +++ b/toolkit/content/tests/widgets/test_ua_widget_sandbox.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>UA Widget sandbox test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const content = document.getElementById("content"); + +const div = content.appendChild(document.createElement("div")); +div.attachShadow({ mode: "open"}); +SpecialPowers.wrap(div.shadowRoot).setIsUAWidget(); + +const sandbox = SpecialPowers.Cu.getUAWidgetScope(SpecialPowers.wrap(div).nodePrincipal); + +SpecialPowers.setWrapped(sandbox, "info", SpecialPowers.wrapFor(info, sandbox)); +SpecialPowers.setWrapped(sandbox, "is", SpecialPowers.wrapFor(is, sandbox)); +SpecialPowers.setWrapped(sandbox, "ok", SpecialPowers.wrapFor(ok, sandbox)); + +const sandboxScript = function(shadowRoot) { + info("UA Widget scope tests"); + is(typeof window, "undefined", "The sandbox has no window"); + is(typeof document, "undefined", "The sandbox has no document"); + + let element = shadowRoot.host; + let doc = element.ownerDocument; + let win = doc.defaultView; + + ok(shadowRoot instanceof win.ShadowRoot, "shadowRoot is a ShadowRoot"); + ok(element instanceof win.HTMLDivElement, "Element is a <div>"); + + is("createElement" in doc, false, "No document.createElement"); + is("createElementNS" in doc, false, "No document.createElementNS"); + is("createTextNode" in doc, false, "No document.createTextNode"); + is("createComment" in doc, false, "No document.createComment"); + is("importNode" in doc, false, "No document.importNode"); + is("adoptNode" in doc, false, "No document.adoptNode"); + + is("insertBefore" in element, false, "No element.insertBefore"); + is("appendChild" in element, false, "No element.appendChild"); + is("replaceChild" in element, false, "No element.replaceChild"); + is("cloneNode" in element, false, "No element.cloneNode"); + + ok("importNodeAndAppendChildAt" in shadowRoot, "shadowRoot.importNodeAndAppendChildAt"); + ok("createElementAndAppendChildAt" in shadowRoot, "shadowRoot.createElementAndAppendChildAt"); + + info("UA Widget special methods tests"); + + const span = shadowRoot.createElementAndAppendChildAt(shadowRoot, "span"); + span.textContent = "Hello from <span>!"; + + is(shadowRoot.lastChild, span, "<span> inserted"); + + const parser = new win.DOMParser(); + let parserDoc = parser.parseFromString( + `<div xmlns="http://www.w3.org/1999/xhtml">Hello from DOMParser!</div>`, "application/xml"); + shadowRoot.importNodeAndAppendChildAt(shadowRoot, parserDoc.documentElement, true); + + ok(shadowRoot.lastChild instanceof win.HTMLDivElement, "<div> inserted"); + is(shadowRoot.lastChild.textContent, "Hello from DOMParser!", "Deep import node worked"); + + info("UA Widget reflectors tests"); + + win.wrappedJSObject.spanElementFromUAWidget = span; + win.wrappedJSObject.divElementFromUAWidget = shadowRoot.lastChild; +}; +SpecialPowers.Cu.evalInSandbox("this.script = " + sandboxScript.toString(), sandbox); +sandbox.script(div.shadowRoot); + +ok(window.spanElementFromUAWidget instanceof HTMLSpanElement, "<span> exposed"); +ok(window.divElementFromUAWidget instanceof HTMLDivElement, "<div> exposed"); + +try { + window.spanElementFromUAWidget.textContent; + ok(false, "Should throw."); +} catch (err) { + ok(/denied/.test(err), "Permission denied to access <span>"); +} + +try { + window.divElementFromUAWidget.textContent; + ok(false, "Should throw."); +} catch (err) { + ok(/denied/.test(err), "Permission denied to access <div>"); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_ua_widget_unbind.html b/toolkit/content/tests/widgets/test_ua_widget_unbind.html new file mode 100644 index 0000000000..89ae6c0f00 --- /dev/null +++ b/toolkit/content/tests/widgets/test_ua_widget_unbind.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>UA Widget unbind test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody"> + +const content = document.getElementById("content"); + +add_task(function() { + const video = document.createElement("video"); + video.controls = true; + ok(!SpecialPowers.wrap(video).openOrClosedShadowRoot, "UA Widget Shadow Root is not created"); + content.appendChild(video); + ok(!!SpecialPowers.wrap(video).openOrClosedShadowRoot, "UA Widget Shadow Root is created"); + ok(!!SpecialPowers.wrap(video).openOrClosedShadowRoot.firstChild, "Widget is constructed"); + content.removeChild(video); + ok(!SpecialPowers.wrap(video).openOrClosedShadowRoot, "UA Widget Shadow Root is removed"); +}); + +add_task(function() { + const marquee = document.createElement("marquee"); + ok(!SpecialPowers.wrap(marquee).openOrClosedShadowRoot, "UA Widget Shadow Root is not created"); + content.appendChild(marquee); + ok(!!SpecialPowers.wrap(marquee).openOrClosedShadowRoot, "UA Widget Shadow Root is created"); + ok(!!SpecialPowers.wrap(marquee).openOrClosedShadowRoot.firstChild, "Widget is constructed"); + content.removeChild(marquee); + ok(SpecialPowers.wrap(marquee).openOrClosedShadowRoot, "UA Widget Shadow Root is not removed for marquee"); +}); + +add_task(function() { + const input = document.createElement("input"); + input.type = "date"; + ok(!SpecialPowers.wrap(input).openOrClosedShadowRoot, "UA Widget Shadow Root is not created"); + content.appendChild(input); + ok(!!SpecialPowers.wrap(input).openOrClosedShadowRoot, "UA Widget Shadow Root is created"); + ok(!!SpecialPowers.wrap(input).openOrClosedShadowRoot.firstChild, "Widget is constructed"); + content.removeChild(input); + ok(!SpecialPowers.wrap(input).openOrClosedShadowRoot, "UA Widget Shadow Root is removed"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols.html b/toolkit/content/tests/widgets/test_videocontrols.html new file mode 100644 index 0000000000..40cb1c2a09 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols.html @@ -0,0 +1,451 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video width="320" height="240" id="video" controls mozNoDynamicControls preload="auto"></video> +</div> + +<div id="host"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/* + * Positions of the UI elements, relative to the upper-left corner of the + * <video> box. + */ +const videoWidth = 320; +const videoHeight = 240; +const videoDuration = 3.8329999446868896; + +const controlBarMargin = 9; + +const playButtonWidth = 30; +const playButtonHeight = 40; +const muteButtonWidth = 30; +const muteButtonHeight = 40; +const positionAndDurationWidth = 75; +const fullscreenButtonWidth = 30; +const fullscreenButtonHeight = 40; +const volumeSliderWidth = 48; +const volumeSliderMarginStart = 4; +const volumeSliderMarginEnd = 6; +const scrubberMargin = 9; +const scrubberWidth = videoWidth - controlBarMargin - playButtonWidth - scrubberMargin * 2 - positionAndDurationWidth - muteButtonWidth - volumeSliderMarginStart - volumeSliderWidth - volumeSliderMarginEnd - fullscreenButtonWidth - controlBarMargin; +const scrubberHeight = 40; + +// Play button is on the bottom-left +const playButtonCenterX = 0 + Math.round(playButtonWidth / 2); +const playButtonCenterY = videoHeight - Math.round(playButtonHeight / 2); +// Mute button is on the bottom-right before the full screen button and volume slider +const muteButtonCenterX = videoWidth - Math.round(muteButtonWidth / 2) - volumeSliderWidth - fullscreenButtonWidth - controlBarMargin; +const muteButtonCenterY = videoHeight - Math.round(muteButtonHeight / 2); +// Fullscreen button is on the bottom-right at the far end +const fullscreenButtonCenterX = videoWidth - Math.round(fullscreenButtonWidth / 2) - controlBarMargin; +const fullscreenButtonCenterY = videoHeight - Math.round(fullscreenButtonHeight / 2); +// Scrubber bar is between the play and mute buttons. We don't need it's +// X center, just the offset of its box. +const scrubberOffsetX = controlBarMargin + playButtonWidth + scrubberMargin; +const scrubberCenterY = videoHeight - Math.round(scrubberHeight / 2); + +const video = document.getElementById("video"); + +let expectingEvents; +let expectingEventPromise; + +async function isMuteButtonMuted() { + const muteButton = getElementWithinVideo(video, "muteButton"); + await new Promise(SimpleTest.executeSoon); + return muteButton.getAttribute("muted") === "true"; +} + +async function isVolumeSliderShowingCorrectVolume(expectedVolume) { + const volumeControl = getElementWithinVideo(video, "volumeControl"); + await new Promise(SimpleTest.executeSoon); + is(+volumeControl.value, expectedVolume * 100, "volume slider should match expected volume"); +} + +function forceReframe() { + // Setting display then getting the bounding rect to force a frame + // reconstruction on the video element. + video.style.display = "block"; + video.getBoundingClientRect(); + video.style.display = ""; + video.getBoundingClientRect(); +} + +function verifyExpectedEvent(event) { + const checkingEvent = expectingEvents.shift(); + + if (event.type == checkingEvent) { + ok(true, "checking event type: " + checkingEvent); + } else { + expectingEventPromise.reject(new Error(`Got event: ${event.type}, expected: ${checkingEvent}`)); + } + + if (!expectingEvents.length) { + SimpleTest.executeSoon(expectingEventPromise.resolve); + } +} + +async function waitForEvent(...eventTypes) { + expectingEvents = eventTypes; + + await new Promise((resolve, reject) => expectingEventPromise = {resolve, reject}).catch(e => { + // Throw error here to get the caller in error stack. + ok(false, e); + }); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + "set": [ + ["media.cache_size", 40000], + ["full-screen-api.enabled", true], + ["full-screen-api.allow-trusted-requests-only", false], + ["full-screen-api.transition-duration.enter", "0 0"], + ["full-screen-api.transition-duration.leave", "0 0"], + ]}); + await new Promise(resolve => { + video.addEventListener("canplaythrough", resolve, {once: true}); + video.src = "seek_with_sound.ogg"; + }); + + video.addEventListener("play", verifyExpectedEvent); + video.addEventListener("pause", verifyExpectedEvent); + video.addEventListener("volumechange", verifyExpectedEvent); + video.addEventListener("seeking", verifyExpectedEvent); + video.addEventListener("seeked", verifyExpectedEvent); + document.addEventListener("mozfullscreenchange", verifyExpectedEvent); + + ["mousedown", "mouseup", "dblclick", "click"] + .forEach((eventType) => { + window.addEventListener(eventType, (evt) => { + // Prevent default action of leaked events and make the tests fail. + evt.preventDefault(); + ok(false, "Event " + eventType + " in videocontrol should not leak to content;" + + "the event was dispatched from the " + evt.target.tagName.toLowerCase() + " element."); + }); + }); + + // Check initial state upon load + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +add_task(async function click_playbutton() { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("play"); + is(video.paused, false, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +add_task(async function click_pausebutton() { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("pause"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +add_task(async function mute_volume() { + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, true, "checking video mute state"); +}); + +add_task(async function unmute_volume() { + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +/* + * Bug 470596: Make sure that having CSS border or padding doesn't + * break the controls (though it should move them) + */ +add_task(async function styled_video() { + video.style.border = "medium solid purple"; + video.style.borderWidth = "30px 40px 50px 60px"; + video.style.padding = "10px 20px 30px 40px"; + // totals: top: 40px, right: 60px, bottom: 80px, left: 100px + + // Click the play button + synthesizeMouse(video, 100 + playButtonCenterX, 40 + playButtonCenterY, { }); + await waitForEvent("play"); + is(video.paused, false, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + // Pause the video + video.pause(); + await waitForEvent("pause"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + // Click the mute button + synthesizeMouse(video, 100 + muteButtonCenterX, 40 + muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, true, "checking video mute state"); + + // Clear the style set + video.style.border = ""; + video.style.borderWidth = ""; + video.style.padding = ""; + + // Unmute the video + video.muted = false; + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +/* + * Previous tests have moved playback postion, reset it to 0. + */ +add_task(async function reset_currentTime() { + ok(true, "video position is at " + video.currentTime); + video.currentTime = 0.0; + await waitForEvent("seeking", "seeked"); + // Bug 477434 -- sometimes we get 0.098999 here instead of 0! + // is(video.currentTime, 0.0, "checking playback position"); + ok(true, "video position is at " + video.currentTime); +}); + +/* + * Drag the slider's thumb to the halfway point with the mouse. + */ +add_task(async function drag_slider() { + const beginDragX = scrubberOffsetX; + const endDragX = scrubberOffsetX + (scrubberWidth / 2); + const expectedTime = videoDuration / 2; + + function mousemoved(evt) { + ok(false, "Mousemove event should not be handled by content while dragging; " + + "the event was dispatched from the " + evt.target.tagName.toLowerCase() + " element."); + } + + window.addEventListener("mousemove", mousemoved); + + synthesizeMouse(video, beginDragX, scrubberCenterY, {type: "mousedown", button: 0}); + synthesizeMouse(video, endDragX, scrubberCenterY, {type: "mousemove", button: 0}); + synthesizeMouse(video, endDragX, scrubberCenterY, {type: "mouseup", button: 0}); + await waitForEvent("seeking", "seeked"); + ok(true, "video position is at " + video.currentTime); + // The width of srubber is not equal in every platform as we use system default font + // in duration and position box. We can not precisely drag to expected position, so + // we just make sure the difference is within 10% of video duration. + ok(Math.abs(video.currentTime - expectedTime) < videoDuration / 10, "checking expected playback position"); + + window.removeEventListener("mousemove", mousemoved); +}); + +/* + * Click the slider at the 1/4 point with the mouse (jump backwards) + */ +add_task(async function click_slider() { + synthesizeMouse(video, scrubberOffsetX + (scrubberWidth / 4), scrubberCenterY, {}); + await waitForEvent("seeking", "seeked"); + ok(true, "video position is at " + video.currentTime); + // The scrubber currently just jumps towards the nearest pageIncrement point, not + // precisely to the point clicked. So, expectedTime isn't (videoDuration / 4). + // We should end up at 1.733, but sometimes we end up at 1.498. I guess + // it's timing depending if the <scale> things it's click-and-hold, or a + // single click. So, just make sure we end up less that the previous + // position. + const lastPosition = (videoDuration / 2) - 0.1; + ok(video.currentTime < lastPosition, "checking expected playback position"); + + // Set volume to 0.1 so one down arrow hit will decrease it to 0. + video.volume = 0.1; + await waitForEvent("volumechange"); + is(video.volume, 0.1, "Volume should be set."); + ok(!video.muted, "Video is not muted."); +}); + +// See bug 694696. +add_task(async function change_volume() { + video.focus(); + + synthesizeKey("KEY_ArrowDown"); + await waitForEvent("volumechange"); + is(video.volume, 0, "Volume should be 0."); + ok(!video.muted, "Video is not muted."); + ok(await isMuteButtonMuted(), "Mute button says it's muted"); + + synthesizeKey("KEY_ArrowUp"); + await waitForEvent("volumechange"); + is(video.volume, 0.1, "Volume is increased."); + ok(!video.muted, "Video is not muted."); + ok(!(await isMuteButtonMuted()), "Mute button says it's not muted"); + + synthesizeKey("KEY_ArrowDown"); + await waitForEvent("volumechange"); + is(video.volume, 0, "Volume should be 0."); + ok(!video.muted, "Video is not muted."); + ok(await isMuteButtonMuted(), "Mute button says it's muted"); + + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.volume, 0.5, "Volume should be 0.5."); + ok(!video.muted, "Video is not muted."); + + synthesizeKey("KEY_ArrowUp"); + await waitForEvent("volumechange"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(!video.muted, "Video is not muted."); + + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(video.muted, "Video is muted."); + ok(await isMuteButtonMuted(), "Mute button says it's muted"); + + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(!video.muted, "Video is not muted."); + ok(!(await isMuteButtonMuted()), "Mute button says it's not muted"); + + synthesizeMouse(video, fullscreenButtonCenterX, fullscreenButtonCenterY, {}); + await waitForEvent("mozfullscreenchange"); + is(video.volume, 0.6, "Volume should still be 0.6"); + await isVolumeSliderShowingCorrectVolume(video.volume); + + synthesizeKey("KEY_Escape"); + await waitForEvent("mozfullscreenchange"); + is(video.volume, 0.6, "Volume should still be 0.6"); + await isVolumeSliderShowingCorrectVolume(video.volume); + forceReframe(); + + video.focus(); + synthesizeKey("KEY_ArrowDown"); + await waitForEvent("volumechange"); + is(video.volume, 0.5, "Volume should be decreased by 0.1"); + await isVolumeSliderShowingCorrectVolume(video.volume); +}); + +add_task(async function whitespace_pause_video() { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("play"); + + video.focus(); + sendString(" "); + await waitForEvent("pause"); + + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("play"); +}); + +/* + * Bug 1352724: Click and hold on timeline should pause video immediately. + */ +add_task(async function click_and_hold_slider() { + synthesizeMouse(video, scrubberOffsetX + 10, scrubberCenterY, {type: "mousedown", button: 0}); + await waitForEvent("pause", "seeking", "seeked"); + + synthesizeMouse(video, scrubberOffsetX + 10, scrubberCenterY, {}); + await waitForEvent("play"); +}); + +/* + * Bug 1402877: Don't let click event dispatch through media controls to video element. + */ +add_task(async function click_event_dispatch() { + const clientScriptClickHandler = (e) => { + ok(false, "Should not receive the event"); + }; + video.addEventListener("click", clientScriptClickHandler); + + video.pause(); + await waitForEvent("pause"); + video.currentTime = 0.0; + await waitForEvent("seeking", "seeked"); + is(video.paused, true, "checking video play state"); + synthesizeMouse(video, scrubberOffsetX + 10, scrubberCenterY, {}); + await waitForEvent("seeking", "seeked"); + + video.removeEventListener("click", clientScriptClickHandler); +}); + +// Bug 1367194: Always ensure video is paused before finishing the test. +add_task(async function ensure_video_pause() { + if (!video.paused) { + video.pause(); + await waitForEvent("pause"); + } +}); + +// Bug 1452342: Make sure the cursor hides and shows on full screen mode. +add_task(async function ensure_fullscreen_cursor() { + video.removeAttribute("mozNoDynamicControls"); + video.play(); + await waitForEvent("play"); + + video.mozRequestFullScreen(); + await waitForEvent("mozfullscreenchange"); + + const controlsSpacer = getElementWithinVideo(video, "controlsSpacer"); + is(controlsSpacer.hasAttribute("hideCursor"), true, "Cursor is hidden"); + + let delta = 1; + await SimpleTest.promiseWaitForCondition(() => { + // Wiggle the mouse a bit + synthesizeMouse(video, playButtonCenterX + delta, playButtonCenterY + delta, { type: "mousemove" }); + delta = !delta; + return !controlsSpacer.hasAttribute("hideCursor"); + }, "Waiting for hideCursor attribute to disappear"); + is(controlsSpacer.hasAttribute("hideCursor"), false, "Cursor is shown"); + + // Restore + video.setAttribute("mozNoDynamicControls", ""); + document.mozCancelFullScreen(); + await waitForEvent("mozfullscreenchange"); + if (!video.paused) { + video.pause(); + await waitForEvent("pause"); + } +}); + +// Bug 1505547: Make sure the fullscreen button works if the video element is in shadow tree. +add_task(async function ensure_fullscreen_button() { + video.removeAttribute("mozNoDynamicControls"); + let host = document.getElementById("host"); + let root = host.attachShadow({ mode: "open" }); + root.appendChild(video); + + video.mozRequestFullScreen(); + await waitForEvent("mozfullscreenchange"); + + // Compute the location to click on to hit the fullscreen button. + // Use the video size instead of the screen size here, because mozfullscreenchange does not + // guarantee that our document covers the screen, see bug 1575630. + const r = video.getBoundingClientRect(); + const buttonCenterX = r.right - fullscreenButtonWidth / 2 - controlBarMargin; + const buttonCenterY = r.bottom - fullscreenButtonHeight / 2; + + // Wiggle the mouse a bit + synthesizeMouse(video, buttonCenterX, buttonCenterY, { type: "mousemove" }); + + synthesizeMouse(video, buttonCenterX, buttonCenterY, {}); + await waitForEvent("mozfullscreenchange"); + + // Restore + video.setAttribute("mozNoDynamicControls", ""); + document.getElementById("content").appendChild(video); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_audio.html b/toolkit/content/tests/widgets/test_videocontrols_audio.html new file mode 100644 index 0000000000..5c095df430 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_audio.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls with Audio file test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="metadata"></video> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> + + const InspectorUtils = SpecialPowers.InspectorUtils; + + function findElementByAttribute(element, aName, aValue) { + if (!("getAttribute" in element)) { + return false; + } + if (element.getAttribute(aName) === aValue) { + return element; + } + let children = + InspectorUtils.getChildrenForNode(element, true); + for (let child of children) { + var result = findElementByAttribute(child, aName, aValue); + if (result) { + return result; + } + } + return false; + } + + function loadedmetadata(event) { + SimpleTest.executeSoon(function() { + var controlBar = findElementByAttribute(video, "class", "controlBar"); + is(controlBar.getAttribute("fullscreen-unavailable"), "true", "Fullscreen button is hidden"); + SimpleTest.finish(); + }); + } + + var video = document.getElementById("video"); + + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startTest); + function startTest() { + // Kick off test once audio has loaded. + video.addEventListener("loadedmetadata", loadedmetadata); + video.src = "audio.ogg"; + } + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html b/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html new file mode 100644 index 0000000000..b656de0103 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls directionality test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var tests = [ + {op: "==", test: "videocontrols_direction-2a.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2b.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2c.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2d.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2e.html", ref: "videocontrols_direction-2-ref.html"}, +]; + +</script> +<script type="text/javascript" src="videocontrols_direction_test.js"></script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_error.html b/toolkit/content/tests/widgets/test_videocontrols_error.html new file mode 100644 index 0000000000..af90a4672a --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_error.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - Error</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + const video = document.getElementById("video"); + const statusOverlay = getElementWithinVideo(video, "statusOverlay"); + const statusIcon = getElementWithinVideo(video, "statusIcon"); + const statusLabelErrorNoSource = getElementWithinVideo(video, "errorNoSource"); + + add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + }); + + add_task(async function check_normal_status() { + await new Promise(resolve => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", () => SimpleTest.executeSoon(resolve)); + }); + + // Wait for the fade out transition to complete in case the throbber + // shows up on slower platforms. + await SimpleTest.promiseWaitForCondition(() => statusOverlay.hidden, + "statusOverlay should not present without error"); + + ok(!statusOverlay.hasAttribute("status"), "statusOverlay should not be showing a state message."); + isnot(statusIcon.getAttribute("type"), "error", "should not show error icon"); + }); + + add_task(async function invalid_source() { + const errorType = "errorNoSource"; + + await new Promise(resolve => { + video.src = "invalid_source.ogg"; + video.addEventListener("error", () => SimpleTest.executeSoon(resolve)); + }); + + ok(!statusOverlay.hidden, `statusOverlay should show when ${errorType}`); + is(statusOverlay.getAttribute("status"), errorType, `statusOverlay should have correct error state: ${errorType}`); + is(statusIcon.getAttribute("type"), "error", `should show error icon when ${errorType}`); + isnot(statusLabelErrorNoSource.getBoundingClientRect().height, 0, + "errorNoSource status label should be visible."); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_focus.html b/toolkit/content/tests/widgets/test_videocontrols_focus.html new file mode 100644 index 0000000000..71282a4d08 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_focus.html @@ -0,0 +1,111 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Video controls test - Focus</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + +let video, controlBar, playButton; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [ + ["media.cache_size", 40000], + ["media.videocontrols.keyboard-tab-to-all-controls", true], + ]}); + + // We must create the video after the keyboard-tab-to-all-controls pref is + // set. Otherwise, the tabindex won't be set correctly. + video = document.createElement("video"); + video.id = "video"; + video.controls = true; + video.preload = "auto"; + video.loop = true; + video.src = "video.ogg"; + const caption = video.addTextTrack("captions", "English", "en"); + caption.mode = "showing"; + const content = document.getElementById("content"); + content.append(video); + controlBar = getElementWithinVideo(video, "controlBar"); + playButton = getElementWithinVideo(video, "playButton"); + info("Waiting for video to load"); + // We must wait for loadeddata here, not loadedmetadata, as the first frame + // isn't shown until loadeddata occurs and the controls won't hide until the + // first frame is shown. + await BrowserTestUtils.waitForEvent(video, "loadeddata"); + + // Play and mouseout to hide the controls. + info("Playing video"); + const playing = BrowserTestUtils.waitForEvent(video, "play"); + video.play(); + await playing; + // controlBar.hidden returns true while the animation is happening. We use + // the controlbarchange event to know when it's fully hidden. Aside from + // avoiding waitForCondition, this is necessary to avoid racing with the + // animation. + const hidden = BrowserTestUtils.waitForEvent(video, "controlbarchange"); + sendMouseEvent({type: "mouseout"}, controlBar); + info("Waiting for controls to hide"); + await hidden; +}); + +add_task(async function testShowControlsOnFocus() { + ok(controlBar.hidden, "Controls initially hidden"); + const shown = BrowserTestUtils.waitForEvent(video, "controlbarchange"); + info("Focusing play button"); + playButton.focus(); + await shown; + ok(!controlBar.hidden, "Controls shown after focus"); + await BrowserTestUtils.waitForEvent(video, "controlbarchange"); + ok(controlBar.hidden, "Controls hidden after timeout"); +}); + +add_task(async function testCcMenuStaysVisible() { + ok(controlBar.hidden, "Controls initially hidden"); + const shown = BrowserTestUtils.waitForEvent(video, "controlbarchange"); + info("Focusing CC button"); + const ccButton = getElementWithinVideo(video, "closedCaptionButton"); + ccButton.focus(); + await shown; + ok(!controlBar.hidden, "Controls shown after focus"); + // Checking this using an implementation detail is ugly, but there's no other + // way to do it without fragile timing. + const { widget } = window.windowGlobalChild.getActor("UAWidgets").widgets.get( + video); + ok(widget.impl.Utils._hideControlsTimeout, "Hide timeout set"); + const ttList = getElementWithinVideo(video, "textTrackListContainer"); + ok(ttList.hidden, "Text track list initially hidden"); + + synthesizeKey(" "); + ok(!ttList.hidden, "Text track list shown after space"); + ok( + !widget.impl.Utils._hideControlsTimeout, + "Hide timeout cleared (controls won't hide)" + ); + const ccOff = ttList.querySelector("button"); + ccOff.focus(); + synthesizeKey(" "); + ok(ttList.hidden, "Text track list hidden after activating Off button"); + ok(!controlBar.hidden, "Controls still shown"); + ok(widget.impl.Utils._hideControlsTimeout, "Hide timeout set"); + + await BrowserTestUtils.waitForEvent(video, "controlbarchange"); + ok(controlBar.hidden, "Controls hidden after timeout"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html new file mode 100644 index 0000000000..0a74b25609 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - iframe</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +<iframe id="ifr1"></iframe> +<iframe id="ifr2" allowfullscreen></iframe> +<iframe id="ifr1" allow="fullscreen 'none'"></iframe> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const testCases = []; + + function checkIframeFullscreenAvailable(ifr) { + let video; + + return () => new Promise(resolve => { + ifr.srcdoc = `<video id="video" controls preload="auto"></video>`; + ifr.addEventListener("load", resolve); + }).then(() => new Promise(resolve => { + video = ifr.contentDocument.getElementById("video"); + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", resolve); + })).then(() => new Promise(resolve => { + const available = video.ownerDocument.fullscreenEnabled; + const controlBar = getElementWithinVideo(video, "controlBar"); + + is(controlBar.getAttribute("fullscreen-unavailable") == "true", !available, "The controlbar should have an attribute marking whether fullscreen is available that corresponds to if the iframe has the allowfullscreen attribute."); + resolve(); + })); + } + + function start() { + testCases.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function load() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, start); + } + + for (let iframe of document.querySelectorAll("iframe")) + testCases.push(checkIframeFullscreenAvailable(iframe)); + testCases.push(SimpleTest.finish); + + window.addEventListener("load", load); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html new file mode 100644 index 0000000000..f3fdecc47f --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function runTest(event) { + info(true, "----- test #" + testnum + " -----"); + + switch (testnum) { + case 1: + is(event.type, "timeupdate", "checking event type"); + is(video.paused, false, "checking video play state"); + video.removeEventListener("timeupdate", runTest); + + // Click to toggle play/pause (now pausing) + synthesizeMouseAtCenter(video, {}, win); + break; + + case 2: + is(event.type, "pause", "checking event type"); + is(video.paused, true, "checking video play state"); + win.close(); + + SimpleTest.finish(); + break; + + default: + ok(false, "unexpected test #" + testnum + " w/ event " + event.type); + throw new Error(`unexpected test #${testnum} w/ event ${event.type}`); + } + + testnum++; +} + +SpecialPowers.pushPrefEnv({"set": [["javascript.enabled", false]]}, startTest); + +var testnum = 1; + +var video; +function loadevent(event) { + is(win.testExpando, undefined, "expando shouldn't exist because js is disabled"); + video = win.document.querySelector("video"); + // Other events expected by the test. + video.addEventListener("timeupdate", runTest); + video.addEventListener("pause", runTest); +} + +var win; +function startTest() { + const TEST_FILE = location.href.replace("test_videocontrols_jsdisabled.html", + "file_videocontrols_jsdisabled.html"); + win = window.open(TEST_FILE); + win.addEventListener("load", loadevent); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html b/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html new file mode 100644 index 0000000000..5b771fc745 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html @@ -0,0 +1,150 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - KeyHandler</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + const video = document.getElementById("video"); + + const playButton = getElementWithinVideo(video, "playButton"); + const scrubber = getElementWithinVideo(video, "scrubber"); + const volumeControl = getElementWithinVideo(video, "volumeControl"); + const muteButton = getElementWithinVideo(video, "muteButton"); + + // Setup video + tests.push(done => { + SpecialPowers.pushPrefEnv({"set": [ + ["media.cache_size", 40000], + ["media.videocontrols.keyboard-tab-to-all-controls", true], + ]}, done); + }, done => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", done); + }); + + // Bug 1350191, video should not seek while changing volume by + // pressing up/down arrow key after clicking the scrubber. + tests.push(done => { + video.addEventListener("play", done, { once: true }); + synthesizeMouseAtCenter(playButton, {}); + }, done => { + video.addEventListener("seeked", done, { once: true }); + synthesizeMouseAtCenter(scrubber, {}); + }, done => { + let counter = 0; + let keys = ["KEY_ArrowDown", "KEY_ArrowDown", "KEY_ArrowUp", "KEY_ArrowDown", "KEY_ArrowUp", "KEY_ArrowUp"]; + + const onSeeked = () => ok(false, "should not trigger seeked event"); + video.addEventListener("seeked", onSeeked); + const onVolumeChange = () => { + if (++counter === keys.length) { + ok(true, "change volume by up/down arrow key without trigger 'seeked' event"); + video.removeEventListener("seeked", onSeeked); + video.removeEventListener("volumechange", onVolumeChange); + done(); + } + + if (counter > keys.length) { + ok(false, "trigger too much volumechange events"); + } + }; + video.addEventListener("volumechange", onVolumeChange); + + for (let key of keys) { + synthesizeKey(key); + } + }); + + // However, if the scrubber is *focused* (e.g. by keyboard), it should handle + // up/down arrow keys. + tests.push(done => { + info("Focusing the scrubber"); + scrubber.focus(); + video.addEventListener("seeked", () => { + ok(true, "DownArrow seeked the video"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowDown"); + }, done => { + video.addEventListener("seeked", () => { + ok(true, "UpArrow seeked the video"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowUp"); + }); + + // Similarly, if the volume control is focused, left/right arrows should + // adjust the volume. + tests.push(done => { + info("Focusing the volume control"); + volumeControl.focus(); + video.addEventListener("volumechange", () => { + ok(true, "LeftArrow changed the volume"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowLeft"); + }, done => { + video.addEventListener("volumechange", () => { + ok(true, "RightArrow changed the volume"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowRight"); + }); + + // If something other than a button has focus, space should pause/play. + tests.push(done => { + ok(volumeControl.matches(":focus"), "Volume control still has focus"); + video.addEventListener("pause", () => { + ok(true, "Space paused the video"); + done(); + }, {once: true}); + synthesizeKey(" "); + }, done => { + video.addEventListener("play", () => { + ok(true, "Space played the video"); + done(); + }, {once: true}); + synthesizeKey(" "); + }); + + // If a button has focus, space should activate it, *not* pause/play. + tests.push(done => { + info("Focusing the mute button"); + muteButton.focus(); + const onPause = () => ok(false, "Shouldn't pause the video"); + video.addEventListener("pause", onPause); + let volChanges = 0; + const onVolChange = () => { + if (++volChanges == 2) { + ok(true, "Space twice muted then unmuted the video"); + video.removeEventListener("pause", onPause); + video.removeEventListener("volumechange", onVolChange); + done(); + } + }; + video.addEventListener("volumechange", onVolChange); + // Press space twice. The first time should mute, the second should unmute. + synthesizeKey(" "); + synthesizeKey(" "); + }); + + tests.push(SimpleTest.finish); + + window.addEventListener("load", executeTests); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html new file mode 100644 index 0000000000..9023512ab7 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls mozNoDynamicControls preload="auto"></video> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +var video = document.getElementById("video"); + +function startMediaLoad() { + // Kick off test once video has loaded, in its canplaythrough event handler. + video.src = "seek_with_sound.ogg"; + video.addEventListener("canplaythrough", runTest); +} + +function loadevent(event) { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startMediaLoad); +} + +window.addEventListener("load", loadevent); + +function runTest() { + video.addEventListener("click", function() { + this.play(); + }); + ok(video.paused, "video should be paused initially"); + + new Promise(resolve => { + let timeupdates = 0; + video.addEventListener("timeupdate", function timeupdate() { + ok(!video.paused, "video should not get paused after clicking in middle"); + + if (++timeupdates == 3) { + video.removeEventListener("timeupdate", timeupdate); + resolve(); + } + }); + + synthesizeMouseAtCenter(video, {}, window); + }).then(function() { + new Promise(resolve => { + video.addEventListener("pause", function onpause() { + setTimeout(() => { + ok(video.paused, "video should still be paused 200ms after pause request"); + // When the video reaches the end of playback it is automatically paused. + // Check during the pause event that the video has not reachd the end + // of playback. + ok(!video.ended, "video should not have paused due to playback ending"); + resolve(); + }, 200); + }); + + synthesizeMouse(video, 10, video.clientHeight - 10, {}, window); + }).then(SimpleTest.finish); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_orientation.html b/toolkit/content/tests/widgets/test_videocontrols_orientation.html new file mode 100644 index 0000000000..26b2144e14 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_orientation.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls mozNoDynamicControls preload="metadata"></video> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +var video = document.getElementById("video"); + +let onLoaded = event => { + SpecialPowers.pushPrefEnv( + {"set": [["full-screen-api.allow-trusted-requests-only", false], + ["media.videocontrols.lock-video-orientation", true]]}, + startMediaLoad); +}; +window.addEventListener("load", onLoaded); + +let startMediaLoad = () => { + // Kick off test once video has loaded, in its canplaythrough event handler. + video.src = "video.ogg"; + video.addEventListener("canplaythrough", runTest); +}; + +function runTest() { + is(document.mozFullScreenElement, null, "should not be in fullscreen initially"); + isnot(window.screen.orientation.type, "landscape-primary", "should not be in landscape"); + isnot(window.screen.orientation.type, "landscape-secondary", "should not be in landscape"); + + let originalOnChange = window.screen.orientation.onchange; + + window.screen.orientation.onchange = () => { + is(document.mozFullScreenElement, video, "should be in fullscreen"); + ok(window.screen.orientation.type === "landscape-primary" || + window.screen.orientation.type === "landscape-secondary", "should be in landscape"); + + window.screen.orientation.onchange = () => { + window.screen.orientation.onchange = originalOnChange; + isnot(window.screen.orientation.type, "landscape-primary", "should not be in landscape"); + isnot(window.screen.orientation.type, "landscape-secondary", "should not be in landscape"); + SimpleTest.finish(); + }; + document.mozCancelFullScreen(); + }; + + video.mozRequestFullScreen(); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_size.html b/toolkit/content/tests/widgets/test_videocontrols_size.html new file mode 100644 index 0000000000..559cc66e86 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_size.html @@ -0,0 +1,179 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - Size</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video controls preload="auto" width="480" height="320"></video> + <video controls preload="auto" width="320" height="320"></video> + <video controls preload="auto" width="280" height="320"></video> + <video controls preload="auto" width="240" height="320"></video> + <video controls preload="auto" width="180" height="320"></video> + <video controls preload="auto" width="120" height="320"></video> + <video controls preload="auto" width="60" height="320"></video> + <video controls preload="auto" width="48" height="320"></video> + <video controls preload="auto" width="20" height="320"></video> + + <video controls preload="auto" width="480" height="240"></video> + <video controls preload="auto" width="480" height="120"></video> + <video controls preload="auto" width="480" height="39"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const videoElems = [...document.getElementsByTagName("video")]; + const testCases = []; + + const isTouchControl = navigator.appVersion.includes("Android"); + + const buttonWidth = isTouchControl ? 40 : 30; + const minSrubberWidth = isTouchControl ? 64 : 48; + const minControlBarHeight = isTouchControl ? 52 : 40; + const minControlBarWidth = isTouchControl ? 58 : 48; + const minClickToPlaySize = isTouchControl ? 64 : 48; + + function getElementName(elem) { + return elem.getAttribute("anonid") || elem.getAttribute("class"); + } + + function testButton(btn) { + if (btn.hidden) return; + + const rect = btn.getBoundingClientRect(); + const name = getElementName(btn); + + is(rect.width, buttonWidth, `${name} should have correct width`); + is(rect.height, minControlBarHeight, `${name} should have correct height`); + } + + function testScrubber(scrubber) { + if (scrubber.hidden) return; + + const rect = scrubber.getBoundingClientRect(); + const name = getElementName(scrubber); + + ok(rect.width >= minSrubberWidth, `${name} should longer than ${minSrubberWidth}`); + } + + function testUI(video) { + video.style.display = "block"; + video.getBoundingClientRect(); + video.style.display = ""; + + const videoRect = video.getBoundingClientRect(); + + const videoHeight = video.clientHeight; + const videoWidth = video.clientWidth; + + const videoSizeMsg = `size:${videoRect.width}x${videoRect.height} -`; + const controlBar = getElementWithinVideo(video, "controlBar"); + const playBtn = getElementWithinVideo(video, "playButton"); + const scrubber = getElementWithinVideo(video, "scrubberStack"); + const positionDurationBox = getElementWithinVideo(video, "positionDurationBox"); + const durationLabel = positionDurationBox.getElementsByTagName("span")[0]; + const muteBtn = getElementWithinVideo(video, "muteButton"); + const volumeStack = getElementWithinVideo(video, "volumeStack"); + const fullscreenBtn = getElementWithinVideo(video, "fullscreenButton"); + const clickToPlay = getElementWithinVideo(video, "clickToPlay"); + + + // Controls should show/hide according to the priority + const prioritizedControls = [ + playBtn, + muteBtn, + fullscreenBtn, + positionDurationBox, + scrubber, + durationLabel, + volumeStack, + ]; + + let stopAppend = false; + prioritizedControls.forEach(control => { + is(control.hidden, stopAppend = stopAppend || control.hidden, + `${videoSizeMsg} ${getElementName(control)} should ${stopAppend ? "hide" : "show"}`); + }); + + + // All controls should fit in control bar container + const controls = [ + playBtn, + scrubber, + positionDurationBox, + muteBtn, + volumeStack, + fullscreenBtn, + ]; + + let widthSum = 0; + controls.forEach(control => { + widthSum += control.clientWidth; + }); + ok(videoWidth >= widthSum, + `${videoSizeMsg} controlBar fit in video's width`); + + + // Control bar should show/hide according to video's dimensions + const shouldHideControlBar = videoHeight <= minControlBarHeight || + videoWidth < minControlBarWidth; + is(controlBar.hidden, shouldHideControlBar, `${videoSizeMsg} controlBar show/hide`); + + if (!shouldHideControlBar) { + is(controlBar.clientWidth, videoWidth, `control bar width should equal to video width`); + + // Check all controls' dimensions + testButton(playBtn); + testButton(muteBtn); + testButton(fullscreenBtn); + testScrubber(scrubber); + testScrubber(volumeStack); + } + + + // ClickToPlay button should show if min size can fit in + const shouldHideClickToPlay = videoWidth <= minClickToPlaySize || + (videoHeight - minClickToPlaySize) / 2 <= minControlBarHeight; + is(clickToPlay.hidden, shouldHideClickToPlay, `${videoSizeMsg} clickToPlay show/hide`); + } + + + testCases.push(() => Promise.all(videoElems.map(video => new Promise(resolve => { + video.addEventListener("loadedmetadata", resolve); + video.src = "seek_with_sound.ogg"; + })))); + + videoElems.forEach(video => { + testCases.push(() => new Promise(resolve => { + SimpleTest.executeSoon(() => { + testUI(video); + resolve(); + }); + })); + }); + + function executeTasks(tasks) { + return tasks.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function start() { + executeTasks(testCases).then(SimpleTest.finish); + } + + function loadevent() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, start); + } + + window.addEventListener("load", loadevent); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_standalone.html b/toolkit/content/tests/widgets/test_videocontrols_standalone.html new file mode 100644 index 0000000000..8615f0606c --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_standalone.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + SimpleTest.expectAssertions(0, 1); + +const videoWidth = 320; +const videoHeight = 240; + +function getMediaElement(aWindow) { + return aWindow.document.getElementsByTagName("video")[0]; +} + +var popup = window.open("seek_with_sound.ogg"); +popup.addEventListener("load", function() { + var video = getMediaElement(popup); + + is(popup.document.activeElement, video, "Document should load with focus moved to the video element."); + + if (!video.paused) { + runTestVideo(video); + } else { + video.addEventListener("play", function() { + runTestVideo(video); + }, {once: true}); + } +}, {once: true}); + +function runTestVideo(aVideo) { + var condition = function() { + var boundingRect = aVideo.getBoundingClientRect(); + return boundingRect.width == videoWidth && + boundingRect.height == videoHeight; + }; + waitForCondition(condition, function() { + var boundingRect = aVideo.getBoundingClientRect(); + is(boundingRect.width, videoWidth, "Width of the video should match expectation"); + is(boundingRect.height, videoHeight, "Height of video should match expectation"); + popup.close(); + runTestAudioPre(); + }, "The media element should eventually be resized to match the intrinsic size of the video."); +} + +function runTestAudioPre() { + popup = window.open("audio.ogg"); + popup.addEventListener("load", function() { + var audio = getMediaElement(popup); + + is(popup.document.activeElement, audio, "Document should load with focus moved to the video element."); + + if (!audio.paused) { + runTestAudio(audio); + } else { + audio.addEventListener("play", function() { + runTestAudio(audio); + }, {once: true}); + } + }, {once: true}); +} + +function runTestAudio(aAudio) { + info("User agent (help diagnose bug #943556): " + navigator.userAgent); + var isAndroid = navigator.userAgent.includes("Android"); + var expectedHeight = isAndroid ? 103 : 40; + var condition = function() { + var boundingRect = aAudio.getBoundingClientRect(); + return boundingRect.height == expectedHeight; + }; + waitForCondition(condition, function() { + var boundingRect = aAudio.getBoundingClientRect(); + is(boundingRect.height, expectedHeight, + "Height of audio element should be " + expectedHeight + ", which is equal to the controls bar."); + popup.close(); + SimpleTest.finish(); + }, "The media element should eventually be resized to match the height of the audio controls."); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_video_direction.html b/toolkit/content/tests/widgets/test_videocontrols_video_direction.html new file mode 100644 index 0000000000..45cf7f6363 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_video_direction.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls directionality test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var tests = [ + {op: "==", test: "videocontrols_direction-1a.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1b.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1c.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1d.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1e.html", ref: "videocontrols_direction-1-ref.html"}, +]; + +</script> +<script type="text/javascript" src="videocontrols_direction_test.js"></script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html b/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html new file mode 100644 index 0000000000..bfc8018466 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + const video = document.getElementById("video"); + const muteButton = getElementWithinVideo(video, "muteButton"); + const volumeStack = getElementWithinVideo(video, "volumeStack"); + + add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + await new Promise(resolve => { + video.src = "video.ogg"; + video.addEventListener("loadedmetadata", () => SimpleTest.executeSoon(resolve)); + }); + }); + + add_task(async function mute_button_icon() { + is(muteButton.getAttribute("noAudio"), "true"); + ok(muteButton.hasAttribute("disabled"), "Mute button should be disabled"); + + if (volumeStack) { + ok(volumeStack.hidden); + } + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_vtt.html b/toolkit/content/tests/widgets/test_videocontrols_vtt.html new file mode 100644 index 0000000000..b389b952e6 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_vtt.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - VTT</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const video = document.getElementById("video"); + const ccBtn = getElementWithinVideo(video, "closedCaptionButton"); + const ttList = getElementWithinVideo(video, "textTrackList"); + const ttListContainer = getElementWithinVideo(video, "textTrackListContainer"); + + add_task(async function wait_for_media_ready() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + await new Promise(resolve => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", resolve); + }); + }); + + add_task(async function check_inital_state() { + ok(ccBtn.hidden, "CC button should hide"); + }); + + add_task(async function check_unsupported_type_added() { + video.addTextTrack("descriptions", "English", "en"); + video.addTextTrack("chapters", "English", "en"); + video.addTextTrack("metadata", "English", "en"); + + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hidden, "CC button should hide if no supported tracks provided"); + }); + + add_task(async function check_cc_button_present() { + const sub = video.addTextTrack("subtitles", "English", "en"); + sub.mode = "disabled"; + + await new Promise(SimpleTest.executeSoon); + ok(!ccBtn.hidden, "CC button should show"); + is(ccBtn.hasAttribute("enabled"), false, "CC button should be disabled"); + }); + + add_task(async function check_cc_button_be_enabled() { + const subtitle = video.addTextTrack("subtitles", "English", "en"); + subtitle.mode = "showing"; + + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled"); + subtitle.mode = "disabled"; + }); + + add_task(async function check_cpations_type() { + const caption = video.addTextTrack("captions", "English", "en"); + caption.mode = "showing"; + + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled"); + }); + + add_task(async function check_track_ui_state() { + synthesizeMouseAtCenter(ccBtn, {}); + + await new Promise(SimpleTest.executeSoon); + ok(!ttListContainer.hidden, "Texttrack menu should show up"); + ok(ttList.lastChild.hasAttribute("on"), "The last added item should be highlighted"); + }); + + add_task(async function check_select_texttrack() { + const tt = ttList.children[1]; + + ok(!tt.hasAttribute("on"), "Item should be off before click"); + synthesizeMouseAtCenter(tt, {}); + + await once(video.textTracks, "change"); + await new Promise(SimpleTest.executeSoon); + ok(tt.hasAttribute("on"), "Selected item should be enabled"); + ok(ttListContainer.hidden, "Should hide texttrack menu once clicked on an item"); + }); + + add_task(async function check_change_texttrack_mode() { + const tts = [...video.textTracks]; + + tts.forEach(tt => tt.mode = "hidden"); + await once(video.textTracks, "change"); + await new Promise(SimpleTest.executeSoon); + ok(!ccBtn.hasAttribute("enabled"), "CC button should be disabled"); + + // enable the last text track. + tts[tts.length - 1].mode = "showing"; + await once(video.textTracks, "change"); + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled"); + ok(ttList.lastChild.hasAttribute("on"), "The last item should be highlighted"); + }); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/tree_shared.js b/toolkit/content/tests/widgets/tree_shared.js new file mode 100644 index 0000000000..7762fb9e36 --- /dev/null +++ b/toolkit/content/tests/widgets/tree_shared.js @@ -0,0 +1,1936 @@ +// This file expects the following globals to be defined at various times. +/* globals getCustomTreeViewCellInfo */ + +// This files relies on these specific Chrome/XBL globals +/* globals TreeColumns, TreeColumn */ + +var columns_simpletree = [ + { name: "name", label: "Name", key: true, properties: "one two" }, + { name: "address", label: "Address" }, +]; + +var columns_hiertree = [ + { + name: "name", + label: "Name", + primary: true, + key: true, + properties: "one two", + }, + { name: "address", label: "Address" }, + { name: "planet", label: "Planet" }, + { name: "gender", label: "Gender", cycler: true }, +]; + +// XXXndeakin still to add some tests for: +// cycler columns, checkbox cells + +// this test function expects a tree to have 8 rows in it when it isn't +// expanded. The tree should only display four rows at a time. If editable, +// the cell at row 1 and column 0 must be editable, and the cell at row 2 and +// column 1 must not be editable. +async function testtag_tree( + treeid, + treerowinfoid, + seltype, + columnstype, + testid +) { + // Stop keystrokes that aren't handled by the tree from leaking out and + // scrolling the main Mochitests window! + function preventDefault(event) { + event.preventDefault(); + } + document.addEventListener("keypress", preventDefault); + + var multiple = seltype == "multiple"; + + var tree = document.getElementById(treeid); + var treerowinfo = document.getElementById(treerowinfoid); + var rowInfo; + if (testid == "tree view") { + rowInfo = getCustomTreeViewCellInfo(); + } else { + rowInfo = convertDOMtoTreeRowInfo(treerowinfo, 0, { value: -1 }); + } + var columnInfo = + columnstype == "simple" ? columns_simpletree : columns_hiertree; + + is(tree.selType, seltype == "multiple" ? "" : seltype, testid + " seltype"); + + // note: the functions below should be in this order due to changes made in later tests + + await testtag_tree_treecolpicker(tree, columnInfo, testid); + testtag_tree_columns(tree, columnInfo, testid); + testtag_tree_TreeSelection(tree, testid, multiple); + testtag_tree_TreeSelection_UI(tree, testid, multiple); + testtag_tree_TreeView(tree, testid, rowInfo); + + is(tree.editable, false, "tree should not be editable"); + // currently, the editable flag means that tree editing cannot be invoked + // by the user. However, editing can still be started with a script. + is(tree.editingRow, -1, testid + " initial editingRow"); + is(tree.editingColumn, null, testid + " initial editingColumn"); + + testtag_tree_UI_editing(tree, testid, rowInfo); + + is( + tree.editable, + false, + "tree should not be editable after testtag_tree_UI_editing" + ); + // currently, the editable flag means that tree editing cannot be invoked + // by the user. However, editing can still be started with a script. + is(tree.editingRow, -1, testid + " initial editingRow (continued)"); + is(tree.editingColumn, null, testid + " initial editingColumn (continued)"); + + var ecolumn = tree.columns[0]; + ok( + !tree.startEditing(1, ecolumn), + "non-editable trees shouldn't start editing" + ); + is( + tree.editingRow, + -1, + testid + " failed startEditing shouldn't set editingRow" + ); + is( + tree.editingColumn, + null, + testid + " failed startEditing shouldn't set editingColumn" + ); + + tree.editable = true; + + ok(tree.startEditing(1, ecolumn), "startEditing should have returned true"); + is(tree.editingRow, 1, testid + " startEditing editingRow"); + is(tree.editingColumn, ecolumn, testid + " startEditing editingColumn"); + is( + tree.getAttribute("editing"), + "true", + testid + " startEditing editing attribute" + ); + + tree.stopEditing(true); + is(tree.editingRow, -1, testid + " stopEditing editingRow"); + is(tree.editingColumn, null, testid + " stopEditing editingColumn"); + is( + tree.hasAttribute("editing"), + false, + testid + " stopEditing editing attribute" + ); + + tree.startEditing(-1, ecolumn); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing -1 editingRow" + ); + tree.startEditing(15, ecolumn); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing 15 editingRow" + ); + tree.startEditing(1, null); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing null column editingRow" + ); + tree.startEditing(2, tree.columns[1]); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing non editable cell editingRow" + ); + + tree.startEditing(1, ecolumn); + var inputField = tree.inputField; + is(inputField.localName, "input", testid + "inputField"); + inputField.value = "Changed Value"; + tree.stopEditing(true); + is( + tree.view.getCellText(1, ecolumn), + "Changed Value", + testid + "edit cell accept" + ); + + // this cell can be edited, but stopEditing(false) means don't accept the change. + tree.startEditing(1, ecolumn); + inputField.value = "Second Value"; + tree.stopEditing(false); + is( + tree.view.getCellText(1, ecolumn), + "Changed Value", + testid + "edit cell no accept" + ); + + tree.editable = false; + + // do the sorting tests last as it will cause the rows to rearrange + // skip them for the custom tree view + if (testid != "tree view") { + testtag_tree_TreeView_rows_sort(tree, testid, rowInfo); + } + + testtag_tree_wheel(tree); + + document.removeEventListener("keypress", preventDefault); + + SimpleTest.finish(); +} + +async function testtag_tree_treecolpicker(tree, expectedColumns, testid) { + testid += " "; + + async function showAndHideTreecolpicker() { + let treecolpicker = tree.querySelector("treecolpicker"); + let treecolpickerMenupopup = treecolpicker.querySelector("menupopup"); + await new Promise(resolve => { + treecolpickerMenupopup.addEventListener("popupshown", resolve, { + once: true, + }); + treecolpicker.click(); + }); + let menuitems = treecolpicker.querySelectorAll("menuitem"); + // Ignore the last "Restore Column Order" menu in the count: + is( + menuitems.length - 1, + expectedColumns.length, + testid + "Same number of columns" + ); + for (var c = 0; c < expectedColumns.length; c++) { + is( + menuitems[c].textContent, + expectedColumns[c].label, + testid + "treecolpicker menu matches" + ); + ok( + !menuitems[c].querySelector("label").hidden, + testid + "label not hidden" + ); + } + await new Promise(resolve => { + treecolpickerMenupopup.addEventListener("popuphidden", resolve, { + once: true, + }); + treecolpickerMenupopup.hidePopup(); + }); + } + + // Regression test for Bug 1549931 (menuitem content being hidden upon second open) + await showAndHideTreecolpicker(); + await showAndHideTreecolpicker(); +} + +function testtag_tree_columns(tree, expectedColumns, testid) { + testid += " "; + + var columns = tree.columns; + + is(columns instanceof TreeColumns, true, testid + "columns is a TreeColumns"); + is(columns.count, expectedColumns.length, testid + "TreeColumns count"); + is(columns.length, expectedColumns.length, testid + "TreeColumns length"); + + var treecols = tree.getElementsByTagName("treecols")[0]; + var treecol = treecols.getElementsByTagName("treecol"); + + var x = 0; + var primary = null, + sorted = null, + key = null; + for (var c = 0; c < expectedColumns.length; c++) { + var adjtestid = testid + " column " + c + " "; + var column = columns[c]; + var expectedColumn = expectedColumns[c]; + is(columns.getColumnAt(c), column, adjtestid + "getColumnAt"); + is( + columns.getNamedColumn(expectedColumn.name), + column, + adjtestid + "getNamedColumn" + ); + is(columns.getColumnFor(treecol[c]), column, adjtestid + "getColumnFor"); + if (expectedColumn.primary) { + primary = column; + } + if (expectedColumn.sorted) { + sorted = column; + } + if (expectedColumn.key) { + key = column; + } + + // XXXndeakin on Windows and Linux, some columns are one pixel to the + // left of where they should be. Could just be a rounding issue. + var adj = 1; + is( + column.x + adj >= x, + true, + adjtestid + + "position is after last column " + + column.x + + "," + + column.width + + "," + + x + ); + is(column.width > 0, true, adjtestid + "width is greater than 0"); + x = column.x + column.width; + + // now check the TreeColumn properties + is(column instanceof TreeColumn, true, adjtestid + "is a TreeColumn"); + is(column.element, treecol[c], adjtestid + "element is treecol"); + is(column.columns, columns, adjtestid + "columns is TreeColumns"); + is(column.id, expectedColumn.name, adjtestid + "name"); + is(column.index, c, adjtestid + "index"); + is(column.primary, primary == column, adjtestid + "column is primary"); + + is( + column.cycler, + "cycler" in expectedColumn && expectedColumn.cycler, + adjtestid + "column is cycler" + ); + is( + column.editable, + "editable" in expectedColumn && expectedColumn.editable, + adjtestid + "column is editable" + ); + + is( + column.type, + "type" in expectedColumn ? expectedColumn.type : 1, + adjtestid + "type" + ); + + is( + column.getPrevious(), + c > 0 ? columns[c - 1] : null, + adjtestid + "getPrevious" + ); + is( + column.getNext(), + c < columns.length - 1 ? columns[c + 1] : null, + adjtestid + "getNext" + ); + + // check the view's getColumnProperties method + var properties = tree.view.getColumnProperties(column); + var expectedProperties = expectedColumn.properties; + is( + properties, + expectedProperties ? expectedProperties : "", + adjtestid + "getColumnProperties" + ); + } + + is(columns.getFirstColumn(), columns[0], testid + "getFirstColumn"); + is( + columns.getLastColumn(), + columns[columns.length - 1], + testid + "getLastColumn" + ); + is(columns.getPrimaryColumn(), primary, testid + "getPrimaryColumn"); + is(columns.getSortedColumn(), sorted, testid + "getSortedColumn"); + is(columns.getKeyColumn(), key, testid + "getKeyColumn"); + + is(columns.getColumnAt(-1), null, testid + "getColumnAt under"); + is(columns.getColumnAt(columns.length), null, testid + "getColumnAt over"); + is(columns.getNamedColumn(""), null, testid + "getNamedColumn null"); + is( + columns.getNamedColumn("unknown"), + null, + testid + "getNamedColumn unknown" + ); + is(columns.getColumnFor(null), null, testid + "getColumnFor null"); + is(columns.getColumnFor(tree), null, testid + "getColumnFor other"); +} + +function testtag_tree_TreeSelection(tree, testid, multiple) { + testid += " selection "; + + var selection = tree.view.selection; + is( + selection instanceof Ci.nsITreeSelection, + true, + testid + "selection is a TreeSelection" + ); + is(selection.single, !multiple, testid + "single"); + + testtag_tree_TreeSelection_State(tree, testid + "initial", -1, []); + is(selection.shiftSelectPivot, -1, testid + "initial shiftSelectPivot"); + + selection.currentIndex = 2; + testtag_tree_TreeSelection_State(tree, testid + "set currentIndex", 2, []); + tree.currentIndex = 3; + testtag_tree_TreeSelection_State( + tree, + testid + "set tree.currentIndex", + 3, + [] + ); + + // test the select() method, which should deselect all rows and select + // a single row + selection.select(1); + testtag_tree_TreeSelection_State(tree, testid + "select 1", 1, [1]); + selection.select(3); + testtag_tree_TreeSelection_State(tree, testid + "select 2", 3, [3]); + selection.select(3); + testtag_tree_TreeSelection_State(tree, testid + "select same", 3, [3]); + + selection.currentIndex = 1; + testtag_tree_TreeSelection_State( + tree, + testid + "set currentIndex with single selection", + 1, + [3] + ); + + tree.currentIndex = 2; + testtag_tree_TreeSelection_State( + tree, + testid + "set tree.currentIndex with single selection", + 2, + [3] + ); + + // check the toggleSelect method. In single selection mode, it only toggles on when + // there isn't currently a selection. + selection.toggleSelect(2); + testtag_tree_TreeSelection_State( + tree, + testid + "toggleSelect 1", + 2, + multiple ? [2, 3] : [3] + ); + selection.toggleSelect(2); + selection.toggleSelect(3); + testtag_tree_TreeSelection_State(tree, testid + "toggleSelect 2", 3, []); + + // the current index doesn't change after a selectAll, so it should still be set to 1 + // selectAll has no effect on single selection trees + selection.currentIndex = 1; + selection.selectAll(); + testtag_tree_TreeSelection_State( + tree, + testid + "selectAll 1", + 1, + multiple ? [0, 1, 2, 3, 4, 5, 6, 7] : [] + ); + selection.toggleSelect(2); + testtag_tree_TreeSelection_State( + tree, + testid + "toggleSelect after selectAll", + 2, + multiple ? [0, 1, 3, 4, 5, 6, 7] : [2] + ); + selection.clearSelection(); + testtag_tree_TreeSelection_State(tree, testid + "clearSelection", 2, []); + selection.toggleSelect(3); + selection.toggleSelect(1); + if (multiple) { + selection.selectAll(); + testtag_tree_TreeSelection_State(tree, testid + "selectAll 2", 1, [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + ]); + } + selection.currentIndex = 2; + selection.clearSelection(); + testtag_tree_TreeSelection_State( + tree, + testid + "clearSelection after selectAll", + 2, + [] + ); + + is(selection.shiftSelectPivot, -1, testid + "shiftSelectPivot set to -1"); + + // rangedSelect and clearRange set the currentIndex to the endIndex. The + // shiftSelectPivot property will be set to startIndex. + selection.rangedSelect(1, 3, false); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect no augment", + multiple ? 3 : 2, + multiple ? [1, 2, 3] : [] + ); + is( + selection.shiftSelectPivot, + multiple ? 1 : -1, + testid + "shiftSelectPivot after rangedSelect no augment" + ); + if (multiple) { + selection.select(1); + selection.rangedSelect(0, 2, true); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment", 2, [ + 0, + 1, + 2, + ]); + is( + selection.shiftSelectPivot, + 0, + testid + "shiftSelectPivot after rangedSelect augment" + ); + + selection.clearRange(1, 3); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment", 3, [ + 0, + ]); + + // check that rangedSelect can take a start value higher than end + selection.rangedSelect(3, 1, false); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect reverse", 1, [ + 1, + 2, + 3, + ]); + is( + selection.shiftSelectPivot, + 3, + testid + "shiftSelectPivot after rangedSelect reverse" + ); + + // check that setting the current index doesn't change the selection + selection.currentIndex = 0; + testtag_tree_TreeSelection_State( + tree, + testid + "currentIndex with range selection", + 0, + [1, 2, 3] + ); + } + + // both values of rangedSelect may be the same + selection.rangedSelect(2, 2, false); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect one row", 2, [ + 2, + ]); + is( + selection.shiftSelectPivot, + 2, + testid + "shiftSelectPivot after selecting one row" + ); + + if (multiple) { + selection.rangedSelect(2, 3, true); + + // a start index of -1 means from the last point + selection.rangedSelect(-1, 0, true); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect -1 existing selection", + 0, + [0, 1, 2, 3] + ); + is( + selection.shiftSelectPivot, + 2, + testid + "shiftSelectPivot after -1 existing selection" + ); + + selection.currentIndex = 2; + selection.rangedSelect(-1, 0, false); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect -1 from currentIndex", + 0, + [0, 1, 2] + ); + is( + selection.shiftSelectPivot, + 2, + testid + "shiftSelectPivot -1 from currentIndex" + ); + } + + // XXXndeakin need to test out of range values but these don't work properly + /* + selection.select(-1); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment -1", -1, []); + + selection.select(8); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment 8", 3, [0]); +*/ +} + +function testtag_tree_TreeSelection_UI(tree, testid, multiple) { + testid += " selection UI "; + + var selection = tree.view.selection; + selection.clearSelection(); + selection.currentIndex = 0; + tree.focus(); + + var keydownFired = 0; + var keypressFired = 0; + function keydownListener(event) { + keydownFired++; + } + function keypressListener(event) { + keypressFired++; + } + + // check that cursor up and down keys navigate up and down + // select event fires after a delay so don't expect it. The reason it fires after a delay + // is so that cursor navigation allows quicking skimming over a set of items without + // actually firing events in-between, improving performance. The select event will only + // be fired on the row where the cursor stops. + window.addEventListener("keydown", keydownListener); + window.addEventListener("keypress", keypressListener); + + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down"); + testtag_tree_TreeSelection_State(tree, testid + "key down", 1, [1], 0); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up"); + testtag_tree_TreeSelection_State(tree, testid + "key up", 0, [0], 0); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up at start"); + testtag_tree_TreeSelection_State(tree, testid + "key up at start", 0, [0], 0); + + // pressing down while the last row is selected should not fire a select event, + // as the selection won't have changed. Also the view is not scrolled in this case. + selection.select(7); + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down at end"); + testtag_tree_TreeSelection_State(tree, testid + "key down at end", 7, [7], 0); + + // pressing keys while at the edge of the visible rows should scroll the list + tree.scrollToRow(4); + selection.select(4); + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up with scroll"); + is(tree.getFirstVisibleRow(), 3, testid + "key up with scroll"); + + tree.scrollToRow(0); + selection.select(3); + synthesizeKeyExpectEvent( + "VK_DOWN", + {}, + tree, + "!select", + "key down with scroll" + ); + is(tree.getFirstVisibleRow(), 1, testid + "key down with scroll"); + + // accel key and cursor movement adjust currentIndex but should not change + // the selection. In single selection mode, the selection will not change, + // but instead will just scroll up or down a line + tree.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent( + "VK_DOWN", + { accelKey: true }, + tree, + "!select", + "key down with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key down with accel", + multiple ? 2 : 1, + [1] + ); + if (!multiple) { + is(tree.getFirstVisibleRow(), 1, testid + "key down with accel and scroll"); + } + + tree.scrollToRow(4); + selection.select(4); + synthesizeKeyExpectEvent( + "VK_UP", + { accelKey: true }, + tree, + "!select", + "key up with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key up with accel", + multiple ? 3 : 4, + [4] + ); + if (!multiple) { + is(tree.getFirstVisibleRow(), 3, testid + "key up with accel and scroll"); + } + + // do this three times, one for each state of pageUpOrDownMovesSelection, + // and then once with the accel key pressed + for (let t = 0; t < 3; t++) { + let testidmod = ""; + if (t == 2) { + testidmod = " with accel"; + } else if (t == 1) { + testidmod = " rev"; + } + var keymod = t == 2 ? { accelKey: true } : {}; + + var moveselection = tree.pageUpOrDownMovesSelection; + if (t == 2) { + moveselection = !moveselection; + } + + tree.scrollToRow(4); + selection.currentIndex = 6; + selection.select(6); + var expected = moveselection ? 4 : 6; + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + keymod, + tree, + "!select", + "key page up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page up" + testidmod, + expected, + [expected], + moveselection ? 4 : 0 + ); + + expected = moveselection ? 0 : 6; + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + keymod, + tree, + "!select", + "key page up again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page up again" + testidmod, + expected, + [expected], + 0 + ); + + expected = moveselection ? 0 : 6; + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + keymod, + tree, + "!select", + "key page up at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page up at start" + testidmod, + expected, + [expected], + 0 + ); + + tree.scrollToRow(0); + selection.currentIndex = 1; + selection.select(1); + expected = moveselection ? 3 : 1; + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + keymod, + tree, + "!select", + "key page down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page down" + testidmod, + expected, + [expected], + moveselection ? 0 : 4 + ); + + expected = moveselection ? 7 : 1; + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + keymod, + tree, + "!select", + "key page down again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page down again" + testidmod, + expected, + [expected], + 4 + ); + + expected = moveselection ? 7 : 1; + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + keymod, + tree, + "!select", + "key page down at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page down at start" + testidmod, + expected, + [expected], + 4 + ); + + if (t < 2) { + tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; + } + } + + tree.scrollToRow(4); + selection.select(6); + synthesizeKeyExpectEvent("VK_HOME", {}, tree, "!select", "key home"); + testtag_tree_TreeSelection_State(tree, testid + "key home", 0, [0], 0); + + tree.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent("VK_END", {}, tree, "!select", "key end"); + testtag_tree_TreeSelection_State(tree, testid + "key end", 7, [7], 4); + + // in single selection mode, the selection doesn't change in this case + tree.scrollToRow(4); + selection.select(6); + synthesizeKeyExpectEvent( + "VK_HOME", + { accelKey: true }, + tree, + "!select", + "key home with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key home with accel", + multiple ? 0 : 6, + [6], + 0 + ); + + tree.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent( + "VK_END", + { accelKey: true }, + tree, + "!select", + "key end with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key end with accel", + multiple ? 7 : 1, + [1], + 4 + ); + + // next, test cursor navigation with selection. Here the select event will be fired + selection.select(1); + var eventExpected = multiple ? "select" : "!select"; + synthesizeKeyExpectEvent( + "VK_DOWN", + { shiftKey: true }, + tree, + eventExpected, + "key shift down to select" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift down to select", + multiple ? 2 : 1, + multiple ? [1, 2] : [1] + ); + is( + selection.shiftSelectPivot, + multiple ? 1 : -1, + testid + "key shift down to select shiftSelectPivot" + ); + synthesizeKeyExpectEvent( + "VK_UP", + { shiftKey: true }, + tree, + eventExpected, + "key shift up to unselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift up to unselect", + 1, + [1] + ); + is( + selection.shiftSelectPivot, + multiple ? 1 : -1, + testid + "key shift up to unselect shiftSelectPivot" + ); + if (multiple) { + synthesizeKeyExpectEvent( + "VK_UP", + { shiftKey: true }, + tree, + "select", + "key shift up to select" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift up to select", + 0, + [0, 1] + ); + is( + selection.shiftSelectPivot, + 1, + testid + "key shift up to select shiftSelectPivot" + ); + synthesizeKeyExpectEvent( + "VK_DOWN", + { shiftKey: true }, + tree, + "select", + "key shift down to unselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift down to unselect", + 1, + [1] + ); + is( + selection.shiftSelectPivot, + 1, + testid + "key shift down to unselect shiftSelectPivot" + ); + } + + // do this twice, one for each state of pageUpOrDownMovesSelection, however + // when selecting with the shift key, pageUpOrDownMovesSelection is ignored + // and the selection always changes + var lastidx = tree.view.rowCount - 1; + for (let t = 0; t < 2; t++) { + let testidmod = t == 0 ? "" : " rev"; + + // If the top or bottom visible row is the current row, pressing shift and + // page down / page up selects one page up or one page down. Otherwise, the + // selection is made to the top or bottom of the visible area. + tree.scrollToRow(lastidx - 3); + selection.currentIndex = 6; + selection.select(6); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + eventExpected, + "key shift page up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up" + testidmod, + multiple ? 4 : 6, + multiple ? [4, 5, 6] : [6] + ); + if (multiple) { + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "select", + "key shift page up again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up again" + testidmod, + 0, + [0, 1, 2, 3, 4, 5, 6] + ); + // no change in the selection, so no select event should be fired + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "!select", + "key shift page up at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up at start" + testidmod, + 0, + [0, 1, 2, 3, 4, 5, 6] + ); + // deselect by paging down again + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "select", + "key shift page down deselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down deselect" + testidmod, + 3, + [3, 4, 5, 6] + ); + } + + tree.scrollToRow(1); + selection.currentIndex = 2; + selection.select(2); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + eventExpected, + "key shift page down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down" + testidmod, + multiple ? 4 : 2, + multiple ? [2, 3, 4] : [2] + ); + if (multiple) { + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "select", + "key shift page down again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down again" + testidmod, + 7, + [2, 3, 4, 5, 6, 7] + ); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "!select", + "key shift page down at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down at start" + testidmod, + 7, + [2, 3, 4, 5, 6, 7] + ); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "select", + "key shift page up deselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up deselect" + testidmod, + 4, + [2, 3, 4] + ); + } + + // test when page down / page up is pressed when the view is scrolled such + // that the selection is not visible + if (multiple) { + tree.scrollToRow(3); + selection.currentIndex = 1; + selection.select(1); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + eventExpected, + "key shift page down with view scrolled down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down with view scrolled down" + testidmod, + 6, + [1, 2, 3, 4, 5, 6], + 3 + ); + + tree.scrollToRow(2); + selection.currentIndex = 6; + selection.select(6); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + eventExpected, + "key shift page up with view scrolled up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up with view scrolled up" + testidmod, + 2, + [2, 3, 4, 5, 6], + 2 + ); + + tree.scrollToRow(2); + selection.currentIndex = 0; + selection.select(0); + // don't expect the select event, as the selection won't have changed + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "!select", + "key shift page up at start with view scrolled down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + + "key shift page up at start with view scrolled down" + + testidmod, + 0, + [0], + 0 + ); + + tree.scrollToRow(0); + selection.currentIndex = 7; + selection.select(7); + // don't expect the select event, as the selection won't have changed + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "!select", + "key shift page down at end with view scrolled up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down at end with view scrolled up" + testidmod, + 7, + [7], + 4 + ); + } + + tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; + } + + tree.scrollToRow(4); + selection.select(5); + synthesizeKeyExpectEvent( + "VK_HOME", + { shiftKey: true }, + tree, + eventExpected, + "key shift home" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift home", + multiple ? 0 : 5, + multiple ? [0, 1, 2, 3, 4, 5] : [5], + multiple ? 0 : 4 + ); + + tree.scrollToRow(0); + selection.select(3); + synthesizeKeyExpectEvent( + "VK_END", + { shiftKey: true }, + tree, + eventExpected, + "key shift end" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift end", + multiple ? 7 : 3, + multiple ? [3, 4, 5, 6, 7] : [3], + multiple ? 4 : 0 + ); + + // pressing space selects a row, pressing accel + space unselects a row + selection.select(2); + selection.currentIndex = 4; + synthesizeKeyExpectEvent(" ", {}, tree, "select", "key space on"); + // in single selection mode, space shouldn't do anything + testtag_tree_TreeSelection_State( + tree, + testid + "key space on", + 4, + multiple ? [2, 4] : [2] + ); + + if (multiple) { + synthesizeKeyExpectEvent( + " ", + { accelKey: true }, + tree, + "select", + "key space off" + ); + testtag_tree_TreeSelection_State(tree, testid + "key space off", 4, [2]); + } + + // check that clicking on a row selects it + tree.scrollToRow(0); + selection.select(2); + selection.currentIndex = 2; + if (0) { + // XXXndeakin disable these tests for now + mouseOnCell(tree, 1, tree.columns[1], "mouse on row"); + testtag_tree_TreeSelection_State( + tree, + testid + "mouse on row", + 1, + [1], + 0, + null + ); + } + + // restore the scroll position to the start of the page + sendKey("HOME"); + + window.removeEventListener("keydown", keydownListener); + window.removeEventListener("keypress", keypressListener); + is(keydownFired, multiple ? 63 : 40, "keydown event wasn't fired properly"); + is(keypressFired, multiple ? 2 : 1, "keypress event wasn't fired properly"); +} + +function testtag_tree_UI_editing(tree, testid, rowInfo) { + testid += " editing UI "; + + // check editing UI + var ecolumn = tree.columns[0]; + var rowIndex = 2; + + // temporary make the tree editable to test mouse double click + var wasEditable = tree.editable; + if (!wasEditable) { + tree.editable = true; + } + + // if this is a container save its current open status + var row = rowInfo.rows[rowIndex]; + var wasOpen = null; + if (tree.view.isContainer(row)) { + wasOpen = tree.view.isContainerOpen(row); + } + + mouseDblClickOnCell(tree, rowIndex, ecolumn, testid + "edit on double click"); + is(tree.editingColumn, ecolumn, testid + "editing column"); + is(tree.editingRow, rowIndex, testid + "editing row"); + + // ensure that we don't expand an expandable container on edit + if (wasOpen != null) { + is( + tree.view.isContainerOpen(row), + wasOpen, + testid + "opened container node on edit" + ); + } + + // ensure to restore editable attribute + if (!wasEditable) { + tree.editable = false; + } + + var ci = tree.currentIndex; + + // cursor navigation should not change the selection while editing + var testKey = function(key) { + synthesizeKeyExpectEvent( + key, + {}, + tree, + "!select", + "key " + key + " with editing" + ); + is( + tree.editingRow == rowIndex && + tree.editingColumn == ecolumn && + tree.currentIndex == ci, + true, + testid + "key " + key + " while editing" + ); + }; + + testKey("VK_DOWN"); + testKey("VK_UP"); + testKey("VK_PAGE_DOWN"); + testKey("VK_PAGE_UP"); + testKey("VK_HOME"); + testKey("VK_END"); + + // XXXndeakin figure out how to send characters to the textbox + // inputField.inputField.focus() + // synthesizeKeyExpectEvent(inputField.inputField, "b", null, ""); + // tree.stopEditing(true); + // is(tree.view.getCellText(0, ecolumn), "b", testid + "edit cell"); + + // Restore initial state. + tree.stopEditing(false); +} + +function testtag_tree_TreeView(tree, testid, rowInfo) { + testid += " view "; + + var columns = tree.columns; + var view = tree.view; + + is(view instanceof Ci.nsITreeView, true, testid + "view is a TreeView"); + is(view.rowCount, rowInfo.rows.length, testid + "rowCount"); + + testtag_tree_TreeView_rows(tree, testid, rowInfo, 0); + + // note that this will only work for content trees currently + view.setCellText(0, columns[1], "Changed Value"); + is(view.getCellText(0, columns[1]), "Changed Value", "setCellText"); + + view.setCellValue(1, columns[0], "Another Changed Value"); + is(view.getCellValue(1, columns[0]), "Another Changed Value", "setCellText"); +} + +function testtag_tree_TreeView_rows(tree, testid, rowInfo, startRow) { + var r; + var columns = tree.columns; + var view = tree.view; + var length = rowInfo.rows.length; + + // methods to test along with the functions which determine the expected value + var checkRowMethods = { + isContainer(row) { + return row.container; + }, + isContainerOpen(row) { + return false; + }, + isContainerEmpty(row) { + return row.children != null && !row.children.rows.length; + }, + isSeparator(row) { + return row.separator; + }, + getRowProperties(row) { + return row.properties; + }, + getLevel(row) { + return row.level; + }, + getParentIndex(row) { + return row.parent; + }, + hasNextSibling(row) { + return r < startRow + length - 1; + }, + }; + + var checkCellMethods = { + getCellText(row, cell) { + return cell.label; + }, + getCellValue(row, cell) { + return cell.value; + }, + getCellProperties(row, cell) { + return cell.properties; + }, + isEditable(row, cell) { + return cell.editable; + }, + getImageSrc(row, cell) { + return cell.image; + }, + }; + + var failedMethods = {}; + var checkMethod, actual, expected; + var toggleOpenStateOK = true; + + for (r = startRow; r < length; r++) { + var row = rowInfo.rows[r]; + for (var c = 0; c < row.cells.length; c++) { + var cell = row.cells[c]; + + for (checkMethod in checkCellMethods) { + expected = checkCellMethods[checkMethod](row, cell); + actual = view[checkMethod](r, columns[c]); + if (actual !== expected) { + failedMethods[checkMethod] = true; + is( + actual, + expected, + testid + + "row " + + r + + " column " + + c + + " " + + checkMethod + + " is incorrect" + ); + } + } + } + + // compare row properties + for (checkMethod in checkRowMethods) { + expected = checkRowMethods[checkMethod](row, r); + if (checkMethod == "hasNextSibling") { + actual = view[checkMethod](r, r); + } else { + actual = view[checkMethod](r); + } + if (actual !== expected) { + failedMethods[checkMethod] = true; + is( + actual, + expected, + testid + "row " + r + " " + checkMethod + " is incorrect" + ); + } + } + /* + // open and recurse into containers + if (row.container) { + view.toggleOpenState(r); + if (!view.isContainerOpen(r)) { + toggleOpenStateOK = false; + is(view.isContainerOpen(r), true, testid + "row " + r + " toggleOpenState open"); + } + testtag_tree_TreeView_rows(tree, testid + "container " + r + " ", row.children, r + 1); + view.toggleOpenState(r); + if (view.isContainerOpen(r)) { + toggleOpenStateOK = false; + is(view.isContainerOpen(r), false, testid + "row " + r + " toggleOpenState close"); + } + } +*/ + } + + for (var failedMethod in failedMethods) { + if (failedMethod in checkRowMethods) { + delete checkRowMethods[failedMethod]; + } + if (failedMethod in checkCellMethods) { + delete checkCellMethods[failedMethod]; + } + } + + for (checkMethod in checkRowMethods) { + is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); + } + for (checkMethod in checkCellMethods) { + is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); + } + if (toggleOpenStateOK) { + is("toggleOpenState ok", "toggleOpenState ok", testid + "toggleOpenState"); + } +} + +function testtag_tree_TreeView_rows_sort(tree, testid, rowInfo) { + // check if cycleHeader sorts the columns + var columnIndex = 0; + var view = tree.view; + var column = tree.columns[columnIndex]; + var columnElement = column.element; + var sortkey = columnElement.getAttribute("sort"); + if (sortkey) { + view.cycleHeader(column); + is(tree.getAttribute("sort"), sortkey, "cycleHeader sort"); + is( + tree.getAttribute("sortDirection"), + "ascending", + "cycleHeader sortDirection ascending" + ); + is( + columnElement.getAttribute("sortDirection"), + "ascending", + "cycleHeader column sortDirection" + ); + is( + columnElement.getAttribute("sortActive"), + "true", + "cycleHeader column sortActive" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "descending", + "cycleHeader sortDirection descending" + ); + is( + columnElement.getAttribute("sortDirection"), + "descending", + "cycleHeader column sortDirection descending" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "", + "cycleHeader sortDirection natural" + ); + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection natural" + ); + // XXXndeakin content view isSorted needs to be tested + } + + // Check that clicking on column header sorts the column. + var columns = getSortedColumnArray(tree); + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection" + ); + + // Click once on column header and check sorting has cycled once. + mouseClickOnColumnHeader(columns, columnIndex, 0, 1); + is( + columnElement.getAttribute("sortDirection"), + "ascending", + "single click cycleHeader column sortDirection ascending" + ); + + // Now simulate a double click. + mouseClickOnColumnHeader(columns, columnIndex, 0, 2); + if (navigator.platform.indexOf("Win") == 0) { + // Windows cycles only once on double click. + is( + columnElement.getAttribute("sortDirection"), + "descending", + "double click cycleHeader column sortDirection descending" + ); + // 1 single clicks should restore natural sorting. + mouseClickOnColumnHeader(columns, columnIndex, 0, 1); + } + + // Check we have gone back to natural sorting. + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection" + ); + + columnElement.setAttribute("sorthints", "twostate"); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "ascending", + "cycleHeader sortDirection ascending twostate" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "descending", + "cycleHeader sortDirection ascending twostate" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "ascending", + "cycleHeader sortDirection ascending twostate again" + ); + columnElement.removeAttribute("sorthints"); + view.cycleHeader(column); + view.cycleHeader(column); + + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection reset" + ); +} + +// checks if the current and selected rows are correct +// current is the index of the current row +// selected is an array of the indicies of the selected rows +// viewidx is the row that should be visible at the top of the tree +function testtag_tree_TreeSelection_State( + tree, + testid, + current, + selected, + viewidx +) { + var selection = tree.view.selection; + + is(selection.count, selected.length, testid + " count"); + is(tree.currentIndex, current, testid + " currentIndex"); + is(selection.currentIndex, current, testid + " TreeSelection currentIndex"); + if (viewidx !== null && viewidx !== undefined) { + is(tree.getFirstVisibleRow(), viewidx, testid + " first visible row"); + } + + var actualSelected = []; + var count = tree.view.rowCount; + for (var s = 0; s < count; s++) { + if (selection.isSelected(s)) { + actualSelected.push(s); + } + } + + is( + compareArrays(selected, actualSelected), + true, + testid + " selection [" + selected + "]" + ); + + actualSelected = []; + var rangecount = selection.getRangeCount(); + for (var r = 0; r < rangecount; r++) { + var start = {}, + end = {}; + selection.getRangeAt(r, start, end); + for (var rs = start.value; rs <= end.value; rs++) { + actualSelected.push(rs); + } + } + + is( + compareArrays(selected, actualSelected), + true, + testid + " range selection [" + selected + "]" + ); +} + +function testtag_tree_column_reorder() { + // Make sure the tree is scrolled into the view, otherwise the test will + // fail + var testframe = window.parent.document.getElementById("testframe"); + if (testframe) { + testframe.scrollIntoView(); + } + + var tree = document.getElementById("tree-column-reorder"); + var numColumns = tree.columns.count; + + var reference = []; + for (let i = 0; i < numColumns; i++) { + reference.push("col_" + i); + } + + // Drag the first column to each position + for (let i = 0; i < numColumns - 1; i++) { + synthesizeColumnDrag(tree, i, i + 1, true); + arrayMove(reference, i, i + 1, true); + checkColumns(tree, reference, "drag first column right"); + } + + // And back + for (let i = numColumns - 1; i >= 1; i--) { + synthesizeColumnDrag(tree, i, i - 1, false); + arrayMove(reference, i, i - 1, false); + checkColumns(tree, reference, "drag last column left"); + } + + // Drag each column one column left + for (let i = 1; i < numColumns; i++) { + synthesizeColumnDrag(tree, i, i - 1, false); + arrayMove(reference, i, i - 1, false); + checkColumns(tree, reference, "drag each column left"); + } + + // And back + for (let i = numColumns - 2; i >= 0; i--) { + synthesizeColumnDrag(tree, i, i + 1, true); + arrayMove(reference, i, i + 1, true); + checkColumns(tree, reference, "drag each column right"); + } + + // Drag each column 5 to the right + for (let i = 0; i < numColumns - 5; i++) { + synthesizeColumnDrag(tree, i, i + 5, true); + arrayMove(reference, i, i + 5, true); + checkColumns(tree, reference, "drag each column 5 to the right"); + } + + // And to the left + for (let i = numColumns - 6; i >= 5; i--) { + synthesizeColumnDrag(tree, i, i - 5, false); + arrayMove(reference, i, i - 5, false); + checkColumns(tree, reference, "drag each column 5 to the left"); + } + + // Test that moving a column after itself does not move anything + synthesizeColumnDrag(tree, 0, 0, true); + checkColumns(tree, reference, "drag to itself"); + is(document.treecolDragging, null, "drag to itself completed"); + + // XXX roc should this be here??? + SimpleTest.finish(); +} + +function testtag_tree_wheel(aTree) { + const deltaModes = [ + WheelEvent.DOM_DELTA_PIXEL, // 0 + WheelEvent.DOM_DELTA_LINE, // 1 + WheelEvent.DOM_DELTA_PAGE, // 2 + ]; + function helper(aStart, aDelta, aIntDelta, aDeltaMode) { + aTree.scrollToRow(aStart); + var expected; + if (!aIntDelta) { + expected = aStart; + } else if (aDeltaMode != WheelEvent.DOM_DELTA_PAGE) { + expected = aStart + aIntDelta; + } else if (aIntDelta > 0) { + expected = aStart + aTree.getPageLength(); + } else { + expected = aStart - aTree.getPageLength(); + } + + if (expected < 0) { + expected = 0; + } + if (expected > aTree.view.rowCount - aTree.getPageLength()) { + expected = aTree.view.rowCount - aTree.getPageLength(); + } + synthesizeWheel(aTree.body, 1, 1, { + deltaMode: aDeltaMode, + deltaY: aDelta, + lineOrPageDeltaY: aIntDelta, + }); + is( + aTree.getFirstVisibleRow(), + expected, + "testtag_tree_wheel: vertical, starting " + + aStart + + " delta " + + aDelta + + " lineOrPageDelta " + + aIntDelta + + " aDeltaMode " + + aDeltaMode + ); + + aTree.scrollToRow(aStart); + // Check that horizontal scrolling has no effect + synthesizeWheel(aTree.body, 1, 1, { + deltaMode: aDeltaMode, + deltaX: aDelta, + lineOrPageDeltaX: aIntDelta, + }); + is( + aTree.getFirstVisibleRow(), + aStart, + "testtag_tree_wheel: horizontal, starting " + + aStart + + " delta " + + aDelta + + " lineOrPageDelta " + + aIntDelta + + " aDeltaMode " + + aDeltaMode + ); + } + + var defaultPrevented = 0; + + function wheelListener(event) { + defaultPrevented++; + } + window.addEventListener("wheel", wheelListener); + + deltaModes.forEach(function(aDeltaMode) { + var delta = aDeltaMode == WheelEvent.DOM_DELTA_PIXEL ? 5.0 : 0.3; + helper(2, -delta, 0, aDeltaMode); + helper(2, -delta, -1, aDeltaMode); + helper(2, delta, 0, aDeltaMode); + helper(2, delta, 1, aDeltaMode); + helper(2, -2 * delta, 0, aDeltaMode); + helper(2, -2 * delta, -1, aDeltaMode); + helper(2, 2 * delta, 0, aDeltaMode); + helper(2, 2 * delta, 1, aDeltaMode); + }); + + window.removeEventListener("wheel", wheelListener); + is(defaultPrevented, 48, "wheel event default prevented"); +} + +function synthesizeColumnDrag( + aTree, + aMouseDownColumnNumber, + aMouseUpColumnNumber, + aAfter +) { + var columns = getSortedColumnArray(aTree); + + var down = columns[aMouseDownColumnNumber].element; + var up = columns[aMouseUpColumnNumber].element; + + // Target the initial mousedown in the middle of the column header so we + // avoid the extra hit test space given to the splitter + var columnWidth = down.getBoundingClientRect().width; + var splitterHitWidth = columnWidth / 2; + synthesizeMouse(down, splitterHitWidth, 3, { type: "mousedown" }); + + var offsetX = 0; + if (aAfter) { + offsetX = columnWidth; + } + + if (aMouseUpColumnNumber > aMouseDownColumnNumber) { + for (let i = aMouseDownColumnNumber; i <= aMouseUpColumnNumber; i++) { + let move = columns[i].element; + synthesizeMouse(move, offsetX, 3, { type: "mousemove" }); + } + } else { + for (let i = aMouseDownColumnNumber; i >= aMouseUpColumnNumber; i--) { + let move = columns[i].element; + synthesizeMouse(move, offsetX, 3, { type: "mousemove" }); + } + } + + synthesizeMouse(up, offsetX, 3, { type: "mouseup" }); +} + +function arrayMove(aArray, aFrom, aTo, aAfter) { + var o = aArray.splice(aFrom, 1)[0]; + if (aTo > aFrom) { + aTo--; + } + + if (aAfter) { + aTo++; + } + + aArray.splice(aTo, 0, o); +} + +function getSortedColumnArray(aTree) { + var columns = aTree.columns; + var array = []; + for (let i = 0; i < columns.length; i++) { + array.push(columns.getColumnAt(i)); + } + + array.sort(function(a, b) { + var o1 = parseInt(a.element.style.MozBoxOrdinalGroup); + var o2 = parseInt(b.element.style.MozBoxOrdinalGroup); + return o1 - o2; + }); + return array; +} + +function checkColumns(aTree, aReference, aMessage) { + var columns = getSortedColumnArray(aTree); + var ids = []; + columns.forEach(function(e) { + ids.push(e.element.id); + }); + is(compareArrays(ids, aReference), true, aMessage); +} + +function mouseOnCell(tree, row, column, testname) { + var rect = tree.getCoordsForCellItem(row, column, "text"); + + synthesizeMouseExpectEvent( + tree.body, + rect.x, + rect.y, + {}, + tree, + "select", + testname + ); +} + +function mouseClickOnColumnHeader( + aColumns, + aColumnIndex, + aButton, + aClickCount +) { + var columnHeader = aColumns[aColumnIndex].element; + var columnHeaderRect = columnHeader.getBoundingClientRect(); + var columnWidth = columnHeaderRect.right - columnHeaderRect.left; + // For multiple click we send separate click events, with increasing + // clickCount. This simulates the common behavior of multiple clicks. + for (let i = 1; i <= aClickCount; i++) { + // Target the middle of the column header. + synthesizeMouse(columnHeader, columnWidth / 2, 3, { + button: aButton, + clickCount: i, + }); + } +} + +function mouseDblClickOnCell(tree, row, column, testname) { + // select the row we will edit + var selection = tree.view.selection; + selection.select(row); + tree.ensureRowIsVisible(row); + + // get cell coordinates + var rect = tree.getCoordsForCellItem(row, column, "text"); + + synthesizeMouse(tree.body, rect.x, rect.y, { clickCount: 2 }); +} + +function compareArrays(arr1, arr2) { + if (arr1.length != arr2.length) { + return false; + } + + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] != arr2[i]) { + return false; + } + } + + return true; +} + +function convertDOMtoTreeRowInfo(treechildren, level, rowidx) { + var obj = { rows: [] }; + + var parentidx = rowidx.value; + + treechildren = treechildren.childNodes; + for (var r = 0; r < treechildren.length; r++) { + rowidx.value++; + + var treeitem = treechildren[r]; + if (treeitem.hasChildNodes()) { + var treerow = treeitem.firstChild; + var cellInfo = []; + for (var c = 0; c < treerow.childNodes.length; c++) { + var cell = treerow.childNodes[c]; + cellInfo.push({ + label: "" + cell.getAttribute("label"), + value: cell.getAttribute("value"), + properties: cell.getAttribute("properties"), + editable: cell.getAttribute("editable") != "false", + selectable: cell.getAttribute("selectable") != "false", + image: cell.getAttribute("src"), + mode: cell.hasAttribute("mode") + ? parseInt(cell.getAttribute("mode")) + : 3, + }); + } + + var descendants = treeitem.lastChild; + var children = + treerow == descendants + ? null + : convertDOMtoTreeRowInfo(descendants, level + 1, rowidx); + obj.rows.push({ + cells: cellInfo, + properties: treerow.getAttribute("properties"), + container: treeitem.getAttribute("container") == "true", + separator: treeitem.localName == "treeseparator", + children, + level, + parent: parentidx, + }); + } + } + + return obj; +} diff --git a/toolkit/content/tests/widgets/video.ogg b/toolkit/content/tests/widgets/video.ogg Binary files differnew file mode 100644 index 0000000000..ac7ece3519 --- /dev/null +++ b/toolkit/content/tests/widgets/video.ogg diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html b/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html new file mode 100644 index 0000000000..1f7e76a7d0 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1a.html b/toolkit/content/tests/widgets/videocontrols_direction-1a.html new file mode 100644 index 0000000000..a4d3546294 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1a.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html dir="rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1b.html b/toolkit/content/tests/widgets/videocontrols_direction-1b.html new file mode 100644 index 0000000000..a14b11d5ff --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1b.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html style="direction: rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1c.html b/toolkit/content/tests/widgets/videocontrols_direction-1c.html new file mode 100644 index 0000000000..0885ebd893 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1c.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="direction: rtl"> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1d.html b/toolkit/content/tests/widgets/videocontrols_direction-1d.html new file mode 100644 index 0000000000..a39accec72 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1d.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav" dir="rtl"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1e.html b/toolkit/content/tests/widgets/videocontrols_direction-1e.html new file mode 100644 index 0000000000..25e7c2c1f9 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1e.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav" style="direction: rtl;"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html b/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html new file mode 100644 index 0000000000..630177883c --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2a.html b/toolkit/content/tests/widgets/videocontrols_direction-2a.html new file mode 100644 index 0000000000..2e40cdc1a7 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2a.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html dir="rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2b.html b/toolkit/content/tests/widgets/videocontrols_direction-2b.html new file mode 100644 index 0000000000..2e4dadb6ff --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2b.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html style="direction: rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2c.html b/toolkit/content/tests/widgets/videocontrols_direction-2c.html new file mode 100644 index 0000000000..a43b03e8f9 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2c.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="direction: rtl"> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2d.html b/toolkit/content/tests/widgets/videocontrols_direction-2d.html new file mode 100644 index 0000000000..52d56f1ccd --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2d.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav" dir="rtl"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2e.html b/toolkit/content/tests/widgets/videocontrols_direction-2e.html new file mode 100644 index 0000000000..58bc30e2b3 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2e.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav" style="direction: rtl;"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction_test.js b/toolkit/content/tests/widgets/videocontrols_direction_test.js new file mode 100644 index 0000000000..33e43be496 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction_test.js @@ -0,0 +1,112 @@ +// This file expects `tests` to have been declared in the global scope. +/* global tests */ + +var RemoteCanvas = function(url, id) { + this.url = url; + this.id = id; + this.snapshot = null; +}; + +RemoteCanvas.CANVAS_WIDTH = 200; +RemoteCanvas.CANVAS_HEIGHT = 200; + +RemoteCanvas.prototype.compare = function(otherCanvas, expected) { + return compareSnapshots(this.snapshot, otherCanvas.snapshot, expected)[0]; +}; + +RemoteCanvas.prototype.load = function(callback) { + var iframe = document.createElement("iframe"); + iframe.id = this.id + "-iframe"; + iframe.width = RemoteCanvas.CANVAS_WIDTH + "px"; + iframe.height = RemoteCanvas.CANVAS_HEIGHT + "px"; + iframe.src = this.url; + var me = this; + iframe.addEventListener("load", function() { + info("iframe loaded"); + var m = iframe.contentDocument.getElementById("av"); + m.addEventListener( + "suspend", + function(aEvent) { + setTimeout(function() { + me.remotePageLoaded(callback); + }, 0); + }, + { once: true } + ); + m.src = m.getAttribute("source"); + }); + window.document.body.appendChild(iframe); +}; + +RemoteCanvas.prototype.remotePageLoaded = function(callback) { + var ldrFrame = document.getElementById(this.id + "-iframe"); + this.snapshot = snapshotWindow(ldrFrame.contentWindow); + this.snapshot.id = this.id + "-canvas"; + window.document.body.appendChild(this.snapshot); + callback(this); +}; + +RemoteCanvas.prototype.cleanup = function() { + var iframe = document.getElementById(this.id + "-iframe"); + iframe.remove(); + var canvas = document.getElementById(this.id + "-canvas"); + canvas.remove(); +}; + +function runTest(index) { + var canvases = []; + function testCallback(canvas) { + canvases.push(canvas); + + if (canvases.length == 2) { + // when both canvases are loaded + var expectedEqual = currentTest.op == "=="; + var result = canvases[0].compare(canvases[1], expectedEqual); + ok( + result, + "Rendering of reftest " + + currentTest.test + + " should " + + (expectedEqual ? "not " : "") + + "be different to the reference" + ); + + if (result) { + canvases[0].cleanup(); + canvases[1].cleanup(); + } else { + info("Snapshot of canvas 1: " + canvases[0].snapshot.toDataURL()); + info("Snapshot of canvas 2: " + canvases[1].snapshot.toDataURL()); + } + + if (index < tests.length - 1) { + runTest(index + 1); + } else { + SimpleTest.finish(); + } + } + } + + var currentTest = tests[index]; + var testCanvas = new RemoteCanvas(currentTest.test, "test-" + index); + testCanvas.load(testCallback); + + var refCanvas = new RemoteCanvas(currentTest.ref, "ref-" + index); + refCanvas.load(testCallback); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); + +window.addEventListener( + "load", + function() { + SpecialPowers.pushPrefEnv( + { set: [["media.cache_size", 40000]] }, + function() { + runTest(0); + } + ); + }, + true +); diff --git a/toolkit/content/tests/widgets/videomask.css b/toolkit/content/tests/widgets/videomask.css new file mode 100644 index 0000000000..066d441388 --- /dev/null +++ b/toolkit/content/tests/widgets/videomask.css @@ -0,0 +1,23 @@ +html, body { + margin: 0; + padding: 0; +} + +audio, video { + width: 140px; + height: 100px; + background-color: black; +} + +/** + * Create a mask for the video direction tests which covers up the throbber. + */ +#mask { + position: absolute; + z-index: 3; + width: 140px; + height: 72px; + background-color: green; + top: 0; + right: 0; +} diff --git a/toolkit/content/tests/widgets/window_label_checkbox.xhtml b/toolkit/content/tests/widgets/window_label_checkbox.xhtml new file mode 100644 index 0000000000..e1a8258b92 --- /dev/null +++ b/toolkit/content/tests/widgets/window_label_checkbox.xhtml @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Label Checkbox Tests" width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<hbox> + <label control="checkbox" value="Label" id="label"/> + <checkbox id="checkbox"/> + <label control="radio2" value="Label" id="label2"/> + <radiogroup> + <radio/> + <radio id="radio2"/> + </radiogroup> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + + let SimpleTest = opener.SimpleTest; + SimpleTest.waitForFocus(() => { + let ok = SimpleTest.ok; + let label = document.getElementById("label"); + let checkbox = document.getElementById("checkbox"); + let label2 = document.getElementById("label2"); + let radio2 = document.getElementById("radio2"); + checkbox.checked = true; + radio2.selected = false; + ok(checkbox.checked, "sanity check"); + ok(!radio2.selected, "sanity check"); + setTimeout(() => { + synthesizeMouseAtCenter(label, {}); + ok(!checkbox.checked, "Checkbox should be unchecked"); + synthesizeMouseAtCenter(label2, {}); + ok(radio2.selected, "Radio2 should be selected"); + opener.postMessage("done", "*"); + window.close(); + }, 0); + }); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/widgets/window_menubar.xhtml b/toolkit/content/tests/widgets/window_menubar.xhtml new file mode 100644 index 0000000000..6408044bb7 --- /dev/null +++ b/toolkit/content/tests/widgets/window_menubar.xhtml @@ -0,0 +1,812 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<!-- the condition in the focus event handler is because pressing Tab + unfocuses and refocuses the window on Windows --> + +<window title="Popup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<!-- + Need to investigate these tests a bit more. Some of the accessibility events + are firing multiple times or in different orders in different circumstances. + Note that this was also the case before bug 279703. + --> + +<hbox style="margin-left: 275px; margin-top: 275px;"> +<menubar id="menubar"> + <menu id="filemenu" label="File" accesskey="F"> + <menupopup id="filepopup"> + <menuitem id="item1" label="Open" accesskey="O"/> + <menuitem id="item2" label="Save" accesskey="S"/> + <menuitem id="item3" label="Close" accesskey="C"/> + </menupopup> + </menu> + <menu id="secretmenu" label="Secret Menu" accesskey="S" disabled="true"> + <menupopup> + <menuitem label="Secret Command" accesskey="S"/> + </menupopup> + </menu> + <menu id="editmenu" label="Edit" accesskey="E"> + <menupopup id="editpopup"> + <menuitem id="cut" label="Cut" accesskey="t" disabled="true"/> + <menuitem id="copy" label="Copy" accesskey="C"/> + <menuitem id="paste" label="Paste" accesskey="P"/> + </menupopup> + </menu> + <menu id="viewmenu" label="View" accesskey="V"> + <menupopup id="viewpopup"> + <menu id="toolbar" label="Toolbar" accesskey="T"> + <menupopup id="toolbarpopup"> + <menuitem id="navigation" label="Navigation" accesskey="N" disabled="true"/> + <menuitem label="Bookmarks" accesskey="B" disabled="true"/> + </menupopup> + </menu> + <menuitem label="Status Bar" accesskey="S"/> + <menu label="Sidebar" accesskey="d"> + <menupopup> + <menuitem label="Bookmarks" accesskey="B"/> + <menuitem label="History" accesskey="H"/> + </menupopup> + </menu> + </menupopup> + </menu> + <menu id="helpmenu" label="Help" accesskey="H"> + <menupopup id="helppopup" > + <label value="Unselectable"/> + <menuitem id="contents" label="Contents" accesskey="C"/> + <menuitem label="More Info" accesskey="I"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem label="Another Menu"/> + <menuitem id="one" label="One"/> + <menu id="only" label="Only Menu"> + <menupopup> + <menuitem label="Test Submenu"/> + </menupopup> + </menu> + <menuitem label="Second Menu"/> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="third" label="Third Menu"/> + <menuitem label="One Other Menu"/> + <label value="Unselectable"/> + <menuitem id="about" label="About" accesskey="A"/> + </menupopup> + </menu> +</menubar> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ +let gFilePopup; +window.opener.SimpleTest.waitForFocus(function () { + gFilePopup = document.getElementById("filepopup"); + var filemenu = document.getElementById("filemenu"); + filemenu.focus(); + is(filemenu.openedWithKey, false, "initial openedWithKey"); + startPopupTests(popupTests); +}, window); + +// On Linux, the first menu opens when F10 is pressed, but on other platforms +// the menubar is focused but no menu is opened. This means that different events +// fire. +function pressF10Events() +{ + return navigator.platform.includes("Linux") ? + [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu", "popupshowing filepopup", "popupshown filepopup"] : + [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ]; +} + +function closeAfterF10Events(extraInactive) +{ + if (navigator.platform.includes("Linux")) { + var events = [ "popuphiding filepopup", "popuphidden filepopup", + "DOMMenuInactive filepopup", "DOMMenuBarInactive menubar", + "DOMMenuItemInactive filemenu" ]; + if (extraInactive) + events.push("DOMMenuItemInactive filemenu"); + return events; + } + + return [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu" ]; +} + +var popupTests = [ +{ + testname: "press on menu", + events: [ "popupshowing filepopup", "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", "popupshown filepopup" ], + test() { synthesizeMouse(document.getElementById("filemenu"), 8, 8, { }); }, + result (testname) { + checkActive(gFilePopup, "", testname); + checkOpen("filemenu", testname); + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // check that pressing cursor down while there is no selection + // highlights the first item + testname: "cursor down no selection", + events: [ "DOMMenuItemActive item1" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { checkActive(gFilePopup, "item1", testname); } +}, +{ + // check that pressing cursor up wraps and highlights the last item + testname: "cursor up wrap", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive item3" ], + test() { synthesizeKey("KEY_ArrowUp"); }, + result(testname) { checkActive(gFilePopup, "item3", testname); } +}, +{ + // check that pressing cursor down wraps and highlights the first item + testname: "cursor down wrap", + events: [ "DOMMenuItemInactive item3", "DOMMenuItemActive item1" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { checkActive(gFilePopup, "item1", testname); } +}, +{ + // check that pressing cursor down highlights the second item + testname: "cursor down", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive item2" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { checkActive(gFilePopup, "item2", testname); } +}, +{ + // check that pressing cursor up highlights the second item + testname: "cursor up", + events: [ "DOMMenuItemInactive item2", "DOMMenuItemActive item1" ], + test() { synthesizeKey("KEY_ArrowUp"); }, + result(testname) { checkActive(gFilePopup, "item1", testname); } +}, + +{ + // cursor right should skip the disabled menu and move to the edit menu + testname: "cursor right skip disabled", + events() { + var elist = [ + // the file menu gets deactivated, the file menu gets hidden, then + // the edit menu is activated + "DOMMenuItemInactive filemenu", "DOMMenuItemActive editmenu", + "popuphiding filepopup", "popuphidden filepopup", + // the popupshowing event gets fired when showing the edit menu. + // The item from the file menu doesn't get deactivated until the + // next item needs to be selected + "popupshowing editpopup", "DOMMenuItemInactive item1", + // not sure why the menu inactivated event is firing so late + "DOMMenuInactive filepopup" + ]; + // finally, the first item is activated and popupshown is fired. + // On Windows, don't skip disabled items. + if (navigator.platform.indexOf("Win") == 0) + elist.push("DOMMenuItemActive cut"); + else + elist.push("DOMMenuItemActive copy"); + elist.push("popupshown editpopup"); + return elist; + }, + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { + var expected = (navigator.platform.indexOf("Win") == 0) ? "cut" : "copy"; + checkActive(document.getElementById("editpopup"), expected, testname); + checkClosed("filemenu", testname); + checkOpen("editmenu", testname); + is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // on Windows, a disabled item is selected, so pressing RETURN should close + // the menu but not fire a command event + testname: "enter on disabled", + events() { + if (navigator.platform.indexOf("Win") == 0) + return [ "popuphiding editpopup", "popuphidden editpopup", + "DOMMenuItemInactive cut", "DOMMenuInactive editpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive editmenu", "DOMMenuItemInactive editmenu" ]; + return [ "DOMMenuItemInactive copy", "DOMMenuInactive editpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive editmenu", "DOMMenuItemInactive editmenu", + "command copy", "popuphiding editpopup", "popuphidden editpopup", + "DOMMenuItemInactive copy" ]; + }, + test() { synthesizeKey("KEY_Enter"); }, + result(testname) { + checkClosed("editmenu", testname); + is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // pressing Alt + a key should open the corresponding menu + testname: "open with accelerator", + events() { + return [ "DOMMenuBarActive menubar", + "popupshowing viewpopup", "DOMMenuItemActive viewmenu", + "DOMMenuItemActive toolbar", "popupshown viewpopup" ]; + }, + test() { synthesizeKey("V", { altKey: true }); }, + result(testname) { + checkOpen("viewmenu", testname); + is(document.getElementById("viewmenu").openedWithKey, true, testname + " openedWithKey"); + } +}, +{ + // open the submenu with the cursor right key + testname: "open submenu with cursor right", + events() { + // on Windows, the disabled 'navigation' item can stll be highlihted + if (navigator.platform.indexOf("Win") == 0) + return [ "popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + } +}, +{ + // close the submenu with the cursor left key + testname: "close submenu with cursor left", + events() { + if (navigator.platform.indexOf("Win") == 0) + return [ "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "DOMMenuItemInactive navigation", "DOMMenuInactive toolbarpopup", + "DOMMenuItemActive toolbar" ]; + return [ "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "DOMMenuInactive toolbarpopup", + "DOMMenuItemActive toolbar" ]; + }, + test() { synthesizeKey("KEY_ArrowLeft"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkClosed("toolbar", testname); + } +}, +{ + // open the submenu with the enter key + testname: "open submenu with enter", + events() { + // on Windows, the disabled 'navigation' item can stll be highlighted + if (navigator.platform.indexOf("Win") == 0) + return [ "popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test() { synthesizeKey("KEY_Enter"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + }, +}, +{ + // close the submenu with the escape key + testname: "close submenu with escape", + events() { + if (navigator.platform.indexOf("Win") == 0) + return [ "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "DOMMenuItemInactive navigation", "DOMMenuInactive toolbarpopup", + "DOMMenuItemActive toolbar" ]; + return [ "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "DOMMenuInactive toolbarpopup", + "DOMMenuItemActive toolbar" ]; + }, + test() { synthesizeKey("KEY_Escape"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkClosed("toolbar", testname); + }, +}, +{ + // open the submenu with the enter key again + testname: "open submenu with enter again", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events() { + // on Windows, the disabled 'navigation' item can stll be highlighted + if (navigator.platform.indexOf("Win") == 0) + return [ "popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test() { synthesizeKey("KEY_Enter"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + }, +}, +{ + // while a submenu is open, switch to the next toplevel menu with the cursor right key + testname: "while a submenu is open, switch to the next menu with the cursor right", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuItemInactive viewmenu", "DOMMenuItemActive helpmenu", + "popuphiding toolbarpopup", "popuphidden toolbarpopup", + "popuphiding viewpopup", "popuphidden viewpopup", + "popupshowing helppopup", "DOMMenuItemInactive navigation", + "DOMMenuInactive toolbarpopup", "DOMMenuItemInactive toolbar", + "DOMMenuInactive viewpopup", "DOMMenuItemActive contents", + "popupshown helppopup" ], + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { + checkOpen("helpmenu", testname); + checkClosed("toolbar", testname); + checkClosed("viewmenu", testname); + } +}, +{ + // close the main menu with the escape key + testname: "close menubar menu with escape", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "popuphiding helppopup", "popuphidden helppopup", + "DOMMenuItemInactive contents", "DOMMenuInactive helppopup", + "DOMMenuBarInactive menubar", "DOMMenuItemInactive helpmenu" ], + test() { synthesizeKey("KEY_Escape"); }, + result(testname) { checkClosed("viewmenu", testname); }, +}, +{ + // close the main menu with the escape key + testname: "close menubar menu with escape", + condition() { return (navigator.platform.indexOf("Win") != 0) }, + events: [ "popuphiding viewpopup", "popuphidden viewpopup", + "DOMMenuItemInactive toolbar", "DOMMenuInactive viewpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive viewmenu" ], + test() { synthesizeKey("KEY_Escape"); }, + result(testname) { checkClosed("viewmenu", testname); }, +}, +{ + // Pressing Alt should highlight the first menu but not open it, + // but it should be ignored if the alt keydown event is consumed. + testname: "alt shouldn't activate menubar if keydown event is consumed", + test() { + document.addEventListener("keydown", function (aEvent) { + aEvent.preventDefault(); + }, {once: true}); + synthesizeKey("KEY_Alt"); + }, + result(testname) { + ok(!document.getElementById("filemenu").openedWithKey, testname); + checkClosed("filemenu", testname); + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it, + // but it should be ignored if the alt keyup event is consumed. + testname: "alt shouldn't activate menubar if keyup event is consumed", + test() { + document.addEventListener("keyup", function (aEvent) { + aEvent.preventDefault(); + }, {once: true}); + synthesizeKey("KEY_Alt"); + }, + result(testname) { + ok(!document.getElementById("filemenu").openedWithKey, testname); + checkClosed("filemenu", testname); + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it. + testname: "alt to activate menubar", + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_Alt"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey"); + checkClosed("filemenu", testname); + }, +}, +{ + // pressing cursor left should select the previous menu but not open it + testname: "cursor left on active menubar", + events: [ "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu" ], + test() { synthesizeKey("KEY_ArrowLeft"); }, + result(testname) { checkClosed("helpmenu", testname); }, +}, +{ + // pressing cursor right should select the previous menu but not open it + testname: "cursor right on active menubar", + events: [ "DOMMenuItemInactive helpmenu", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { checkClosed("filemenu", testname); }, +}, +{ + // pressing a character should act as an accelerator and open the menu + testname: "accelerator on active menubar", + events: [ "popupshowing helppopup", + "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu", + "DOMMenuItemActive contents", "popupshown helppopup" ], + test() { sendChar("h"); }, + result(testname) { + checkOpen("helpmenu", testname); + is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey"); + }, +}, +{ + // check that pressing cursor up skips non menuitems + testname: "cursor up wrap", + events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive about" ], + test() { synthesizeKey("KEY_ArrowUp"); }, + result(testname) { } +}, +{ + // check that pressing cursor down skips non menuitems + testname: "cursor down wrap", + events: [ "DOMMenuItemInactive about", "DOMMenuItemActive contents" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { } +}, +{ + // check that pressing a menuitem's accelerator selects it + testname: "menuitem accelerator", + events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", "DOMMenuInactive helppopup", + "DOMMenuBarInactive menubar", "DOMMenuItemInactive helpmenu", + "DOMMenuItemInactive helpmenu", + "command amenu", "popuphiding helppopup", "popuphidden helppopup", + "DOMMenuItemInactive amenu", + ], + test() { sendChar("m"); }, + result(testname) { checkClosed("helpmenu", testname); } +}, +{ + // pressing F10 should highlight the first menu. On Linux, the menu is opened. + testname: "F10 to activate menubar", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey"); + if (navigator.platform.includes("Linux")) + checkOpen("filemenu", testname); + else + checkClosed("filemenu", testname); + }, +}, +{ + // pressing cursor left then down should open a menu + testname: "cursor down on menu", + events: (navigator.platform.includes("Linux")) ? + [ "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu", + // This is in a different order than the + // "accelerator on active menubar" because menus opened from a + // shortcut key are fired asynchronously + "popuphiding filepopup", "popuphidden filepopup", + "popupshowing helppopup", + "DOMMenuItemActive item1", "DOMMenuItemInactive item1", + "DOMMenuInactive filepopup", + "popupshown helppopup" ] : + [ "popupshowing helppopup", "DOMMenuItemInactive filemenu", + "DOMMenuItemActive helpmenu", + // This is in a different order than the + // "accelerator on active menubar" because menus opened from a + // shortcut key are fired asynchronously + "DOMMenuItemActive contents", "popupshown helppopup" ], + test() { synthesizeKey("KEY_ArrowLeft"); synthesizeKey("KEY_ArrowDown"); }, + result(testname) { + is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey"); + } +}, +{ + // pressing a letter that doesn't correspond to an accelerator. The menu + // should not close because there is more than one item corresponding to + // that letter + testname: "menuitem with no accelerator", + events: (navigator.platform.includes("Linux")) ? + [ "DOMMenuItemActive one" ] : + [ "DOMMenuItemInactive contents", "DOMMenuItemActive one" ], + test() { sendChar("o"); }, + result(testname) { checkOpen("helpmenu", testname); } +}, +{ + // pressing the letter again should select the next one that starts with + // that letter + testname: "menuitem with no accelerator again", + events: [ "DOMMenuItemInactive one", "DOMMenuItemActive only" ], + test() { sendChar("o"); }, + result(testname) { + // 'only' is a menu but it should not be open + checkOpen("helpmenu", testname); + checkClosed("only", testname); + } +}, +{ + // pressing the letter again when the next item is disabled should still + // select the disabled item + testname: "menuitem with no accelerator disabled", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuItemInactive only", "DOMMenuItemActive other" ], + test() { sendChar("o"); }, + result(testname) { } +}, +{ + // when only one menuitem starting with that letter exists, it should be + // selected and the menu closed + testname: "menuitem with no accelerator single", + events() { + var elist = [ "DOMMenuItemInactive other", "DOMMenuItemActive third", + "DOMMenuItemInactive third", "DOMMenuInactive helppopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive helpmenu", + "DOMMenuItemInactive helpmenu", + "command third", "popuphiding helppopup", + "popuphidden helppopup", "DOMMenuItemInactive third", + ]; + if (!navigator.platform.includes("Win")) + elist[0] = "DOMMenuItemInactive only"; + return elist; + }, + test() { sendChar("t"); }, + result(testname) { checkClosed("helpmenu", testname); } +}, +{ + // pressing F10 should highlight the first menu but not open it + testname: "F10 to activate menubar again", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_F10"); }, + result(testname) { checkClosed("filemenu", testname); }, +}, +{ + // pressing an accelerator for a disabled item should deactivate the menubar + testname: "accelerator for disabled menu", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + test() { sendChar("s"); }, + result(testname) { + checkClosed("secretmenu", testname); + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + }, +}, +{ + testname: "press on disabled menu", + test() { + synthesizeMouse(document.getElementById("secretmenu"), 8, 8, { }); + }, + result (testname) { + checkClosed("secretmenu", testname); + } +}, +{ + testname: "press on second menu with shift", + events: [ "popupshowing editpopup", "DOMMenuBarActive menubar", + "DOMMenuItemActive editmenu", "popupshown editpopup" ], + test() { + synthesizeMouse(document.getElementById("editmenu"), 8, 8, { shiftKey : true }); + }, + result (testname) { + checkOpen("editmenu", testname); + checkActive(document.getElementById("menubar"), "editmenu", testname); + } +}, +{ + testname: "press on disabled menuitem", + test() { + synthesizeMouse(document.getElementById("cut"), 8, 8, { }); + }, + result (testname) { + checkOpen("editmenu", testname); + } +}, +{ + testname: "press on menuitem", + events: [ "DOMMenuInactive editpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive editmenu", + "DOMMenuItemInactive editmenu", + "command copy", "popuphiding editpopup", "popuphidden editpopup", + "DOMMenuItemInactive copy", + ], + test() { + synthesizeMouse(document.getElementById("copy"), 8, 8, { }); + }, + result (testname) { + checkClosed("editmenu", testname); + } +}, +{ + // this test ensures that the menu can still be opened by clicking after selecting + // a menuitem from the menu. See bug 399350. + testname: "press on menu after menuitem selected", + events: [ "popupshowing editpopup", "DOMMenuBarActive menubar", + "DOMMenuItemActive editmenu", "popupshown editpopup" ], + test() { synthesizeMouse(document.getElementById("editmenu"), 8, 8, { }); }, + result (testname) { + checkActive(document.getElementById("editpopup"), "", testname); + checkOpen("editmenu", testname); + } +}, +{ // try selecting a different command + testname: "press on menuitem again", + events: [ "DOMMenuInactive editpopup", + "DOMMenuBarInactive menubar", + "DOMMenuItemInactive editmenu", + "DOMMenuItemInactive editmenu", + "command paste", "popuphiding editpopup", "popuphidden editpopup", + "DOMMenuItemInactive paste", + ], + test() { + synthesizeMouse(document.getElementById("paste"), 8, 8, { }); + }, + result (testname) { + checkClosed("editmenu", testname); + } +}, +{ + testname: "F10 to activate menubar for tab deactivation", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with tab key", + events: closeAfterF10Events(true), + test() { synthesizeKey("KEY_Tab"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for escape deactivation", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with escape key", + events: closeAfterF10Events(false), + test() { synthesizeKey("KEY_Escape"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for f10 deactivation", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with f10 key", + events: closeAfterF10Events(true), + test() { synthesizeKey("KEY_F10"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for alt deactivation", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with alt key", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events: [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu" ], + test() { synthesizeKey("KEY_Alt"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "Don't activate menubar with mousedown during alt key auto-repeat", + test() { + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mousedown", altKey: true }); + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mouseup", altKey: true }); + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeKey("KEY_Alt", {type: "keyup"}); + }, + result (testname) { + checkActive(document.getElementById("menubar"), "", testname); + } +}, + +{ + testname: "Open menu and press alt key by itself - open menu", + events: [ "DOMMenuBarActive menubar", + "popupshowing filepopup", "DOMMenuItemActive filemenu", + "DOMMenuItemActive item1", "popupshown filepopup" ], + test() { synthesizeKey("F", { altKey: true }); }, + result (testname) { + checkOpen("filemenu", testname); + } +}, +{ + testname: "Open menu and press alt key by itself - close menu", + events: [ "popuphiding filepopup", "popuphidden filepopup", + "DOMMenuItemInactive item1", "DOMMenuInactive filepopup", + "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu", + "DOMMenuItemInactive filemenu" ], + test() { + synthesizeKey("KEY_Alt"); + }, + result (testname) { + checkClosed("filemenu", testname); + } +}, + +// Fllowing 4 tests are a test of bug 616797, don't insert any new tests +// between them. +{ + testname: "Open file menu by accelerator", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events() { + return [ "DOMMenuBarActive menubar", "popupshowing filepopup", + "DOMMenuItemActive filemenu", "DOMMenuItemActive item1", + "popupshown filepopup" ]; + }, + test() { + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeKey("f", {altKey: true}); + synthesizeKey("KEY_Alt", {type: "keyup"}); + } +}, +{ + testname: "Close file menu by click at outside of popup menu", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events() { + return [ "popuphiding filepopup", "popuphidden filepopup", + "DOMMenuItemInactive item1", "DOMMenuInactive filepopup", + "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu", + "DOMMenuItemInactive filemenu" ]; + }, + test() { + // XXX hidePopup() causes DOMMenuItemInactive event to be fired twice. + document.getElementById("filepopup").hidePopup(); + } +}, +{ + testname: "Alt keydown set focus the menubar", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events() { + return [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ]; + }, + test() { + synthesizeKey("KEY_Alt"); + }, + result (testname) { + checkClosed("filemenu", testname); + } +}, +{ + testname: "unset focus the menubar", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events() { + return [ "DOMMenuBarInactive menubar", "DOMMenuItemInactive filemenu" ]; + }, + test() { + synthesizeKey("KEY_Alt"); + } +}, + +// bug 625151 +{ + testname: "Alt key state before deactivating the window shouldn't prevent " + + "next Alt key handling", + condition() { return (navigator.platform.indexOf("Win") == 0) }, + events() { + return [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ]; + }, + test() { + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeKey("KEY_Tab", {type: "keydown"}); // cancels the Alt key + var thisWindow = window; + var newWindow = + window.open("javascript:'<html><body>dummy</body></html>';", "_blank", "width=100,height=100"); + newWindow.addEventListener("focus", function () { + thisWindow.addEventListener("focus", function () { + setTimeout(function () { + synthesizeKey("KEY_Alt", {}, thisWindow); + }, 0); + }, {once: true}); + newWindow.close(); + thisWindow.focus(); + }, {once: true}); + } +} + +]; + +]]> +</script> + +</window> diff --git a/toolkit/content/timepicker.xhtml b/toolkit/content/timepicker.xhtml new file mode 100644 index 0000000000..b7ec30400d --- /dev/null +++ b/toolkit/content/timepicker.xhtml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> + <title>Time Picker</title> + <link rel="stylesheet" href="chrome://global/skin/datetimeinputpickers.css"/> + <script src="chrome://global/content/bindings/timekeeper.js"></script> + <script src="chrome://global/content/bindings/spinner.js"></script> + <script src="chrome://global/content/bindings/timepicker.js"></script> +</head> +<body> + <div id="time-picker"></div> + <template id="spinner-template"> + <div class="spinner-container"> + <button class="up"/> + <div class="spinner"></div> + <button class="down"/> + </div> + </template> + <script> + /* import-globals-from widgets/timepicker.js */ + // Create a TimePicker instance and prepare to be + // initialized by the "TimePickerInit" event from timepicker.xml + new TimePicker(document.getElementById("time-picker")); + </script> +</body> +</html> diff --git a/toolkit/content/treeUtils.js b/toolkit/content/treeUtils.js new file mode 100644 index 0000000000..98673ee845 --- /dev/null +++ b/toolkit/content/treeUtils.js @@ -0,0 +1,87 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* 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 gTreeUtils = { + deleteAll(aTree, aView, aItems, aDeletedItems) { + for (var i = 0; i < aItems.length; ++i) { + aDeletedItems.push(aItems[i]); + } + aItems.splice(0, aItems.length); + var oldCount = aView.rowCount; + aView._rowCount = 0; + aTree.rowCountChanged(0, -oldCount); + }, + + deleteSelectedItems(aTree, aView, aItems, aDeletedItems) { + var selection = aTree.view.selection; + selection.selectEventsSuppressed = true; + + var rc = selection.getRangeCount(); + for (var i = 0; i < rc; ++i) { + var min = {}; + var max = {}; + selection.getRangeAt(i, min, max); + for (let j = min.value; j <= max.value; ++j) { + aDeletedItems.push(aItems[j]); + aItems[j] = null; + } + } + + var nextSelection = 0; + for (i = 0; i < aItems.length; ++i) { + if (!aItems[i]) { + let j = i; + while (j < aItems.length && !aItems[j]) { + ++j; + } + aItems.splice(i, j - i); + nextSelection = j < aView.rowCount ? j - 1 : j - 2; + aView._rowCount -= j - i; + aTree.rowCountChanged(i, i - j); + } + } + + if (aItems.length) { + selection.select(nextSelection); + aTree.ensureRowIsVisible(nextSelection); + aTree.focus(); + } + selection.selectEventsSuppressed = false; + }, + + sort( + aTree, + aView, + aDataSet, + aColumn, + aComparator, + aLastSortColumn, + aLastSortAscending + ) { + var ascending = aColumn == aLastSortColumn ? !aLastSortAscending : true; + if (!aDataSet.length) { + return ascending; + } + + var sortFunction = null; + if (aComparator) { + sortFunction = function(a, b) { + return aComparator(a[aColumn], b[aColumn]); + }; + } + aDataSet.sort(sortFunction); + if (!ascending) { + aDataSet.reverse(); + } + + aTree.view.selection.clearSelection(); + aTree.view.selection.select(0); + aTree.invalidate(); + aTree.ensureRowIsVisible(0); + + return ascending; + }, +}; diff --git a/toolkit/content/viewZoomOverlay.js b/toolkit/content/viewZoomOverlay.js new file mode 100644 index 0000000000..877651a7b1 --- /dev/null +++ b/toolkit/content/viewZoomOverlay.js @@ -0,0 +1,133 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + +/* 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/. */ + +/** Document Zoom Management Code + * + * To use this, you'll need to have a global gBrowser variable + * or use the methods that accept a browser to be modified. + **/ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var ZoomManager = { + get MIN() { + delete this.MIN; + return (this.MIN = Services.prefs.getIntPref("zoom.minPercent") / 100); + }, + + get MAX() { + delete this.MAX; + return (this.MAX = Services.prefs.getIntPref("zoom.maxPercent") / 100); + }, + + get useFullZoom() { + return Services.prefs.getBoolPref("browser.zoom.full"); + }, + + set useFullZoom(aVal) { + Services.prefs.setBoolPref("browser.zoom.full", aVal); + return aVal; + }, + + get zoom() { + return this.getZoomForBrowser(gBrowser); + }, + + useFullZoomForBrowser: function ZoomManager_useFullZoomForBrowser(aBrowser) { + return this.useFullZoom || aBrowser.isSyntheticDocument; + }, + + getFullZoomForBrowser: function ZoomManager_getFullZoomForBrowser(aBrowser) { + if (!this.useFullZoomForBrowser(aBrowser)) { + return 1.0; + } + return this.getZoomForBrowser(aBrowser); + }, + + getZoomForBrowser: function ZoomManager_getZoomForBrowser(aBrowser) { + let zoom = this.useFullZoomForBrowser(aBrowser) + ? aBrowser.fullZoom + : aBrowser.textZoom; + // Round to remove any floating-point error. + return Number(zoom ? zoom.toFixed(2) : 1); + }, + + set zoom(aVal) { + this.setZoomForBrowser(gBrowser, aVal); + return aVal; + }, + + setZoomForBrowser: function ZoomManager_setZoomForBrowser(aBrowser, aVal) { + if (aVal < this.MIN || aVal > this.MAX) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); + } + + if (this.useFullZoomForBrowser(aBrowser)) { + aBrowser.textZoom = 1; + aBrowser.fullZoom = aVal; + } else { + aBrowser.textZoom = aVal; + aBrowser.fullZoom = 1; + } + }, + + get zoomValues() { + var zoomValues = Services.prefs + .getCharPref("toolkit.zoomManager.zoomValues") + .split(",") + .map(parseFloat); + zoomValues.sort((a, b) => a - b); + + while (zoomValues[0] < this.MIN) { + zoomValues.shift(); + } + + while (zoomValues[zoomValues.length - 1] > this.MAX) { + zoomValues.pop(); + } + + delete this.zoomValues; + return (this.zoomValues = zoomValues); + }, + + enlarge: function ZoomManager_enlarge() { + var i = this.zoomValues.indexOf(this.snap(this.zoom)) + 1; + if (i < this.zoomValues.length) { + this.zoom = this.zoomValues[i]; + } + }, + + reduce: function ZoomManager_reduce() { + var i = this.zoomValues.indexOf(this.snap(this.zoom)) - 1; + if (i >= 0) { + this.zoom = this.zoomValues[i]; + } + }, + + reset: function ZoomManager_reset() { + this.zoom = 1; + }, + + toggleZoom: function ZoomManager_toggleZoom() { + var zoomLevel = this.zoom; + + this.useFullZoom = !this.useFullZoom; + this.zoom = zoomLevel; + }, + + snap: function ZoomManager_snap(aVal) { + var values = this.zoomValues; + for (var i = 0; i < values.length; i++) { + if (values[i] >= aVal) { + if (i > 0 && aVal - values[i - 1] < values[i] - aVal) { + i--; + } + return values[i]; + } + } + return values[i - 1]; + }, +}; diff --git a/toolkit/content/widgets.css b/toolkit/content/widgets.css new file mode 100644 index 0000000000..28c8753f50 --- /dev/null +++ b/toolkit/content/widgets.css @@ -0,0 +1,25 @@ +/* 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/. */ + +/* ===== widgets.css ===================================================== + == Styles ported from XBL <resources>, loaded by "global.css". + ======================================================================= */ + +@import url("chrome://global/content/autocomplete.css"); +@import url("chrome://global/skin/autocomplete.css"); +@import url("chrome://global/skin/button.css"); +@import url("chrome://global/skin/checkbox.css"); +@import url("chrome://global/skin/findBar.css"); +@import url("chrome://global/skin/menu.css"); +@import url("chrome://global/skin/notification.css"); +@import url("chrome://global/skin/numberinput.css"); +@import url("chrome://global/skin/popup.css"); +@import url("chrome://global/skin/popupnotification.css"); +@import url("chrome://global/skin/radio.css"); +@import url("chrome://global/skin/richlistbox.css"); +@import url("chrome://global/skin/splitter.css"); +@import url("chrome://global/skin/tabbox.css"); +@import url("chrome://global/skin/toolbar.css"); +@import url("chrome://global/skin/toolbarbutton.css"); +@import url("chrome://global/skin/tree.css"); diff --git a/toolkit/content/widgets/arrowscrollbox.js b/toolkit/content/widgets/arrowscrollbox.js new file mode 100644 index 0000000000..eda0c37d0f --- /dev/null +++ b/toolkit/content/widgets/arrowscrollbox.js @@ -0,0 +1,864 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + class MozArrowScrollbox extends MozElements.BaseControl { + static get inheritedAttributes() { + return { + "#scrollbutton-up": "disabled=scrolledtostart", + ".scrollbox-clip": "orient", + scrollbox: "orient,align,pack,dir,smoothscroll", + "#scrollbutton-down": "disabled=scrolledtoend", + }; + } + + get markup() { + return ` + <html:link rel="stylesheet" href="chrome://global/skin/toolbarbutton.css"/> + <html:link rel="stylesheet" href="chrome://global/skin/arrowscrollbox.css"/> + <toolbarbutton id="scrollbutton-up" part="scrollbutton-up"/> + <spacer part="overflow-start-indicator"/> + <box class="scrollbox-clip" part="scrollbox-clip" flex="1"> + <scrollbox part="scrollbox" flex="1"> + <html:slot/> + </scrollbox> + </box> + <spacer part="overflow-end-indicator"/> + <toolbarbutton id="scrollbutton-down" part="scrollbutton-down"/> + `; + } + + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(this.fragment); + + this.scrollbox = this.shadowRoot.querySelector("scrollbox"); + this._scrollButtonUp = this.shadowRoot.getElementById("scrollbutton-up"); + this._scrollButtonDown = this.shadowRoot.getElementById( + "scrollbutton-down" + ); + + this._arrowScrollAnim = { + scrollbox: this, + requestHandle: 0, + /* 0 indicates there is no pending request */ + start: function arrowSmoothScroll_start() { + this.lastFrameTime = window.performance.now(); + if (!this.requestHandle) { + this.requestHandle = window.requestAnimationFrame( + this.sample.bind(this) + ); + } + }, + stop: function arrowSmoothScroll_stop() { + window.cancelAnimationFrame(this.requestHandle); + this.requestHandle = 0; + }, + sample: function arrowSmoothScroll_handleEvent(timeStamp) { + const scrollIndex = this.scrollbox._scrollIndex; + const timePassed = timeStamp - this.lastFrameTime; + this.lastFrameTime = timeStamp; + + const scrollDelta = 0.5 * timePassed * scrollIndex; + this.scrollbox.scrollByPixels(scrollDelta, true); + this.requestHandle = window.requestAnimationFrame( + this.sample.bind(this) + ); + }, + }; + + this._scrollIndex = 0; + this._scrollIncrement = null; + this._ensureElementIsVisibleAnimationFrame = 0; + this._prevMouseScrolls = [null, null]; + this._touchStart = -1; + this._scrollButtonUpdatePending = false; + this._isScrolling = false; + this._destination = 0; + this._direction = 0; + + this.addEventListener("wheel", this.on_wheel); + this.addEventListener("touchstart", this.on_touchstart); + this.addEventListener("touchmove", this.on_touchmove); + this.addEventListener("touchend", this.on_touchend); + this.shadowRoot.addEventListener("click", this.on_click.bind(this)); + this.shadowRoot.addEventListener( + "mousedown", + this.on_mousedown.bind(this) + ); + this.shadowRoot.addEventListener( + "mouseover", + this.on_mouseover.bind(this) + ); + this.shadowRoot.addEventListener("mouseup", this.on_mouseup.bind(this)); + this.shadowRoot.addEventListener("mouseout", this.on_mouseout.bind(this)); + + // These events don't get retargeted outside of the shadow root, but + // some callers like tests wait for these events. So run handlers + // and then retarget events from the scrollbox to the host. + this.scrollbox.addEventListener( + "underflow", + event => { + this.on_underflow(event); + this.dispatchEvent(new Event("underflow")); + }, + true + ); + this.scrollbox.addEventListener( + "overflow", + event => { + this.on_overflow(event); + this.dispatchEvent(new Event("overflow")); + }, + true + ); + this.scrollbox.addEventListener("scroll", event => { + this.on_scroll(event); + this.dispatchEvent(new Event("scroll")); + }); + + this.scrollbox.addEventListener("scrollend", event => { + this.on_scrollend(event); + this.dispatchEvent(new Event("scrollend")); + }); + } + + connectedCallback() { + if (this.hasConnected) { + return; + } + this.hasConnected = true; + + if (!this.hasAttribute("smoothscroll")) { + this.smoothScroll = Services.prefs.getBoolPref( + "toolkit.scrollbox.smoothScroll", + true + ); + } + + this.removeAttribute("overflowing"); + this.initializeAttributeInheritance(); + this._updateScrollButtonsDisabledState(); + } + + get fragment() { + if (!this.constructor.hasOwnProperty("_fragment")) { + this.constructor._fragment = MozXULElement.parseXULToFragment( + this.markup + ); + } + return document.importNode(this.constructor._fragment, true); + } + + get _clickToScroll() { + return this.hasAttribute("clicktoscroll"); + } + + get _scrollDelay() { + if (this._clickToScroll) { + return Services.prefs.getIntPref( + "toolkit.scrollbox.clickToScroll.scrollDelay", + 150 + ); + } + + // Use the same REPEAT_DELAY as "nsRepeatService.h". + return /Mac/.test(navigator.platform) ? 25 : 50; + } + + get scrollIncrement() { + if (this._scrollIncrement === null) { + this._scrollIncrement = Services.prefs.getIntPref( + "toolkit.scrollbox.scrollIncrement", + 20 + ); + } + return this._scrollIncrement; + } + + set smoothScroll(val) { + this.setAttribute("smoothscroll", !!val); + return val; + } + + get smoothScroll() { + return this.getAttribute("smoothscroll") == "true"; + } + + get scrollClientRect() { + return this.scrollbox.getBoundingClientRect(); + } + + get scrollClientSize() { + return this.getAttribute("orient") == "vertical" + ? this.scrollbox.clientHeight + : this.scrollbox.clientWidth; + } + + get scrollSize() { + return this.getAttribute("orient") == "vertical" + ? this.scrollbox.scrollHeight + : this.scrollbox.scrollWidth; + } + + get lineScrollAmount() { + // line scroll amout should be the width (at horizontal scrollbox) or + // the height (at vertical scrollbox) of the scrolled elements. + // However, the elements may have different width or height. So, + // for consistent speed, let's use avalage with of the elements. + var elements = this._getScrollableElements(); + return elements.length && this.scrollSize / elements.length; + } + + get scrollPosition() { + return this.getAttribute("orient") == "vertical" + ? this.scrollbox.scrollTop + : this.scrollbox.scrollLeft; + } + + get startEndProps() { + if (!this._startEndProps) { + this._startEndProps = + this.getAttribute("orient") == "vertical" + ? ["top", "bottom"] + : ["left", "right"]; + } + return this._startEndProps; + } + + get isRTLScrollbox() { + if (!this._isRTLScrollbox) { + this._isRTLScrollbox = + this.getAttribute("orient") != "vertical" && + document.defaultView.getComputedStyle(this.scrollbox).direction == + "rtl"; + } + return this._isRTLScrollbox; + } + + _onButtonClick(event) { + if (this._clickToScroll) { + this._distanceScroll(event); + } + } + + _onButtonMouseDown(event, index) { + if (this._clickToScroll && event.button == 0) { + this._startScroll(index); + } + } + + _onButtonMouseUp(event) { + if (this._clickToScroll && event.button == 0) { + this._stopScroll(); + } + } + + _onButtonMouseOver(index) { + if (this._clickToScroll) { + this._continueScroll(index); + } else { + this._startScroll(index); + } + } + + _onButtonMouseOut() { + if (this._clickToScroll) { + this._pauseScroll(); + } else { + this._stopScroll(); + } + } + + _boundsWithoutFlushing(element) { + if (!("_DOMWindowUtils" in this)) { + this._DOMWindowUtils = window.windowUtils; + } + + return this._DOMWindowUtils + ? this._DOMWindowUtils.getBoundsWithoutFlushing(element) + : element.getBoundingClientRect(); + } + + _canScrollToElement(element) { + if (element.hidden) { + return false; + } + + // See if the element is hidden via CSS without the hidden attribute. + // If we get only zeros for the client rect, this means the element + // is hidden. As a performance optimization, we don't flush layout + // here which means that on the fly changes aren't fully supported. + let rect = this._boundsWithoutFlushing(element); + return !!(rect.top || rect.left || rect.width || rect.height); + } + + ensureElementIsVisible(element, aInstant) { + if (!this._canScrollToElement(element)) { + return; + } + + if (this._ensureElementIsVisibleAnimationFrame) { + window.cancelAnimationFrame(this._ensureElementIsVisibleAnimationFrame); + } + this._ensureElementIsVisibleAnimationFrame = window.requestAnimationFrame( + () => { + element.scrollIntoView({ + block: "nearest", + behavior: aInstant ? "instant" : "auto", + }); + this._ensureElementIsVisibleAnimationFrame = 0; + } + ); + } + + scrollByIndex(index, aInstant) { + if (index == 0) { + return; + } + + var rect = this.scrollClientRect; + var [start, end] = this.startEndProps; + var x = index > 0 ? rect[end] + 1 : rect[start] - 1; + var nextElement = this._elementFromPoint(x, index); + if (!nextElement) { + return; + } + + var targetElement; + if (this.isRTLScrollbox) { + index *= -1; + } + while (index < 0 && nextElement) { + if (this._canScrollToElement(nextElement)) { + targetElement = nextElement; + } + nextElement = nextElement.previousElementSibling; + index++; + } + while (index > 0 && nextElement) { + if (this._canScrollToElement(nextElement)) { + targetElement = nextElement; + } + nextElement = nextElement.nextElementSibling; + index--; + } + if (!targetElement) { + return; + } + + this.ensureElementIsVisible(targetElement, aInstant); + } + + _getScrollableElements() { + let nodes = this.children; + if (nodes.length == 1) { + let node = nodes[0]; + if ( + node.localName == "slot" && + node.namespaceURI == "http://www.w3.org/1999/xhtml" + ) { + nodes = node.getRootNode().host.children; + } + } + return Array.prototype.filter.call(nodes, this._canScrollToElement, this); + } + + _elementFromPoint(aX, aPhysicalScrollDir) { + var elements = this._getScrollableElements(); + if (!elements.length) { + return null; + } + + if (this.isRTLScrollbox) { + elements.reverse(); + } + + var [start, end] = this.startEndProps; + var low = 0; + var high = elements.length - 1; + + if ( + aX < elements[low].getBoundingClientRect()[start] || + aX > elements[high].getBoundingClientRect()[end] + ) { + return null; + } + + var mid, rect; + while (low <= high) { + mid = Math.floor((low + high) / 2); + rect = elements[mid].getBoundingClientRect(); + if (rect[start] > aX) { + high = mid - 1; + } else if (rect[end] < aX) { + low = mid + 1; + } else { + return elements[mid]; + } + } + + // There's no element at the requested coordinate, but the algorithm + // from above yields an element next to it, in a random direction. + // The desired scrolling direction leads to the correct element. + + if (!aPhysicalScrollDir) { + return null; + } + + if (aPhysicalScrollDir < 0 && rect[start] > aX) { + mid = Math.max(mid - 1, 0); + } else if (aPhysicalScrollDir > 0 && rect[end] < aX) { + mid = Math.min(mid + 1, elements.length - 1); + } + + return elements[mid]; + } + + _startScroll(index) { + if (this.isRTLScrollbox) { + index *= -1; + } + + if (this._clickToScroll) { + this._scrollIndex = index; + this._mousedown = true; + + if (this.smoothScroll) { + this._arrowScrollAnim.start(); + return; + } + } + + if (!this._scrollTimer) { + this._scrollTimer = Cc["@mozilla.org/timer;1"].createInstance( + Ci.nsITimer + ); + } else { + this._scrollTimer.cancel(); + } + + let callback; + if (this._clickToScroll) { + callback = () => { + if (!document && this._scrollTimer) { + this._scrollTimer.cancel(); + } + this.scrollByIndex(this._scrollIndex); + }; + } else { + callback = () => this.scrollByPixels(this.scrollIncrement * index); + } + + this._scrollTimer.initWithCallback( + callback, + this._scrollDelay, + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + + callback(); + } + + _stopScroll() { + if (this._scrollTimer) { + this._scrollTimer.cancel(); + } + + if (this._clickToScroll) { + this._mousedown = false; + if (!this._scrollIndex || !this.smoothScroll) { + return; + } + + this.scrollByIndex(this._scrollIndex); + this._scrollIndex = 0; + + this._arrowScrollAnim.stop(); + } + } + + _pauseScroll() { + if (this._mousedown) { + this._stopScroll(); + this._mousedown = true; + document.addEventListener("mouseup", this); + document.addEventListener("blur", this, true); + } + } + + _continueScroll(index) { + if (this._mousedown) { + this._startScroll(index); + } + } + + _distanceScroll(aEvent) { + if (aEvent.detail < 2 || aEvent.detail > 3) { + return; + } + + var scrollBack = aEvent.originalTarget == this._scrollButtonUp; + var scrollLeftOrUp = this.isRTLScrollbox ? !scrollBack : scrollBack; + var targetElement; + + if (aEvent.detail == 2) { + // scroll by the size of the scrollbox + let [start, end] = this.startEndProps; + let x; + if (scrollLeftOrUp) { + x = this.scrollClientRect[start] - this.scrollClientSize; + } else { + x = this.scrollClientRect[end] + this.scrollClientSize; + } + targetElement = this._elementFromPoint(x, scrollLeftOrUp ? -1 : 1); + + // the next partly-hidden element will become fully visible, + // so don't scroll too far + if (targetElement) { + targetElement = scrollBack + ? targetElement.nextElementSibling + : targetElement.previousElementSibling; + } + } + + if (!targetElement) { + // scroll to the first resp. last element + let elements = this._getScrollableElements(); + targetElement = scrollBack + ? elements[0] + : elements[elements.length - 1]; + } + + this.ensureElementIsVisible(targetElement); + } + + handleEvent(aEvent) { + if ( + aEvent.type == "mouseup" || + (aEvent.type == "blur" && aEvent.target == document) + ) { + this._mousedown = false; + document.removeEventListener("mouseup", this); + document.removeEventListener("blur", this, true); + } + } + + scrollByPixels(aPixels, aInstant) { + let scrollOptions = { behavior: aInstant ? "instant" : "auto" }; + scrollOptions[this.startEndProps[0]] = aPixels; + this.scrollbox.scrollBy(scrollOptions); + } + + _updateScrollButtonsDisabledState() { + if (!this.hasAttribute("overflowing")) { + this.setAttribute("scrolledtoend", "true"); + this.setAttribute("scrolledtostart", "true"); + return; + } + + if (this._scrollButtonUpdatePending) { + return; + } + this._scrollButtonUpdatePending = true; + + // Wait until after the next paint to get current layout data from + // getBoundsWithoutFlushing. + window.requestAnimationFrame(() => { + setTimeout(() => { + if (!this.isConnected) { + // We've been destroyed in the meantime. + return; + } + + this._scrollButtonUpdatePending = false; + + let scrolledToStart = false; + let scrolledToEnd = false; + + if (!this.hasAttribute("overflowing")) { + scrolledToStart = true; + scrolledToEnd = true; + } else { + let [leftOrTop, rightOrBottom] = this.startEndProps; + let leftOrTopEdge = ele => + Math.round(this._boundsWithoutFlushing(ele)[leftOrTop]); + let rightOrBottomEdge = ele => + Math.round(this._boundsWithoutFlushing(ele)[rightOrBottom]); + + let elements = this._getScrollableElements(); + let [leftOrTopElement, rightOrBottomElement] = [ + elements[0], + elements[elements.length - 1], + ]; + if (this.isRTLScrollbox) { + [leftOrTopElement, rightOrBottomElement] = [ + rightOrBottomElement, + leftOrTopElement, + ]; + } + + if ( + leftOrTopElement && + leftOrTopEdge(leftOrTopElement) >= leftOrTopEdge(this.scrollbox) + ) { + scrolledToStart = !this.isRTLScrollbox; + scrolledToEnd = this.isRTLScrollbox; + } else if ( + rightOrBottomElement && + rightOrBottomEdge(rightOrBottomElement) <= + rightOrBottomEdge(this.scrollbox) + ) { + scrolledToStart = this.isRTLScrollbox; + scrolledToEnd = !this.isRTLScrollbox; + } + } + + if (scrolledToEnd) { + this.setAttribute("scrolledtoend", "true"); + } else { + this.removeAttribute("scrolledtoend"); + } + + if (scrolledToStart) { + this.setAttribute("scrolledtostart", "true"); + } else { + this.removeAttribute("scrolledtostart"); + } + }, 0); + }); + } + + disconnectedCallback() { + // Release timer to avoid reference cycles. + if (this._scrollTimer) { + this._scrollTimer.cancel(); + this._scrollTimer = null; + } + } + + on_wheel(event) { + // Don't consume the event if we can't scroll. + if (!this.hasAttribute("overflowing")) { + return; + } + + let doScroll = false; + let instant; + let scrollAmount = 0; + if (this.getAttribute("orient") == "vertical") { + doScroll = true; + if (event.deltaMode == event.DOM_DELTA_PIXEL) { + scrollAmount = event.deltaY; + } else if (event.deltaMode == event.DOM_DELTA_PAGE) { + scrollAmount = event.deltaY * this.scrollClientSize; + } else { + scrollAmount = event.deltaY * this.lineScrollAmount; + } + } else { + // We allow vertical scrolling to scroll a horizontal scrollbox + // because many users have a vertical scroll wheel but no + // horizontal support. + // Because of this, we need to avoid scrolling chaos on trackpads + // and mouse wheels that support simultaneous scrolling in both axes. + // We do this by scrolling only when the last two scroll events were + // on the same axis as the current scroll event. + // For diagonal scroll events we only respect the dominant axis. + let isVertical = Math.abs(event.deltaY) > Math.abs(event.deltaX); + let delta = isVertical ? event.deltaY : event.deltaX; + let scrollByDelta = isVertical && this.isRTLScrollbox ? -delta : delta; + + if (this._prevMouseScrolls.every(prev => prev == isVertical)) { + doScroll = true; + if (event.deltaMode == event.DOM_DELTA_PIXEL) { + scrollAmount = scrollByDelta; + instant = true; + } else if (event.deltaMode == event.DOM_DELTA_PAGE) { + scrollAmount = scrollByDelta * this.scrollClientSize; + } else { + scrollAmount = scrollByDelta * this.lineScrollAmount; + } + } + + if (this._prevMouseScrolls.length > 1) { + this._prevMouseScrolls.shift(); + } + this._prevMouseScrolls.push(isVertical); + } + + if (doScroll) { + let direction = scrollAmount < 0 ? -1 : 1; + let startPos = this.scrollPosition; + + if (!this._isScrolling || this._direction != direction) { + this._destination = startPos + scrollAmount; + this._direction = direction; + } else { + // We were already in the process of scrolling in this direction + this._destination = this._destination + scrollAmount; + scrollAmount = this._destination - startPos; + } + this.scrollByPixels(scrollAmount, instant); + } + + event.stopPropagation(); + event.preventDefault(); + } + + on_touchstart(event) { + if (event.touches.length > 1) { + // Multiple touch points detected, abort. In particular this aborts + // the panning gesture when the user puts a second finger down after + // already panning with one finger. Aborting at this point prevents + // the pan gesture from being resumed until all fingers are lifted + // (as opposed to when the user is back down to one finger). + this._touchStart = -1; + } else { + this._touchStart = + this.getAttribute("orient") == "vertical" + ? event.touches[0].screenY + : event.touches[0].screenX; + } + } + + on_touchmove(event) { + if (event.touches.length == 1 && this._touchStart >= 0) { + var touchPoint = + this.getAttribute("orient") == "vertical" + ? event.touches[0].screenY + : event.touches[0].screenX; + var delta = this._touchStart - touchPoint; + if (Math.abs(delta) > 0) { + this.scrollByPixels(delta, true); + this._touchStart = touchPoint; + } + event.preventDefault(); + } + } + + on_touchend(event) { + this._touchStart = -1; + } + + on_underflow(event) { + // Ignore underflow events: + // - from nested scrollable elements + // - corresponding to an overflow event that we ignored + if (event.target != this.scrollbox || !this.hasAttribute("overflowing")) { + return; + } + + // Ignore events that doesn't match our orientation. + // Scrollport event orientation: + // 0: vertical + // 1: horizontal + // 2: both + if (this.getAttribute("orient") == "vertical") { + if (event.detail == 1) { + return; + } + } else if (event.detail == 0) { + // horizontal scrollbox + return; + } + + this.removeAttribute("overflowing"); + this._updateScrollButtonsDisabledState(); + } + + on_overflow(event) { + // Ignore overflow events: + // - from nested scrollable elements + if (event.target != this.scrollbox) { + return; + } + + // Ignore events that doesn't match our orientation. + // Scrollport event orientation: + // 0: vertical + // 1: horizontal + // 2: both + if (this.getAttribute("orient") == "vertical") { + if (event.detail == 1) { + return; + } + } else if (event.detail == 0) { + // horizontal scrollbox + return; + } + + this.setAttribute("overflowing", "true"); + this._updateScrollButtonsDisabledState(); + } + + on_scroll(event) { + this._isScrolling = true; + this._updateScrollButtonsDisabledState(); + } + + on_scrollend(event) { + this._isScrolling = false; + this._destination = 0; + this._direction = 0; + } + + on_click(event) { + if ( + event.originalTarget != this._scrollButtonUp && + event.originalTarget != this._scrollButtonDown + ) { + return; + } + this._onButtonClick(event); + } + + on_mousedown(event) { + if (event.originalTarget == this._scrollButtonUp) { + this._onButtonMouseDown(event, -1); + } + if (event.originalTarget == this._scrollButtonDown) { + this._onButtonMouseDown(event, 1); + } + } + + on_mouseup(event) { + if ( + event.originalTarget != this._scrollButtonUp && + event.originalTarget != this._scrollButtonDown + ) { + return; + } + this._onButtonMouseUp(event); + } + + on_mouseover(event) { + if (event.originalTarget == this._scrollButtonUp) { + this._onButtonMouseOver(-1); + } + if (event.originalTarget == this._scrollButtonDown) { + this._onButtonMouseOver(1); + } + } + + on_mouseout(event) { + if ( + event.originalTarget != this._scrollButtonUp && + event.originalTarget != this._scrollButtonDown + ) { + return; + } + this._onButtonMouseOut(); + } + } + + customElements.define("arrowscrollbox", MozArrowScrollbox); +} diff --git a/toolkit/content/widgets/autocomplete-input.js b/toolkit/content/widgets/autocomplete-input.js new file mode 100644 index 0000000000..935590cd14 --- /dev/null +++ b/toolkit/content/widgets/autocomplete-input.js @@ -0,0 +1,674 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" + ); + + class AutocompleteInput extends HTMLInputElement { + constructor() { + super(); + + this.popupSelectedIndex = -1; + + ChromeUtils.defineModuleGetter( + this, + "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm" + ); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "disablePopupAutohide", + "ui.popup.disable_autohide", + false + ); + + this.addEventListener("input", event => { + this.onInput(event); + }); + + this.addEventListener("keydown", event => this.handleKeyDown(event)); + + this.addEventListener( + "compositionstart", + event => { + if ( + this.mController.input.wrappedJSObject == this.nsIAutocompleteInput + ) { + this.mController.handleStartComposition(); + } + }, + true + ); + + this.addEventListener( + "compositionend", + event => { + if ( + this.mController.input.wrappedJSObject == this.nsIAutocompleteInput + ) { + this.mController.handleEndComposition(); + } + }, + true + ); + + this.addEventListener( + "focus", + event => { + this.attachController(); + if ( + window.gBrowser && + window.gBrowser.selectedBrowser.hasAttribute("usercontextid") + ) { + this.userContextId = parseInt( + window.gBrowser.selectedBrowser.getAttribute("usercontextid") + ); + } else { + this.userContextId = 0; + } + }, + true + ); + + this.addEventListener( + "blur", + event => { + if (!this._dontBlur) { + if (this.forceComplete && this.mController.matchCount >= 1) { + // If forceComplete is requested, we need to call the enter processing + // on blur so the input will be forced to the closest match. + // Thunderbird is the only consumer of forceComplete and this is used + // to force an recipient's email to the exact address book entry. + this.mController.handleEnter(true); + } + if (!this.ignoreBlurWhileSearching) { + this._dontClosePopup = this.disablePopupAutohide; + this.detachController(); + } + } + }, + true + ); + } + + connectedCallback() { + this.setAttribute("is", "autocomplete-input"); + this.setAttribute("autocomplete", "off"); + + this.mController = Cc[ + "@mozilla.org/autocomplete/controller;1" + ].getService(Ci.nsIAutoCompleteController); + this.mSearchNames = null; + this.mIgnoreInput = false; + this.noRollupOnEmptySearch = false; + + this._popup = null; + + this.nsIAutocompleteInput = this.getCustomInterfaceCallback( + Ci.nsIAutoCompleteInput + ); + + this.valueIsTyped = false; + } + + get popup() { + // Memoize the result in a field rather than replacing this property, + // so that it can be reset along with the binding. + if (this._popup) { + return this._popup; + } + + let popup = null; + let popupId = this.getAttribute("autocompletepopup"); + if (popupId) { + popup = document.getElementById(popupId); + } + + /* This path is only used in tests, we have the <popupset> and <panel> + in document for other usages */ + if (!popup) { + popup = document.createXULElement("panel", { + is: "autocomplete-richlistbox-popup", + }); + popup.setAttribute("type", "autocomplete-richlistbox"); + popup.setAttribute("noautofocus", "true"); + + if (!this._popupset) { + this._popupset = document.createXULElement("popupset"); + document.documentElement.appendChild(this._popupset); + } + + this._popupset.appendChild(popup); + } + popup.mInput = this; + + return (this._popup = popup); + } + + get popupElement() { + return this.popup; + } + + get controller() { + return this.mController; + } + + set popupOpen(val) { + if (val) { + this.openPopup(); + } else { + this.closePopup(); + } + } + + get popupOpen() { + return this.popup.popupOpen; + } + + set disableAutoComplete(val) { + this.setAttribute("disableautocomplete", val); + return val; + } + + get disableAutoComplete() { + return this.getAttribute("disableautocomplete") == "true"; + } + + set completeDefaultIndex(val) { + this.setAttribute("completedefaultindex", val); + return val; + } + + get completeDefaultIndex() { + return this.getAttribute("completedefaultindex") == "true"; + } + + set completeSelectedIndex(val) { + this.setAttribute("completeselectedindex", val); + return val; + } + + get completeSelectedIndex() { + return this.getAttribute("completeselectedindex") == "true"; + } + + set forceComplete(val) { + this.setAttribute("forcecomplete", val); + return val; + } + + get forceComplete() { + return this.getAttribute("forcecomplete") == "true"; + } + + set minResultsForPopup(val) { + this.setAttribute("minresultsforpopup", val); + return val; + } + + get minResultsForPopup() { + var m = parseInt(this.getAttribute("minresultsforpopup")); + return isNaN(m) ? 1 : m; + } + + set timeout(val) { + this.setAttribute("timeout", val); + return val; + } + + get timeout() { + var t = parseInt(this.getAttribute("timeout")); + return isNaN(t) ? 50 : t; + } + + set searchParam(val) { + this.setAttribute("autocompletesearchparam", val); + return val; + } + + get searchParam() { + return this.getAttribute("autocompletesearchparam") || ""; + } + + get searchCount() { + this.initSearchNames(); + return this.mSearchNames.length; + } + + get inPrivateContext() { + return this.PrivateBrowsingUtils.isWindowPrivate(window); + } + + get noRollupOnCaretMove() { + return this.popup.getAttribute("norolluponanchor") == "true"; + } + + set textValue(val) { + // "input" event is automatically dispatched by the editor if + // necessary. + this._setValueInternal(val, true); + + return this.value; + } + + get textValue() { + return this.value; + } + /** + * =================== nsIDOMXULMenuListElement =================== + */ + get editable() { + return true; + } + + set crop(val) { + return false; + } + + get crop() { + return false; + } + + set open(val) { + if (val) { + this.showHistoryPopup(); + } else { + this.closePopup(); + } + } + + get open() { + return this.getAttribute("open") == "true"; + } + + set value(val) { + return this._setValueInternal(val, false); + } + + get value() { + return super.value; + } + + get focused() { + return this === document.activeElement; + } + /** + * maximum number of rows to display at a time when opening the popup normally + * (e.g., focus element and press the down arrow) + */ + set maxRows(val) { + this.setAttribute("maxrows", val); + return val; + } + + get maxRows() { + return parseInt(this.getAttribute("maxrows")) || 0; + } + /** + * maximum number of rows to display at a time when opening the popup by + * clicking the dropmarker (for inputs that have one) + */ + set maxdropmarkerrows(val) { + this.setAttribute("maxdropmarkerrows", val); + return val; + } + + get maxdropmarkerrows() { + return parseInt(this.getAttribute("maxdropmarkerrows"), 10) || 14; + } + /** + * option to allow scrolling through the list via the tab key, rather than + * tab moving focus out of the textbox + */ + set tabScrolling(val) { + this.setAttribute("tabscrolling", val); + return val; + } + + get tabScrolling() { + return this.getAttribute("tabscrolling") == "true"; + } + /** + * option to completely ignore any blur events while searches are + * still going on. + */ + set ignoreBlurWhileSearching(val) { + this.setAttribute("ignoreblurwhilesearching", val); + return val; + } + + get ignoreBlurWhileSearching() { + return this.getAttribute("ignoreblurwhilesearching") == "true"; + } + /** + * option to highlight entries that don't have any matches + */ + set highlightNonMatches(val) { + this.setAttribute("highlightnonmatches", val); + return val; + } + + get highlightNonMatches() { + return this.getAttribute("highlightnonmatches") == "true"; + } + + getSearchAt(aIndex) { + this.initSearchNames(); + return this.mSearchNames[aIndex]; + } + + setTextValueWithReason(aValue, aReason) { + this.textValue = aValue; + } + + selectTextRange(aStartIndex, aEndIndex) { + super.setSelectionRange(aStartIndex, aEndIndex); + } + + onSearchBegin() { + if (this.popup && typeof this.popup.onSearchBegin == "function") { + this.popup.onSearchBegin(); + } + } + + onSearchComplete() { + if (this.mController.matchCount == 0) { + this.setAttribute("nomatch", "true"); + } else { + this.removeAttribute("nomatch"); + } + + if (this.ignoreBlurWhileSearching && !this.focused) { + this.handleEnter(); + this.detachController(); + } + } + + onTextEntered(event) { + if (this.getAttribute("notifylegacyevents") === "true") { + let e = new CustomEvent("textEntered", { + bubbles: false, + cancelable: true, + detail: { rootEvent: event }, + }); + return !this.dispatchEvent(e); + } + return false; + } + + onTextReverted(event) { + if (this.getAttribute("notifylegacyevents") === "true") { + let e = new CustomEvent("textReverted", { + bubbles: false, + cancelable: true, + detail: { rootEvent: event }, + }); + return !this.dispatchEvent(e); + } + return false; + } + + /** + * =================== PRIVATE MEMBERS =================== + */ + + /* + * ::::::::::::: autocomplete controller ::::::::::::: + */ + + attachController() { + this.mController.input = this.nsIAutocompleteInput; + } + + detachController() { + if ( + this.mController.input && + this.mController.input.wrappedJSObject == this.nsIAutocompleteInput + ) { + this.mController.input = null; + } + } + + /** + * ::::::::::::: popup opening ::::::::::::: + */ + openPopup() { + if (this.focused) { + this.popup.openAutocompletePopup(this.nsIAutocompleteInput, this); + } + } + + closePopup() { + if (this._dontClosePopup) { + delete this._dontClosePopup; + return; + } + this.popup.closePopup(); + } + + showHistoryPopup() { + // Store our "normal" maxRows on the popup, so that it can reset the + // value when the popup is hidden. + this.popup._normalMaxRows = this.maxRows; + + // Temporarily change our maxRows, since we want the dropdown to be a + // different size in this case. The popup's popupshowing/popuphiding + // handlers will take care of resetting this. + this.maxRows = this.maxdropmarkerrows; + + // Ensure that we have focus. + if (!this.focused) { + this.focus(); + } + this.attachController(); + this.mController.startSearch(""); + } + + toggleHistoryPopup() { + if (!this.popup.popupOpen) { + this.showHistoryPopup(); + } else { + this.closePopup(); + } + } + + handleKeyDown(aEvent) { + // Re: urlbarDeferred, see the comment in urlbarBindings.xml. + if (aEvent.defaultPrevented && !aEvent.urlbarDeferred) { + return false; + } + + if ( + typeof this.onBeforeHandleKeyDown == "function" && + this.onBeforeHandleKeyDown(aEvent) + ) { + return true; + } + + const isMac = AppConstants.platform == "macosx"; + var cancel = false; + + // Catch any keys that could potentially move the caret. Ctrl can be + // used in combination with these keys on Windows and Linux; and Alt + // can be used on OS X, so make sure the unused one isn't used. + let metaKey = isMac ? aEvent.ctrlKey : aEvent.altKey; + if (!metaKey) { + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_LEFT: + case KeyEvent.DOM_VK_RIGHT: + case KeyEvent.DOM_VK_HOME: + cancel = this.mController.handleKeyNavigation(aEvent.keyCode); + break; + } + } + + // Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt) + if (!aEvent.ctrlKey && !aEvent.altKey) { + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_TAB: + if (this.tabScrolling && this.popup.popupOpen) { + cancel = this.mController.handleKeyNavigation( + aEvent.shiftKey ? KeyEvent.DOM_VK_UP : KeyEvent.DOM_VK_DOWN + ); + } else if (this.forceComplete && this.mController.matchCount >= 1) { + this.mController.handleTab(); + } + break; + case KeyEvent.DOM_VK_UP: + case KeyEvent.DOM_VK_DOWN: + case KeyEvent.DOM_VK_PAGE_UP: + case KeyEvent.DOM_VK_PAGE_DOWN: + cancel = this.mController.handleKeyNavigation(aEvent.keyCode); + break; + } + } + + // Handle readline/emacs-style navigation bindings on Mac. + if ( + isMac && + this.popup.popupOpen && + aEvent.ctrlKey && + (aEvent.key === "n" || aEvent.key === "p") + ) { + const effectiveKey = + aEvent.key === "p" ? KeyEvent.DOM_VK_UP : KeyEvent.DOM_VK_DOWN; + cancel = this.mController.handleKeyNavigation(effectiveKey); + } + + // Handle keys we know aren't part of a shortcut, even with Alt or + // Ctrl. + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_ESCAPE: + cancel = this.mController.handleEscape(); + break; + case KeyEvent.DOM_VK_RETURN: + if (isMac) { + // Prevent the default action, since it will beep on Mac + if (aEvent.metaKey) { + aEvent.preventDefault(); + } + } + if (this.popup.selectedIndex >= 0) { + this.popupSelectedIndex = this.popup.selectedIndex; + } + cancel = this.handleEnter(aEvent); + break; + case KeyEvent.DOM_VK_DELETE: + if (isMac && !aEvent.shiftKey) { + break; + } + cancel = this.handleDelete(); + break; + case KeyEvent.DOM_VK_BACK_SPACE: + if (isMac && aEvent.shiftKey) { + cancel = this.handleDelete(); + } + break; + case KeyEvent.DOM_VK_DOWN: + case KeyEvent.DOM_VK_UP: + if (aEvent.altKey) { + this.toggleHistoryPopup(); + } + break; + case KeyEvent.DOM_VK_F4: + if (!isMac) { + this.toggleHistoryPopup(); + } + break; + } + + if (cancel) { + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + + return true; + } + + handleEnter(event) { + return this.mController.handleEnter(false, event || null); + } + + handleDelete() { + return this.mController.handleDelete(); + } + + /** + * ::::::::::::: miscellaneous ::::::::::::: + */ + initSearchNames() { + if (!this.mSearchNames) { + var names = this.getAttribute("autocompletesearch"); + if (!names) { + this.mSearchNames = []; + } else { + this.mSearchNames = names.split(" "); + } + } + } + + _focus() { + this._dontBlur = true; + this.focus(); + this._dontBlur = false; + } + + resetActionType() { + if (this.mIgnoreInput) { + return; + } + this.removeAttribute("actiontype"); + } + + _setValueInternal(value, isUserInput) { + this.mIgnoreInput = true; + + if (typeof this.onBeforeValueSet == "function") { + value = this.onBeforeValueSet(value); + } + + this.valueIsTyped = false; + if (isUserInput) { + super.setUserInput(value); + } else { + super.value = value; + } + + this.mIgnoreInput = false; + var event = document.createEvent("Events"); + event.initEvent("ValueChange", true, true); + super.dispatchEvent(event); + return value; + } + + onInput(aEvent) { + if ( + !this.mIgnoreInput && + this.mController.input.wrappedJSObject == this.nsIAutocompleteInput + ) { + this.valueIsTyped = true; + this.mController.handleText(); + } + this.resetActionType(); + } + } + + MozHTMLElement.implementCustomInterface(AutocompleteInput, [ + Ci.nsIAutoCompleteInput, + Ci.nsIDOMXULMenuListElement, + ]); + customElements.define("autocomplete-input", AutocompleteInput, { + extends: "input", + }); +} diff --git a/toolkit/content/widgets/autocomplete-popup.js b/toolkit/content/widgets/autocomplete-popup.js new file mode 100644 index 0000000000..ebd549aaa7 --- /dev/null +++ b/toolkit/content/widgets/autocomplete-popup.js @@ -0,0 +1,621 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const MozPopupElement = MozElements.MozElementMixin(XULPopupElement); + MozElements.MozAutocompleteRichlistboxPopup = class MozAutocompleteRichlistboxPopup extends MozPopupElement { + constructor() { + super(); + + this.mInput = null; + this.mPopupOpen = false; + this._currentIndex = 0; + this._disabledItemClicked = false; + + this.setListeners(); + } + + initialize() { + this.setAttribute("ignorekeys", "true"); + this.setAttribute("level", "top"); + this.setAttribute("consumeoutsideclicks", "never"); + + this.textContent = ""; + this.appendChild(this.constructor.fragment); + + /** + * This is the default number of rows that we give the autocomplete + * popup when the textbox doesn't have a "maxrows" attribute + * for us to use. + */ + this.defaultMaxRows = 6; + + /** + * In some cases (e.g. when the input's dropmarker button is clicked), + * the input wants to display a popup with more rows. In that case, it + * should increase its maxRows property and store the "normal" maxRows + * in this field. When the popup is hidden, we restore the input's + * maxRows to the value stored in this field. + * + * This field is set to -1 between uses so that we can tell when it's + * been set by the input and when we need to set it in the popupshowing + * handler. + */ + this._normalMaxRows = -1; + this._previousSelectedIndex = -1; + this.mLastMoveTime = Date.now(); + this.mousedOverIndex = -1; + this._richlistbox = this.querySelector(".autocomplete-richlistbox"); + + if (!this.listEvents) { + this.listEvents = { + handleEvent: event => { + if (!this.parentNode) { + return; + } + + switch (event.type) { + case "mousedown": + this._disabledItemClicked = !!event.target.closest( + "richlistitem" + )?.disabled; + break; + case "mouseup": + // Don't call onPopupClick for the scrollbar buttons, thumb, + // slider, etc. If we hit the richlistbox and not a + // richlistitem, we ignore the event. + if ( + event.target.closest("richlistbox,richlistitem").localName == + "richlistitem" && + !this._disabledItemClicked + ) { + this.onPopupClick(event); + } + this._disabledItemClicked = false; + break; + case "mousemove": + if (Date.now() - this.mLastMoveTime <= 30) { + return; + } + + let item = event.target.closest("richlistbox,richlistitem"); + + // If we hit the richlistbox and not a richlistitem, we ignore + // the event. + if (item.localName == "richlistbox") { + return; + } + + let index = this.richlistbox.getIndexOfItem(item); + + this.mousedOverIndex = index; + + if (item.selectedByMouseOver) { + this.richlistbox.selectedIndex = index; + } + + this.mLastMoveTime = Date.now(); + break; + } + }, + }; + } + this.richlistbox.addEventListener("mousedown", this.listEvents); + this.richlistbox.addEventListener("mouseup", this.listEvents); + this.richlistbox.addEventListener("mousemove", this.listEvents); + } + + get richlistbox() { + if (!this._richlistbox) { + this.initialize(); + } + return this._richlistbox; + } + + static get markup() { + return ` + <richlistbox class="autocomplete-richlistbox" flex="1"/> + `; + } + + /** + * nsIAutoCompletePopup + */ + get input() { + return this.mInput; + } + + get overrideValue() { + return null; + } + + get popupOpen() { + return this.mPopupOpen; + } + + get maxRows() { + return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows; + } + + set selectedIndex(val) { + if (val != this.richlistbox.selectedIndex) { + this._previousSelectedIndex = this.richlistbox.selectedIndex; + } + this.richlistbox.selectedIndex = val; + // Since ensureElementIsVisible may cause an expensive Layout flush, + // invoke it only if there may be a scrollbar, so if we could fetch + // more results than we can show at once. + // maxResults is the maximum number of fetched results, maxRows is the + // maximum number of rows we show at once, without a scrollbar. + if (this.mPopupOpen && this.maxResults > this.maxRows) { + // when clearing the selection (val == -1, so selectedItem will be + // null), we want to scroll back to the top. see bug #406194 + this.richlistbox.ensureElementIsVisible( + this.richlistbox.selectedItem || this.richlistbox.firstElementChild + ); + } + return val; + } + + get selectedIndex() { + return this.richlistbox.selectedIndex; + } + + get maxResults() { + // This is how many richlistitems will be kept around. + // Note, this getter may be overridden, or instances + // can have the nomaxresults attribute set to have no + // limit. + if (this.getAttribute("nomaxresults") == "true") { + return Infinity; + } + return 20; + } + + get matchCount() { + return Math.min(this.mInput.controller.matchCount, this.maxResults); + } + + get overflowPadding() { + return Number(this.getAttribute("overflowpadding")); + } + + set view(val) { + return val; + } + + get view() { + return this.mInput.controller; + } + + closePopup() { + if (this.mPopupOpen) { + this.hidePopup(); + this.removeAttribute("width"); + } + } + + getNextIndex(aReverse, aAmount, aIndex, aMaxRow) { + if (aMaxRow < 0) { + return -1; + } + + var newIdx = aIndex + (aReverse ? -1 : 1) * aAmount; + if ( + (aReverse && aIndex == -1) || + (newIdx > aMaxRow && aIndex != aMaxRow) + ) { + newIdx = aMaxRow; + } else if ((!aReverse && aIndex == -1) || (newIdx < 0 && aIndex != 0)) { + newIdx = 0; + } + + if ( + (newIdx < 0 && aIndex == 0) || + (newIdx > aMaxRow && aIndex == aMaxRow) + ) { + aIndex = -1; + } else { + aIndex = newIdx; + } + + return aIndex; + } + + onPopupClick(aEvent) { + this.input.controller.handleEnter(true, aEvent); + } + + onSearchBegin() { + this.mousedOverIndex = -1; + + if (typeof this._onSearchBegin == "function") { + this._onSearchBegin(); + } + } + + openAutocompletePopup(aInput, aElement) { + // until we have "baseBinding", (see bug #373652) this allows + // us to override openAutocompletePopup(), but still call + // the method on the base class + this._openAutocompletePopup(aInput, aElement); + } + + _openAutocompletePopup(aInput, aElement) { + if (!this._richlistbox) { + this.initialize(); + } + + if (!this.mPopupOpen) { + // It's possible that the panel is hidden initially + // to avoid impacting startup / new window performance + aInput.popup.hidden = false; + + this.mInput = aInput; + // clear any previous selection, see bugs 400671 and 488357 + this.selectedIndex = -1; + + var width = aElement.getBoundingClientRect().width; + this.setAttribute("width", width > 100 ? width : 100); + // invalidate() depends on the width attribute + this._invalidate(); + + this.openPopup(aElement, "after_start", 0, 0, false, false); + } + } + + invalidate(reason) { + // Don't bother doing work if we're not even showing + if (!this.mPopupOpen) { + return; + } + + this._invalidate(reason); + } + + _invalidate(reason) { + // collapsed if no matches + this.richlistbox.collapsed = this.matchCount == 0; + + // Update the richlistbox height. + if (this._adjustHeightRAFToken) { + cancelAnimationFrame(this._adjustHeightRAFToken); + this._adjustHeightRAFToken = null; + } + + if (this.mPopupOpen) { + delete this._adjustHeightOnPopupShown; + this._adjustHeightRAFToken = requestAnimationFrame(() => + this.adjustHeight() + ); + } else { + this._adjustHeightOnPopupShown = true; + } + + this._currentIndex = 0; + if (this._appendResultTimeout) { + clearTimeout(this._appendResultTimeout); + } + this._appendCurrentResult(reason); + } + + _collapseUnusedItems() { + let existingItemsCount = this.richlistbox.children.length; + for (let i = this.matchCount; i < existingItemsCount; ++i) { + let item = this.richlistbox.children[i]; + + item.collapsed = true; + if (typeof item._onCollapse == "function") { + item._onCollapse(); + } + } + } + + adjustHeight() { + // Figure out how many rows to show + let rows = this.richlistbox.children; + let numRows = Math.min(this.matchCount, this.maxRows, rows.length); + + // Default the height to 0 if we have no rows to show + let height = 0; + if (numRows) { + let firstRowRect = rows[0].getBoundingClientRect(); + if (this._rlbPadding == undefined) { + let style = window.getComputedStyle(this.richlistbox); + let paddingTop = parseInt(style.paddingTop) || 0; + let paddingBottom = parseInt(style.paddingBottom) || 0; + this._rlbPadding = paddingTop + paddingBottom; + } + + // The class `forceHandleUnderflow` is for the item might need to + // handle OverUnderflow or Overflow when the height of an item will + // be changed dynamically. + for (let i = 0; i < numRows; i++) { + if (rows[i].classList.contains("forceHandleUnderflow")) { + rows[i].handleOverUnderflow(); + } + } + + let lastRowRect = rows[numRows - 1].getBoundingClientRect(); + // Calculate the height to have the first row to last row shown + height = lastRowRect.bottom - firstRowRect.top + this._rlbPadding; + } + + this._collapseUnusedItems(); + + this.richlistbox.style.removeProperty("height"); + // We need to get the ceiling of the calculated value to ensure that the box fully contains + // all of its contents and doesn't cause a scrollbar since nsIBoxObject only expects a + // `long`. e.g. if `height` is 99.5 the richlistbox would render at height 99px with a + // scrollbar for the extra 0.5px. + this.richlistbox.height = Math.ceil(height); + } + + _appendCurrentResult(invalidateReason) { + var controller = this.mInput.controller; + var matchCount = this.matchCount; + var existingItemsCount = this.richlistbox.children.length; + + // Process maxRows per chunk to improve performance and user experience + for (let i = 0; i < this.maxRows; i++) { + if (this._currentIndex >= matchCount) { + break; + } + let item; + let itemExists = this._currentIndex < existingItemsCount; + + let originalValue, originalText, originalType; + let style = controller.getStyleAt(this._currentIndex); + let value = + style && style.includes("autofill") + ? controller.getFinalCompleteValueAt(this._currentIndex) + : controller.getValueAt(this._currentIndex); + let label = controller.getLabelAt(this._currentIndex); + let comment = controller.getCommentAt(this._currentIndex); + let image = controller.getImageAt(this._currentIndex); + // trim the leading/trailing whitespace + let trimmedSearchString = controller.searchString + .replace(/^\s+/, "") + .replace(/\s+$/, ""); + + let reusable = false; + if (itemExists) { + item = this.richlistbox.children[this._currentIndex]; + + // Url may be a modified version of value, see _adjustAcItem(). + originalValue = + item.getAttribute("url") || item.getAttribute("ac-value"); + originalText = item.getAttribute("ac-text"); + originalType = item.getAttribute("originaltype"); + + // The styles on the list which have different <content> structure and overrided + // _adjustAcItem() are unreusable. + const UNREUSEABLE_STYLES = [ + "autofill-profile", + "autofill-footer", + "autofill-clear-button", + "autofill-insecureWarning", + "generatedPassword", + "importableLearnMore", + "importableLogins", + "insecureWarning", + "loginsFooter", + "loginWithOrigin", + ]; + // Reuse the item when its style is exactly equal to the previous style or + // neither of their style are in the UNREUSEABLE_STYLES. + reusable = + originalType === style || + !( + UNREUSEABLE_STYLES.includes(style) || + UNREUSEABLE_STYLES.includes(originalType) + ); + } + + // If no reusable item available, then create a new item. + if (!reusable) { + let options = null; + switch (style) { + case "autofill-profile": + options = { is: "autocomplete-profile-listitem" }; + break; + case "autofill-footer": + options = { is: "autocomplete-profile-listitem-footer" }; + break; + case "autofill-clear-button": + options = { is: "autocomplete-profile-listitem-clear-button" }; + break; + case "autofill-insecureWarning": + options = { is: "autocomplete-creditcard-insecure-field" }; + break; + case "importableLearnMore": + options = { + is: "autocomplete-importable-learn-more-richlistitem", + }; + break; + case "importableLogins": + options = { is: "autocomplete-importable-logins-richlistitem" }; + break; + case "generatedPassword": + options = { is: "autocomplete-generated-password-richlistitem" }; + break; + case "insecureWarning": + options = { is: "autocomplete-richlistitem-insecure-warning" }; + break; + case "loginsFooter": + options = { is: "autocomplete-richlistitem-logins-footer" }; + break; + case "loginWithOrigin": + options = { is: "autocomplete-login-richlistitem" }; + break; + default: + options = { is: "autocomplete-richlistitem" }; + } + item = document.createXULElement("richlistitem", options); + item.className = "autocomplete-richlistitem"; + } + + item.setAttribute("dir", this.style.direction); + item.setAttribute("ac-image", image); + item.setAttribute("ac-value", value); + item.setAttribute("ac-label", label); + item.setAttribute("ac-comment", comment); + item.setAttribute("ac-text", trimmedSearchString); + + // Completely reuse the existing richlistitem for invalidation + // due to new results, but only when: the item is the same, *OR* + // we are about to replace the currently moused-over item, to + // avoid surprising the user. + let iface = Ci.nsIAutoCompletePopup; + if ( + reusable && + originalText == trimmedSearchString && + invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT && + (originalValue == value || + this.mousedOverIndex === this._currentIndex) + ) { + // try to re-use the existing item + item._reuseAcItem(); + this._currentIndex++; + continue; + } else { + if (typeof item._cleanup == "function") { + item._cleanup(); + } + item.setAttribute("originaltype", style); + } + + if (reusable) { + // Adjust only when the result's type is reusable for existing + // item's. Otherwise, we might insensibly call old _adjustAcItem() + // as new binding has not been attached yet. + // We don't need to worry about switching to new binding, since + // _adjustAcItem() will fired by its own constructor accordingly. + item._adjustAcItem(); + item.collapsed = false; + } else if (itemExists) { + let oldItem = this.richlistbox.children[this._currentIndex]; + this.richlistbox.replaceChild(item, oldItem); + } else { + this.richlistbox.appendChild(item); + } + + this._currentIndex++; + } + + if (typeof this.onResultsAdded == "function") { + // The items bindings may not be attached yet, so we must delay this + // before we can properly handle items properly without breaking + // the richlistbox. + Services.tm.dispatchToMainThread(() => this.onResultsAdded()); + } + + if (this._currentIndex < matchCount) { + // yield after each batch of items so that typing the url bar is + // responsive + this._appendResultTimeout = setTimeout( + () => this._appendCurrentResult(), + 0 + ); + } + } + + selectBy(aReverse, aPage) { + try { + var amount = aPage ? 5 : 1; + + // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount + this.selectedIndex = this.getNextIndex( + aReverse, + amount, + this.selectedIndex, + this.matchCount - 1 + ); + if (this.selectedIndex == -1) { + this.input._focus(); + } + } catch (ex) { + // do nothing - occasionally timer-related js errors happen here + // e.g. "this.selectedIndex has no properties", when you type fast and hit a + // navigation key before this popup has opened + } + } + + disconnectedCallback() { + if (this.listEvents) { + this.richlistbox.removeEventListener("mousedown", this.listEvents); + this.richlistbox.removeEventListener("mouseup", this.listEvents); + this.richlistbox.removeEventListener("mousemove", this.listEvents); + delete this.listEvents; + } + } + + setListeners() { + this.addEventListener("popupshowing", event => { + // If normalMaxRows wasn't already set by the input, then set it here + // so that we restore the correct number when the popup is hidden. + + // Null-check this.mInput; see bug 1017914 + if (this._normalMaxRows < 0 && this.mInput) { + this._normalMaxRows = this.mInput.maxRows; + } + + this.mPopupOpen = true; + }); + + this.addEventListener("popupshown", event => { + if (this._adjustHeightOnPopupShown) { + delete this._adjustHeightOnPopupShown; + this.adjustHeight(); + } + }); + + this.addEventListener("popuphiding", event => { + var isListActive = true; + if (this.selectedIndex == -1) { + isListActive = false; + } + this.input.controller.stopSearch(); + + this.mPopupOpen = false; + + // Reset the maxRows property to the cached "normal" value (if there's + // any), and reset normalMaxRows so that we can detect whether it was set + // by the input when the popupshowing handler runs. + + // Null-check this.mInput; see bug 1017914 + if (this.mInput && this._normalMaxRows > 0) { + this.mInput.maxRows = this._normalMaxRows; + } + this._normalMaxRows = -1; + // If the list was being navigated and then closed, make sure + // we fire accessible focus event back to textbox + + // Null-check this.mInput; see bug 1017914 + if (isListActive && this.mInput) { + this.mInput.mIgnoreFocus = true; + this.mInput._focus(); + this.mInput.mIgnoreFocus = false; + } + }); + } + }; + + MozPopupElement.implementCustomInterface( + MozElements.MozAutocompleteRichlistboxPopup, + [Ci.nsIAutoCompletePopup] + ); + + customElements.define( + "autocomplete-richlistbox-popup", + MozElements.MozAutocompleteRichlistboxPopup, + { + extends: "panel", + } + ); +} diff --git a/toolkit/content/widgets/autocomplete-richlistitem.js b/toolkit/content/widgets/autocomplete-richlistitem.js new file mode 100644 index 0000000000..8fc0c12363 --- /dev/null +++ b/toolkit/content/widgets/autocomplete-richlistitem.js @@ -0,0 +1,884 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + MozElements.MozAutocompleteRichlistitem = class MozAutocompleteRichlistitem extends MozElements.MozRichlistitem { + constructor() { + super(); + + /** + * This overrides listitem's mousedown handler because we want to set the + * selected item even when the shift or accel keys are pressed. + */ + this.addEventListener("mousedown", event => { + // Call this.control only once since it's not a simple getter. + let control = this.control; + if (!control || control.disabled) { + return; + } + if (!this.selected) { + control.selectItem(this); + } + control.currentItem = this; + }); + + this.addEventListener("mouseover", event => { + // The point of implementing this handler is to allow drags to change + // the selected item. If the user mouses down on an item, it becomes + // selected. If they then drag the mouse to another item, select it. + // Handle all three primary mouse buttons: right, left, and wheel, since + // all three change the selection on mousedown. + let mouseDown = event.buttons & 0b111; + if (!mouseDown) { + return; + } + // Call this.control only once since it's not a simple getter. + let control = this.control; + if (!control || control.disabled) { + return; + } + if (!this.selected) { + control.selectItem(this); + } + control.currentItem = this; + }); + + this.addEventListener("overflow", () => this._onOverflow()); + this.addEventListener("underflow", () => this._onUnderflow()); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.textContent = ""; + this.appendChild(this.constructor.fragment); + this.initializeAttributeInheritance(); + + this._boundaryCutoff = null; + this._inOverflow = false; + + this._adjustAcItem(); + } + + static get inheritedAttributes() { + return { + ".ac-type-icon": "selected,current,type", + ".ac-site-icon": "src=image,selected,type", + ".ac-title": "selected", + ".ac-title-text": "selected", + ".ac-separator": "selected,type", + ".ac-url": "selected", + ".ac-url-text": "selected", + }; + } + + static get markup() { + return ` + <image class="ac-type-icon"/> + <image class="ac-site-icon"/> + <hbox class="ac-title" align="center"> + <description class="ac-text-overflow-container"> + <description class="ac-title-text"/> + </description> + </hbox> + <hbox class="ac-separator" align="center"> + <description class="ac-separator-text" value="—"/> + </hbox> + <hbox class="ac-url" align="center" aria-hidden="true"> + <description class="ac-text-overflow-container"> + <description class="ac-url-text"/> + </description> + </hbox> + `; + } + + get _typeIcon() { + return this.querySelector(".ac-type-icon"); + } + + get _titleText() { + return this.querySelector(".ac-title-text"); + } + + get _separator() { + return this.querySelector(".ac-separator"); + } + + get _urlText() { + return this.querySelector(".ac-url-text"); + } + + get _stringBundle() { + if (!this.__stringBundle) { + this.__stringBundle = Services.strings.createBundle( + "chrome://global/locale/autocomplete.properties" + ); + } + return this.__stringBundle; + } + + get boundaryCutoff() { + if (!this._boundaryCutoff) { + this._boundaryCutoff = Services.prefs.getIntPref( + "toolkit.autocomplete.richBoundaryCutoff" + ); + } + return this._boundaryCutoff; + } + + _cleanup() { + this.removeAttribute("url"); + this.removeAttribute("image"); + this.removeAttribute("title"); + this.removeAttribute("text"); + } + + _onOverflow() { + this._inOverflow = true; + this._handleOverflow(); + } + + _onUnderflow() { + this._inOverflow = false; + this._handleOverflow(); + } + + _getBoundaryIndices(aText, aSearchTokens) { + // Short circuit for empty search ([""] == "") + if (aSearchTokens == "") { + return [0, aText.length]; + } + + // Find which regions of text match the search terms + let regions = []; + for (let search of Array.prototype.slice.call(aSearchTokens)) { + let matchIndex = -1; + let searchLen = search.length; + + // Find all matches of the search terms, but stop early for perf + let lowerText = aText.substr(0, this.boundaryCutoff).toLowerCase(); + while ((matchIndex = lowerText.indexOf(search, matchIndex + 1)) >= 0) { + regions.push([matchIndex, matchIndex + searchLen]); + } + } + + // Sort the regions by start position then end position + regions = regions.sort((a, b) => { + let start = a[0] - b[0]; + return start == 0 ? a[1] - b[1] : start; + }); + + // Generate the boundary indices from each region + let start = 0; + let end = 0; + let boundaries = []; + let len = regions.length; + for (let i = 0; i < len; i++) { + // We have a new boundary if the start of the next is past the end + let region = regions[i]; + if (region[0] > end) { + // First index is the beginning of match + boundaries.push(start); + // Second index is the beginning of non-match + boundaries.push(end); + + // Track the new region now that we've stored the previous one + start = region[0]; + } + + // Push back the end index for the current or new region + end = Math.max(end, region[1]); + } + + // Add the last region + boundaries.push(start); + boundaries.push(end); + + // Put on the end boundary if necessary + if (end < aText.length) { + boundaries.push(aText.length); + } + + // Skip the first item because it's always 0 + return boundaries.slice(1); + } + + _getSearchTokens(aSearch) { + let search = aSearch.toLowerCase(); + return search.split(/\s+/); + } + + _setUpDescription(aDescriptionElement, aText) { + // Get rid of all previous text + if (!aDescriptionElement) { + return; + } + while (aDescriptionElement.hasChildNodes()) { + aDescriptionElement.firstChild.remove(); + } + + // Get the indices that separate match and non-match text + let search = this.getAttribute("text"); + let tokens = this._getSearchTokens(search); + let indices = this._getBoundaryIndices(aText, tokens); + + this._appendDescriptionSpans( + indices, + aText, + aDescriptionElement, + aDescriptionElement + ); + } + + _appendDescriptionSpans( + indices, + text, + spansParentElement, + descriptionElement + ) { + let next; + let start = 0; + let len = indices.length; + // Even indexed boundaries are matches, so skip the 0th if it's empty + for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) { + next = indices[i]; + let spanText = text.substr(start, next - start); + start = next; + + if (i % 2 == 0) { + // Emphasize the text for even indices + let span = spansParentElement.appendChild( + document.createElementNS("http://www.w3.org/1999/xhtml", "span") + ); + this._setUpEmphasisSpan(span, descriptionElement); + span.textContent = spanText; + } else { + // Otherwise, it's plain text + spansParentElement.appendChild(document.createTextNode(spanText)); + } + } + } + + _setUpEmphasisSpan(aSpan, aDescriptionElement) { + aSpan.classList.add("ac-emphasize-text"); + switch (aDescriptionElement) { + case this._titleText: + aSpan.classList.add("ac-emphasize-text-title"); + break; + case this._urlText: + aSpan.classList.add("ac-emphasize-text-url"); + break; + } + } + + /** + * This will generate an array of emphasis pairs for use with + * _setUpEmphasisedSections(). Each pair is a tuple (array) that + * represents a block of text - containing the text of that block, and a + * boolean for whether that block should have an emphasis styling applied + * to it. + * + * These pairs are generated by parsing a localised string (aSourceString) + * with parameters, in the format that is used by + * nsIStringBundle.formatStringFromName(): + * + * "textA %1$S textB textC %2$S" + * + * Or: + * + * "textA %S" + * + * Where "%1$S", "%2$S", and "%S" are intended to be replaced by provided + * replacement strings. These are specified an array of tuples + * (aReplacements), each containing the replacement text and a boolean for + * whether that text should have an emphasis styling applied. This is used + * as a 1-based array - ie, "%1$S" is replaced by the item in the first + * index of aReplacements, "%2$S" by the second, etc. "%S" will always + * match the first index. + */ + _generateEmphasisPairs(aSourceString, aReplacements) { + let pairs = []; + + // Split on %S, %1$S, %2$S, etc. ie: + // "textA %S" + // becomes ["textA ", "%S"] + // "textA %1$S textB textC %2$S" + // becomes ["textA ", "%1$S", " textB textC ", "%2$S"] + let parts = aSourceString.split(/(%(?:[0-9]+\$)?S)/); + + for (let part of parts) { + // The above regex will actually give us an empty string at the + // end - we don't want that, as we don't want to later generate an + // empty text node for it. + if (part.length === 0) { + continue; + } + + // Determine if this token is a replacement token or a normal text + // token. If it is a replacement token, we want to extract the + // numerical number. However, we still want to match on "$S". + let match = part.match(/^%(?:([0-9]+)\$)?S$/); + + if (match) { + // "%S" doesn't have a numerical number in it, but will always + // be assumed to be 1. Furthermore, the input string specifies + // these with a 1-based index, but we want a 0-based index. + let index = (match[1] || 1) - 1; + + if (index >= 0 && index < aReplacements.length) { + pairs.push([...aReplacements[index]]); + } + } else { + pairs.push([part]); + } + } + + return pairs; + } + + /** + * _setUpEmphasisedSections() has the same use as _setUpDescription, + * except instead of taking a string and highlighting given tokens, it takes + * an array of pairs generated by _generateEmphasisPairs(). This allows + * control over emphasising based on specific blocks of text, rather than + * search for substrings. + */ + _setUpEmphasisedSections(aDescriptionElement, aTextPairs) { + // Get rid of all previous text + while (aDescriptionElement.hasChildNodes()) { + aDescriptionElement.firstChild.remove(); + } + + for (let [text, emphasise] of aTextPairs) { + if (emphasise) { + let span = aDescriptionElement.appendChild( + document.createElementNS("http://www.w3.org/1999/xhtml", "span") + ); + span.textContent = text; + switch (emphasise) { + case "match": + this._setUpEmphasisSpan(span, aDescriptionElement); + break; + } + } else { + aDescriptionElement.appendChild(document.createTextNode(text)); + } + } + } + + _unescapeUrl(url) { + return Services.textToSubURI.unEscapeURIForUI(url); + } + + _reuseAcItem() { + this.collapsed = false; + + // The popup may have changed size between now and the last + // time the item was shown, so always handle over/underflow. + let dwu = window.windowUtils; + let popupWidth = dwu.getBoundsWithoutFlushing(this.parentNode).width; + if (!this._previousPopupWidth || this._previousPopupWidth != popupWidth) { + this._previousPopupWidth = popupWidth; + this.handleOverUnderflow(); + } + } + + _adjustAcItem() { + let originalUrl = this.getAttribute("ac-value"); + let title = this.getAttribute("ac-comment"); + this.setAttribute("url", originalUrl); + this.setAttribute("image", this.getAttribute("ac-image")); + this.setAttribute("title", title); + this.setAttribute("text", this.getAttribute("ac-text")); + + let type = this.getAttribute("originaltype"); + let types = new Set(type.split(/\s+/)); + // Remove types that should ultimately not be in the `type` string. + types.delete("autofill"); + type = [...types][0] || ""; + this.setAttribute("type", type); + + let displayUrl = this._unescapeUrl(originalUrl); + + // Show the domain as the title if we don't have a title. + if (!title) { + try { + let uri = Services.io.newURI(originalUrl); + // Not all valid URLs have a domain. + if (uri.host) { + title = uri.host; + } + } catch (e) {} + if (!title) { + title = displayUrl; + } + } + + if (Array.isArray(title)) { + this._setUpEmphasisedSections(this._titleText, title); + } else { + this._setUpDescription(this._titleText, title); + } + this._setUpDescription(this._urlText, displayUrl); + + // Removing the max-width may be jarring when the item is visible, but + // we have no other choice to properly crop the text. + // Removing max-widths may cause overflow or underflow events, that + // will set the _inOverflow property. In case both the old and the new + // text are overflowing, the overflow event won't happen, and we must + // enforce an _handleOverflow() call to update the max-widths. + let wasInOverflow = this._inOverflow; + this._removeMaxWidths(); + if (wasInOverflow && this._inOverflow) { + this._handleOverflow(); + } + } + + _removeMaxWidths() { + if (this._hasMaxWidths) { + this._titleText.style.removeProperty("max-width"); + this._urlText.style.removeProperty("max-width"); + this._hasMaxWidths = false; + } + } + + /** + * This method truncates the displayed strings as necessary. + */ + _handleOverflow() { + let itemRect = this.parentNode.getBoundingClientRect(); + let titleRect = this._titleText.getBoundingClientRect(); + let separatorRect = this._separator.getBoundingClientRect(); + let urlRect = this._urlText.getBoundingClientRect(); + let separatorURLWidth = separatorRect.width + urlRect.width; + + // Total width for the title and URL is the width of the item + // minus the start of the title text minus a little optional extra padding. + // This extra padding amount is basically arbitrary but keeps the text + // from getting too close to the popup's edge. + let dir = this.getAttribute("dir"); + let titleStart = + dir == "rtl" + ? itemRect.right - titleRect.right + : titleRect.left - itemRect.left; + + let popup = this.parentNode.parentNode; + let itemWidth = + itemRect.width - + titleStart - + popup.overflowPadding - + (popup.margins ? popup.margins.end : 0); + + let titleWidth = titleRect.width; + if (titleWidth + separatorURLWidth > itemWidth) { + // The percentage of the item width allocated to the title. + let titlePct = 0.66; + + let titleAvailable = itemWidth - separatorURLWidth; + let titleMaxWidth = Math.max(titleAvailable, itemWidth * titlePct); + if (titleWidth > titleMaxWidth) { + this._titleText.style.maxWidth = titleMaxWidth + "px"; + } + let urlMaxWidth = Math.max( + itemWidth - titleWidth, + itemWidth * (1 - titlePct) + ); + urlMaxWidth -= separatorRect.width; + this._urlText.style.maxWidth = urlMaxWidth + "px"; + this._hasMaxWidths = true; + } + } + + handleOverUnderflow() { + this._removeMaxWidths(); + this._handleOverflow(); + } + }; + + MozXULElement.implementCustomInterface( + MozElements.MozAutocompleteRichlistitem, + [Ci.nsIDOMXULSelectControlItemElement] + ); + + class MozAutocompleteRichlistitemInsecureWarning extends MozElements.MozAutocompleteRichlistitem { + constructor() { + super(); + + this.addEventListener("click", event => { + if (event.button != 0) { + return; + } + + let baseURL = Services.urlFormatter.formatURLPref( + "app.support.baseURL" + ); + window.openTrustedLinkIn(baseURL + "insecure-password", "tab", { + relatedToCurrent: true, + }); + }); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + super.connectedCallback(); + + // Unlike other autocomplete items, the height of the insecure warning + // increases by wrapping. So "forceHandleUnderflow" is for container to + // recalculate an item's height and width. + this.classList.add("forceHandleUnderflow"); + } + + static get inheritedAttributes() { + return { + ".ac-type-icon": "selected,current,type", + ".ac-site-icon": "src=image,selected,type", + ".ac-title-text": "selected", + ".ac-separator": "selected,type", + ".ac-url": "selected", + ".ac-url-text": "selected", + }; + } + + static get markup() { + return ` + <image class="ac-type-icon"/> + <image class="ac-site-icon"/> + <vbox class="ac-title" align="left"> + <description class="ac-text-overflow-container"> + <description class="ac-title-text"/> + </description> + </vbox> + <hbox class="ac-separator" align="center"> + <description class="ac-separator-text" value="—"/> + </hbox> + <hbox class="ac-url" align="center"> + <description class="ac-text-overflow-container"> + <description class="ac-url-text"/> + </description> + </hbox> + `; + } + + get _learnMoreString() { + if (!this.__learnMoreString) { + this.__learnMoreString = Services.strings + .createBundle("chrome://passwordmgr/locale/passwordmgr.properties") + .GetStringFromName("insecureFieldWarningLearnMore"); + } + return this.__learnMoreString; + } + + /** + * Override _getSearchTokens to have the Learn More text emphasized + */ + _getSearchTokens(aSearch) { + return [this._learnMoreString.toLowerCase()]; + } + } + + class MozAutocompleteRichlistitemLoginsFooter extends MozElements.MozAutocompleteRichlistitem { + constructor() { + super(); + + function handleEvent(event) { + if (event.button != 0) { + return; + } + + const { LoginHelper } = ChromeUtils.import( + "resource://gre/modules/LoginHelper.jsm" + ); + + LoginHelper.openPasswordManager(this.ownerGlobal, { + entryPoint: "autocomplete", + }); + } + + this.addEventListener("click", handleEvent); + } + } + + class MozAutocompleteImportableLearnMoreRichlistitem extends MozElements.MozAutocompleteRichlistitem { + constructor() { + super(); + MozXULElement.insertFTLIfNeeded("toolkit/main-window/autocomplete.ftl"); + + this.addEventListener("click", event => { + window.openTrustedLinkIn( + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "password-import", + "tab", + { relatedToCurrent: true } + ); + }); + } + + static get markup() { + return ` + <image class="ac-type-icon"/> + <image class="ac-site-icon"/> + <vbox class="ac-title" align="left"> + <description class="ac-text-overflow-container"> + <description class="ac-title-text" + data-l10n-id="autocomplete-import-learn-more"/> + </description> + </vbox> + <hbox class="ac-separator" align="center"> + <description class="ac-separator-text" value="—"/> + </hbox> + <hbox class="ac-url" align="center"> + <description class="ac-text-overflow-container"> + <description class="ac-url-text"/> + </description> + </hbox> + `; + } + + // Override to avoid clearing out fluent description. + _setUpDescription() {} + } + + class MozAutocompleteTwoLineRichlistitem extends MozElements.MozRichlistitem { + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.textContent = ""; + this.appendChild(this.constructor.fragment); + this.initializeAttributeInheritance(); + this._adjustAcItem(); + } + + static get inheritedAttributes() { + return { + // getLabelAt: + ".line1-label": "text=ac-value", + // getCommentAt: + ".line2-label": "text=ac-label", + }; + } + + static get markup() { + return ` + <div xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="two-line-wrapper"> + <xul:image class="ac-site-icon"></xul:image> + <div class="labels-wrapper"> + <div class="label-row line1-label"></div> + <div class="label-row line2-label"></div> + </div> + </div> + `; + } + + _adjustAcItem() { + const popup = this.parentNode.parentNode; + const minWidth = getComputedStyle(popup).minWidth.replace("px", ""); + // Make item fit in popup as XUL box could not constrain + // item's width + // popup.width is equal to the input field's width from the content process + this.firstElementChild.style.width = + Math.max(minWidth, popup.width) + "px"; + } + + _onOverflow() {} + + _onUnderflow() {} + + handleOverUnderflow() {} + } + + class MozAutocompleteLoginRichlistitem extends MozAutocompleteTwoLineRichlistitem { + static get inheritedAttributes() { + return { + // getLabelAt: + ".line1-label": "text=ac-value", + // Don't inherit ac-label with getCommentAt since the label is JSON. + }; + } + + _adjustAcItem() { + super._adjustAcItem(); + + let details = JSON.parse(this.getAttribute("ac-label")); + this.querySelector(".line2-label").textContent = details.comment; + } + } + + class MozAutocompleteGeneratedPasswordRichlistitem extends MozAutocompleteTwoLineRichlistitem { + constructor() { + super(); + + // Line 2 and line 3 both display text with a different line-height than + // line 1 but we want the line-height to be the same so we wrap the text + // in <span> and only adjust the line-height via font CSS properties on them. + this.generatedPasswordText = document.createElement("span"); + + this.line3Text = document.createElement("span"); + this.line3 = document.createElement("div"); + this.line3.className = "label-row generated-password-autosave"; + this.line3.append(this.line3Text); + } + + get _autoSaveString() { + if (!this.__autoSaveString) { + let brandShorterName = Services.strings + .createBundle("chrome://branding/locale/brand.properties") + .GetStringFromName("brandShorterName"); + this.__autoSaveString = Services.strings + .createBundle("chrome://passwordmgr/locale/passwordmgr.properties") + .formatStringFromName("generatedPasswordWillBeSaved", [ + brandShorterName, + ]); + } + return this.__autoSaveString; + } + + _adjustAcItem() { + let { generatedPassword, willAutoSaveGeneratedPassword } = JSON.parse( + this.getAttribute("ac-label") + ); + let line2Label = this.querySelector(".line2-label"); + line2Label.textContent = ""; + this.generatedPasswordText.textContent = generatedPassword; + line2Label.append(this.generatedPasswordText); + + if (willAutoSaveGeneratedPassword) { + this.line3Text.textContent = this._autoSaveString; + this.querySelector(".labels-wrapper").append(this.line3); + } else { + this.line3.remove(); + } + + super._adjustAcItem(); + } + } + + class MozAutocompleteImportableLoginsRichlistitem extends MozAutocompleteTwoLineRichlistitem { + constructor() { + super(); + MozXULElement.insertFTLIfNeeded("toolkit/main-window/autocomplete.ftl"); + + this.addEventListener("click", event => { + if (event.button != 0) { + return; + } + + // Let the login manager parent handle this importable browser click. + gBrowser.selectedBrowser.browsingContext.currentWindowGlobal + .getActor("LoginManager") + .receiveMessage({ + name: "PasswordManager:HandleImportable", + data: { + browserId: this.getAttribute("ac-value"), + }, + }); + }); + } + + static get markup() { + return ` + <div xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="two-line-wrapper"> + <xul:image class="ac-site-icon" /> + <div class="labels-wrapper"> + <div class="label-row line1-label" data-l10n-name="line1" /> + <div class="label-row line2-label" data-l10n-name="line2" /> + </div> + </div> + `; + } + + _adjustAcItem() { + document.l10n.setAttributes( + this.querySelector(".labels-wrapper"), + `autocomplete-import-logins-${this.getAttribute("ac-value")}`, + { + host: this.getAttribute("ac-label").replace(/^www\./, ""), + } + ); + super._adjustAcItem(); + } + } + + customElements.define( + "autocomplete-richlistitem", + MozElements.MozAutocompleteRichlistitem, + { + extends: "richlistitem", + } + ); + + customElements.define( + "autocomplete-richlistitem-insecure-warning", + MozAutocompleteRichlistitemInsecureWarning, + { + extends: "richlistitem", + } + ); + + customElements.define( + "autocomplete-richlistitem-logins-footer", + MozAutocompleteRichlistitemLoginsFooter, + { + extends: "richlistitem", + } + ); + + customElements.define( + "autocomplete-two-line-richlistitem", + MozAutocompleteTwoLineRichlistitem, + { + extends: "richlistitem", + } + ); + + customElements.define( + "autocomplete-login-richlistitem", + MozAutocompleteLoginRichlistitem, + { + extends: "richlistitem", + } + ); + + customElements.define( + "autocomplete-generated-password-richlistitem", + MozAutocompleteGeneratedPasswordRichlistitem, + { + extends: "richlistitem", + } + ); + + customElements.define( + "autocomplete-importable-learn-more-richlistitem", + MozAutocompleteImportableLearnMoreRichlistitem, + { + extends: "richlistitem", + } + ); + + customElements.define( + "autocomplete-importable-logins-richlistitem", + MozAutocompleteImportableLoginsRichlistitem, + { + extends: "richlistitem", + } + ); +} diff --git a/toolkit/content/widgets/browser-custom-element.js b/toolkit/content/widgets/browser-custom-element.js new file mode 100644 index 0000000000..e7a2c7e642 --- /dev/null +++ b/toolkit/content/widgets/browser-custom-element.js @@ -0,0 +1,1898 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + const { BrowserUtils } = ChromeUtils.import( + "resource://gre/modules/BrowserUtils.jsm" + ); + + const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" + ); + + let LazyModules = {}; + + ChromeUtils.defineModuleGetter( + LazyModules, + "E10SUtils", + "resource://gre/modules/E10SUtils.jsm" + ); + + ChromeUtils.defineModuleGetter( + LazyModules, + "RemoteWebNavigation", + "resource://gre/modules/RemoteWebNavigation.jsm" + ); + + ChromeUtils.defineModuleGetter( + LazyModules, + "PopupBlocker", + "resource://gre/actors/PopupBlockingParent.jsm" + ); + + let lazyPrefs = {}; + XPCOMUtils.defineLazyPreferenceGetter( + lazyPrefs, + "unloadTimeoutMs", + "dom.beforeunload_timeout_ms" + ); + + const elementsToDestroyOnUnload = new Set(); + + window.addEventListener( + "unload", + () => { + for (let element of elementsToDestroyOnUnload.values()) { + element.destroy(); + } + elementsToDestroyOnUnload.clear(); + }, + { mozSystemGroup: true, once: true } + ); + + class MozBrowser extends MozElements.MozElementMixin(XULFrameElement) { + static get observedAttributes() { + return ["remote"]; + } + + constructor() { + super(); + + this.onPageHide = this.onPageHide.bind(this); + + this.isNavigating = false; + + this._documentURI = null; + this._characterSet = null; + this._documentContentType = null; + + this._inPermitUnload = new WeakSet(); + + /** + * These are managed by the tabbrowser: + */ + this.droppedLinkHandler = null; + this.mIconURL = null; + this.lastURI = null; + + XPCOMUtils.defineLazyGetter(this, "popupBlocker", () => { + return new LazyModules.PopupBlocker(this); + }); + + this.addEventListener( + "dragover", + event => { + if (!this.droppedLinkHandler || event.defaultPrevented) { + return; + } + + // For drags that appear to be internal text (for example, tab drags), + // set the dropEffect to 'none'. This prevents the drop even if some + // other listener cancelled the event. + var types = event.dataTransfer.types; + if ( + types.includes("text/x-moz-text-internal") && + !types.includes("text/plain") + ) { + event.dataTransfer.dropEffect = "none"; + event.stopPropagation(); + event.preventDefault(); + } + + // No need to handle "dragover" in e10s, since nsDocShellTreeOwner.cpp in the child process + // handles that case using "@mozilla.org/content/dropped-link-handler;1" service. + if (this.isRemoteBrowser) { + return; + } + + let linkHandler = Services.droppedLinkHandler; + if (linkHandler.canDropLink(event, false)) { + event.preventDefault(); + } + }, + { mozSystemGroup: true } + ); + + this.addEventListener( + "drop", + event => { + // No need to handle "drop" in e10s, since nsDocShellTreeOwner.cpp in the child process + // handles that case using "@mozilla.org/content/dropped-link-handler;1" service. + if ( + !this.droppedLinkHandler || + event.defaultPrevented || + this.isRemoteBrowser + ) { + return; + } + + let linkHandler = Services.droppedLinkHandler; + try { + // Pass true to prevent the dropping of javascript:/data: URIs + var links = linkHandler.dropLinks(event, true); + } catch (ex) { + return; + } + + if (links.length) { + let triggeringPrincipal = linkHandler.getTriggeringPrincipal(event); + this.droppedLinkHandler(event, links, triggeringPrincipal); + } + }, + { mozSystemGroup: true } + ); + + this.addEventListener("dragstart", event => { + // If we're a remote browser dealing with a dragstart, stop it + // from propagating up, since our content process should be dealing + // with the mouse movement. + if (this.isRemoteBrowser) { + event.stopPropagation(); + } + }); + } + + resetFields() { + if (this.observer) { + try { + Services.obs.removeObserver( + this.observer, + "browser:purge-session-history" + ); + } catch (ex) { + // It's not clear why this sometimes throws an exception. + } + this.observer = null; + } + + let browser = this; + this.observer = { + observe(aSubject, aTopic, aState) { + if (aTopic == "browser:purge-session-history") { + browser.purgeSessionHistory(); + } else if (aTopic == "apz:cancel-autoscroll") { + if (aState == browser._autoScrollScrollId) { + // Set this._autoScrollScrollId to null, so in stopScroll() we + // don't call stopApzAutoscroll() (since it's APZ that + // initiated the stopping). + browser._autoScrollScrollId = null; + browser._autoScrollPresShellId = null; + + browser._autoScrollPopup.hidePopup(); + } + } + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + }; + + this._documentURI = null; + + this._documentContentType = null; + + this._loadContext = null; + + this._webBrowserFind = null; + + this._finder = null; + + this._remoteFinder = null; + + this._fastFind = null; + + this._lastSearchString = null; + + this._characterSet = ""; + + this._mayEnableCharacterEncodingMenu = null; + + this._charsetAutodetected = false; + + this._contentPrincipal = null; + + this._contentPartitionedPrincipal = null; + + this._csp = null; + + this._referrerInfo = null; + + this._contentRequestContextID = null; + + this._rdmFullZoom = 1.0; + + this._isSyntheticDocument = false; + + this.mPrefs = Services.prefs; + + this._mStrBundle = null; + + this._audioMuted = false; + + this._hasAnyPlayingMediaBeenBlocked = false; + + this._unselectedTabHoverMessageListenerCount = 0; + + this.urlbarChangeTracker = { + _startedLoadSinceLastUserTyping: false, + + startedLoad() { + this._startedLoadSinceLastUserTyping = true; + }, + finishedLoad() { + this._startedLoadSinceLastUserTyping = false; + }, + userTyped() { + this._startedLoadSinceLastUserTyping = false; + }, + }; + + this._userTypedValue = null; + + this._AUTOSCROLL_SNAP = 10; + + this._autoScrollBrowsingContext = null; + + this._startX = null; + + this._startY = null; + + this._autoScrollPopup = null; + + /** + * These IDs identify the scroll frame being autoscrolled. + */ + this._autoScrollScrollId = null; + + this._autoScrollPresShellId = null; + } + + connectedCallback() { + // We typically use this to avoid running JS that triggers a layout during parse + // (see comment on the delayConnectedCallback implementation). In this case, we + // are using it to avoid a leak - see https://bugzilla.mozilla.org/show_bug.cgi?id=1441935#c20. + if (this.delayConnectedCallback()) { + return; + } + + this.construct(); + } + + disconnectedCallback() { + this.destroy(); + } + + get autoscrollEnabled() { + if (this.getAttribute("autoscroll") == "false") { + return false; + } + + return this.mPrefs.getBoolPref("general.autoScroll", true); + } + + get canGoBack() { + return this.webNavigation.canGoBack; + } + + get canGoForward() { + return this.webNavigation.canGoForward; + } + + get currentURI() { + if (this.webNavigation) { + return this.webNavigation.currentURI; + } + return null; + } + + get documentURI() { + return this.isRemoteBrowser + ? this._documentURI + : this.contentDocument.documentURIObject; + } + + get documentContentType() { + if (this.isRemoteBrowser) { + return this._documentContentType; + } + return this.contentDocument ? this.contentDocument.contentType : null; + } + + set documentContentType(aContentType) { + if (aContentType != null) { + if (this.isRemoteBrowser) { + this._documentContentType = aContentType; + } else { + this.contentDocument.documentContentType = aContentType; + } + } + } + + get loadContext() { + if (this._loadContext) { + return this._loadContext; + } + + let { frameLoader } = this; + if (!frameLoader) { + return null; + } + this._loadContext = frameLoader.loadContext; + return this._loadContext; + } + + get autoCompletePopup() { + return document.getElementById(this.getAttribute("autocompletepopup")); + } + + get dateTimePicker() { + return document.getElementById(this.getAttribute("datetimepicker")); + } + + /** + * Provides a node to hang popups (such as the datetimepicker) from. + * If this <browser> isn't the descendant of a <stack>, null is returned + * instead and popup code must handle this case. + */ + get popupAnchor() { + let stack = this.closest("stack"); + if (!stack) { + return null; + } + + let popupAnchor = stack.querySelector(".popup-anchor"); + if (popupAnchor) { + return popupAnchor; + } + + // Create an anchor for the popup + popupAnchor = document.createElement("div"); + popupAnchor.className = "popup-anchor"; + popupAnchor.hidden = true; + stack.appendChild(popupAnchor); + return popupAnchor; + } + + set suspendMediaWhenInactive(val) { + this.browsingContext.suspendMediaWhenInactive = val; + } + + get suspendMediaWhenInactive() { + return !!this.browsingContext?.suspendMediaWhenInactive; + } + + set docShellIsActive(val) { + this.browsingContext.isActive = val; + if (this.isRemoteBrowser) { + let remoteTab = this.frameLoader?.remoteTab; + if (remoteTab) { + remoteTab.renderLayers = val; + } + } + } + + get docShellIsActive() { + return !!this.browsingContext?.isActive; + } + + set renderLayers(val) { + if (this.isRemoteBrowser) { + let remoteTab = this.frameLoader?.remoteTab; + if (remoteTab) { + remoteTab.renderLayers = val; + } + } else { + this.docShellIsActive = val; + } + } + + get renderLayers() { + if (this.isRemoteBrowser) { + return !!this.frameLoader?.remoteTab?.renderLayers; + } + return this.docShellIsActive; + } + + get hasLayers() { + if (this.isRemoteBrowser) { + return !!this.frameLoader?.remoteTab?.hasLayers; + } + return this.docShellIsActive; + } + + get isRemoteBrowser() { + return this.getAttribute("remote") == "true"; + } + + get remoteType() { + return this.browsingContext?.currentRemoteType; + } + + get isCrashed() { + if (!this.isRemoteBrowser || !this.frameLoader) { + return false; + } + + return !this.frameLoader.remoteTab; + } + + get messageManager() { + // Bug 1524084 - Trying to get at the message manager while in the crashed state will + // create a new message manager that won't shut down properly when the crashed browser + // is removed from the DOM. We work around that right now by returning null if we're + // in the crashed state. + if (this.frameLoader && !this.isCrashed) { + return this.frameLoader.messageManager; + } + return null; + } + + get webBrowserFind() { + if (!this._webBrowserFind) { + this._webBrowserFind = this.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebBrowserFind); + } + return this._webBrowserFind; + } + + get finder() { + if (this.isRemoteBrowser) { + if (!this._remoteFinder) { + let jsm = "resource://gre/modules/FinderParent.jsm"; + let { FinderParent } = ChromeUtils.import(jsm, {}); + this._remoteFinder = new FinderParent(this); + } + return this._remoteFinder; + } + if (!this._finder) { + if (!this.docShell) { + return null; + } + + let Finder = ChromeUtils.import("resource://gre/modules/Finder.jsm", {}) + .Finder; + this._finder = new Finder(this.docShell); + } + return this._finder; + } + + get fastFind() { + if (!this._fastFind) { + if (!("@mozilla.org/typeaheadfind;1" in Cc)) { + return null; + } + + var tabBrowser = this.getTabBrowser(); + if (tabBrowser && "fastFind" in tabBrowser) { + return (this._fastFind = tabBrowser.fastFind); + } + + if (!this.docShell) { + return null; + } + + this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance( + Ci.nsITypeAheadFind + ); + this._fastFind.init(this.docShell); + } + return this._fastFind; + } + + get outerWindowID() { + return this.browsingContext?.currentWindowGlobal?.outerWindowId; + } + + get innerWindowID() { + return this.browsingContext?.currentWindowGlobal?.innerWindowId || null; + } + + get browsingContext() { + if (this.frameLoader) { + return this.frameLoader.browsingContext; + } + return null; + } + /** + * Note that this overrides webNavigation on XULFrameElement, and duplicates the return value for the non-remote case + */ + get webNavigation() { + return this.isRemoteBrowser + ? this._remoteWebNavigation + : this.docShell && this.docShell.QueryInterface(Ci.nsIWebNavigation); + } + + get webProgress() { + return this.browsingContext?.webProgress; + } + + get sessionHistory() { + return this.webNavigation.sessionHistory; + } + + get contentTitle() { + return this.isRemoteBrowser + ? this.browsingContext?.currentWindowGlobal?.documentTitle + : this.contentDocument.title; + } + + set characterSet(val) { + if (this.isRemoteBrowser) { + this.sendMessageToActor( + "UpdateCharacterSet", + { value: val }, + "BrowserTab" + ); + this._characterSet = val; + } else { + this.docShell.charset = val; + this.docShell.gatherCharsetMenuTelemetry(); + } + } + + get characterSet() { + return this.isRemoteBrowser ? this._characterSet : this.docShell.charset; + } + + get mayEnableCharacterEncodingMenu() { + return this.isRemoteBrowser + ? this._mayEnableCharacterEncodingMenu + : this.docShell.mayEnableCharacterEncodingMenu; + } + + set mayEnableCharacterEncodingMenu(aMayEnable) { + if (this.isRemoteBrowser) { + this._mayEnableCharacterEncodingMenu = aMayEnable; + } + } + + get charsetAutodetected() { + return this.isRemoteBrowser + ? this._charsetAutodetected + : this.docShell.charsetAutodetected; + } + + set charsetAutodetected(aAutodetected) { + if (this.isRemoteBrowser) { + this._charsetAutodetected = aAutodetected; + } + } + + get contentPrincipal() { + return this.isRemoteBrowser + ? this._contentPrincipal + : this.contentDocument.nodePrincipal; + } + + get contentPartitionedPrincipal() { + return this.isRemoteBrowser + ? this._contentPartitionedPrincipal + : this.contentDocument.partitionedPrincipal; + } + + get cookieJarSettings() { + return this.isRemoteBrowser + ? this.browsingContext?.currentWindowGlobal?.cookieJarSettings + : this.contentDocument.cookieJarSettings; + } + + get csp() { + return this.isRemoteBrowser ? this._csp : this.contentDocument.csp; + } + + get contentRequestContextID() { + if (this.isRemoteBrowser) { + return this._contentRequestContextID; + } + try { + return this.contentDocument.documentLoadGroup.requestContextID; + } catch (e) { + return null; + } + } + + get referrerInfo() { + return this.isRemoteBrowser + ? this._referrerInfo + : this.contentDocument.referrerInfo; + } + + set fullZoom(val) { + if (val.toFixed(2) == this.fullZoom.toFixed(2)) { + return; + } + if (this.browsingContext.inRDMPane) { + this._rdmFullZoom = val; + let event = document.createEvent("Events"); + event.initEvent("FullZoomChange", true, false); + this.dispatchEvent(event); + } else { + this.browsingContext.fullZoom = val; + } + } + + get fullZoom() { + if (this.browsingContext.inRDMPane) { + return this._rdmFullZoom; + } + return this.browsingContext.fullZoom; + } + + set textZoom(val) { + if (val.toFixed(2) == this.textZoom.toFixed(2)) { + return; + } + this.browsingContext.textZoom = val; + } + + get textZoom() { + return this.browsingContext.textZoom; + } + + enterResponsiveMode() { + if (this.browsingContext.inRDMPane) { + return; + } + this.browsingContext.inRDMPane = true; + this._rdmFullZoom = this.browsingContext.fullZoom; + this.browsingContext.fullZoom = 1.0; + } + + leaveResponsiveMode() { + if (!this.browsingContext.inRDMPane) { + return; + } + this.browsingContext.inRDMPane = false; + this.browsingContext.fullZoom = this._rdmFullZoom; + } + + get isSyntheticDocument() { + if (this.isRemoteBrowser) { + return this._isSyntheticDocument; + } + return this.contentDocument.mozSyntheticDocument; + } + + get hasContentOpener() { + return !!this.browsingContext.opener; + } + + get mStrBundle() { + if (!this._mStrBundle) { + // need to create string bundle manually instead of using <xul:stringbundle/> + // see bug 63370 for details + this._mStrBundle = Services.strings.createBundle( + "chrome://global/locale/browser.properties" + ); + } + return this._mStrBundle; + } + + get audioMuted() { + return this._audioMuted; + } + + get shouldHandleUnselectedTabHover() { + return this._unselectedTabHoverMessageListenerCount > 0; + } + + set shouldHandleUnselectedTabHover(value) { + this._unselectedTabHoverMessageListenerCount += value ? 1 : -1; + } + + get securityUI() { + return this.browsingContext.secureBrowserUI; + } + + set userTypedValue(val) { + this.urlbarChangeTracker.userTyped(); + this._userTypedValue = val; + } + + get userTypedValue() { + return this._userTypedValue; + } + + get dontPromptAndDontUnload() { + return 1; + } + + get dontPromptAndUnload() { + return 2; + } + + _wrapURIChangeCall(fn) { + if (!this.isRemoteBrowser) { + this.isNavigating = true; + try { + fn(); + } finally { + this.isNavigating = false; + } + } else { + fn(); + } + } + + goBack( + requireUserInteraction = BrowserUtils.navigationRequireUserInteraction + ) { + var webNavigation = this.webNavigation; + if (webNavigation.canGoBack) { + this._wrapURIChangeCall(() => + webNavigation.goBack(requireUserInteraction) + ); + } + } + + goForward( + requireUserInteraction = BrowserUtils.navigationRequireUserInteraction + ) { + var webNavigation = this.webNavigation; + if (webNavigation.canGoForward) { + this._wrapURIChangeCall(() => + webNavigation.goForward(requireUserInteraction) + ); + } + } + + reload() { + const nsIWebNavigation = Ci.nsIWebNavigation; + const flags = nsIWebNavigation.LOAD_FLAGS_NONE; + this.reloadWithFlags(flags); + } + + reloadWithFlags(aFlags) { + this.webNavigation.reload(aFlags); + } + + stop() { + const nsIWebNavigation = Ci.nsIWebNavigation; + const flags = nsIWebNavigation.STOP_ALL; + this.webNavigation.stop(flags); + } + + /** + * throws exception for unknown schemes + */ + loadURI(aURI, aParams = {}) { + if (!aURI) { + aURI = "about:blank"; + } + let { + referrerInfo, + triggeringPrincipal, + postData, + headers, + csp, + } = aParams; + let loadFlags = + aParams.loadFlags || + aParams.flags || + Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + let loadURIOptions = { + triggeringPrincipal, + csp, + referrerInfo, + loadFlags, + postData, + headers, + }; + this._wrapURIChangeCall(() => + this.webNavigation.loadURI(aURI, loadURIOptions) + ); + } + + gotoIndex(aIndex) { + this._wrapURIChangeCall(() => this.webNavigation.gotoIndex(aIndex)); + } + + preserveLayers(preserve) { + if (!this.isRemoteBrowser) { + return; + } + let { frameLoader } = this; + if (frameLoader.remoteTab) { + frameLoader.remoteTab.preserveLayers(preserve); + } + } + + deprioritize() { + if (!this.isRemoteBrowser) { + return; + } + let { frameLoader } = this; + if (frameLoader.remoteTab) { + frameLoader.remoteTab.deprioritize(); + } + } + + getTabBrowser() { + if ( + this.ownerGlobal.gBrowser && + this.ownerGlobal.gBrowser.getTabForBrowser && + this.ownerGlobal.gBrowser.getTabForBrowser(this) + ) { + return this.ownerGlobal.gBrowser; + } + return null; + } + + addProgressListener(aListener, aNotifyMask) { + if (!aNotifyMask) { + aNotifyMask = Ci.nsIWebProgress.NOTIFY_ALL; + } + + this.webProgress.addProgressListener(aListener, aNotifyMask); + } + + removeProgressListener(aListener) { + this.webProgress.removeProgressListener(aListener); + } + + onPageHide(aEvent) { + if (!this.docShell || !this.fastFind) { + return; + } + var tabBrowser = this.getTabBrowser(); + if ( + !tabBrowser || + !("fastFind" in tabBrowser) || + tabBrowser.selectedBrowser == this + ) { + this.fastFind.setDocShell(this.docShell); + } + } + + audioPlaybackStarted() { + if (this._audioMuted) { + return; + } + let event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackStarted", true, false); + this.dispatchEvent(event); + } + + audioPlaybackStopped() { + let event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackStopped", true, false); + this.dispatchEvent(event); + } + + /** + * When the pref "media.block-autoplay-until-in-foreground" is on, + * Gecko delays starting playback of media resources in tabs until the + * tab has been in the foreground or resumed by tab's play tab icon. + * - When Gecko delays starting playback of a media resource in a window, + * it sends a message to call activeMediaBlockStarted(). This causes the + * tab audio indicator to show. + * - When a tab is foregrounded, Gecko starts playing all delayed media + * resources in that tab, and sends a message to call + * activeMediaBlockStopped(). This causes the tab audio indicator to hide. + */ + activeMediaBlockStarted() { + this._hasAnyPlayingMediaBeenBlocked = true; + let event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackBlockStarted", true, false); + this.dispatchEvent(event); + } + + activeMediaBlockStopped() { + if (!this._hasAnyPlayingMediaBeenBlocked) { + return; + } + this._hasAnyPlayingMediaBeenBlocked = false; + let event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackBlockStopped", true, false); + this.dispatchEvent(event); + } + + mute(transientState) { + if (!transientState) { + this._audioMuted = true; + } + let context = this.frameLoader.browsingContext; + context.notifyMediaMutedChanged(true); + } + + unmute() { + this._audioMuted = false; + let context = this.frameLoader.browsingContext; + context.notifyMediaMutedChanged(false); + } + + resumeMedia() { + this.frameLoader.browsingContext.notifyStartDelayedAutoplayMedia(); + if (this._hasAnyPlayingMediaBeenBlocked) { + this._hasAnyPlayingMediaBeenBlocked = false; + let event = document.createEvent("Events"); + event.initEvent("DOMAudioPlaybackBlockStopped", true, false); + this.dispatchEvent(event); + } + } + + unselectedTabHover(hovered) { + if (!this.shouldHandleUnselectedTabHover) { + return; + } + this.sendMessageToActor( + "Browser:UnselectedTabHover", + { + hovered, + }, + "UnselectedTabHover", + "roots" + ); + } + + didStartLoadSinceLastUserTyping() { + return ( + !this.isNavigating && + this.urlbarChangeTracker._startedLoadSinceLastUserTyping + ); + } + + construct() { + elementsToDestroyOnUnload.add(this); + this.resetFields(); + this.mInitialized = true; + if (this.isRemoteBrowser) { + /* + * Don't try to send messages from this function. The message manager for + * the <browser> element may not be initialized yet. + */ + + this._remoteWebNavigation = new LazyModules.RemoteWebNavigation(this); + + // Initialize contentPrincipal to the about:blank principal for this loadcontext + let aboutBlank = Services.io.newURI("about:blank"); + let ssm = Services.scriptSecurityManager; + this._contentPrincipal = ssm.getLoadContextContentPrincipal( + aboutBlank, + this.loadContext + ); + // CSP for about:blank is null; if we ever change _contentPrincipal above, + // we should re-evaluate the CSP here. + this._csp = null; + + this.messageManager.loadFrameScript( + "chrome://global/content/browser-child.js", + true + ); + + if (!this.hasAttribute("disablehistory")) { + Services.obs.addObserver( + this.observer, + "browser:purge-session-history", + true + ); + } + } + + try { + // |webNavigation.sessionHistory| will have been set by the frame + // loader when creating the docShell as long as this xul:browser + // doesn't have the 'disablehistory' attribute set. + if (this.docShell && this.webNavigation.sessionHistory) { + Services.obs.addObserver( + this.observer, + "browser:purge-session-history", + true + ); + + // enable global history if we weren't told otherwise + if ( + !this.hasAttribute("disableglobalhistory") && + !this.isRemoteBrowser + ) { + try { + this.docShell.browsingContext.useGlobalHistory = true; + } catch (ex) { + // This can occur if the Places database is locked + Cu.reportError("Error enabling browser global history: " + ex); + } + } + } + } catch (e) { + Cu.reportError(e); + } + try { + // Ensures the securityUI is initialized. + var securityUI = this.securityUI; // eslint-disable-line no-unused-vars + } catch (e) {} + + if (!this.isRemoteBrowser) { + this._remoteWebNavigation = null; + this.addEventListener("pagehide", this.onPageHide, true); + } + } + + /** + * This is necessary because the destructor doesn't always get called when + * we are removed from a tabbrowser. This will be explicitly called by tabbrowser. + */ + destroy() { + elementsToDestroyOnUnload.delete(this); + + // Make sure that any open select is closed. + if (this.hasAttribute("selectmenulist")) { + let menulist = document.getElementById( + this.getAttribute("selectmenulist") + ); + if (menulist && menulist.open) { + let resourcePath = "resource://gre/actors/SelectParent.jsm"; + let { SelectParentHelper } = ChromeUtils.import(resourcePath); + SelectParentHelper.hide(menulist, this); + } + } + + this.resetFields(); + + if (!this.mInitialized) { + return; + } + + this.mInitialized = false; + this.lastURI = null; + + if (!this.isRemoteBrowser) { + this.removeEventListener("pagehide", this.onPageHide, true); + } + } + + updateForStateChange(aCharset, aDocumentURI, aContentType) { + if (this.isRemoteBrowser && this.messageManager) { + if (aCharset != null) { + this._characterSet = aCharset; + } + + if (aDocumentURI != null) { + this._documentURI = aDocumentURI; + } + + if (aContentType != null) { + this._documentContentType = aContentType; + } + } + } + + updateWebNavigationForLocationChange(aCanGoBack, aCanGoForward) { + if ( + this.isRemoteBrowser && + this.messageManager && + !Services.appinfo.sessionHistoryInParent + ) { + this._remoteWebNavigation._canGoBack = aCanGoBack; + this._remoteWebNavigation._canGoForward = aCanGoForward; + } + } + + updateForLocationChange( + aLocation, + aCharset, + aMayEnableCharacterEncodingMenu, + aCharsetAutodetected, + aDocumentURI, + aTitle, + aContentPrincipal, + aContentPartitionedPrincipal, + aCSP, + aReferrerInfo, + aIsSynthetic, + aHaveRequestContextID, + aRequestContextID, + aContentType + ) { + if (this.isRemoteBrowser && this.messageManager) { + if (aCharset != null) { + this._characterSet = aCharset; + this._mayEnableCharacterEncodingMenu = aMayEnableCharacterEncodingMenu; + this._charsetAutodetected = aCharsetAutodetected; + } + + if (aContentType != null) { + this._documentContentType = aContentType; + } + + this._remoteWebNavigation._currentURI = aLocation; + this._documentURI = aDocumentURI; + this._contentPrincipal = aContentPrincipal; + this._contentPartitionedPrincipal = aContentPartitionedPrincipal; + this._csp = aCSP; + this._referrerInfo = aReferrerInfo; + this._isSyntheticDocument = aIsSynthetic; + this._contentRequestContextID = aHaveRequestContextID + ? aRequestContextID + : null; + } + } + + purgeSessionHistory() { + if (this.isRemoteBrowser && !Services.appinfo.sessionHistoryInParent) { + this._remoteWebNavigation._canGoBack = false; + this._remoteWebNavigation._canGoForward = false; + } + + try { + if (Services.appinfo.sessionHistoryInParent) { + let sessionHistory = this.browsingContext?.sessionHistory; + if (!sessionHistory) { + return; + } + + // place the entry at current index at the end of the history list, so it won't get removed + if (sessionHistory.index < sessionHistory.count - 1) { + let indexEntry = sessionHistory.getEntryAtIndex( + sessionHistory.index + ); + sessionHistory.addEntry(indexEntry, true); + } + + let purge = sessionHistory.count; + if ( + this.browsingContext.currentWindowGlobal.documentURI != + "about:blank" + ) { + --purge; // Don't remove the page the user's staring at from shistory + } + + if (purge > 0) { + sessionHistory.purgeHistory(purge); + } + + return; + } + + this.sendMessageToActor( + "Browser:PurgeSessionHistory", + {}, + "PurgeSessionHistory", + "roots" + ); + } catch (ex) { + // This can throw if the browser has started to go away. + if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) { + throw ex; + } + } + } + + createAboutBlankContentViewer(aPrincipal, aPartitionedPrincipal) { + if (this.isRemoteBrowser) { + // Ensure that the content process has the permissions which are + // needed to create a document with the given principal. + let permissionPrincipal = BrowserUtils.principalWithMatchingOA( + aPrincipal, + this.contentPrincipal + ); + this.frameLoader.remoteTab.transmitPermissionsForPrincipal( + permissionPrincipal + ); + + // This still uses the message manager, for the following reasons: + // + // 1. Due to bug 1523638, it's virtually certain that, if we've just created + // this <xul:browser>, that the WindowGlobalParent for the top-level frame + // of this browser doesn't exist yet, so it's not possible to get at a + // JS Window Actor for it. + // + // 2. JS Window Actors are tied to the principals for the frames they're running + // in - switching principals is therefore self-destructive and unexpected. + // + // So we'll continue to use the message manager until we come up with a better + // solution. + this.messageManager.sendAsyncMessage( + "BrowserElement:CreateAboutBlank", + { principal: aPrincipal, partitionedPrincipal: aPartitionedPrincipal } + ); + return; + } + let principal = BrowserUtils.principalWithMatchingOA( + aPrincipal, + this.contentPrincipal + ); + let partitionedPrincipal = BrowserUtils.principalWithMatchingOA( + aPartitionedPrincipal, + this.contentPartitionedPrincipal + ); + this.docShell.createAboutBlankContentViewer( + principal, + partitionedPrincipal + ); + } + + stopScroll() { + if (this._autoScrollBrowsingContext) { + window.removeEventListener("mousemove", this, true); + window.removeEventListener("mousedown", this, true); + window.removeEventListener("mouseup", this, true); + window.removeEventListener("DOMMouseScroll", this, true); + window.removeEventListener("contextmenu", this, true); + window.removeEventListener("keydown", this, true); + window.removeEventListener("keypress", this, true); + window.removeEventListener("keyup", this, true); + + let autoScrollWnd = this._autoScrollBrowsingContext.currentWindowGlobal; + if (autoScrollWnd) { + autoScrollWnd + .getActor("AutoScroll") + .sendAsyncMessage("Autoscroll:Stop", {}); + } + this._autoScrollBrowsingContext = null; + + try { + Services.obs.removeObserver(this.observer, "apz:cancel-autoscroll"); + } catch (ex) { + // It's not clear why this sometimes throws an exception + } + + if (this.isRemoteBrowser && this._autoScrollScrollId != null) { + let { remoteTab } = this.frameLoader; + if (remoteTab) { + remoteTab.stopApzAutoscroll( + this._autoScrollScrollId, + this._autoScrollPresShellId + ); + } + this._autoScrollScrollId = null; + this._autoScrollPresShellId = null; + } + } + } + + _getAndMaybeCreateAutoScrollPopup() { + let autoscrollPopup = document.getElementById("autoscroller"); + if (!autoscrollPopup) { + autoscrollPopup = document.createXULElement("panel"); + autoscrollPopup.className = "autoscroller"; + autoscrollPopup.setAttribute("consumeoutsideclicks", "true"); + autoscrollPopup.setAttribute("rolluponmousewheel", "true"); + autoscrollPopup.id = "autoscroller"; + } + + return autoscrollPopup; + } + + startScroll({ + scrolldir, + screenX, + screenY, + scrollId, + presShellId, + browsingContext, + }) { + if (!this.autoscrollEnabled) { + return { autoscrollEnabled: false, usingApz: false }; + } + + const POPUP_SIZE = 32; + if (!this._autoScrollPopup) { + this._autoScrollPopup = this._getAndMaybeCreateAutoScrollPopup(); + document.documentElement.appendChild(this._autoScrollPopup); + this._autoScrollPopup.removeAttribute("hidden"); + this._autoScrollPopup.setAttribute("noautofocus", "true"); + this._autoScrollPopup.style.height = POPUP_SIZE + "px"; + this._autoScrollPopup.style.width = POPUP_SIZE + "px"; + this._autoScrollPopup.style.margin = -POPUP_SIZE / 2 + "px"; + } + + let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService( + Ci.nsIScreenManager + ); + let screen = screenManager.screenForRect(screenX, screenY, 1, 1); + + // we need these attributes so themers don't need to create per-platform packages + if (screen.colorDepth > 8) { + // need high color for transparency + // Exclude second-rate platforms + this._autoScrollPopup.setAttribute( + "transparent", + !/BeOS|OS\/2/.test(navigator.appVersion) + ); + // Enable translucency on Windows and Mac + this._autoScrollPopup.setAttribute( + "translucent", + AppConstants.platform == "win" || AppConstants.platform == "macosx" + ); + } + + this._autoScrollPopup.setAttribute("scrolldir", scrolldir); + this._autoScrollPopup.addEventListener("popuphidden", this, true); + + // Sanitize screenX/screenY for available screen size with half the size + // of the popup removed. The popup uses negative margins to center on the + // coordinates we pass. Unfortunately `window.screen.availLeft` can be negative + // on Windows in multi-monitor setups, so we use nsIScreenManager instead: + let left = {}, + top = {}, + width = {}, + height = {}; + screen.GetAvailRect(left, top, width, height); + + // We need to get screen CSS-pixel (rather than display-pixel) coordinates. + // With 175% DPI, the actual ratio of display pixels to CSS pixels is + // 1.7647 because of rounding inside gecko. Unfortunately defaultCSSScaleFactor + // returns the original 1.75 dpi factor. While window.devicePixelRatio would + // get us the correct ratio, if the window is split between 2 screens, + // window.devicePixelRatio isn't guaranteed to match the screen we're + // autoscrolling on. So instead we do the same math as Gecko. + const scaleFactor = 60 / Math.round(60 / screen.defaultCSSScaleFactor); + let minX = left.value / scaleFactor + 0.5 * POPUP_SIZE; + let maxX = (left.value + width.value) / scaleFactor - 0.5 * POPUP_SIZE; + let minY = top.value / scaleFactor + 0.5 * POPUP_SIZE; + let maxY = (top.value + height.value) / scaleFactor - 0.5 * POPUP_SIZE; + let popupX = Math.max(minX, Math.min(maxX, screenX)); + let popupY = Math.max(minY, Math.min(maxY, screenY)); + this._autoScrollPopup.openPopupAtScreen(popupX, popupY); + this._ignoreMouseEvents = true; + this._startX = screenX; + this._startY = screenY; + this._autoScrollBrowsingContext = browsingContext; + + window.addEventListener("mousemove", this, true); + window.addEventListener("mousedown", this, true); + window.addEventListener("mouseup", this, true); + window.addEventListener("DOMMouseScroll", this, true); + window.addEventListener("contextmenu", this, true); + window.addEventListener("keydown", this, true); + window.addEventListener("keypress", this, true); + window.addEventListener("keyup", this, true); + + let usingApz = false; + if ( + this.isRemoteBrowser && + scrollId != null && + this.mPrefs.getBoolPref("apz.autoscroll.enabled", false) + ) { + let { remoteTab } = this.frameLoader; + if (remoteTab) { + // If APZ is handling the autoscroll, it may decide to cancel + // it of its own accord, so register an observer to allow it + // to notify us of that. + Services.obs.addObserver( + this.observer, + "apz:cancel-autoscroll", + true + ); + + usingApz = remoteTab.startApzAutoscroll( + screenX, + screenY, + scrollId, + presShellId + ); + } + // Save the IDs for later + this._autoScrollScrollId = scrollId; + this._autoScrollPresShellId = presShellId; + } + + return { autoscrollEnabled: true, usingApz }; + } + + cancelScroll() { + this._autoScrollPopup.hidePopup(); + } + + handleEvent(aEvent) { + if (this._autoScrollBrowsingContext) { + switch (aEvent.type) { + case "mousemove": { + var x = aEvent.screenX - this._startX; + var y = aEvent.screenY - this._startY; + + if ( + x > this._AUTOSCROLL_SNAP || + x < -this._AUTOSCROLL_SNAP || + y > this._AUTOSCROLL_SNAP || + y < -this._AUTOSCROLL_SNAP + ) { + this._ignoreMouseEvents = false; + } + break; + } + case "mouseup": + case "mousedown": + case "contextmenu": { + if (!this._ignoreMouseEvents) { + // Use a timeout to prevent the mousedown from opening the popup again. + // Ideally, we could use preventDefault here, but contenteditable + // and middlemouse paste don't interact well. See bug 1188536. + setTimeout(() => this._autoScrollPopup.hidePopup(), 0); + } + this._ignoreMouseEvents = false; + break; + } + case "DOMMouseScroll": { + this._autoScrollPopup.hidePopup(); + aEvent.preventDefault(); + break; + } + case "popuphidden": { + this._autoScrollPopup.removeEventListener( + "popuphidden", + this, + true + ); + this.stopScroll(); + break; + } + case "keydown": { + if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { + // the escape key will be processed by + // nsXULPopupManager::KeyDown and the panel will be closed. + // So, don't consume the key event here. + break; + } + // don't break here. we need to eat keydown events. + } + // fall through + case "keypress": + case "keyup": { + // All keyevents should be eaten here during autoscrolling. + aEvent.stopPropagation(); + aEvent.preventDefault(); + break; + } + } + } + } + + closeBrowser() { + // The request comes from a XPCOM component, we'd want to redirect + // the request to tabbrowser. + let tabbrowser = this.getTabBrowser(); + if (tabbrowser) { + let tab = tabbrowser.getTabForBrowser(this); + if (tab) { + tabbrowser.removeTab(tab); + return; + } + } + + throw new Error( + "Closing a browser which was not attached to a tabbrowser is unsupported." + ); + } + + swapBrowsers(aOtherBrowser) { + // The request comes from a XPCOM component, we'd want to redirect + // the request to tabbrowser so tabbrowser will be setup correctly, + // and it will eventually call swapDocShells. + let ourTabBrowser = this.getTabBrowser(); + let otherTabBrowser = aOtherBrowser.getTabBrowser(); + if (ourTabBrowser && otherTabBrowser) { + let ourTab = ourTabBrowser.getTabForBrowser(this); + let otherTab = otherTabBrowser.getTabForBrowser(aOtherBrowser); + ourTabBrowser.swapBrowsers(ourTab, otherTab); + return; + } + + // One of us is not connected to a tabbrowser, so just swap. + this.swapDocShells(aOtherBrowser); + } + + swapDocShells(aOtherBrowser) { + if (this.isRemoteBrowser != aOtherBrowser.isRemoteBrowser) { + throw new Error( + "Can only swap docshells between browsers in the same process." + ); + } + + // Give others a chance to swap state. + // IMPORTANT: Since a swapDocShells call does not swap the messageManager + // instances attached to a browser to aOtherBrowser, others + // will need to add the message listeners to the new + // messageManager. + // This is not a bug in swapDocShells or the FrameLoader, + // merely a design decision: If message managers were swapped, + // so that no new listeners were needed, the new + // aOtherBrowser.messageManager would have listeners pointing + // to the JS global of the current browser, which would rather + // easily create leaks while swapping. + // IMPORTANT2: When the current browser element is removed from DOM, + // which is quite common after a swapDocShells call, its + // frame loader is destroyed, and that destroys the relevant + // message manager, which will remove the listeners. + let event = new CustomEvent("SwapDocShells", { detail: aOtherBrowser }); + this.dispatchEvent(event); + event = new CustomEvent("SwapDocShells", { detail: this }); + aOtherBrowser.dispatchEvent(event); + + // We need to swap fields that are tied to our docshell or related to + // the loaded page + // Fields which are built as a result of notifactions (pageshow/hide, + // DOMLinkAdded/Removed, onStateChange) should not be swapped here, + // because these notifications are dispatched again once the docshells + // are swapped. + var fieldsToSwap = ["_webBrowserFind", "_rdmFullZoom"]; + + if (this.isRemoteBrowser) { + fieldsToSwap.push( + ...[ + "_remoteWebNavigation", + "_remoteFinder", + "_documentURI", + "_documentContentType", + "_characterSet", + "_mayEnableCharacterEncodingMenu", + "_charsetAutodetected", + "_contentPrincipal", + "_contentPartitionedPrincipal", + "_isSyntheticDocument", + ] + ); + } + + var ourFieldValues = {}; + var otherFieldValues = {}; + for (let field of fieldsToSwap) { + ourFieldValues[field] = this[field]; + otherFieldValues[field] = aOtherBrowser[field]; + } + + if (window.PopupNotifications) { + PopupNotifications._swapBrowserNotifications(aOtherBrowser, this); + } + + try { + this.swapFrameLoaders(aOtherBrowser); + } catch (ex) { + // This may not be implemented for browser elements that are not + // attached to a BrowserDOMWindow. + } + + for (let field of fieldsToSwap) { + this[field] = otherFieldValues[field]; + aOtherBrowser[field] = ourFieldValues[field]; + } + + if (!this.isRemoteBrowser) { + // Null the current nsITypeAheadFind instances so that they're + // lazily re-created on access. We need to do this because they + // might have attached the wrong docShell. + this._fastFind = aOtherBrowser._fastFind = null; + } else { + // Rewire the remote listeners + this._remoteWebNavigation.swapBrowser(this); + aOtherBrowser._remoteWebNavigation.swapBrowser(aOtherBrowser); + + if (this._remoteFinder) { + this._remoteFinder.swapBrowser(this); + } + if (aOtherBrowser._remoteFinder) { + aOtherBrowser._remoteFinder.swapBrowser(aOtherBrowser); + } + } + + event = new CustomEvent("EndSwapDocShells", { detail: aOtherBrowser }); + this.dispatchEvent(event); + event = new CustomEvent("EndSwapDocShells", { detail: this }); + aOtherBrowser.dispatchEvent(event); + } + + getInPermitUnload(aCallback) { + if (this.isRemoteBrowser) { + let { remoteTab } = this.frameLoader; + if (!remoteTab) { + // If we're crashed, we're definitely not in this state anymore. + aCallback(false); + return; + } + + aCallback( + this._inPermitUnload.has(this.browsingContext.currentWindowGlobal) + ); + return; + } + + if (!this.docShell || !this.docShell.contentViewer) { + aCallback(false); + return; + } + aCallback(this.docShell.contentViewer.inPermitUnload); + } + + async asyncPermitUnload(action) { + let wgp = this.browsingContext.currentWindowGlobal; + if (this._inPermitUnload.has(wgp)) { + throw new Error("permitUnload is already running for this tab."); + } + + this._inPermitUnload.add(wgp); + try { + let permitUnload = await wgp.permitUnload( + action, + lazyPrefs.unloadTimeoutMs + ); + return { permitUnload }; + } finally { + this._inPermitUnload.delete(wgp); + } + } + + get hasBeforeUnload() { + function hasBeforeUnload(bc) { + if (bc.currentWindowContext?.hasBeforeUnload) { + return true; + } + return bc.children.some(hasBeforeUnload); + } + return hasBeforeUnload(this.browsingContext); + } + + permitUnload(action) { + if (this.isRemoteBrowser) { + if (!this.hasBeforeUnload) { + return { permitUnload: true }; + } + + let result; + let success; + + this.asyncPermitUnload(action).then( + val => { + result = val; + success = true; + }, + err => { + result = err; + success = false; + } + ); + + // The permitUnload() promise will, alas, not call its resolution + // callbacks after the browser window the promise lives in has closed, + // so we have to check for that case explicitly. + Services.tm.spinEventLoopUntilOrShutdown( + () => window.closed || success !== undefined + ); + if (success) { + return result; + } + throw result; + } + + if (!this.docShell || !this.docShell.contentViewer) { + return { permitUnload: true }; + } + return { + permitUnload: this.docShell.contentViewer.permitUnload(), + }; + } + + print(aOuterWindowID, aPrintSettings) { + if (!this.frameLoader) { + throw Components.Exception("No frame loader.", Cr.NS_ERROR_FAILURE); + } + + return this.frameLoader.print(aOuterWindowID, aPrintSettings); + } + + async drawSnapshot(x, y, w, h, scale, backgroundColor) { + let rect = new DOMRect(x, y, w, h); + try { + return this.browsingContext.currentWindowGlobal.drawSnapshot( + rect, + scale, + backgroundColor + ); + } catch (e) { + return false; + } + } + + dropLinks(aLinks, aTriggeringPrincipal) { + if (!this.droppedLinkHandler) { + return false; + } + let links = []; + for (let i = 0; i < aLinks.length; i += 3) { + links.push({ + url: aLinks[i], + name: aLinks[i + 1], + type: aLinks[i + 2], + }); + } + this.droppedLinkHandler(null, links, aTriggeringPrincipal); + return true; + } + + getContentBlockingLog() { + let windowGlobal = this.browsingContext.currentWindowGlobal; + if (!windowGlobal) { + return null; + } + return windowGlobal.contentBlockingLog; + } + + getContentBlockingEvents() { + let windowGlobal = this.browsingContext.currentWindowGlobal; + if (!windowGlobal) { + return 0; + } + return windowGlobal.contentBlockingEvents; + } + + // Send an asynchronous message to the remote child via an actor. + // Note: use this only for messages through an actor. For old-style + // messages, use the message manager. + // The value of the scope argument determines which browsing contexts + // are sent to: + // 'all' - send to actors associated with all descendant child frames. + // 'roots' - send only to actors associated with process roots. + // undefined/'' - send only to the top-level actor and not any descendants. + sendMessageToActor(messageName, args, actorName, scope) { + if (!this.frameLoader) { + return; + } + + function sendToChildren(browsingContext, childScope) { + let windowGlobal = browsingContext.currentWindowGlobal; + // If 'roots' is set, only send if windowGlobal.isProcessRoot is true. + if ( + windowGlobal && + (childScope != "roots" || windowGlobal.isProcessRoot) + ) { + windowGlobal.getActor(actorName).sendAsyncMessage(messageName, args); + } + + // Iterate as long as scope in assigned. Note that we use the original + // passed in scope, not childScope here. + if (scope) { + for (let context of browsingContext.children) { + sendToChildren(context, scope); + } + } + } + + // Pass no second argument to always send to the top-level browsing context. + sendToChildren(this.browsingContext); + } + + enterModalState() { + this.sendMessageToActor("EnterModalState", {}, "BrowserElement", "roots"); + } + + leaveModalState() { + this.sendMessageToActor( + "LeaveModalState", + { forceLeave: true }, + "BrowserElement", + "roots" + ); + } + + /** + * Can be called for a window with or without modal state. + * If the window is not in modal state, this is a no-op. + */ + maybeLeaveModalState() { + this.sendMessageToActor( + "LeaveModalState", + { forceLeave: false }, + "BrowserElement", + "roots" + ); + } + + getDevicePermissionOrigins(key) { + if (typeof key !== "string" || key.length === 0) { + throw new Error("Key must be non empty string."); + } + if (!this._devicePermissionOrigins) { + this._devicePermissionOrigins = new Map(); + } + let origins = this._devicePermissionOrigins.get(key); + if (!origins) { + origins = new Set(); + this._devicePermissionOrigins.set(key, origins); + } + return origins; + } + + get processSwitchBehavior() { + // If a `remotenessChangeHandler` is attached to this browser, it supports + // having its toplevel process switched dynamically in response to + // navigations. + if (this.hasAttribute("maychangeremoteness")) { + return Ci.nsIBrowser.PROCESS_BEHAVIOR_STANDARD; + } + + // For backwards compatibility, we need to mark remote, but + // non-`allowremote`, frames as `PROCESS_BEHAVIOR_SUBFRAME_ONLY`, as some + // tests rely on it. + // FIXME: Remove this? + if (this.isRemoteBrowser) { + return Ci.nsIBrowser.PROCESS_BEHAVIOR_SUBFRAME_ONLY; + } + // Otherwise, don't allow gecko-initiated toplevel process switches. + return Ci.nsIBrowser.PROCESS_BEHAVIOR_DISABLED; + } + + // This method is replaced by frontend code in order to delay performing the + // process switch until some async operatin is completed. + // + // This is used by tabbrowser to flush SessionStore before a process switch. + async prepareToChangeRemoteness() { + /* no-op unless replaced */ + } + + // This method is replaced by frontend code in order to handle restoring + // remote session history + // + // Called immediately after changing remoteness. If this method returns + // `true`, Gecko will assume frontend handled resuming the load, and will + // not attempt to resume the load itself. + afterChangeRemoteness(browser, redirectLoadSwitchId) { + /* no-op unless replaced */ + return false; + } + + // Called by Gecko before the remoteness change happens, allowing for + // listeners, etc. to be stashed before the process switch. + beforeChangeRemoteness() { + // Fire the `WillChangeBrowserRemoteness` event, which may be hooked by + // frontend code for custom behaviour. + let event = document.createEvent("Events"); + event.initEvent("WillChangeBrowserRemoteness", true, false); + this.dispatchEvent(event); + + // Destroy ourselves to unregister from observer notifications + // FIXME: Can we get away with something less destructive here? + this.destroy(); + } + + finishChangeRemoteness(redirectLoadSwitchId) { + // Re-construct ourselves after the destroy in `beforeChangeRemoteness`. + this.construct(); + + // Fire the `DidChangeBrowserRemoteness` event, which may be hooked by + // frontend code for custom behaviour. + let event = document.createEvent("Events"); + event.initEvent("DidChangeBrowserRemoteness", true, false); + this.dispatchEvent(event); + + // Call into frontend code which may want to handle the load (e.g. to + // while restoring session state). + return this.afterChangeRemoteness(redirectLoadSwitchId); + } + } + + MozXULElement.implementCustomInterface(MozBrowser, [Ci.nsIBrowser]); + customElements.define("browser", MozBrowser); +} diff --git a/toolkit/content/widgets/button.js b/toolkit/content/widgets/button.js new file mode 100644 index 0000000000..b3873d8646 --- /dev/null +++ b/toolkit/content/widgets/button.js @@ -0,0 +1,317 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + class MozButtonBase extends MozElements.BaseText { + constructor() { + super(); + + /** + * While it would seem we could do this by handling oncommand, we can't + * because any external oncommand handlers might get called before ours, + * and then they would see the incorrect value of checked. Additionally + * a command attribute would redirect the command events anyway. + */ + this.addEventListener("click", event => { + if (event.button != 0) { + return; + } + this._handleClick(); + }); + + this.addEventListener("keypress", event => { + if (event.key != " ") { + return; + } + this._handleClick(); + // Prevent page from scrolling on the space key. + event.preventDefault(); + }); + + this.addEventListener("keypress", event => { + if (this.hasMenu()) { + if (this.open) { + return; + } + } else { + if ( + event.keyCode == KeyEvent.DOM_VK_UP || + (event.keyCode == KeyEvent.DOM_VK_LEFT && + document.defaultView.getComputedStyle(this.parentNode) + .direction == "ltr") || + (event.keyCode == KeyEvent.DOM_VK_RIGHT && + document.defaultView.getComputedStyle(this.parentNode) + .direction == "rtl") + ) { + event.preventDefault(); + window.document.commandDispatcher.rewindFocus(); + return; + } + + if ( + event.keyCode == KeyEvent.DOM_VK_DOWN || + (event.keyCode == KeyEvent.DOM_VK_RIGHT && + document.defaultView.getComputedStyle(this.parentNode) + .direction == "ltr") || + (event.keyCode == KeyEvent.DOM_VK_LEFT && + document.defaultView.getComputedStyle(this.parentNode) + .direction == "rtl") + ) { + event.preventDefault(); + window.document.commandDispatcher.advanceFocus(); + return; + } + } + + if ( + event.keyCode || + event.charCode <= 32 || + event.altKey || + event.ctrlKey || + event.metaKey + ) { + return; + } // No printable char pressed, not a potential accesskey + + // Possible accesskey pressed + var charPressedLower = String.fromCharCode( + event.charCode + ).toLowerCase(); + + // If the accesskey of the current button is pressed, just activate it + if (this.accessKey.toLowerCase() == charPressedLower) { + this.click(); + return; + } + + // Search for accesskey in the list of buttons for this doc and each subdoc + // Get the buttons for the main document and all sub-frames + for ( + var frameCount = -1; + frameCount < window.top.frames.length; + frameCount++ + ) { + var doc = + frameCount == -1 + ? window.top.document + : window.top.frames[frameCount].document; + if (this.fireAccessKeyButton(doc.documentElement, charPressedLower)) { + return; + } + } + + // Test dialog buttons + let buttonBox = window.top.document.documentElement.buttonBox; + if (buttonBox) { + this.fireAccessKeyButton(buttonBox, charPressedLower); + } + }); + } + + set type(val) { + this.setAttribute("type", val); + return val; + } + + get type() { + return this.getAttribute("type"); + } + + set disabled(val) { + if (val) { + this.setAttribute("disabled", "true"); + } else { + this.removeAttribute("disabled"); + } + return val; + } + + get disabled() { + return this.getAttribute("disabled") == "true"; + } + + set group(val) { + this.setAttribute("group", val); + return val; + } + + get group() { + return this.getAttribute("group"); + } + + set open(val) { + if (this.hasMenu()) { + this.openMenu(val); + } else if (val) { + // Fall back to just setting the attribute + this.setAttribute("open", "true"); + } else { + this.removeAttribute("open"); + } + return val; + } + + get open() { + return this.hasAttribute("open"); + } + + set checked(val) { + if (this.type == "radio" && val) { + var sibs = this.parentNode.getElementsByAttribute("group", this.group); + for (var i = 0; i < sibs.length; ++i) { + sibs[i].removeAttribute("checked"); + } + } + + if (val) { + this.setAttribute("checked", "true"); + } else { + this.removeAttribute("checked"); + } + + return val; + } + + get checked() { + return this.hasAttribute("checked"); + } + + filterButtons(node) { + // if the node isn't visible, don't descend into it. + var cs = node.ownerGlobal.getComputedStyle(node); + if (cs.visibility != "visible" || cs.display == "none") { + return NodeFilter.FILTER_REJECT; + } + // but it may be a popup element, in which case we look at "state"... + if (cs.display == "-moz-popup" && node.state != "open") { + return NodeFilter.FILTER_REJECT; + } + // OK - the node seems visible, so it is a candidate. + if (node.localName == "button" && node.accessKey && !node.disabled) { + return NodeFilter.FILTER_ACCEPT; + } + return NodeFilter.FILTER_SKIP; + } + + fireAccessKeyButton(aSubtree, aAccessKeyLower) { + var iterator = aSubtree.ownerDocument.createTreeWalker( + aSubtree, + NodeFilter.SHOW_ELEMENT, + this.filterButtons + ); + while (iterator.nextNode()) { + var test = iterator.currentNode; + if ( + test.accessKey.toLowerCase() == aAccessKeyLower && + !test.disabled && + !test.collapsed && + !test.hidden + ) { + test.focus(); + test.click(); + return true; + } + } + return false; + } + + _handleClick() { + if (!this.disabled) { + if (this.type == "checkbox") { + this.checked = !this.checked; + } else if (this.type == "radio") { + this.checked = true; + } + } + } + } + + MozXULElement.implementCustomInterface(MozButtonBase, [ + Ci.nsIDOMXULButtonElement, + ]); + + MozElements.ButtonBase = MozButtonBase; + + class MozButton extends MozButtonBase { + static get inheritedAttributes() { + return { + ".box-inherit": "align,dir,pack,orient", + ".button-icon": "src=image", + ".button-text": "value=label,accesskey,crop", + ".button-menu-dropmarker": "open,disabled,label", + }; + } + + get icon() { + return this.querySelector(".button-icon"); + } + + static get buttonFragment() { + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <hbox class="box-inherit button-box" align="center" pack="center" flex="1" anonid="button-box"> + <image class="button-icon"/> + <label class="button-text"/> + </hbox>`), + true + ); + Object.defineProperty(this, "buttonFragment", { value: frag }); + return frag; + } + + static get menuFragment() { + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <hbox class="box-inherit button-box" align="center" pack="center" flex="1"> + <hbox class="box-inherit" align="center" pack="center" flex="1"> + <image class="button-icon"/> + <label class="button-text"/> + </hbox> + <dropmarker class="button-menu-dropmarker"/> + </hbox>`), + true + ); + Object.defineProperty(this, "menuFragment", { value: frag }); + return frag; + } + + get _hasConnected() { + return this.querySelector(":scope > .button-box") != null; + } + + connectedCallback() { + if (this.delayConnectedCallback() || this._hasConnected) { + return; + } + + let fragment; + if (this.type === "menu") { + fragment = MozButton.menuFragment; + + this.addEventListener("keypress", event => { + if (event.keyCode != KeyEvent.DOM_VK_RETURN && event.key != " ") { + return; + } + + this.open = true; + // Prevent page from scrolling on the space key. + if (event.key == " ") { + event.preventDefault(); + } + }); + } else { + fragment = this.constructor.buttonFragment; + } + + this.appendChild(fragment.cloneNode(true)); + this.initializeAttributeInheritance(); + } + } + + customElements.define("button", MozButton); +} diff --git a/toolkit/content/widgets/calendar.js b/toolkit/content/widgets/calendar.js new file mode 100644 index 0000000000..707dc9b818 --- /dev/null +++ b/toolkit/content/widgets/calendar.js @@ -0,0 +1,170 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Initialize the Calendar and generate nodes for week headers and days, and + * attach event listeners. + * + * @param {Object} options + * { + * {Number} calViewSize: Number of days to appear on a calendar view + * {Function} getDayString: Transform day number to string + * {Function} getWeekHeaderString: Transform day of week number to string + * {Function} setSelection: Set selection for dateKeeper + * } + * @param {Object} context + * { + * {DOMElement} weekHeader + * {DOMElement} daysView + * } + */ +function Calendar(options, context) { + const DAYS_IN_A_WEEK = 7; + + this.context = context; + this.state = { + days: [], + weekHeaders: [], + setSelection: options.setSelection, + getDayString: options.getDayString, + getWeekHeaderString: options.getWeekHeaderString, + }; + this.elements = { + weekHeaders: this._generateNodes(DAYS_IN_A_WEEK, context.weekHeader), + daysView: this._generateNodes(options.calViewSize, context.daysView), + }; + + this._attachEventListeners(); +} + +Calendar.prototype = { + /** + * Set new properties and render them. + * + * @param {Object} props + * { + * {Boolean} isVisible: Whether or not the calendar is in view + * {Array<Object>} days: Data for days + * { + * {Date} dateObj + * {Number} content + * {Array<String>} classNames + * {Boolean} enabled + * } + * {Array<Object>} weekHeaders: Data for weekHeaders + * { + * {Number} content + * {Array<String>} classNames + * } + * } + */ + setProps(props) { + if (props.isVisible) { + // Transform the days and weekHeaders array for rendering + const days = props.days.map( + ({ dateObj, content, classNames, enabled }) => { + return { + dateObj, + textContent: this.state.getDayString(content), + className: classNames.join(" "), + enabled, + }; + } + ); + const weekHeaders = props.weekHeaders.map(({ content, classNames }) => { + return { + textContent: this.state.getWeekHeaderString(content), + className: classNames.join(" "), + }; + }); + // Update the DOM nodes states + this._render({ + elements: this.elements.daysView, + items: days, + prevState: this.state.days, + }); + this._render({ + elements: this.elements.weekHeaders, + items: weekHeaders, + prevState: this.state.weekHeaders, + }); + // Update the state to current + this.state.days = days; + this.state.weekHeaders = weekHeaders; + } + }, + + /** + * Render the items onto the DOM nodes + * @param {Object} + * { + * {Array<DOMElement>} elements + * {Array<Object>} items + * {Array<Object>} prevState: state of items from last render + * } + */ + _render({ elements, items, prevState }) { + for (let i = 0, l = items.length; i < l; i++) { + let el = elements[i]; + + // Check if state from last render has changed, if so, update the elements + if (!prevState[i] || prevState[i].textContent != items[i].textContent) { + el.textContent = items[i].textContent; + } + if (!prevState[i] || prevState[i].className != items[i].className) { + el.className = items[i].className; + } + } + }, + + /** + * Generate DOM nodes + * + * @param {Number} size: Number of nodes to generate + * @param {DOMElement} context: Element to append the nodes to + * @return {Array<DOMElement>} + */ + _generateNodes(size, context) { + let frag = document.createDocumentFragment(); + let refs = []; + + for (let i = 0; i < size; i++) { + let el = document.createElement("div"); + el.dataset.id = i; + refs.push(el); + frag.appendChild(el); + } + context.appendChild(frag); + + return refs; + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + switch (event.type) { + case "click": { + if (event.target.parentNode == this.context.daysView) { + let targetId = event.target.dataset.id; + let targetObj = this.state.days[targetId]; + if (targetObj.enabled) { + this.state.setSelection(targetObj.dateObj); + } + } + break; + } + } + }, + + /** + * Attach event listener to daysView + */ + _attachEventListeners() { + this.context.daysView.addEventListener("click", this); + }, +}; diff --git a/toolkit/content/widgets/checkbox.js b/toolkit/content/widgets/checkbox.js new file mode 100644 index 0000000000..8b695dbe0e --- /dev/null +++ b/toolkit/content/widgets/checkbox.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + class MozCheckbox extends MozElements.BaseText { + static get markup() { + return ` + <image class="checkbox-check"/> + <hbox class="checkbox-label-box" flex="1"> + <image class="checkbox-icon"/> + <label class="checkbox-label" flex="1"/> + </hbox> + `; + } + + constructor() { + super(); + + // While it would seem we could do this by handling oncommand, we need can't + // because any external oncommand handlers might get called before ours, and + // then they would see the incorrect value of checked. + this.addEventListener("click", event => { + if (event.button === 0 && !this.disabled) { + this.checked = !this.checked; + } + }); + this.addEventListener("keypress", event => { + if (event.key == " ") { + this.checked = !this.checked; + // Prevent page from scrolling on the space key. + event.preventDefault(); + } + }); + } + + static get inheritedAttributes() { + return { + ".checkbox-check": "disabled,checked", + ".checkbox-label": "text=label,accesskey", + ".checkbox-icon": "src", + }; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.textContent = ""; + this.appendChild(this.constructor.fragment); + + this.initializeAttributeInheritance(); + } + + set checked(val) { + let change = val != (this.getAttribute("checked") == "true"); + if (val) { + this.setAttribute("checked", "true"); + } else { + this.removeAttribute("checked"); + } + + if (change) { + let event = document.createEvent("Events"); + event.initEvent("CheckboxStateChange", true, true); + this.dispatchEvent(event); + } + return val; + } + + get checked() { + return this.getAttribute("checked") == "true"; + } + } + + MozCheckbox.contentFragment = null; + + customElements.define("checkbox", MozCheckbox); +} diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js new file mode 100644 index 0000000000..9ba08aeb37 --- /dev/null +++ b/toolkit/content/widgets/datekeeper.js @@ -0,0 +1,381 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * DateKeeper keeps track of the date states. + */ +function DateKeeper(props) { + this.init(props); +} + +{ + const DAYS_IN_A_WEEK = 7, + MONTHS_IN_A_YEAR = 12, + YEAR_VIEW_SIZE = 200, + YEAR_BUFFER_SIZE = 10, + // The min value is 0001-01-01 based on HTML spec: + // https://html.spec.whatwg.org/#valid-date-string + MIN_DATE = -62135596800000, + // The max value is derived from the ECMAScript spec (275760-09-13): + // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1 + MAX_DATE = 8640000000000000, + MAX_YEAR = 275760, + MAX_MONTH = 9; + + DateKeeper.prototype = { + get year() { + return this.state.dateObj.getUTCFullYear(); + }, + + get month() { + return this.state.dateObj.getUTCMonth(); + }, + + get selection() { + return this.state.selection; + }, + + /** + * Initialize DateKeeper + * @param {Number} year + * @param {Number} month + * @param {Number} day + * @param {Number} min + * @param {Number} max + * @param {Number} step + * @param {Number} stepBase + * @param {Number} firstDayOfWeek + * @param {Array<Number>} weekends + * @param {Number} calViewSize + */ + init({ + year, + month, + day, + min, + max, + step, + stepBase, + firstDayOfWeek = 0, + weekends = [0], + calViewSize = 42, + }) { + const today = new Date(); + + this.state = { + step, + firstDayOfWeek, + weekends, + calViewSize, + // min & max are NaN if empty or invalid + min: new Date(Number.isNaN(min) ? MIN_DATE : min), + max: new Date(Number.isNaN(max) ? MAX_DATE : max), + stepBase: new Date(stepBase), + today: this._newUTCDate( + today.getFullYear(), + today.getMonth(), + today.getDate() + ), + weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends), + years: [], + dateObj: new Date(0), + selection: { year, month, day }, + }; + + this.setCalendarMonth({ + year: year === undefined ? today.getFullYear() : year, + month: month === undefined ? today.getMonth() : month, + }); + }, + + /** + * Set new calendar month. The year is always treated as full year, so the + * short-form is not supported. + * @param {Object} date parts + * { + * {Number} year [optional] + * {Number} month [optional] + * } + */ + setCalendarMonth({ year = this.year, month = this.month }) { + // Make sure the date is valid before setting. + // Use setUTCFullYear so that year 99 doesn't get parsed as 1999 + if (year > MAX_YEAR || (year === MAX_YEAR && month >= MAX_MONTH)) { + this.state.dateObj.setUTCFullYear(MAX_YEAR, MAX_MONTH - 1, 1); + } else if (year < 1 || (year === 1 && month < 0)) { + this.state.dateObj.setUTCFullYear(1, 0, 1); + } else { + this.state.dateObj.setUTCFullYear(year, month, 1); + } + }, + + /** + * Set selection date + * @param {Number} year + * @param {Number} month + * @param {Number} day + */ + setSelection({ year, month, day }) { + this.state.selection.year = year; + this.state.selection.month = month; + this.state.selection.day = day; + }, + + /** + * Set month. Makes sure the day is <= the last day of the month + * @param {Number} month + */ + setMonth(month) { + this.setCalendarMonth({ year: this.year, month }); + }, + + /** + * Set year. Makes sure the day is <= the last day of the month + * @param {Number} year + */ + setYear(year) { + this.setCalendarMonth({ year, month: this.month }); + }, + + /** + * Set month by offset. Makes sure the day is <= the last day of the month + * @param {Number} offset + */ + setMonthByOffset(offset) { + this.setCalendarMonth({ year: this.year, month: this.month + offset }); + }, + + /** + * Generate the array of months + * @return {Array<Object>} + * { + * {Number} value: Month in int + * {Boolean} enabled + * } + */ + getMonths() { + let months = []; + + for (let i = 0; i < MONTHS_IN_A_YEAR; i++) { + months.push({ + value: i, + enabled: true, + }); + } + + return months; + }, + + /** + * Generate the array of years + * @return {Array<Object>} + * { + * {Number} value: Year in int + * {Boolean} enabled + * } + */ + getYears() { + let years = []; + + const firstItem = this.state.years[0]; + const lastItem = this.state.years[this.state.years.length - 1]; + const currentYear = this.year; + + // Generate new years array when the year is outside of the first & + // last item range. If not, return the cached result. + if ( + !firstItem || + !lastItem || + currentYear <= firstItem.value + YEAR_BUFFER_SIZE || + currentYear >= lastItem.value - YEAR_BUFFER_SIZE + ) { + // The year is set in the middle with items on both directions + for (let i = -(YEAR_VIEW_SIZE / 2); i < YEAR_VIEW_SIZE / 2; i++) { + const year = currentYear + i; + if (year >= 1 && year <= MAX_YEAR) { + years.push({ + value: year, + enabled: true, + }); + } + } + this.state.years = years; + } + return this.state.years; + }, + + /** + * Get days for calendar + * @return {Array<Object>} + * { + * {Date} dateObj + * {Number} content + * {Array<String>} classNames + * {Boolean} enabled + * } + */ + getDays() { + const firstDayOfMonth = this._getFirstCalendarDate( + this.state.dateObj, + this.state.firstDayOfWeek + ); + const month = this.month; + let days = []; + + for (let i = 0; i < this.state.calViewSize; i++) { + const dateObj = this._newUTCDate( + firstDayOfMonth.getUTCFullYear(), + firstDayOfMonth.getUTCMonth(), + firstDayOfMonth.getUTCDate() + i + ); + + let classNames = []; + let enabled = true; + + const isValid = + dateObj.getTime() >= MIN_DATE && dateObj.getTime() <= MAX_DATE; + if (!isValid) { + classNames.push("out-of-range"); + enabled = false; + + days.push({ + classNames, + enabled, + }); + continue; + } + + const isWeekend = this.state.weekends.includes(dateObj.getUTCDay()); + const isCurrentMonth = month == dateObj.getUTCMonth(); + const isSelection = + this.state.selection.year == dateObj.getUTCFullYear() && + this.state.selection.month == dateObj.getUTCMonth() && + this.state.selection.day == dateObj.getUTCDate(); + const isOutOfRange = + dateObj.getTime() < this.state.min.getTime() || + dateObj.getTime() > this.state.max.getTime(); + const isToday = this.state.today.getTime() == dateObj.getTime(); + const isOffStep = this._checkIsOffStep( + dateObj, + this._newUTCDate( + dateObj.getUTCFullYear(), + dateObj.getUTCMonth(), + dateObj.getUTCDate() + 1 + ) + ); + + if (isWeekend) { + classNames.push("weekend"); + } + if (!isCurrentMonth) { + classNames.push("outside"); + } + if (isSelection && !isOutOfRange && !isOffStep) { + classNames.push("selection"); + } + if (isOutOfRange) { + classNames.push("out-of-range"); + enabled = false; + } + if (isToday) { + classNames.push("today"); + } + if (isOffStep) { + classNames.push("off-step"); + enabled = false; + } + days.push({ + dateObj, + content: dateObj.getUTCDate(), + classNames, + enabled, + }); + } + return days; + }, + + /** + * Check if a date is off step given a starting point and the next increment + * @param {Date} start + * @param {Date} next + * @return {Boolean} + */ + _checkIsOffStep(start, next) { + // If the increment is larger or equal to the step, it must not be off-step. + if (next - start >= this.state.step) { + return false; + } + // Calculate the last valid date + const lastValidStep = Math.floor( + (next - 1 - this.state.stepBase) / this.state.step + ); + const lastValidTimeInMs = + lastValidStep * this.state.step + this.state.stepBase.getTime(); + // The date is off-step if the last valid date is smaller than the start date + return lastValidTimeInMs < start.getTime(); + }, + + /** + * Get week headers for calendar + * @param {Number} firstDayOfWeek + * @param {Array<Number>} weekends + * @return {Array<Object>} + * { + * {Number} content + * {Array<String>} classNames + * } + */ + _getWeekHeaders(firstDayOfWeek, weekends) { + let headers = []; + let dayOfWeek = firstDayOfWeek; + + for (let i = 0; i < DAYS_IN_A_WEEK; i++) { + headers.push({ + content: dayOfWeek % DAYS_IN_A_WEEK, + classNames: weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) + ? ["weekend"] + : [], + }); + dayOfWeek++; + } + return headers; + }, + + /** + * Get the first day on a calendar month + * @param {Date} dateObj + * @param {Number} firstDayOfWeek + * @return {Date} + */ + _getFirstCalendarDate(dateObj, firstDayOfWeek) { + const daysOffset = 1 - DAYS_IN_A_WEEK; + let firstDayOfMonth = this._newUTCDate( + dateObj.getUTCFullYear(), + dateObj.getUTCMonth() + ); + let dayOfWeek = firstDayOfMonth.getUTCDay(); + + return this._newUTCDate( + firstDayOfMonth.getUTCFullYear(), + firstDayOfMonth.getUTCMonth(), + // When first calendar date is the same as first day of the week, add + // another row on top of it. + firstDayOfWeek == dayOfWeek + ? daysOffset + : (firstDayOfWeek - dayOfWeek + daysOffset) % DAYS_IN_A_WEEK + ); + }, + + /** + * Helper function for creating UTC dates + * @param {...[Number]} parts + * @return {Date} + */ + _newUTCDate(...parts) { + return new Date(new Date(0).setUTCFullYear(...parts)); + }, + }; +} diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js new file mode 100644 index 0000000000..193e896ac8 --- /dev/null +++ b/toolkit/content/widgets/datepicker.js @@ -0,0 +1,450 @@ +/* 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/. */ + +/* import-globals-from datekeeper.js */ +/* import-globals-from calendar.js */ +/* import-globals-from spinner.js */ + +"use strict"; + +function DatePicker(context) { + this.context = context; + this._attachEventListeners(); +} + +{ + const CAL_VIEW_SIZE = 42; + + DatePicker.prototype = { + /** + * Initializes the date picker. Set the default states and properties. + * @param {Object} props + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * {Number} min + * {Number} max + * {Number} step + * {Number} stepBase + * {Number} firstDayOfWeek + * {Array<Number>} weekends + * {Array<String>} monthStrings + * {Array<String>} weekdayStrings + * {String} locale [optional]: User preferred locale + * } + */ + init(props = {}) { + this.props = props; + this._setDefaultState(); + this._createComponents(); + this._update(); + document.dispatchEvent(new CustomEvent("PickerReady")); + }, + + /* + * Set initial date picker states. + */ + _setDefaultState() { + const { + year, + month, + day, + min, + max, + step, + stepBase, + firstDayOfWeek, + weekends, + monthStrings, + weekdayStrings, + locale, + dir, + } = this.props; + const dateKeeper = new DateKeeper({ + year, + month, + day, + min, + max, + step, + stepBase, + firstDayOfWeek, + weekends, + calViewSize: CAL_VIEW_SIZE, + }); + + document.dir = dir; + + this.state = { + dateKeeper, + locale, + isMonthPickerVisible: false, + datetimeOrders: new Intl.DateTimeFormat(locale) + .formatToParts(new Date(0)) + .map(part => part.type), + getDayString: day => + day ? new Intl.NumberFormat(locale).format(day) : "", + getWeekHeaderString: weekday => weekdayStrings[weekday], + getMonthString: month => monthStrings[month], + setSelection: date => { + dateKeeper.setSelection({ + year: date.getUTCFullYear(), + month: date.getUTCMonth(), + day: date.getUTCDate(), + }); + this._update(); + this._dispatchState(); + this._closePopup(); + }, + setYear: year => { + dateKeeper.setYear(year); + dateKeeper.setSelection({ + year, + month: dateKeeper.selection.month, + day: dateKeeper.selection.day, + }); + this._update(); + this._dispatchState(); + }, + setMonth: month => { + dateKeeper.setMonth(month); + dateKeeper.setSelection({ + year: dateKeeper.selection.year, + month, + day: dateKeeper.selection.day, + }); + this._update(); + this._dispatchState(); + }, + toggleMonthPicker: () => { + this.state.isMonthPickerVisible = !this.state.isMonthPickerVisible; + this._update(); + }, + }; + }, + + /** + * Initalize the date picker components. + */ + _createComponents() { + this.components = { + calendar: new Calendar( + { + calViewSize: CAL_VIEW_SIZE, + locale: this.state.locale, + setSelection: this.state.setSelection, + getDayString: this.state.getDayString, + getWeekHeaderString: this.state.getWeekHeaderString, + }, + { + weekHeader: this.context.weekHeader, + daysView: this.context.daysView, + } + ), + monthYear: new MonthYear( + { + setYear: this.state.setYear, + setMonth: this.state.setMonth, + getMonthString: this.state.getMonthString, + datetimeOrders: this.state.datetimeOrders, + locale: this.state.locale, + }, + { + monthYear: this.context.monthYear, + monthYearView: this.context.monthYearView, + } + ), + }; + }, + + /** + * Update date picker and its components. + */ + _update(options = {}) { + const { dateKeeper, isMonthPickerVisible } = this.state; + + if (isMonthPickerVisible) { + this.state.months = dateKeeper.getMonths(); + this.state.years = dateKeeper.getYears(); + } else { + this.state.days = dateKeeper.getDays(); + } + + this.components.monthYear.setProps({ + isVisible: isMonthPickerVisible, + dateObj: dateKeeper.state.dateObj, + months: this.state.months, + years: this.state.years, + toggleMonthPicker: this.state.toggleMonthPicker, + noSmoothScroll: options.noSmoothScroll, + }); + this.components.calendar.setProps({ + isVisible: !isMonthPickerVisible, + days: this.state.days, + weekHeaders: dateKeeper.state.weekHeaders, + }); + + isMonthPickerVisible + ? this.context.monthYearView.classList.remove("hidden") + : this.context.monthYearView.classList.add("hidden"); + }, + + /** + * Use postMessage to close the picker. + */ + _closePopup() { + window.postMessage( + { + name: "ClosePopup", + }, + "*" + ); + }, + + /** + * Use postMessage to pass the state of picker to the panel. + */ + _dispatchState() { + const { year, month, day } = this.state.dateKeeper.selection; + // The panel is listening to window for postMessage event, so we + // do postMessage to itself to send data to input boxes. + window.postMessage( + { + name: "PickerPopupChanged", + detail: { + year, + month, + day, + }, + }, + "*" + ); + }, + + /** + * Attach event listeners + */ + _attachEventListeners() { + window.addEventListener("message", this); + document.addEventListener("mouseup", this, { passive: true }); + document.addEventListener("mousedown", this); + }, + + /** + * Handle events. + * + * @param {Event} event + */ + handleEvent(event) { + switch (event.type) { + case "message": { + this.handleMessage(event); + break; + } + case "mousedown": { + // Use preventDefault to keep focus on input boxes + event.preventDefault(); + event.target.setCapture(); + + if (event.target == this.context.buttonPrev) { + event.target.classList.add("active"); + this.state.dateKeeper.setMonthByOffset(-1); + this._update(); + } else if (event.target == this.context.buttonNext) { + event.target.classList.add("active"); + this.state.dateKeeper.setMonthByOffset(1); + this._update(); + } + break; + } + case "mouseup": { + if ( + event.target == this.context.buttonPrev || + event.target == this.context.buttonNext + ) { + event.target.classList.remove("active"); + } + } + } + }, + + /** + * Handle postMessage events. + * + * @param {Event} event + */ + handleMessage(event) { + switch (event.data.name) { + case "PickerSetValue": { + this.set(event.data.detail); + break; + } + case "PickerInit": { + this.init(event.data.detail); + break; + } + } + }, + + /** + * Set the date state and update the components with the new state. + * + * @param {Object} dateState + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * } + */ + set({ year, month, day }) { + if (!this.state) { + return; + } + + const { dateKeeper } = this.state; + + dateKeeper.setCalendarMonth({ + year, + month, + }); + dateKeeper.setSelection({ + year, + month, + day, + }); + this._update({ noSmoothScroll: true }); + }, + }; + + /** + * MonthYear is a component that handles the month & year spinners + * + * @param {Object} options + * { + * {String} locale + * {Function} setYear + * {Function} setMonth + * {Function} getMonthString + * {Array<String>} datetimeOrders + * } + * @param {DOMElement} context + */ + function MonthYear(options, context) { + const spinnerSize = 5; + const yearFormat = new Intl.DateTimeFormat(options.locale, { + year: "numeric", + timeZone: "UTC", + }).format; + const dateFormat = new Intl.DateTimeFormat(options.locale, { + year: "numeric", + month: "long", + timeZone: "UTC", + }).format; + const spinnerOrder = + options.datetimeOrders.indexOf("month") < + options.datetimeOrders.indexOf("year") + ? "order-month-year" + : "order-year-month"; + + context.monthYearView.classList.add(spinnerOrder); + + this.context = context; + this.state = { dateFormat }; + this.props = {}; + this.components = { + month: new Spinner( + { + id: "spinner-month", + setValue: month => { + this.state.isMonthSet = true; + options.setMonth(month); + }, + getDisplayString: options.getMonthString, + viewportSize: spinnerSize, + }, + context.monthYearView + ), + year: new Spinner( + { + id: "spinner-year", + setValue: year => { + this.state.isYearSet = true; + options.setYear(year); + }, + getDisplayString: year => + yearFormat(new Date(new Date(0).setUTCFullYear(year))), + viewportSize: spinnerSize, + }, + context.monthYearView + ), + }; + + this._attachEventListeners(); + } + + MonthYear.prototype = { + /** + * Set new properties and pass them to components + * + * @param {Object} props + * { + * {Boolean} isVisible + * {Date} dateObj + * {Array<Object>} months + * {Array<Object>} years + * {Function} toggleMonthPicker + * } + */ + setProps(props) { + this.context.monthYear.textContent = this.state.dateFormat(props.dateObj); + + if (props.isVisible) { + this.context.monthYear.classList.add("active"); + this.components.month.setState({ + value: props.dateObj.getUTCMonth(), + items: props.months, + isInfiniteScroll: true, + isValueSet: this.state.isMonthSet, + smoothScroll: !(this.state.firstOpened || props.noSmoothScroll), + }); + this.components.year.setState({ + value: props.dateObj.getUTCFullYear(), + items: props.years, + isInfiniteScroll: false, + isValueSet: this.state.isYearSet, + smoothScroll: !(this.state.firstOpened || props.noSmoothScroll), + }); + this.state.firstOpened = false; + } else { + this.context.monthYear.classList.remove("active"); + this.state.isMonthSet = false; + this.state.isYearSet = false; + this.state.firstOpened = true; + } + + this.props = Object.assign(this.props, props); + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + switch (event.type) { + case "click": { + this.props.toggleMonthPicker(); + break; + } + } + }, + + /** + * Attach event listener to monthYear button + */ + _attachEventListeners() { + this.context.monthYear.addEventListener("click", this); + }, + }; +} diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css new file mode 100644 index 0000000000..8b694f2efc --- /dev/null +++ b/toolkit/content/widgets/datetimebox.css @@ -0,0 +1,62 @@ +/* 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/. */ + +@namespace url("http://www.w3.org/1999/xhtml"); +@namespace svg url("http://www.w3.org/2000/svg"); + +.datetimebox { + display: flex; + /* TODO: Enable selection once bug 1455893 is fixed */ + user-select: none; +} + +.datetime-input-box-wrapper { + display: inline-flex; + flex: 1; + background-color: inherit; + min-width: 0; + justify-content: space-between; + align-items: center; +} + +.datetime-input-edit-wrapper { + overflow: hidden; + white-space: nowrap; + flex-grow: 1; +} + +.datetime-edit-field { + display: inline; + text-align: center; + padding: 1px 3px; + border: 0; + margin: 0; + ime-mode: disabled; + outline: none; +} + +.datetime-edit-field:not([disabled="true"]):focus { + background-color: Highlight; + color: HighlightText; + outline: none; +} + +.datetime-edit-field[disabled="true"], +.datetime-edit-field[readonly="true"] { + user-select: none; +} + +.datetime-reset-button { + color: inherit; + fill: currentColor; + opacity: .5; + background-color: transparent; + border: none; + flex: none; + padding-inline: 2px; +} + +svg|svg.datetime-reset-button-svg { + pointer-events: none; +} diff --git a/toolkit/content/widgets/datetimebox.js b/toolkit/content/widgets/datetimebox.js new file mode 100644 index 0000000000..db7127f11d --- /dev/null +++ b/toolkit/content/widgets/datetimebox.js @@ -0,0 +1,1760 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is a UA widget. It runs in per-origin UA widget scope, +// to be loaded by UAWidgetsChild.jsm. + +/* + * This is the class of entry. It will construct the actual implementation + * according to the value of the "type" property. + */ +this.DateTimeBoxWidget = class { + constructor(shadowRoot) { + this.shadowRoot = shadowRoot; + this.element = shadowRoot.host; + this.document = this.element.ownerDocument; + this.window = this.document.defaultView; + } + + /* + * Callback called by UAWidgets right after constructor. + */ + onsetup() { + this.switchImpl(); + } + + /* + * Callback called by UAWidgets when the "type" property changes. + */ + onchange() { + this.switchImpl(); + } + + /* + * Actually switch the implementation. + * - With type equal to "date", DateInputImplWidget should load. + * - With type equal to "time", TimeInputImplWidget should load. + * - Otherwise, nothing should load and loaded impl should be unloaded. + */ + switchImpl() { + let newImpl; + if (this.element.type == "date") { + newImpl = DateInputImplWidget; + } else if (this.element.type == "time") { + newImpl = TimeInputImplWidget; + } + // Skip if we are asked to load the same implementation. + // This can happen if the property is set again w/o value change. + if (this.impl && this.impl.constructor == newImpl) { + return; + } + if (this.impl) { + this.impl.destructor(); + this.shadowRoot.firstChild.remove(); + } + if (newImpl) { + this.impl = new newImpl(this.shadowRoot); + this.impl.onsetup(); + } else { + this.impl = undefined; + } + } + + destructor() { + if (!this.impl) { + return; + } + this.impl.destructor(); + this.shadowRoot.firstChild.remove(); + delete this.impl; + } +}; + +this.DateTimeInputBaseImplWidget = class { + constructor(shadowRoot) { + this.shadowRoot = shadowRoot; + this.element = shadowRoot.host; + this.document = this.element.ownerDocument; + this.window = this.document.defaultView; + } + + onsetup() { + this.generateContent(); + + this.DEBUG = false; + this.mDateTimeBoxElement = this.shadowRoot.firstChild; + this.mInputElement = this.element; + this.mLocales = this.window.getWebExposedLocales(); + + this.mIsRTL = false; + let intlUtils = this.window.intlUtils; + if (intlUtils) { + this.mIsRTL = intlUtils.isAppLocaleRTL(); + } + + if (this.mIsRTL) { + let inputBoxWrapper = this.shadowRoot.getElementById("input-box-wrapper"); + inputBoxWrapper.dir = "rtl"; + } + + this.mMin = this.mInputElement.min; + this.mMax = this.mInputElement.max; + this.mStep = this.mInputElement.step; + this.mIsPickerOpen = false; + + this.mResetButton = this.shadowRoot.getElementById("reset-button"); + this.mResetButton.style.visibility = "hidden"; + this.mResetButton.addEventListener("mousedown", this, { + mozSystemGroup: true, + }); + + this.mInputElement.addEventListener( + "keypress", + this, + { + capture: true, + mozSystemGroup: true, + }, + false + ); + // This is to open the picker when input element is clicked (this + // includes padding area). + this.mInputElement.addEventListener( + "click", + this, + { mozSystemGroup: true }, + false + ); + + // Those events are dispatched to <div class="datetimebox"> with bubble set + // to false. They are trapped inside UA Widget Shadow DOM and are not + // dispatched to the document. + this.CONTROL_EVENTS.forEach(eventName => { + this.mDateTimeBoxElement.addEventListener(eventName, this, {}, false); + }); + } + + generateContent() { + /* + * Pass the markup through XML parser purely for the reason of loading the localization DTD. + * Remove it when migrate to Fluent (bug 1504363). + */ + const parser = new this.window.DOMParser(); + parser.forceEnableDTD(); + let parserDoc = parser.parseFromString( + `<!DOCTYPE bindings [ + <!ENTITY % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd"> + %datetimeboxDTD; + ]> + <div class="datetimebox" xmlns="http://www.w3.org/1999/xhtml" role="none"> + <link rel="stylesheet" type="text/css" href="chrome://global/content/bindings/datetimebox.css" /> + <div class="datetime-input-box-wrapper" id="input-box-wrapper" role="presentation"> + <span class="datetime-input-edit-wrapper" + id="edit-wrapper"> + <!-- Each of the date/time input types will append their input child + - elements here --> + </span> + + <button class="datetime-reset-button" id="reset-button" tabindex="-1" aria-label="&datetime.reset.label;"> + <svg xmlns="http://www.w3.org/2000/svg" class="datetime-reset-button-svg" width="12" height="12"> + <path d="M 3.9,3 3,3.9 5.1,6 3,8.1 3.9,9 6,6.9 8.1,9 9,8.1 6.9,6 9,3.9 8.1,3 6,5.1 Z M 12,6 A 6,6 0 0 1 6,12 6,6 0 0 1 0,6 6,6 0 0 1 6,0 6,6 0 0 1 12,6 Z"/> + </svg> + </button> + </div> + <div id="strings" + data-m-year-place-holder="&date.year.placeholder;" + data-m-year-label="&date.year.label;" + data-m-month-place-holder="&date.month.placeholder;" + data-m-month-label="&date.month.label;" + data-m-day-place-holder="&date.day.placeholder;" + data-m-day-label="&date.day.label;" + + data-m-hour-place-holder="&time.hour.placeholder;" + data-m-hour-label="&time.hour.label;" + data-m-minute-place-holder="&time.minute.placeholder;" + data-m-minute-label="&time.minute.label;" + data-m-second-place-holder="&time.second.placeholder;" + data-m-second-label="&time.second.label;" + data-m-millisecond-place-holder="&time.millisecond.placeholder;" + data-m-millisecond-label="&time.millisecond.label;" + data-m-day-period-place-holder="&time.dayperiod.placeholder;" + data-m-day-period-label="&time.dayperiod.label;" + ></div> + </div>`, + "application/xml" + ); + + /* + * The <div id="strings"> is also parsed in the document so that there is no + * need to create another XML document just to get the strings. + */ + let stringsElement = parserDoc.getElementById("strings"); + stringsElement.remove(); + for (let key in stringsElement.dataset) { + // key will be camelCase version of the attribute key above, + // like mYearPlaceHolder. + this[key] = stringsElement.dataset[key]; + } + + this.shadowRoot.importNodeAndAppendChildAt( + this.shadowRoot, + parserDoc.documentElement, + true + ); + } + + destructor() { + this.mResetButton.addEventListener("mousedown", this, { + mozSystemGroup: true, + }); + + this.mInputElement.removeEventListener("keypress", this, { + capture: true, + mozSystemGroup: true, + }); + this.mInputElement.removeEventListener("click", this, { + mozSystemGroup: true, + }); + + this.CONTROL_EVENTS.forEach(eventName => { + this.mDateTimeBoxElement.removeEventListener(eventName, this); + }); + this.mInputElement = null; + } + + get FIELD_EVENTS() { + return ["focus", "blur", "copy", "cut", "paste"]; + } + + get CONTROL_EVENTS() { + return [ + "MozDateTimeValueChanged", + "MozNotifyMinMaxStepAttrChanged", + "MozFocusInnerTextBox", + "MozBlurInnerTextBox", + "MozDateTimeAttributeChanged", + "MozPickerValueChanged", + "MozSetDateTimePickerState", + ]; + } + + addEventListenersToField(aElement) { + // These events don't bubble out of the Shadow DOM, so we'll have to add + // event listeners specifically on each of the fields, not just + // on the <input> + this.FIELD_EVENTS.forEach(eventName => { + aElement.addEventListener( + eventName, + this, + { mozSystemGroup: true }, + false + ); + }); + } + + removeEventListenersToField(aElement) { + if (!aElement) { + return; + } + + this.FIELD_EVENTS.forEach(eventName => { + aElement.removeEventListener(eventName, this, { mozSystemGroup: true }); + }); + } + + log(aMsg) { + if (this.DEBUG) { + this.window.dump("[DateTimeBox] " + aMsg + "\n"); + } + } + + createEditFieldAndAppend( + aPlaceHolder, + aLabel, + aIsNumeric, + aMinDigits, + aMaxLength, + aMinValue, + aMaxValue, + aPageUpDownInterval + ) { + let root = this.shadowRoot.getElementById("edit-wrapper"); + let field = this.shadowRoot.createElementAndAppendChildAt(root, "span"); + field.classList.add("datetime-edit-field"); + field.textContent = aPlaceHolder; + field.placeholder = aPlaceHolder; + field.tabIndex = this.mInputElement.tabIndex; + + field.setAttribute("readonly", this.mInputElement.readOnly); + field.setAttribute("disabled", this.mInputElement.disabled); + // Set property as well for convenience. + field.disabled = this.mInputElement.disabled; + field.readOnly = this.mInputElement.readOnly; + field.setAttribute("aria-label", aLabel); + + // Used to store the non-formatted value, cleared when value is + // cleared. + // DateTimeInputTypeBase::HasBadInput() will read this to decide + // if the input has value. + field.setAttribute("value", ""); + + if (aIsNumeric) { + field.classList.add("numeric"); + // Maximum value allowed. + field.setAttribute("min", aMinValue); + // Minumim value allowed. + field.setAttribute("max", aMaxValue); + // Interval when pressing pageUp/pageDown key. + field.setAttribute("pginterval", aPageUpDownInterval); + // Used to store what the user has already typed in the field, + // cleared when value is cleared and when field is blurred. + field.setAttribute("typeBuffer", ""); + // Minimum digits to display, padded with leading 0s. + field.setAttribute("mindigits", aMinDigits); + // Maximum length for the field, will be advance to the next field + // automatically if exceeded. + field.setAttribute("maxlength", aMaxLength); + // Set spinbutton ARIA role + field.setAttribute("role", "spinbutton"); + + if (this.mIsRTL) { + // Force the direction to be "ltr", so that the field stays in the + // same order even when it's empty (with placeholder). By using + // "embed", the text inside the element is still displayed based + // on its directionality. + field.style.unicodeBidi = "embed"; + field.style.direction = "ltr"; + } + } else { + // Set generic textbox ARIA role + field.setAttribute("role", "textbox"); + } + + return field; + } + + updateResetButtonVisibility() { + if (this.isAnyFieldAvailable(false) && !this.isRequired()) { + this.mResetButton.style.visibility = ""; + } else { + this.mResetButton.style.visibility = "hidden"; + } + } + + focusInnerTextBox() { + this.log("Focus inner editable field."); + + let editRoot = this.shadowRoot.getElementById("edit-wrapper"); + for (let child of editRoot.querySelectorAll( + ":scope > span.datetime-edit-field" + )) { + this.mLastFocusedField = child; + child.focus(); + this.log("focused"); + break; + } + } + + blurInnerTextBox() { + this.log("Blur inner editable field."); + + if (this.mLastFocusedField) { + this.mLastFocusedField.blur(); + } else { + // If .mLastFocusedField hasn't been set, blur all editable fields, + // so that the bound element will actually be blurred. Note that + // blurring on a element that has no focus won't have any effect. + let editRoot = this.shadowRoot.getElementById("edit-wrapper"); + for (let child of editRoot.querySelectorAll( + ":scope > span.datetime-edit-field" + )) { + child.blur(); + } + } + } + + notifyInputElementValueChanged() { + this.log("inputElementValueChanged"); + this.setFieldsFromInputValue(); + } + + notifyMinMaxStepAttrChanged() { + // No operation by default + } + + setValueFromPicker(aValue) { + this.setFieldsFromPicker(aValue); + } + + advanceToNextField(aReverse) { + this.log("advanceToNextField"); + + let focusedInput = this.mLastFocusedField; + let next = aReverse + ? focusedInput.previousElementSibling + : focusedInput.nextElementSibling; + if (!next && !aReverse) { + this.setInputValueFromFields(); + return; + } + + while (next) { + if (next.matches("span.datetime-edit-field")) { + next.focus(); + break; + } + next = aReverse ? next.previousElementSibling : next.nextElementSibling; + } + } + + setPickerState(aIsOpen) { + this.log("picker is now " + (aIsOpen ? "opened" : "closed")); + this.mIsPickerOpen = aIsOpen; + } + + updateEditAttributes() { + this.log("updateEditAttributes"); + + let editRoot = this.shadowRoot.getElementById("edit-wrapper"); + + for (let child of editRoot.querySelectorAll( + ":scope > span.datetime-edit-field" + )) { + // "disabled" and "readonly" must be set as attributes because they + // are not defined properties of HTMLSpanElement, and the stylesheet + // checks the literal string attribute values. + child.setAttribute("disabled", this.mInputElement.disabled); + child.setAttribute("readonly", this.mInputElement.readOnly); + + // Set property as well for convenience. + child.disabled = this.mInputElement.disabled; + child.readOnly = this.mInputElement.readOnly; + + // tabIndex works on all elements + child.tabIndex = this.mInputElement.tabIndex; + } + + this.mResetButton.disabled = + this.mInputElement.disabled || this.mInputElement.readOnly; + this.updateResetButtonVisibility(); + } + + isEmpty(aValue) { + return aValue == undefined || 0 === aValue.length; + } + + getFieldValue(aField) { + if (!aField || !aField.classList.contains("numeric")) { + return undefined; + } + + let value = aField.getAttribute("value"); + // Avoid returning 0 when field is empty. + return this.isEmpty(value) ? undefined : Number(value); + } + + clearFieldValue(aField) { + aField.textContent = aField.placeholder; + aField.setAttribute("value", ""); + if (aField.classList.contains("numeric")) { + aField.setAttribute("typeBuffer", ""); + } + this.updateResetButtonVisibility(); + } + + setFieldValue() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + clearInputFields() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + setFieldsFromInputValue() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + setInputValueFromFields() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + setFieldsFromPicker() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + handleKeypress() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + handleKeyboardNav() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + getCurrentValue() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + isAnyFieldAvailable() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + notifyPicker() { + if (this.mIsPickerOpen && this.isAnyFieldAvailable(true)) { + this.mInputElement.updateDateTimePicker(this.getCurrentValue()); + } + } + + isDisabled() { + return this.mInputElement.hasAttribute("disabled"); + } + + isReadonly() { + return this.mInputElement.hasAttribute("readonly"); + } + + isEditable() { + return !this.isDisabled() && !this.isReadonly(); + } + + isRequired() { + return this.mInputElement.hasAttribute("required"); + } + + containingTree() { + return this.mInputElement.containingShadowRoot || this.document; + } + + handleEvent(aEvent) { + this.log("handleEvent: " + aEvent.type); + + if (!aEvent.isTrusted) { + return; + } + + switch (aEvent.type) { + case "MozDateTimeValueChanged": { + this.notifyInputElementValueChanged(); + break; + } + case "MozNotifyMinMaxStepAttrChanged": { + this.notifyMinMaxStepAttrChanged(); + break; + } + case "MozFocusInnerTextBox": { + this.focusInnerTextBox(); + break; + } + case "MozBlurInnerTextBox": { + this.blurInnerTextBox(); + break; + } + case "MozDateTimeAttributeChanged": { + this.updateEditAttributes(); + break; + } + case "MozPickerValueChanged": { + this.setValueFromPicker(aEvent.detail); + break; + } + case "MozSetDateTimePickerState": { + this.setPickerState(aEvent.detail); + break; + } + case "keypress": { + this.onKeyPress(aEvent); + break; + } + case "click": { + this.onClick(aEvent); + break; + } + case "focus": { + this.onFocus(aEvent); + break; + } + case "blur": { + this.onBlur(aEvent); + break; + } + case "mousedown": + case "copy": + case "cut": + case "paste": { + aEvent.preventDefault(); + break; + } + default: + break; + } + } + + onFocus(aEvent) { + this.log("onFocus originalTarget: " + aEvent.originalTarget); + if (this.containingTree().activeElement != this.mInputElement) { + return; + } + + let target = aEvent.originalTarget; + if (target.matches("span.datetime-edit-field")) { + if (target.disabled) { + return; + } + this.mLastFocusedField = target; + this.mInputElement.setFocusState(true); + } + } + + onBlur(aEvent) { + this.log( + "onBlur originalTarget: " + + aEvent.originalTarget + + " target: " + + aEvent.target + + " rt: " + + aEvent.relatedTarget + ); + + let target = aEvent.originalTarget; + target.setAttribute("typeBuffer", ""); + this.setInputValueFromFields(); + // No need to set and unset the focus state if the focus is staying within + // our input. Same about closing the picker. + if (aEvent.relatedTarget != this.mInputElement) { + this.mInputElement.setFocusState(false); + if (this.mIsPickerOpen) { + this.mInputElement.closeDateTimePicker(); + } + } + } + + onKeyPress(aEvent) { + this.log("onKeyPress key: " + aEvent.key); + + switch (aEvent.key) { + // Toggle the picker on space/enter, close on Escape. + case "Enter": + case "Escape": + case " ": { + if (this.mIsPickerOpen) { + this.mInputElement.closeDateTimePicker(); + } else if (aEvent.key != "Escape") { + this.mInputElement.openDateTimePicker(this.getCurrentValue()); + } else { + // Don't preventDefault(); + break; + } + aEvent.preventDefault(); + break; + } + case "Backspace": { + // TODO(emilio, bug 1571533): These functions should look at + // defaultPrevented. + if (this.isEditable()) { + let targetField = aEvent.originalTarget; + this.clearFieldValue(targetField); + this.setInputValueFromFields(); + aEvent.preventDefault(); + } + break; + } + case "ArrowRight": + case "ArrowLeft": { + this.advanceToNextField(!(aEvent.key == "ArrowRight")); + aEvent.preventDefault(); + break; + } + case "ArrowUp": + case "ArrowDown": + case "PageUp": + case "PageDown": + case "Home": + case "End": { + this.handleKeyboardNav(aEvent); + aEvent.preventDefault(); + break; + } + default: { + // printable characters + if ( + aEvent.keyCode == 0 && + !(aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey) + ) { + this.handleKeypress(aEvent); + aEvent.preventDefault(); + } + break; + } + } + } + + onClick(aEvent) { + this.log( + "onClick originalTarget: " + + aEvent.originalTarget + + " target: " + + aEvent.target + ); + + if (aEvent.defaultPrevented || !this.isEditable()) { + return; + } + + if (aEvent.originalTarget == this.mResetButton) { + this.clearInputFields(false); + } else if (!this.mIsPickerOpen) { + this.mInputElement.openDateTimePicker(this.getCurrentValue()); + } + } +}; + +this.DateInputImplWidget = class extends DateTimeInputBaseImplWidget { + constructor(shadowRoot) { + super(shadowRoot); + } + + onsetup() { + super.onsetup(); + + this.mMinMonth = 1; + this.mMaxMonth = 12; + this.mMinDay = 1; + this.mMaxDay = 31; + this.mMinYear = 1; + // Maximum year limited by ECMAScript date object range, year <= 275760. + this.mMaxYear = 275760; + this.mMonthDayLength = 2; + this.mYearLength = 4; + this.mMonthPageUpDownInterval = 3; + this.mDayPageUpDownInterval = 7; + this.mYearPageUpDownInterval = 10; + + this.buildEditFields(); + this.updateEditAttributes(); + + if (this.mInputElement.value) { + this.setFieldsFromInputValue(); + } + } + + destructor() { + this.removeEventListenersToField(this.mYearField); + this.removeEventListenersToField(this.mMonthField); + this.removeEventListenersToField(this.mDayField); + super.destructor(); + } + + buildEditFields() { + let root = this.shadowRoot.getElementById("edit-wrapper"); + + let yearMaxLength = this.mMaxYear.toString().length; + + let formatter = Intl.DateTimeFormat(this.mLocales, { + year: "numeric", + month: "numeric", + day: "numeric", + }); + formatter.formatToParts(Date.now()).map(part => { + switch (part.type) { + case "year": + this.mYearField = this.createEditFieldAndAppend( + this.mYearPlaceHolder, + this.mYearLabel, + true, + this.mYearLength, + yearMaxLength, + this.mMinYear, + this.mMaxYear, + this.mYearPageUpDownInterval + ); + this.addEventListenersToField(this.mYearField); + break; + case "month": + this.mMonthField = this.createEditFieldAndAppend( + this.mMonthPlaceHolder, + this.mMonthLabel, + true, + this.mMonthDayLength, + this.mMonthDayLength, + this.mMinMonth, + this.mMaxMonth, + this.mMonthPageUpDownInterval + ); + this.addEventListenersToField(this.mMonthField); + break; + case "day": + this.mDayField = this.createEditFieldAndAppend( + this.mDayPlaceHolder, + this.mDayLabel, + true, + this.mMonthDayLength, + this.mMonthDayLength, + this.mMinDay, + this.mMaxDay, + this.mDayPageUpDownInterval + ); + this.addEventListenersToField(this.mDayField); + break; + default: + let span = this.shadowRoot.createElementAndAppendChildAt( + root, + "span" + ); + span.textContent = part.value; + break; + } + }); + } + + clearInputFields(aFromInputElement) { + this.log("clearInputFields"); + + if (this.mMonthField) { + this.clearFieldValue(this.mMonthField); + } + + if (this.mDayField) { + this.clearFieldValue(this.mDayField); + } + + if (this.mYearField) { + this.clearFieldValue(this.mYearField); + } + + if (!aFromInputElement) { + if (this.mInputElement.value) { + this.mInputElement.setUserInput(""); + } else { + this.mInputElement.updateValidityState(); + } + } + } + + setFieldsFromInputValue() { + let value = this.mInputElement.value; + if (!value) { + this.clearInputFields(true); + return; + } + + this.log("setFieldsFromInputValue: " + value); + let [year, month, day] = value.split("-"); + + this.setFieldValue(this.mYearField, year); + this.setFieldValue(this.mMonthField, month); + this.setFieldValue(this.mDayField, day); + + this.notifyPicker(); + } + + setInputValueFromFields() { + if (this.isAnyFieldEmpty()) { + // Clear input element's value if any of the field has been cleared, + // otherwise update the validity state, since it may become "not" + // invalid if fields are not complete. + if (this.mInputElement.value) { + this.mInputElement.setUserInput(""); + } else { + this.mInputElement.updateValidityState(); + } + // We still need to notify picker in case any of the field has + // changed. + this.notifyPicker(); + return; + } + + let { year, month, day } = this.getCurrentValue(); + + // Convert to a valid date string according to: + // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-date-string + year = year.toString().padStart(this.mYearLength, "0"); + month = month < 10 ? "0" + month : month; + day = day < 10 ? "0" + day : day; + + let date = [year, month, day].join("-"); + + if (date == this.mInputElement.value) { + return; + } + + this.log("setInputValueFromFields: " + date); + this.notifyPicker(); + this.mInputElement.setUserInput(date); + } + + setFieldsFromPicker(aValue) { + let year = aValue.year; + let month = aValue.month; + let day = aValue.day; + + if (!this.isEmpty(year)) { + this.setFieldValue(this.mYearField, year); + } + + if (!this.isEmpty(month)) { + this.setFieldValue(this.mMonthField, month); + } + + if (!this.isEmpty(day)) { + this.setFieldValue(this.mDayField, day); + } + + // Update input element's .value if needed. + this.setInputValueFromFields(); + } + + handleKeypress(aEvent) { + if (!this.isEditable()) { + return; + } + + let targetField = aEvent.originalTarget; + let key = aEvent.key; + + if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) { + let buffer = targetField.getAttribute("typeBuffer") || ""; + + buffer = buffer.concat(key); + this.setFieldValue(targetField, buffer); + + let n = Number(buffer); + let max = targetField.getAttribute("max"); + let maxLength = targetField.getAttribute("maxlength"); + if (buffer.length >= maxLength || n * 10 > max) { + buffer = ""; + this.advanceToNextField(); + } + targetField.setAttribute("typeBuffer", buffer); + if (!this.isAnyFieldEmpty()) { + this.setInputValueFromFields(); + } + } + } + + incrementFieldValue(aTargetField, aTimes) { + let value = this.getFieldValue(aTargetField); + + // Use current date if field is empty. + if (this.isEmpty(value)) { + let now = new Date(); + + if (aTargetField == this.mYearField) { + value = now.getFullYear(); + } else if (aTargetField == this.mMonthField) { + value = now.getMonth() + 1; + } else if (aTargetField == this.mDayField) { + value = now.getDate(); + } else { + this.log("Field not supported in incrementFieldValue."); + return; + } + } + + let min = Number(aTargetField.getAttribute("min")); + let max = Number(aTargetField.getAttribute("max")); + + value += Number(aTimes); + if (value > max) { + value -= max - min + 1; + } else if (value < min) { + value += max - min + 1; + } + + this.setFieldValue(aTargetField, value); + } + + handleKeyboardNav(aEvent) { + if (!this.isEditable()) { + return; + } + + let targetField = aEvent.originalTarget; + let key = aEvent.key; + + // Home/End key does nothing on year field. + if (targetField == this.mYearField && (key == "Home" || key == "End")) { + return; + } + + switch (key) { + case "ArrowUp": + this.incrementFieldValue(targetField, 1); + break; + case "ArrowDown": + this.incrementFieldValue(targetField, -1); + break; + case "PageUp": { + let interval = targetField.getAttribute("pginterval"); + this.incrementFieldValue(targetField, interval); + break; + } + case "PageDown": { + let interval = targetField.getAttribute("pginterval"); + this.incrementFieldValue(targetField, 0 - interval); + break; + } + case "Home": + let min = targetField.getAttribute("min"); + this.setFieldValue(targetField, min); + break; + case "End": + let max = targetField.getAttribute("max"); + this.setFieldValue(targetField, max); + break; + } + this.setInputValueFromFields(); + } + + getCurrentValue() { + let year = this.getFieldValue(this.mYearField); + let month = this.getFieldValue(this.mMonthField); + let day = this.getFieldValue(this.mDayField); + + let date = { year, month, day }; + + this.log("getCurrentValue: " + JSON.stringify(date)); + return date; + } + + setFieldValue(aField, aValue) { + if (!aField || !aField.classList.contains("numeric")) { + return; + } + + let value = Number(aValue); + if (isNaN(value)) { + this.log("NaN on setFieldValue!"); + return; + } + + let maxLength = aField.getAttribute("maxlength"); + if (aValue.length == maxLength) { + let min = Number(aField.getAttribute("min")); + let max = Number(aField.getAttribute("max")); + + if (value < min) { + value = min; + } else if (value > max) { + value = max; + } + } + + aField.setAttribute("value", value); + + // Display formatted value based on locale. + let minDigits = aField.getAttribute("mindigits"); + let formatted = value.toLocaleString(this.mLocales, { + minimumIntegerDigits: minDigits, + useGrouping: false, + }); + + aField.textContent = formatted; + aField.setAttribute("aria-valuetext", formatted); + this.updateResetButtonVisibility(); + } + + isAnyFieldAvailable(aForPicker) { + let { year, month, day } = this.getCurrentValue(); + + return !this.isEmpty(year) || !this.isEmpty(month) || !this.isEmpty(day); + } + + isAnyFieldEmpty() { + let { year, month, day } = this.getCurrentValue(); + + return this.isEmpty(year) || this.isEmpty(month) || this.isEmpty(day); + } +}; + +this.TimeInputImplWidget = class extends DateTimeInputBaseImplWidget { + constructor(shadowRoot) { + super(shadowRoot); + } + + onsetup() { + super.onsetup(); + + const kDefaultAMString = "AM"; + const kDefaultPMString = "PM"; + + let { amString, pmString } = this.getStringsForLocale(this.mLocales); + + this.mAMIndicator = amString || kDefaultAMString; + this.mPMIndicator = pmString || kDefaultPMString; + + this.mHour12 = this.is12HourTime(this.mLocales); + this.mMillisecSeparatorText = "."; + this.mMaxLength = 2; + this.mMillisecMaxLength = 3; + this.mDefaultStep = 60 * 1000; // in milliseconds + + this.mMinHour = this.mHour12 ? 1 : 0; + this.mMaxHour = this.mHour12 ? 12 : 23; + this.mMinMinute = 0; + this.mMaxMinute = 59; + this.mMinSecond = 0; + this.mMaxSecond = 59; + this.mMinMillisecond = 0; + this.mMaxMillisecond = 999; + + this.mHourPageUpDownInterval = 3; + this.mMinSecPageUpDownInterval = 10; + + this.buildEditFields(); + this.updateEditAttributes(); + + if (this.mInputElement.value) { + this.setFieldsFromInputValue(); + } + } + + destructor() { + this.removeEventListenersToField(this.mHourField); + this.removeEventListenersToField(this.mMinuteField); + this.removeEventListenersToField(this.mSecondField); + this.removeEventListenersToField(this.mMillisecField); + this.removeEventListenersToField(this.mDayPeriodField); + super.destructor(); + } + + get kMsPerSecond() { + return 1000; + } + + get kMsPerMinute() { + return 60 * 1000; + } + + getInputElementValues() { + let value = this.mInputElement.value; + if (value.length === 0) { + return {}; + } + + let hour, minute, second, millisecond; + [hour, minute, second] = value.split(":"); + if (second) { + [second, millisecond] = second.split("."); + + // Convert fraction of second to milliseconds. + if (millisecond && millisecond.length === 1) { + millisecond *= 100; + } else if (millisecond && millisecond.length === 2) { + millisecond *= 10; + } + } + + return { hour, minute, second, millisecond }; + } + + hasSecondField() { + return !!this.mSecondField; + } + + hasMillisecField() { + return !!this.mMillisecField; + } + + hasDayPeriodField() { + return !!this.mDayPeriodField; + } + + shouldShowSecondField() { + let { second } = this.getInputElementValues(); + if (second != undefined) { + return true; + } + + let stepBase = this.mInputElement.getStepBase(); + if (stepBase % this.kMsPerMinute != 0) { + return true; + } + + let step = this.mInputElement.getStep(); + if (step % this.kMsPerMinute != 0) { + return true; + } + + return false; + } + + shouldShowMillisecField() { + let { millisecond } = this.getInputElementValues(); + if (millisecond != undefined) { + return true; + } + + let stepBase = this.mInputElement.getStepBase(); + if (stepBase % this.kMsPerSecond != 0) { + return true; + } + + let step = this.mInputElement.getStep(); + if (step % this.kMsPerSecond != 0) { + return true; + } + + return false; + } + + rebuildEditFieldsIfNeeded() { + if ( + this.shouldShowSecondField() == this.hasSecondField() && + this.shouldShowMillisecField() == this.hasMillisecField() + ) { + return; + } + + let focused = this.mInputElement.matches(":focus"); + + let root = this.shadowRoot.getElementById("edit-wrapper"); + while (root.firstChild) { + root.firstChild.remove(); + } + + this.removeEventListenersToField(this.mHourField); + this.removeEventListenersToField(this.mMinuteField); + this.removeEventListenersToField(this.mSecondField); + this.removeEventListenersToField(this.mMillisecField); + this.removeEventListenersToField(this.mDayPeriodField); + + this.mHourField = null; + this.mMinuteField = null; + this.mSecondField = null; + this.mMillisecField = null; + this.mDayPeriodField = null; + + this.buildEditFields(); + if (focused) { + this.focusInnerTextBox(); + } + } + + buildEditFields() { + let root = this.shadowRoot.getElementById("edit-wrapper"); + + let options = { + hour: "numeric", + minute: "numeric", + hour12: this.mHour12, + }; + + if (this.shouldShowSecondField()) { + options.second = "numeric"; + } + + let formatter = Intl.DateTimeFormat(this.mLocales, options); + formatter.formatToParts(Date.now()).map(part => { + switch (part.type) { + case "hour": + this.mHourField = this.createEditFieldAndAppend( + this.mHourPlaceHolder, + this.mHourLabel, + true, + this.mMaxLength, + this.mMaxLength, + this.mMinHour, + this.mMaxHour, + this.mHourPageUpDownInterval + ); + this.addEventListenersToField(this.mHourField); + break; + case "minute": + this.mMinuteField = this.createEditFieldAndAppend( + this.mMinutePlaceHolder, + this.mMinuteLabel, + true, + this.mMaxLength, + this.mMaxLength, + this.mMinMinute, + this.mMaxMinute, + this.mMinSecPageUpDownInterval + ); + this.addEventListenersToField(this.mMinuteField); + break; + case "second": + this.mSecondField = this.createEditFieldAndAppend( + this.mSecondPlaceHolder, + this.mSecondLabel, + true, + this.mMaxLength, + this.mMaxLength, + this.mMinSecond, + this.mMaxSecond, + this.mMinSecPageUpDownInterval + ); + this.addEventListenersToField(this.mSecondField); + if (this.shouldShowMillisecField()) { + // Intl.DateTimeFormat does not support millisecond, so we + // need to handle this on our own. + let span = this.shadowRoot.createElementAndAppendChildAt( + root, + "span" + ); + span.textContent = this.mMillisecSeparatorText; + this.mMillisecField = this.createEditFieldAndAppend( + this.mMillisecPlaceHolder, + this.mMillisecLabel, + true, + this.mMillisecMaxLength, + this.mMillisecMaxLength, + this.mMinMillisecond, + this.mMaxMillisecond, + this.mMinSecPageUpDownInterval + ); + this.addEventListenersToField(this.mMillisecField); + } + break; + case "dayPeriod": + this.mDayPeriodField = this.createEditFieldAndAppend( + this.mDayPeriodPlaceHolder, + this.mDayPeriodLabel, + false + ); + this.addEventListenersToField(this.mDayPeriodField); + + // Give aria autocomplete hint for am/pm + this.mDayPeriodField.setAttribute("aria-autocomplete", "inline"); + break; + default: + let span = this.shadowRoot.createElementAndAppendChildAt( + root, + "span" + ); + span.textContent = part.value; + break; + } + }); + } + + getStringsForLocale(aLocales) { + this.log("getStringsForLocale: " + aLocales); + + let intlUtils = this.window.intlUtils; + if (!intlUtils) { + return {}; + } + + let amString, pmString; + let keys = [ + "dates/gregorian/dayperiods/am", + "dates/gregorian/dayperiods/pm", + ]; + + let result = intlUtils.getDisplayNames(this.mLocales, { + style: "short", + keys, + }); + + [amString, pmString] = keys.map(key => result.values[key]); + + return { amString, pmString }; + } + + is12HourTime(aLocales) { + let options = new Intl.DateTimeFormat(aLocales, { + hour: "numeric", + }).resolvedOptions(); + + return options.hour12; + } + + setFieldsFromInputValue() { + let { hour, minute, second, millisecond } = this.getInputElementValues(); + + if (this.isEmpty(hour) && this.isEmpty(minute)) { + this.clearInputFields(true); + return; + } + + // Second and millisecond part are optional, rebuild edit fields if + // needed. + this.rebuildEditFieldsIfNeeded(); + + this.setFieldValue(this.mHourField, hour); + this.setFieldValue(this.mMinuteField, minute); + if (this.mHour12) { + this.setDayPeriodValue( + hour >= this.mMaxHour ? this.mPMIndicator : this.mAMIndicator + ); + } + + if (this.hasSecondField()) { + this.setFieldValue(this.mSecondField, second != undefined ? second : 0); + } + + if (this.hasMillisecField()) { + this.setFieldValue( + this.mMillisecField, + millisecond != undefined ? millisecond : 0 + ); + } + + this.notifyPicker(); + } + + setInputValueFromFields() { + if (this.isAnyFieldEmpty()) { + // Clear input element's value if any of the field has been cleared, + // otherwise update the validity state, since it may become "not" + // invalid if fields are not complete. + if (this.mInputElement.value) { + this.mInputElement.setUserInput(""); + } else { + this.mInputElement.updateValidityState(); + } + // We still need to notify picker in case any of the field has + // changed. + this.notifyPicker(); + return; + } + + let { hour, minute, second, millisecond } = this.getCurrentValue(); + let dayPeriod = this.getDayPeriodValue(); + + // Convert to a valid time string according to: + // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-time-string + if (this.mHour12) { + if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) { + hour += this.mMaxHour; + } else if (dayPeriod == this.mAMIndicator && hour == this.mMaxHour) { + hour = 0; + } + } + + hour = hour < 10 ? "0" + hour : hour; + minute = minute < 10 ? "0" + minute : minute; + + let time = hour + ":" + minute; + if (second != undefined) { + second = second < 10 ? "0" + second : second; + time += ":" + second; + } + + if (millisecond != undefined) { + // Convert milliseconds to fraction of second. + millisecond = millisecond + .toString() + .padStart(this.mMillisecMaxLength, "0"); + time += "." + millisecond; + } + + if (time == this.mInputElement.value) { + return; + } + + this.log("setInputValueFromFields: " + time); + this.notifyPicker(); + this.mInputElement.setUserInput(time); + } + + setFieldsFromPicker(aValue) { + let hour = aValue.hour; + let minute = aValue.minute; + this.log("setFieldsFromPicker: " + hour + ":" + minute); + + if (!this.isEmpty(hour)) { + this.setFieldValue(this.mHourField, hour); + if (this.mHour12) { + this.setDayPeriodValue( + hour >= this.mMaxHour ? this.mPMIndicator : this.mAMIndicator + ); + } + } + + if (!this.isEmpty(minute)) { + this.setFieldValue(this.mMinuteField, minute); + } + + // Update input element's .value if needed. + this.setInputValueFromFields(); + } + + clearInputFields(aFromInputElement) { + this.log("clearInputFields"); + + if (this.mHourField) { + this.clearFieldValue(this.mHourField); + } + + if (this.mMinuteField) { + this.clearFieldValue(this.mMinuteField); + } + + if (this.hasSecondField()) { + this.clearFieldValue(this.mSecondField); + } + + if (this.hasMillisecField()) { + this.clearFieldValue(this.mMillisecField); + } + + if (this.hasDayPeriodField()) { + this.clearFieldValue(this.mDayPeriodField); + } + + if (!aFromInputElement) { + if (this.mInputElement.value) { + this.mInputElement.setUserInput(""); + } else { + this.mInputElement.updateValidityState(); + } + } + } + + notifyMinMaxStepAttrChanged() { + // Second and millisecond part are optional, rebuild edit fields if + // needed. + this.rebuildEditFieldsIfNeeded(); + // Fill in values again. + this.setFieldsFromInputValue(); + } + + incrementFieldValue(aTargetField, aTimes) { + let value = this.getFieldValue(aTargetField); + + // Use current time if field is empty. + if (this.isEmpty(value)) { + let now = new Date(); + + if (aTargetField == this.mHourField) { + value = now.getHours(); + if (this.mHour12) { + value = value % this.mMaxHour || this.mMaxHour; + } + } else if (aTargetField == this.mMinuteField) { + value = now.getMinutes(); + } else if (aTargetField == this.mSecondField) { + value = now.getSeconds(); + } else if (aTargetField == this.mMillisecField) { + value = now.getMilliseconds(); + } else { + this.log("Field not supported in incrementFieldValue."); + return; + } + } + + let min = aTargetField.getAttribute("min"); + let max = aTargetField.getAttribute("max"); + + value += Number(aTimes); + if (value > max) { + value -= max - min + 1; + } else if (value < min) { + value += max - min + 1; + } + + this.setFieldValue(aTargetField, value); + } + + handleKeyboardNav(aEvent) { + if (!this.isEditable()) { + return; + } + + let targetField = aEvent.originalTarget; + let key = aEvent.key; + + if (this.hasDayPeriodField() && targetField == this.mDayPeriodField) { + // Home/End key does nothing on AM/PM field. + if (key == "Home" || key == "End") { + return; + } + + this.setDayPeriodValue( + this.getDayPeriodValue() == this.mAMIndicator + ? this.mPMIndicator + : this.mAMIndicator + ); + this.setInputValueFromFields(); + return; + } + + switch (key) { + case "ArrowUp": + this.incrementFieldValue(targetField, 1); + break; + case "ArrowDown": + this.incrementFieldValue(targetField, -1); + break; + case "PageUp": { + let interval = targetField.getAttribute("pginterval"); + this.incrementFieldValue(targetField, interval); + break; + } + case "PageDown": { + let interval = targetField.getAttribute("pginterval"); + this.incrementFieldValue(targetField, 0 - interval); + break; + } + case "Home": + let min = targetField.getAttribute("min"); + this.setFieldValue(targetField, min); + break; + case "End": + let max = targetField.getAttribute("max"); + this.setFieldValue(targetField, max); + break; + } + this.setInputValueFromFields(); + } + + handleKeypress(aEvent) { + if (!this.isEditable()) { + return; + } + + let targetField = aEvent.originalTarget; + let key = aEvent.key; + + if (this.hasDayPeriodField() && targetField == this.mDayPeriodField) { + if (key == "a" || key == "A") { + this.setDayPeriodValue(this.mAMIndicator); + } else if (key == "p" || key == "P") { + this.setDayPeriodValue(this.mPMIndicator); + } + if (!this.isAnyFieldEmpty()) { + this.setInputValueFromFields(); + } + return; + } + + if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) { + let buffer = targetField.getAttribute("typeBuffer") || ""; + + buffer = buffer.concat(key); + this.setFieldValue(targetField, buffer); + + let n = Number(buffer); + let max = targetField.getAttribute("max"); + let maxLength = targetField.getAttribute("maxlength"); + if (buffer.length >= maxLength || n * 10 > max) { + buffer = ""; + this.advanceToNextField(); + } + targetField.setAttribute("typeBuffer", buffer); + if (!this.isAnyFieldEmpty()) { + this.setInputValueFromFields(); + } + } + } + + setFieldValue(aField, aValue) { + if (!aField || !aField.classList.contains("numeric")) { + return; + } + + let value = Number(aValue); + if (isNaN(value)) { + this.log("NaN on setFieldValue!"); + return; + } + + if (aField == this.mHourField) { + if (this.mHour12) { + // Try to change to 12hr format if user input is 0 or greater + // than 12. + let maxLength = aField.getAttribute("maxlength"); + if (value == 0 && aValue.length == maxLength) { + value = this.mMaxHour; + } else { + value = value > this.mMaxHour ? value % this.mMaxHour : value; + } + } else if (value > this.mMaxHour) { + value = this.mMaxHour; + } + } + + aField.setAttribute("value", value); + + let minDigits = aField.getAttribute("mindigits"); + let formatted = value.toLocaleString(this.mLocales, { + minimumIntegerDigits: minDigits, + useGrouping: false, + }); + + aField.textContent = formatted; + aField.setAttribute("aria-valuetext", formatted); + this.updateResetButtonVisibility(); + } + + getDayPeriodValue(aValue) { + if (!this.hasDayPeriodField()) { + return ""; + } + + let placeholder = this.mDayPeriodField.placeholder; + let value = this.mDayPeriodField.textContent; + + return value == placeholder ? "" : value; + } + + setDayPeriodValue(aValue) { + if (!this.hasDayPeriodField()) { + return; + } + + this.mDayPeriodField.textContent = aValue; + this.mDayPeriodField.setAttribute("value", aValue); + this.updateResetButtonVisibility(); + } + + isAnyFieldAvailable(aForPicker) { + let { hour, minute, second, millisecond } = this.getCurrentValue(); + let dayPeriod = this.getDayPeriodValue(); + + let available = !this.isEmpty(hour) || !this.isEmpty(minute); + if (available) { + return true; + } + + // Picker only cares about hour:minute. + if (aForPicker) { + return false; + } + + return ( + (this.hasDayPeriodField() && !this.isEmpty(dayPeriod)) || + (this.hasSecondField() && !this.isEmpty(second)) || + (this.hasMillisecField() && !this.isEmpty(millisecond)) + ); + } + + isAnyFieldEmpty() { + let { hour, minute, second, millisecond } = this.getCurrentValue(); + let dayPeriod = this.getDayPeriodValue(); + + return ( + this.isEmpty(hour) || + this.isEmpty(minute) || + (this.hasDayPeriodField() && this.isEmpty(dayPeriod)) || + (this.hasSecondField() && this.isEmpty(second)) || + (this.hasMillisecField() && this.isEmpty(millisecond)) + ); + } + + getCurrentValue() { + let hour = this.getFieldValue(this.mHourField); + if (!this.isEmpty(hour)) { + if (this.mHour12) { + let dayPeriod = this.getDayPeriodValue(); + if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) { + hour += this.mMaxHour; + } else if (dayPeriod == this.mAMIndicator && hour == this.mMaxHour) { + hour = 0; + } + } + } + + let minute = this.getFieldValue(this.mMinuteField); + let second = this.getFieldValue(this.mSecondField); + let millisecond = this.getFieldValue(this.mMillisecField); + + let time = { hour, minute, second, millisecond }; + + this.log("getCurrentValue: " + JSON.stringify(time)); + return time; + } +}; diff --git a/toolkit/content/widgets/dialog.js b/toolkit/content/widgets/dialog.js new file mode 100644 index 0000000000..e6d70934eb --- /dev/null +++ b/toolkit/content/widgets/dialog.js @@ -0,0 +1,517 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + class MozDialog extends MozXULElement { + constructor() { + super(); + + this.attachShadow({ mode: "open" }); + + document.addEventListener( + "keypress", + event => { + if (event.keyCode == KeyEvent.DOM_VK_RETURN) { + this._hitEnter(event); + } else if ( + event.keyCode == KeyEvent.DOM_VK_ESCAPE && + !event.defaultPrevented + ) { + this.cancelDialog(); + } + }, + { mozSystemGroup: true } + ); + + if (AppConstants.platform == "macosx") { + document.addEventListener( + "keypress", + event => { + if (event.key == "." && event.metaKey) { + this.cancelDialog(); + } + }, + true + ); + } else { + this.addEventListener("focus", this, true); + this.shadowRoot.addEventListener("focus", this, true); + } + + // listen for when window is closed via native close buttons + window.addEventListener("close", event => { + if (!this.cancelDialog()) { + event.preventDefault(); + } + }); + + // for things that we need to initialize after onload fires + window.addEventListener("load", event => this.postLoadInit(event)); + } + + static get observedAttributes() { + return super.observedAttributes.concat("subdialog"); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name == "subdialog") { + console.assert( + newValue, + `Turning off subdialog style is not supported` + ); + if (this.isConnectedAndReady && !oldValue && newValue) { + this.shadowRoot.appendChild( + MozXULElement.parseXULToFragment(this.inContentStyle) + ); + } + return; + } + super.attributeChangedCallback(name, oldValue, newValue); + } + + static get inheritedAttributes() { + return { + ".dialog-button-box": + "pack=buttonpack,align=buttonalign,dir=buttondir,orient=buttonorient", + "[dlgtype='accept']": "disabled=buttondisabledaccept", + }; + } + + get inContentStyle() { + return ` + <html:link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> + `; + } + + get _markup() { + let buttons = AppConstants.XP_UNIX + ? ` + <hbox class="dialog-button-box"> + <button dlgtype="disclosure" hidden="true"/> + <button dlgtype="help" hidden="true"/> + <button dlgtype="extra2" hidden="true"/> + <button dlgtype="extra1" hidden="true"/> + <spacer class="button-spacer" part="button-spacer" flex="1"/> + <button dlgtype="cancel"/> + <button dlgtype="accept"/> + </hbox>` + : ` + <hbox class="dialog-button-box" pack="end"> + <button dlgtype="extra2" hidden="true"/> + <spacer class="button-spacer" part="button-spacer" flex="1" hidden="true"/> + <button dlgtype="accept"/> + <button dlgtype="extra1" hidden="true"/> + <button dlgtype="cancel"/> + <button dlgtype="help" hidden="true"/> + <button dlgtype="disclosure" hidden="true"/> + </hbox>`; + + let key = + AppConstants.platform == "macosx" + ? `<key phase="capturing" + oncommand="document.querySelector('dialog').openHelp(event)" + key="&openHelpMac.commandkey;" modifiers="accel"/>` + : `<key phase="capturing" + oncommand="document.querySelector('dialog').openHelp(event)" + keycode="&openHelp.commandkey;"/>`; + + return ` + <html:link rel="stylesheet" href="chrome://global/skin/button.css"/> + <html:link rel="stylesheet" href="chrome://global/skin/dialog.css"/> + ${this.hasAttribute("subdialog") ? this.inContentStyle : ""} + <vbox class="box-inherit dialog-content-box" part="content-box" flex="1"> + <html:slot></html:slot> + </vbox> + ${buttons} + <keyset>${key}</keyset>`; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + document.documentElement.setAttribute("role", "dialog"); + + this.shadowRoot.textContent = ""; + this.shadowRoot.appendChild( + MozXULElement.parseXULToFragment(this._markup, [ + "chrome://global/locale/globalKeys.dtd", + ]) + ); + this.initializeAttributeInheritance(); + + /** + * Gets populated by elements that are passed to document.l10n.setAttributes + * to localize the dialog buttons. Needed to properly size the dialog after + * the asynchronous translation. + */ + this._l10nButtons = []; + + this._configureButtons(this.buttons); + + window.moveToAlertPosition = this.moveToAlertPosition; + window.centerWindowOnScreen = this.centerWindowOnScreen; + } + + set buttons(val) { + this._configureButtons(val); + return val; + } + + get buttons() { + return this.getAttribute("buttons"); + } + + set defaultButton(val) { + this._setDefaultButton(val); + return val; + } + + get defaultButton() { + if (this.hasAttribute("defaultButton")) { + return this.getAttribute("defaultButton"); + } + return "accept"; // default to the accept button + } + + get _strBundle() { + if (!this.__stringBundle) { + this.__stringBundle = Services.strings.createBundle( + "chrome://global/locale/dialog.properties" + ); + } + return this.__stringBundle; + } + + acceptDialog() { + return this._doButtonCommand("accept"); + } + + cancelDialog() { + return this._doButtonCommand("cancel"); + } + + getButton(aDlgType) { + return this._buttons[aDlgType]; + } + + get buttonBox() { + return this.shadowRoot.querySelector(".dialog-button-box"); + } + + moveToAlertPosition() { + // hack. we need this so the window has something like its final size + if (window.outerWidth == 1) { + dump( + "Trying to position a sizeless window; caller should have called sizeToContent() or sizeTo(). See bug 75649.\n" + ); + sizeToContent(); + } + + if (opener) { + var xOffset = (opener.outerWidth - window.outerWidth) / 2; + var yOffset = opener.outerHeight / 5; + + var newX = opener.screenX + xOffset; + var newY = opener.screenY + yOffset; + } else { + newX = (screen.availWidth - window.outerWidth) / 2; + newY = (screen.availHeight - window.outerHeight) / 2; + } + + // ensure the window is fully onscreen (if smaller than the screen) + if (newX < screen.availLeft) { + newX = screen.availLeft + 20; + } + if (newX + window.outerWidth > screen.availLeft + screen.availWidth) { + newX = screen.availLeft + screen.availWidth - window.outerWidth - 20; + } + + if (newY < screen.availTop) { + newY = screen.availTop + 20; + } + if (newY + window.outerHeight > screen.availTop + screen.availHeight) { + newY = screen.availTop + screen.availHeight - window.outerHeight - 60; + } + + window.moveTo(newX, newY); + } + + centerWindowOnScreen() { + var xOffset = screen.availWidth / 2 - window.outerWidth / 2; + var yOffset = screen.availHeight / 2 - window.outerHeight / 2; + + xOffset = xOffset > 0 ? xOffset : 0; + yOffset = yOffset > 0 ? yOffset : 0; + window.moveTo(xOffset, yOffset); + } + + postLoadInit(aEvent) { + let focusInit = () => { + const defaultButton = this.getButton(this.defaultButton); + + // give focus to the first focusable element in the dialog + let focusedElt = document.commandDispatcher.focusedElement; + if (!focusedElt) { + document.commandDispatcher.advanceFocusIntoSubtree(this); + + focusedElt = document.commandDispatcher.focusedElement; + if (focusedElt) { + var initialFocusedElt = focusedElt; + while ( + focusedElt.localName == "tab" || + focusedElt.getAttribute("noinitialfocus") == "true" + ) { + document.commandDispatcher.advanceFocusIntoSubtree(focusedElt); + focusedElt = document.commandDispatcher.focusedElement; + if (focusedElt) { + if (focusedElt == initialFocusedElt) { + if (focusedElt.getAttribute("noinitialfocus") == "true") { + focusedElt.blur(); + } + break; + } + } + } + + if (initialFocusedElt.localName == "tab") { + if (focusedElt.hasAttribute("dlgtype")) { + // We don't want to focus on anonymous OK, Cancel, etc. buttons, + // so return focus to the tab itself + initialFocusedElt.focus(); + } + } else if ( + AppConstants.platform != "macosx" && + focusedElt.hasAttribute("dlgtype") && + focusedElt != defaultButton + ) { + defaultButton.focus(); + } + } + } + + try { + if (defaultButton) { + window.notifyDefaultButtonLoaded(defaultButton); + } + } catch (e) {} + }; + + // Give focus after onload completes, see bug 103197. + setTimeout(focusInit, 0); + + if (this._l10nButtons.length) { + document.l10n.translateElements(this._l10nButtons).then(() => { + window.sizeToContent(); + }); + } + } + + openHelp(event) { + var helpButton = this.getButton("help"); + if (helpButton.disabled || helpButton.hidden) { + return; + } + this._fireButtonEvent("help"); + event.stopPropagation(); + event.preventDefault(); + } + + _configureButtons(aButtons) { + // by default, get all the anonymous button elements + var buttons = {}; + this._buttons = buttons; + + for (let type of [ + "accept", + "cancel", + "extra1", + "extra2", + "help", + "disclosure", + ]) { + buttons[type] = this.shadowRoot.querySelector(`[dlgtype="${type}"]`); + } + + // look for any overriding explicit button elements + var exBtns = this.getElementsByAttribute("dlgtype", "*"); + var dlgtype; + for (let i = 0; i < exBtns.length; ++i) { + dlgtype = exBtns[i].getAttribute("dlgtype"); + buttons[dlgtype].hidden = true; // hide the anonymous button + buttons[dlgtype] = exBtns[i]; + } + + // add the label and oncommand handler to each button + for (dlgtype in buttons) { + var button = buttons[dlgtype]; + button.addEventListener( + "command", + this._handleButtonCommand.bind(this), + true + ); + + // don't override custom labels with pre-defined labels on explicit buttons + if (!button.hasAttribute("label")) { + // dialog attributes override the default labels in dialog.properties + if (this.hasAttribute("buttonlabel" + dlgtype)) { + button.setAttribute( + "label", + this.getAttribute("buttonlabel" + dlgtype) + ); + if (this.hasAttribute("buttonaccesskey" + dlgtype)) { + button.setAttribute( + "accesskey", + this.getAttribute("buttonaccesskey" + dlgtype) + ); + } + } else if (this.hasAttribute("buttonid" + dlgtype)) { + document.l10n.setAttributes( + button, + this.getAttribute("buttonid" + dlgtype) + ); + this._l10nButtons.push(button); + } else if (dlgtype != "extra1" && dlgtype != "extra2") { + button.setAttribute( + "label", + this._strBundle.GetStringFromName("button-" + dlgtype) + ); + var accessKey = this._strBundle.GetStringFromName( + "accesskey-" + dlgtype + ); + if (accessKey) { + button.setAttribute("accesskey", accessKey); + } + } + } + } + + // ensure that hitting enter triggers the default button command + // eslint-disable-next-line no-self-assign + this.defaultButton = this.defaultButton; + + // if there is a special button configuration, use it + if (aButtons) { + // expect a comma delimited list of dlgtype values + var list = aButtons.split(","); + + // mark shown dlgtypes as true + var shown = { + accept: false, + cancel: false, + help: false, + disclosure: false, + extra1: false, + extra2: false, + }; + for (let i = 0; i < list.length; ++i) { + shown[list[i].replace(/ /g, "")] = true; + } + + // hide/show the buttons we want + for (dlgtype in buttons) { + buttons[dlgtype].hidden = !shown[dlgtype]; + } + + // show the spacer on Windows only when the extra2 button is present + if (AppConstants.platform == "win") { + let spacer = this.shadowRoot.querySelector(".button-spacer"); + spacer.removeAttribute("hidden"); + spacer.setAttribute("flex", shown.extra2 ? "1" : "0"); + } + } + } + + _setDefaultButton(aNewDefault) { + // remove the default attribute from the previous default button, if any + var oldDefaultButton = this.getButton(this.defaultButton); + if (oldDefaultButton) { + oldDefaultButton.removeAttribute("default"); + } + + var newDefaultButton = this.getButton(aNewDefault); + if (newDefaultButton) { + this.setAttribute("defaultButton", aNewDefault); + newDefaultButton.setAttribute("default", "true"); + } else { + this.setAttribute("defaultButton", "none"); + if (aNewDefault != "none") { + dump( + "invalid new default button: " + aNewDefault + ", assuming: none\n" + ); + } + } + } + + _handleButtonCommand(aEvent) { + return this._doButtonCommand(aEvent.target.getAttribute("dlgtype")); + } + + _doButtonCommand(aDlgType) { + var button = this.getButton(aDlgType); + if (!button.disabled) { + var noCancel = this._fireButtonEvent(aDlgType); + if (noCancel) { + if (aDlgType == "accept" || aDlgType == "cancel") { + var closingEvent = new CustomEvent("dialogclosing", { + bubbles: true, + detail: { button: aDlgType }, + }); + this.dispatchEvent(closingEvent); + window.close(); + } + } + return noCancel; + } + return true; + } + + _fireButtonEvent(aDlgType) { + var event = document.createEvent("Events"); + event.initEvent("dialog" + aDlgType, true, true); + + // handle dom event handlers + return this.dispatchEvent(event); + } + + _hitEnter(evt) { + if (evt.defaultPrevented) { + return; + } + + var btn = this.getButton(this.defaultButton); + if (btn) { + this._doButtonCommand(this.defaultButton); + } + } + + on_focus(event) { + let btn = this.getButton(this.defaultButton); + if (btn) { + btn.setAttribute( + "default", + event.originalTarget == btn || + !( + event.originalTarget.localName == "button" || + event.originalTarget.localName == "toolbarbutton" + ) + ); + } + } + } + + customElements.define("dialog", MozDialog); +} diff --git a/toolkit/content/widgets/docs/index.rst b/toolkit/content/widgets/docs/index.rst new file mode 100644 index 0000000000..2e576357f3 --- /dev/null +++ b/toolkit/content/widgets/docs/index.rst @@ -0,0 +1,10 @@ +=============== +Toolkit Widgets +=============== + +The ``/toolkit/content/widgets`` directory contains XBL bindings, Mozilla Custom Elements, and UA Widgets usable for all applications. + +.. toctree:: + :maxdepth: 1 + + ua_widget diff --git a/toolkit/content/widgets/docs/ua_widget.rst b/toolkit/content/widgets/docs/ua_widget.rst new file mode 100644 index 0000000000..80f3792713 --- /dev/null +++ b/toolkit/content/widgets/docs/ua_widget.rst @@ -0,0 +1,58 @@ +UA Widgets +========== + +Introduction +------------ + +UA Widgets are intended to be a replacement of our usage of XBL bindings in web content. These widgets run JavaScript inside extended principal per-origin sandboxes. They insert their own DOM inside of a special, closed Shadow Root inaccessible to the page, called a UA Widget Shadow Root. + +UA Widget lifecycle +------------------- + +UA Widgets are generally constructed when the element is appended to the document and destroyed when the element is removed from the tree. Yet, in order to be fast, specialization was made to each of the widgets. + +When the element is appended to the tree, a chrome-only ``UAWidgetSetupOrChange`` event is dispatched and is caught by a frame script, namely UAWidgetsChild. + +UAWidgetsChild then grabs the sandbox for that origin (lazily creating it as needed), loads the script as needed, and initializes an instance by calling the JS constructor with a reference to the UA Widget Shadow Root created by the DOM. We will discuss the sandbox in the latter section. + +The ``onsetup`` method is called right after the instance is constructed. The call to constructor must not throw, or UAWidgetsChild will be confused since an instance of the widget will not be returned, but the widget is already half-initalized. If the ``onsetup`` method call throws, UAWidgetsChild will still be able to hold the reference of the widget and call the destructor later on. + +When the element is removed from the tree, ``UAWidgetTeardown`` is dispatched so UAWidgetsChild can destroy the widget, if it exists. If so, the UAWidgetsChild calls the ``destructor()`` method on the widget, causing the widget to destruct itself. + +Counter-intuitively, elements are not considered "removed from the tree" when the document is unloaded. This is considered safe as anything the widget touches should be reset or cleaned up when the document unloads. Please do not violate the assumption by having any browser state toggled by the destructor. + +When a UA Widget initializes, it should create its own DOM inside the passed UA Widget Shadow Root, including the ``<link>`` element necessary to load the stylesheet, add event listeners, etc. When destroyed (i.e. the destructor method is called), it should do the opposite. + +**Specialization**: for video controls, we do not want to do the work if the control is not needed (i.e. when the ``<video>`` or ``<audio>`` element has no "controls" attribute set), so we forgo dispatching the event from HTMLMediaElement in the BindToTree method. Instead, another ``UAWidgetSetupOrChange`` event will cause the sandbox and the widget instance to construct when the attribute is set to true. The same event is also responsible for triggering the ``onchange()`` method on UA Widgets if the widget is already initialized. + +Likewise, the datetime box widget is only loaded when the ``type`` attribute of an ``<input>`` is either `date` or `time`. + +The specialization does not apply to the lifecycle of the UA Widget Shadow Root. It is always constructed in order to suppress children of the DOM element from the web content from receiving a layout frame. + +UA Widget Shadow Root +--------------------- + +The UA Widget Shadow Root is a closed shadow root, with the UA Widget flag turned on. As a closed shadow root, it may not be accessed by other scripts. It is attached on host element which the spec disallow a shadow root to be attached. + +The UA Widget flag enables the security feature covered in the next section. + +**Side note**: XML pretty print hides its transformed content inside a UA Widget Shadow DOM as well, even though there isn't any JavaScript to run. This is set in order to leverage the same security feature and behaviors there. + +The JavaScript sandbox +---------------------- + +The sandboxes created for UA Widgets are per-origin and set to the expanded principal. This allows the script to access other DOM APIs unavailable to the web content, while keeping its principal tied to the document origin. They are created as needed, backing the lifecycle of the UA Widgets as previously mentioned. These sandbox globals are not associated with any window object because they are shared across all same-origin documents. It is the job of the UA Widget script to hold and manage the references of the window and document objects the widget is being initiated on, by accessing them from the UA Widget Shadow Root instance passed. + +While the closed shadow root technically prevents content from accessing the contents, we want a stronger guarantee to protect against accidental leakage of references to the UA Widget shadow tree into content script. Access to the UA Widget DOM is restricted by having their reflectors set in the UA Widgets scope, as opposed to the normal scope. To accomplish this, we avoid having any script (UA Widget script included) getting a hold of the reference of any created DOM element before appending to the Shadow DOM. Once the element is in the Shadow DOM, the binding mechanism will put the reflector in the desired scope as it is being accessed. + +To avoid creating reflectors before DOM insertion, the available DOM interfaces are limited. For example, instead of ``createElement()`` and ``appendChild()``, the script would have to call ``createElementAndAppendChildAt()`` available on the UA Widget Shadow Root instance, to avoid receiving a reference to the DOM element and thus triggering the creation of its reflector in the wrong scope, before the element is properly associated with the UA Widget shadow tree. To find out the differences, search for ``Func="IsChromeOrUAWidget"`` and ``Func="IsNotUAWidget"`` in in-tree WebIDL files. + +Other things to watch out for +----------------------------- + +As part of the implementation of the Web Platform, it is important to make sure the web-observable characteristics of the widget correctly reflect what the script on the web expects. + +* Do not dispatch non-spec compliant events on the UA Widget Shadow Root host element, as event listeners in web content scripts can access them. +* The layout and the dimensions of the widget should be ready by the time the constructor and ``onsetup`` returns, since they can be detectable as soon as the content script gets the reference of the host element (i.e. when ``appendChild()`` returns). In order to make this easier we load ``<link>`` elements load chrome stylesheets synchronously when inside a UA Widget Shadow DOM. +* There shouldn't be any white-spaces nodes in the Shadow DOM, because UA Widget could be placed inside ``white-space: pre``. See bug 1502205. +* CSP will block inline styles in the Shadow DOM. ``<link>`` is the only safe way to load styles. diff --git a/toolkit/content/widgets/editor.js b/toolkit/content/widgets/editor.js new file mode 100644 index 0000000000..ef0f02a044 --- /dev/null +++ b/toolkit/content/widgets/editor.js @@ -0,0 +1,215 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +{ + /* globals XULFrameElement */ + + class MozEditor extends XULFrameElement { + connectedCallback() { + this._editorContentListener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIURIContentListener", + "nsISupportsWeakReference", + ]), + doContent(contentType, isContentPreferred, request, contentHandler) { + return false; + }, + isPreferred(contentType, desiredContentType) { + return false; + }, + canHandleContent(contentType, isContentPreferred, desiredContentType) { + return false; + }, + loadCookie: null, + parentContentListener: null, + }; + + this._finder = null; + + this._fastFind = null; + + this._lastSearchString = null; + + // Make window editable immediately only + // if the "editortype" attribute is supplied + // This allows using same contentWindow for different editortypes, + // where the type is determined during the apps's window.onload handler. + if (this.editortype) { + this.makeEditable(this.editortype, true); + } + } + + get finder() { + if (!this._finder) { + if (!this.docShell) { + return null; + } + + let Finder = ChromeUtils.import("resource://gre/modules/Finder.jsm", {}) + .Finder; + this._finder = new Finder(this.docShell); + } + return this._finder; + } + + get fastFind() { + if (!this._fastFind) { + if (!("@mozilla.org/typeaheadfind;1" in Cc)) { + return null; + } + + if (!this.docShell) { + return null; + } + + this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance( + Ci.nsITypeAheadFind + ); + this._fastFind.init(this.docShell); + } + return this._fastFind; + } + + set editortype(val) { + this.setAttribute("editortype", val); + } + + get editortype() { + return this.getAttribute("editortype"); + } + + get currentURI() { + return this.webNavigation.currentURI; + } + + get webBrowserFind() { + return this.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebBrowserFind); + } + + get markupDocumentViewer() { + return this.docShell.contentViewer; + } + + get editingSession() { + return this.docShell.editingSession; + } + + get commandManager() { + return this.webNavigation + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsICommandManager); + } + + set fullZoom(val) { + this.browsingContext.fullZoom = val; + } + + get fullZoom() { + return this.browsingContext.fullZoom; + } + + set textZoom(val) { + this.browsingContext.textZoom = val; + } + + get textZoom() { + return this.browsingContext.textZoom; + } + + get isSyntheticDocument() { + return this.contentDocument.isSyntheticDocument; + } + + get messageManager() { + if (this.frameLoader) { + return this.frameLoader.messageManager; + } + return null; + } + + // Copied from toolkit/content/widgets/browser-custom-element.js. + // Send an asynchronous message to the remote child via an actor. + // Note: use this only for messages through an actor. For old-style + // messages, use the message manager. + // The value of the scope argument determines which browsing contexts + // are sent to: + // 'all' - send to actors associated with all descendant child frames. + // 'roots' - send only to actors associated with process roots. + // undefined/'' - send only to the top-level actor and not any descendants. + sendMessageToActor(messageName, args, actorName, scope) { + if (!this.frameLoader) { + return; + } + + function sendToChildren(browsingContext, childScope) { + let windowGlobal = browsingContext.currentWindowGlobal; + // If 'roots' is set, only send if windowGlobal.isProcessRoot is true. + if ( + windowGlobal && + (childScope != "roots" || windowGlobal.isProcessRoot) + ) { + windowGlobal.getActor(actorName).sendAsyncMessage(messageName, args); + } + + // Iterate as long as scope in assigned. Note that we use the original + // passed in scope, not childScope here. + if (scope) { + for (let context of browsingContext.children) { + sendToChildren(context, scope); + } + } + } + + // Pass no second argument to always send to the top-level browsing context. + sendToChildren(this.browsingContext); + } + + get outerWindowID() { + return this.docShell.outerWindowID; + } + + makeEditable(editortype, waitForUrlLoad) { + let win = this.contentWindow; + this.editingSession.makeWindowEditable( + win, + editortype, + waitForUrlLoad, + true, + false + ); + this.setAttribute("editortype", editortype); + + this.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface( + Ci.nsIURIContentListener + ).parentContentListener = this._editorContentListener; + } + + getEditor(containingWindow) { + return this.editingSession.getEditorForWindow(containingWindow); + } + + getHTMLEditor(containingWindow) { + var editor = this.editingSession.getEditorForWindow(containingWindow); + return editor.QueryInterface(Ci.nsIHTMLEditor); + } + + print(aOuterWindowID, aPrintSettings) { + if (!this.frameLoader) { + throw Components.Exception("No frame loader.", Cr.NS_ERROR_FAILURE); + } + + return this.frameLoader.print(aOuterWindowID, aPrintSettings); + } + } + + customElements.define("editor", MozEditor); +} diff --git a/toolkit/content/widgets/findbar.js b/toolkit/content/widgets/findbar.js new file mode 100644 index 0000000000..a1bd3fd58b --- /dev/null +++ b/toolkit/content/widgets/findbar.js @@ -0,0 +1,1398 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +{ + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + let LazyConstants = {}; + ChromeUtils.defineModuleGetter( + LazyConstants, + "PluralForm", + "resource://gre/modules/PluralForm.jsm" + ); + + const PREFS_TO_OBSERVE_BOOL = new Map([ + ["findAsYouType", "accessibility.typeaheadfind"], + ["manualFAYT", "accessibility.typeaheadfind.manual"], + ["typeAheadLinksOnly", "accessibility.typeaheadfind.linksonly"], + ["entireWord", "findbar.entireword"], + ["highlightAll", "findbar.highlightAll"], + ["useModalHighlight", "findbar.modalHighlight"], + ]); + const PREFS_TO_OBSERVE_INT = new Map([ + ["typeAheadCaseSensitive", "accessibility.typeaheadfind.casesensitive"], + ["matchDiacritics", "findbar.matchdiacritics"], + ]); + const PREFS_TO_OBSERVE_ALL = new Map([ + ...PREFS_TO_OBSERVE_BOOL, + ...PREFS_TO_OBSERVE_INT, + ]); + const TOPIC_MAC_APP_ACTIVATE = "mac_app_activate"; + + class MozFindbar extends MozXULElement { + static get markup() { + return ` + <hbox anonid="findbar-container" class="findbar-container" flex="1" align="center"> + <hbox anonid="findbar-textbox-wrapper" align="stretch"> + <html:input anonid="findbar-textbox" class="findbar-textbox findbar-find-fast" /> + <toolbarbutton anonid="find-previous" class="findbar-find-previous tabbable" + data-l10n-attrs="tooltiptext" data-l10n-id="findbar-previous" + oncommand="onFindAgainCommand(true);" disabled="true" /> + <toolbarbutton anonid="find-next" class="findbar-find-next tabbable" + data-l10n-id="findbar-next" oncommand="onFindAgainCommand(false);" disabled="true" /> + </hbox> + <toolbarbutton anonid="highlight" class="findbar-highlight findbar-button tabbable" + data-l10n-id="findbar-highlight-all2" oncommand="toggleHighlight(this.checked);" type="checkbox" /> + <toolbarbutton anonid="find-case-sensitive" class="findbar-case-sensitive findbar-button tabbable" + data-l10n-id="findbar-case-sensitive" oncommand="_setCaseSensitivity(this.checked ? 1 : 0);" type="checkbox" /> + <toolbarbutton anonid="find-match-diacritics" class="findbar-match-diacritics findbar-button tabbable" + data-l10n-id="findbar-match-diacritics" oncommand="_setDiacriticMatching(this.checked ? 1 : 0);" type="checkbox" /> + <toolbarbutton anonid="find-entire-word" class="findbar-entire-word findbar-button tabbable" + data-l10n-id="findbar-entire-word" oncommand="toggleEntireWord(this.checked);" type="checkbox" /> + <label anonid="match-case-status" class="findbar-find-fast" /> + <label anonid="match-diacritics-status" class="findbar-find-fast" /> + <label anonid="entire-word-status" class="findbar-find-fast" /> + <label anonid="found-matches" class="findbar-find-fast found-matches" hidden="true" /> + <image anonid="find-status-icon" class="findbar-find-fast find-status-icon" /> + <description anonid="find-status" control="findbar-textbox" class="findbar-find-fast findbar-find-status" /> + </hbox> + <toolbarbutton anonid="find-closebutton" class="findbar-closebutton close-icon" + data-l10n-id="findbar-find-button-close" oncommand="close();" /> + `; + } + + constructor() { + super(); + MozXULElement.insertFTLIfNeeded("toolkit/main-window/findbar.ftl"); + this.destroy = this.destroy.bind(this); + + // We have to guard against `this.close` being |null| due to an unknown + // issue, which is tracked in bug 957999. + this.addEventListener( + "keypress", + event => { + if (event.keyCode == event.DOM_VK_ESCAPE) { + if (this.close) { + this.close(); + } + event.preventDefault(); + } + }, + true + ); + } + + connectedCallback() { + // Hide the findbar immediately without animation. This prevents a flicker in the case where + // we'll never be shown (i.e. adopting a tab that has a previously-opened-but-now-closed + // findbar into a new window). + this.setAttribute("noanim", "true"); + this.hidden = true; + this.appendChild(this.constructor.fragment); + + /** + * Please keep in sync with toolkit/modules/FindBarContent.jsm + */ + this.FIND_NORMAL = 0; + + this.FIND_TYPEAHEAD = 1; + + this.FIND_LINKS = 2; + + this._findMode = 0; + + this._flashFindBar = 0; + + this._initialFlashFindBarCount = 6; + + /** + * For tests that need to know when the find bar is finished + * initializing, we store a promise to notify on. + */ + this._startFindDeferred = null; + + this._browser = null; + + this._destroyed = false; + + this._strBundle = null; + + this._xulBrowserWindow = null; + + // These elements are accessed frequently and are therefore cached. + this._findField = this.getElement("findbar-textbox"); + this._foundMatches = this.getElement("found-matches"); + this._findStatusIcon = this.getElement("find-status-icon"); + this._findStatusDesc = this.getElement("find-status"); + + this._foundURL = null; + + let prefsvc = Services.prefs; + + this.quickFindTimeoutLength = prefsvc.getIntPref( + "accessibility.typeaheadfind.timeout" + ); + this._flashFindBar = prefsvc.getIntPref( + "accessibility.typeaheadfind.flashBar" + ); + + let observe = (this._observe = this.observe.bind(this)); + for (let [propName, prefName] of PREFS_TO_OBSERVE_ALL) { + prefsvc.addObserver(prefName, observe); + let prefGetter = PREFS_TO_OBSERVE_BOOL.has(propName) ? "Bool" : "Int"; + this["_" + propName] = prefsvc[`get${prefGetter}Pref`](prefName); + } + Services.obs.addObserver(observe, TOPIC_MAC_APP_ACTIVATE); + + this._findResetTimeout = -1; + + // Make sure the FAYT keypress listener is attached by initializing the + // browser property. + if (this.getAttribute("browserid")) { + setTimeout(() => { + // eslint-disable-next-line no-self-assign + this.browser = this.browser; + }, 0); + } + + window.addEventListener("unload", this.destroy); + + this._findField.addEventListener("input", event => { + // We should do nothing during composition. E.g., composing string + // before converting may matches a forward word of expected word. + // After that, even if user converts the composition string to the + // expected word, it may find second or later searching word in the + // document. + if (this._isIMEComposing) { + return; + } + + const value = this._findField.value; + if (this._hadValue && !value) { + this._willfullyDeleted = true; + this._hadValue = false; + } else if (value.trim()) { + this._hadValue = true; + this._willfullyDeleted = false; + } + this._find(value); + }); + + this._findField.addEventListener("keypress", event => { + switch (event.keyCode) { + case KeyEvent.DOM_VK_RETURN: + if (this.findMode == this.FIND_NORMAL) { + let findString = this._findField; + if (!findString.value) { + return; + } + if (event.getModifierState("Accel")) { + this.getElement("highlight").click(); + return; + } + + this.onFindAgainCommand(event.shiftKey); + } else { + this._finishFAYT(event); + } + break; + case KeyEvent.DOM_VK_TAB: + let shouldHandle = + !event.altKey && !event.ctrlKey && !event.metaKey; + if (shouldHandle && this.findMode != this.FIND_NORMAL) { + this._finishFAYT(event); + } + break; + case KeyEvent.DOM_VK_PAGE_UP: + case KeyEvent.DOM_VK_PAGE_DOWN: + if ( + !event.altKey && + !event.ctrlKey && + !event.metaKey && + !event.shiftKey + ) { + this.browser.finder.keyPress(event); + event.preventDefault(); + } + break; + case KeyEvent.DOM_VK_UP: + case KeyEvent.DOM_VK_DOWN: + this.browser.finder.keyPress(event); + event.preventDefault(); + break; + } + }); + + this._findField.addEventListener("blur", event => { + // Note: This code used to remove the selection + // if it matched an editable. + this.browser.finder.enableSelection(); + }); + + this._findField.addEventListener("focus", event => { + this._updateBrowserWithState(); + }); + + this._findField.addEventListener("compositionstart", event => { + // Don't close the find toolbar while IME is composing. + let findbar = this; + findbar._isIMEComposing = true; + if (findbar._quickFindTimeout) { + clearTimeout(findbar._quickFindTimeout); + findbar._quickFindTimeout = null; + findbar._updateBrowserWithState(); + } + }); + + this._findField.addEventListener("compositionend", event => { + this._isIMEComposing = false; + if (this.findMode != this.FIND_NORMAL) { + this._setFindCloseTimeout(); + } + }); + + this._findField.addEventListener("dragover", event => { + if (event.dataTransfer.types.includes("text/plain")) { + event.preventDefault(); + } + }); + + this._findField.addEventListener("drop", event => { + let value = event.dataTransfer.getData("text/plain"); + this._findField.value = value; + this._find(value); + event.stopPropagation(); + event.preventDefault(); + }); + } + + set findMode(val) { + this._findMode = val; + this._updateBrowserWithState(); + return val; + } + + get findMode() { + return this._findMode; + } + + set prefillWithSelection(val) { + this.setAttribute("prefillwithselection", val); + return val; + } + + get prefillWithSelection() { + return this.getAttribute("prefillwithselection") != "false"; + } + + get hasTransactions() { + if (this._findField.value) { + return true; + } + + // Watch out for lazy editor init + if (this._findField.editor) { + let tm = this._findField.editor.transactionManager; + return !!(tm.numberOfUndoItems || tm.numberOfRedoItems); + } + return false; + } + + set browser(val) { + function setFindbarInActor(browser, findbar) { + if (!browser.frameLoader) { + return; + } + + let windowGlobal = browser.browsingContext.currentWindowGlobal; + if (windowGlobal) { + let findbarParent = windowGlobal.getActor("FindBar"); + if (findbarParent) { + findbarParent.setFindbar(browser, findbar); + } + } + } + + if (this._browser) { + setFindbarInActor(this._browser, null); + + let finder = this._browser.finder; + if (finder) { + finder.removeResultListener(this); + } + } + + this._browser = val; + if (this._browser) { + // Need to do this to ensure the correct initial state. + this._updateBrowserWithState(); + + setFindbarInActor(this._browser, this); + + this._browser.finder.addResultListener(this); + } + return val; + } + + get browser() { + if (!this._browser) { + this._browser = document.getElementById(this.getAttribute("browserid")); + } + return this._browser; + } + + get strBundle() { + if (!this._strBundle) { + this._strBundle = Services.strings.createBundle( + "chrome://global/locale/findbar.properties" + ); + } + return this._strBundle; + } + + observe(subject, topic, prefName) { + if (topic == TOPIC_MAC_APP_ACTIVATE) { + this._onAppActivateMac(); + return; + } + + if (topic != "nsPref:changed") { + return; + } + + let prefsvc = Services.prefs; + + switch (prefName) { + case "accessibility.typeaheadfind": + this._findAsYouType = prefsvc.getBoolPref(prefName); + break; + case "accessibility.typeaheadfind.manual": + this._manualFAYT = prefsvc.getBoolPref(prefName); + break; + case "accessibility.typeaheadfind.timeout": + this.quickFindTimeoutLength = prefsvc.getIntPref(prefName); + break; + case "accessibility.typeaheadfind.linksonly": + this._typeAheadLinksOnly = prefsvc.getBoolPref(prefName); + break; + case "accessibility.typeaheadfind.casesensitive": + this._setCaseSensitivity(prefsvc.getIntPref(prefName)); + break; + case "findbar.entireword": + this._entireWord = prefsvc.getBoolPref(prefName); + this.toggleEntireWord(this._entireWord, true); + break; + case "findbar.highlightAll": + this.toggleHighlight(prefsvc.getBoolPref(prefName), true); + break; + case "findbar.matchdiacritics": + this._setDiacriticMatching(prefsvc.getIntPref(prefName)); + break; + case "findbar.modalHighlight": + this._useModalHighlight = prefsvc.getBoolPref(prefName); + if (this.browser.finder) { + this.browser.finder.onModalHighlightChange(this._useModalHighlight); + } + break; + } + } + + getElement(aAnonymousID) { + return this.querySelector(`[anonid=${aAnonymousID}]`); + } + + /** + * This is necessary because the destructor isn't called when we are removed + * from a document that is not destroyed. This needs to be explicitly called + * in this case. + */ + destroy() { + if (this._destroyed) { + return; + } + window.removeEventListener("unload", this.destroy); + this._destroyed = true; + + if (this.browser && this.browser.finder) { + this.browser.finder.destroy(); + } + + // Invoking this setter also removes the message listeners. + this.browser = null; + + let prefsvc = Services.prefs; + let observe = this._observe; + for (let [, prefName] of PREFS_TO_OBSERVE_ALL) { + prefsvc.removeObserver(prefName, observe); + } + + Services.obs.removeObserver(observe, TOPIC_MAC_APP_ACTIVATE); + + // Clear all timers that might still be running. + this._cancelTimers(); + } + + _cancelTimers() { + if (this._flashFindBarTimeout) { + clearInterval(this._flashFindBarTimeout); + this._flashFindBarTimeout = null; + } + if (this._quickFindTimeout) { + clearTimeout(this._quickFindTimeout); + this._quickFindTimeout = null; + } + if (this._findResetTimeout) { + clearTimeout(this._findResetTimeout); + this._findResetTimeout = null; + } + } + + _setFindCloseTimeout() { + if (this._quickFindTimeout) { + clearTimeout(this._quickFindTimeout); + } + + // Don't close the find toolbar while IME is composing OR when the + // findbar is already hidden. + if (this._isIMEComposing || this.hidden) { + this._quickFindTimeout = null; + this._updateBrowserWithState(); + return; + } + + if (this.quickFindTimeoutLength < 1) { + this._quickFindTimeout = null; + } else { + this._quickFindTimeout = setTimeout(() => { + if (this.findMode != this.FIND_NORMAL) { + this.close(); + } + this._quickFindTimeout = null; + }, this.quickFindTimeoutLength); + } + this._updateBrowserWithState(); + } + + /** + * Updates the search match count after each find operation on a new string. + */ + _updateMatchesCount() { + if (!this._dispatchFindEvent("matchescount")) { + return; + } + + this.browser.finder.requestMatchesCount( + this._findField.value, + this.findMode == this.FIND_LINKS + ); + } + + /** + * Turns highlighting of all occurrences on or off. + * + * @param {Boolean} highlight Whether to turn the highlight on or off. + * @param {Boolean} fromPrefObserver Whether the callee is the pref + * observer, which means we should not set + * the same pref again. + */ + toggleHighlight(highlight, fromPrefObserver) { + if (highlight === this._highlightAll) { + return; + } + + this.browser.finder.onHighlightAllChange(highlight); + + this._setHighlightAll(highlight, fromPrefObserver); + + if (!this._dispatchFindEvent("highlightallchange")) { + return; + } + + let word = this._findField.value; + // Bug 429723. Don't attempt to highlight "" + if (highlight && !word) { + return; + } + + this.browser.finder.highlight( + highlight, + word, + this.findMode == this.FIND_LINKS + ); + + // Update the matches count + this._updateMatchesCount(Ci.nsITypeAheadFind.FIND_FOUND); + } + + /** + * Updates the highlight-all mode of the findbar and its UI. + * + * @param {Boolean} highlight Whether to turn the highlight on or off. + * @param {Boolean} fromPrefObserver Whether the callee is the pref + * observer, which means we should not set + * the same pref again. + */ + _setHighlightAll(highlight, fromPrefObserver) { + if (typeof highlight != "boolean") { + highlight = this._highlightAll; + } + if (highlight !== this._highlightAll) { + this._highlightAll = highlight; + if (!fromPrefObserver) { + Services.telemetry.scalarAdd("findbar.highlight_all", 1); + Services.prefs.setBoolPref("findbar.highlightAll", highlight); + } + } + let checkbox = this.getElement("highlight"); + checkbox.checked = this._highlightAll; + } + + /** + * Updates the case-sensitivity mode of the findbar and its UI. + * + * @param {String} [str] The string for which case sensitivity might be + * turned on. This only used when case-sensitivity is + * in auto mode, see `_shouldBeCaseSensitive`. The + * default value for this parameter is the find-field + * value. + * @see _shouldBeCaseSensitive + */ + _updateCaseSensitivity(str) { + let val = str || this._findField.value; + + let caseSensitive = this._shouldBeCaseSensitive(val); + let checkbox = this.getElement("find-case-sensitive"); + let statusLabel = this.getElement("match-case-status"); + checkbox.checked = caseSensitive; + + statusLabel.value = caseSensitive ? this._caseSensitiveStr : ""; + + // Show the checkbox on the full Find bar in non-auto mode. + // Show the label in all other cases. + let hideCheckbox = + this.findMode != this.FIND_NORMAL || + (this._typeAheadCaseSensitive != 0 && + this._typeAheadCaseSensitive != 1); + checkbox.hidden = hideCheckbox; + statusLabel.hidden = !hideCheckbox; + + this.browser.finder.caseSensitive = caseSensitive; + } + + /** + * Sets the findbar case-sensitivity mode. + * + * @param {Number} caseSensitivity 0 - case insensitive, + * 1 - case sensitive, + * 2 - auto = case sensitive if the matching + * string contains upper case letters. + * @see _shouldBeCaseSensitive + */ + _setCaseSensitivity(caseSensitivity) { + this._typeAheadCaseSensitive = caseSensitivity; + this._updateCaseSensitivity(); + this._findFailedString = null; + this._find(); + + this._dispatchFindEvent("casesensitivitychange"); + Services.telemetry.scalarAdd("findbar.match_case", 1); + } + + /** + * Updates the diacritic-matching mode of the findbar and its UI. + * + * @param {String} [str] The string for which diacritic matching might be + * turned on. This is only used when diacritic + * matching is in auto mode, see + * `_shouldMatchDiacritics`. The default value for + * this parameter is the find-field value. + * @see _shouldMatchDiacritics. + */ + _updateDiacriticMatching(str) { + let val = str || this._findField.value; + + let matchDiacritics = this._shouldMatchDiacritics(val); + let checkbox = this.getElement("find-match-diacritics"); + let statusLabel = this.getElement("match-diacritics-status"); + checkbox.checked = matchDiacritics; + + statusLabel.value = matchDiacritics ? this._matchDiacriticsStr : ""; + + // Show the checkbox on the full Find bar in non-auto mode. + // Show the label in all other cases. + let hideCheckbox = + this.findMode != this.FIND_NORMAL || + (this._matchDiacritics != 0 && this._matchDiacritics != 1); + checkbox.hidden = hideCheckbox; + statusLabel.hidden = !hideCheckbox; + + this.browser.finder.matchDiacritics = matchDiacritics; + } + + /** + * Sets the findbar diacritic-matching mode + * @param {Number} diacriticMatching 0 - ignore diacritics, + * 1 - match diacritics, + * 2 - auto = match diacritics if the + * matching string contains + * diacritics. + * @see _shouldMatchDiacritics + */ + _setDiacriticMatching(diacriticMatching) { + this._matchDiacritics = diacriticMatching; + this._updateDiacriticMatching(); + this._findFailedString = null; + this._find(); + + this._dispatchFindEvent("diacriticmatchingchange"); + + Services.telemetry.scalarAdd("findbar.match_diacritics", 1); + } + + /** + * Updates the entire-word mode of the findbar and its UI. + */ + _setEntireWord() { + let entireWord = this._entireWord; + let checkbox = this.getElement("find-entire-word"); + let statusLabel = this.getElement("entire-word-status"); + checkbox.checked = entireWord; + + statusLabel.value = entireWord ? this._entireWordStr : ""; + + // Show the checkbox on the full Find bar in non-auto mode. + // Show the label in all other cases. + let hideCheckbox = this.findMode != this.FIND_NORMAL; + checkbox.hidden = hideCheckbox; + statusLabel.hidden = !hideCheckbox; + + this.browser.finder.entireWord = entireWord; + } + + /** + * Sets the findbar entire-word mode. + * + * @param {Boolean} entireWord Whether or not entire-word mode should be + * turned on. + */ + toggleEntireWord(entireWord, fromPrefObserver) { + if (!fromPrefObserver) { + // Just set the pref; our observer will change the find bar behavior. + Services.prefs.setBoolPref("findbar.entireword", entireWord); + + Services.telemetry.scalarAdd("findbar.whole_words", 1); + return; + } + + this._findFailedString = null; + this._find(); + } + + /** + * Opens and displays the find bar. + * + * @param {Number} mode The find mode to be used, which is either + * FIND_NORMAL, FIND_TYPEAHEAD or FIND_LINKS. If not + * passed, we revert to the last find mode if any or + * FIND_NORMAL. + * @return {Boolean} `true` if the find bar wasn't previously open, `false` + * otherwise. + */ + open(mode) { + if (mode != undefined) { + this.findMode = mode; + } + + if (!this._notFoundStr) { + var bundle = this.strBundle; + this._notFoundStr = bundle.GetStringFromName("NotFound"); + this._wrappedToTopStr = bundle.GetStringFromName("WrappedToTop"); + this._wrappedToBottomStr = bundle.GetStringFromName("WrappedToBottom"); + this._normalFindStr = bundle.GetStringFromName("NormalFind"); + this._fastFindStr = bundle.GetStringFromName("FastFind"); + this._fastFindLinksStr = bundle.GetStringFromName("FastFindLinks"); + this._caseSensitiveStr = bundle.GetStringFromName("CaseSensitive"); + this._matchDiacriticsStr = bundle.GetStringFromName("MatchDiacritics"); + this._entireWordStr = bundle.GetStringFromName("EntireWord"); + } + + this._findFailedString = null; + + this._updateFindUI(); + if (this.hidden) { + Services.telemetry.scalarAdd("findbar.shown", 1); + this.removeAttribute("noanim"); + this.hidden = false; + + this._updateStatusUI(Ci.nsITypeAheadFind.FIND_FOUND); + + let event = document.createEvent("Events"); + event.initEvent("findbaropen", true, false); + this.dispatchEvent(event); + + this.browser.finder.onFindbarOpen(); + + return true; + } + return false; + } + + /** + * Closes the findbar. + * + * @param {Boolean} [noAnim] Whether to disable to closing animation. Used + * to close instantly and synchronously, when + * other operations depend on this state. + */ + close(noAnim) { + if (this.hidden) { + return; + } + + if (noAnim) { + this.setAttribute("noanim", true); + } + this.hidden = true; + + let event = document.createEvent("Events"); + event.initEvent("findbarclose", true, false); + this.dispatchEvent(event); + + // 'focusContent()' iterates over all listeners in the chrome + // process, so we need to call it from here. + this.browser.finder.focusContent(); + this.browser.finder.onFindbarClose(); + + this._cancelTimers(); + this._updateBrowserWithState(); + + this._findFailedString = null; + } + + clear() { + this.browser.finder.removeSelection(); + // Clear value and undo/redo transactions + this._findField.value = ""; + if (this._findField.editor) { + this._findField.editor.transactionManager.clear(); + } + this.toggleHighlight(false); + this._updateStatusUI(); + this._enableFindButtons(false); + } + + _dispatchKeypressEvent(target, fakeEvent) { + if (!target) { + return; + } + + // The event information comes from the child process. + let event = new target.ownerGlobal.KeyboardEvent( + fakeEvent.type, + fakeEvent + ); + target.dispatchEvent(event); + } + + _updateStatusUIBar(foundURL) { + if (!this._xulBrowserWindow) { + try { + this._xulBrowserWindow = window.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).XULBrowserWindow; + } catch (ex) {} + if (!this._xulBrowserWindow) { + return false; + } + } + + // Call this has the same effect like hovering over link, + // the browser shows the URL as a tooltip. + this._xulBrowserWindow.setOverLink(foundURL || ""); + return true; + } + + _finishFAYT(keypressEvent) { + this.browser.finder.focusContent(); + + if (keypressEvent) { + keypressEvent.preventDefault(); + } + + this.browser.finder.keyPress(keypressEvent); + + this.close(); + return true; + } + + _shouldBeCaseSensitive(str) { + if (this._typeAheadCaseSensitive == 0) { + return false; + } + if (this._typeAheadCaseSensitive == 1) { + return true; + } + + return str != str.toLowerCase(); + } + + _shouldMatchDiacritics(str) { + if (this._matchDiacritics == 0) { + return false; + } + if (this._matchDiacritics == 1) { + return true; + } + + return str != str.normalize("NFD"); + } + + onMouseUp() { + if (!this.hidden && this.findMode != this.FIND_NORMAL) { + this.close(); + } + } + + /** + * We get a fake event object through an IPC message when FAYT is being used + * from within the browser. We then stuff that input in the find bar here. + * + * @param {Object} fakeEvent Event object that looks and quacks like a + * native DOM KeyPress event. + */ + _onBrowserKeypress(fakeEvent) { + const FAYT_LINKS_KEY = "'"; + const FAYT_TEXT_KEY = "/"; + + if (!this.hidden && this._findField == document.activeElement) { + this._dispatchKeypressEvent(this._findField, fakeEvent); + return; + } + + if (this.findMode != this.FIND_NORMAL && this._quickFindTimeout) { + this._findField.select(); + this._findField.focus(); + this._dispatchKeypressEvent(this._findField, fakeEvent); + return; + } + + let key = fakeEvent.charCode + ? String.fromCharCode(fakeEvent.charCode) + : null; + let manualstartFAYT = + (key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY) && this._manualFAYT; + let autostartFAYT = + !manualstartFAYT && this._findAsYouType && key && key != " "; + if (manualstartFAYT || autostartFAYT) { + let mode = + key == FAYT_LINKS_KEY || (autostartFAYT && this._typeAheadLinksOnly) + ? this.FIND_LINKS + : this.FIND_TYPEAHEAD; + + // Clear bar first, so that when openFindBar() calls setCaseSensitivity() + // it doesn't get confused by a lingering value + this._findField.value = ""; + + this.open(mode); + this._setFindCloseTimeout(); + this._findField.select(); + this._findField.focus(); + + if (autostartFAYT) { + this._dispatchKeypressEvent(this._findField, fakeEvent); + } else { + this._updateStatusUI(Ci.nsITypeAheadFind.FIND_FOUND); + } + } + } + + _updateBrowserWithState() { + if (this._browser) { + this._browser.sendMessageToActor( + "Findbar:UpdateState", + { + findMode: this.findMode, + isOpenAndFocused: + !this.hidden && document.activeElement == this._findField, + hasQuickFindTimeout: !!this._quickFindTimeout, + }, + "FindBar", + "all" + ); + } + } + + _enableFindButtons(aEnable) { + this.getElement("find-next").disabled = this.getElement( + "find-previous" + ).disabled = !aEnable; + } + + /** + * Determines whether minimalist or general-purpose search UI is to be + * displayed when the find bar is activated. + */ + _updateFindUI() { + let showMinimalUI = this.findMode != this.FIND_NORMAL; + + let nodes = this.getElement("findbar-container").children; + let wrapper = this.getElement("findbar-textbox-wrapper"); + let foundMatches = this._foundMatches; + for (let node of nodes) { + if (node == wrapper || node == foundMatches) { + continue; + } + node.hidden = showMinimalUI; + } + this.getElement("find-next").hidden = this.getElement( + "find-previous" + ).hidden = showMinimalUI; + foundMatches.hidden = showMinimalUI || !foundMatches.value; + this._updateCaseSensitivity(); + this._updateDiacriticMatching(); + this._setEntireWord(); + this._setHighlightAll(); + + if (showMinimalUI) { + this._findField.classList.add("minimal"); + } else { + this._findField.classList.remove("minimal"); + } + + if (this.findMode == this.FIND_TYPEAHEAD) { + this._findField.placeholder = this._fastFindStr; + } else if (this.findMode == this.FIND_LINKS) { + this._findField.placeholder = this._fastFindLinksStr; + } else { + this._findField.placeholder = this._normalFindStr; + } + } + + _find(value) { + if (!this._dispatchFindEvent("")) { + return; + } + + let val = value || this._findField.value; + + // We have to carry around an explicit version of this, because + // finder.searchString doesn't update on failed searches. + this.browser._lastSearchString = val; + + // Only search on input if we don't have a last-failed string, + // or if the current search string doesn't start with it. + // In entire-word mode we always attemp a find; since sequential matching + // is not guaranteed, the first character typed may not be a word (no + // match), but the with the second character it may well be a word, + // thus a match. + if ( + !this._findFailedString || + !val.startsWith(this._findFailedString) || + this._entireWord + ) { + // Getting here means the user commanded a find op. Make sure any + // initial prefilling is ignored if it hasn't happened yet. + if (this._startFindDeferred) { + this._startFindDeferred.resolve(); + this._startFindDeferred = null; + } + + this._enableFindButtons(val); + this._updateCaseSensitivity(val); + this._updateDiacriticMatching(val); + this._setEntireWord(); + + this.browser.finder.fastFind( + val, + this.findMode == this.FIND_LINKS, + this.findMode != this.FIND_NORMAL + ); + } + + if (this.findMode != this.FIND_NORMAL) { + this._setFindCloseTimeout(); + } + + if (this._findResetTimeout != -1) { + clearTimeout(this._findResetTimeout); + } + + // allow a search to happen on input again after a second has expired + // since the previous input, to allow for dynamic content and/ or page + // loading. + this._findResetTimeout = setTimeout(() => { + this._findFailedString = null; + this._findResetTimeout = -1; + }, 1000); + } + + _flash() { + if (this._flashFindBarCount === undefined) { + this._flashFindBarCount = this._initialFlashFindBarCount; + } + + if (this._flashFindBarCount-- == 0) { + clearInterval(this._flashFindBarTimeout); + this._findField.removeAttribute("flash"); + this._flashFindBarCount = 6; + return; + } + + this._findField.setAttribute( + "flash", + this._flashFindBarCount % 2 == 0 ? "false" : "true" + ); + } + + _findAgain(findPrevious) { + this.browser.finder.findAgain( + this._findField.value, + findPrevious, + this.findMode == this.FIND_LINKS, + this.findMode != this.FIND_NORMAL + ); + } + + _updateStatusUI(res, findPrevious) { + switch (res) { + case Ci.nsITypeAheadFind.FIND_WRAPPED: + this._findStatusIcon.setAttribute("status", "wrapped"); + this._findStatusDesc.textContent = findPrevious + ? this._wrappedToBottomStr + : this._wrappedToTopStr; + this._findField.removeAttribute("status"); + break; + case Ci.nsITypeAheadFind.FIND_NOTFOUND: + this._findStatusIcon.setAttribute("status", "notfound"); + this._findStatusDesc.textContent = this._notFoundStr; + this._findField.setAttribute("status", "notfound"); + break; + case Ci.nsITypeAheadFind.FIND_PENDING: + this._findStatusIcon.setAttribute("status", "pending"); + this._findStatusDesc.textContent = ""; + this._findField.removeAttribute("status"); + break; + case Ci.nsITypeAheadFind.FIND_FOUND: + default: + this._findStatusIcon.removeAttribute("status"); + this._findStatusDesc.textContent = ""; + this._findField.removeAttribute("status"); + break; + } + } + + updateControlState(result, findPrevious) { + this._updateStatusUI(result, findPrevious); + this._enableFindButtons( + result !== Ci.nsITypeAheadFind.FIND_NOTFOUND && !!this._findField.value + ); + } + + _dispatchFindEvent(type, findPrevious) { + let event = document.createEvent("CustomEvent"); + event.initCustomEvent("find" + type, true, true, { + query: this._findField.value, + caseSensitive: !!this._typeAheadCaseSensitive, + matchDiacritics: !!this._matchDiacritics, + entireWord: this._entireWord, + highlightAll: this._highlightAll, + findPrevious, + }); + return this.dispatchEvent(event); + } + + /** + * Opens the findbar, focuses the findfield and selects its contents. + * Also flashes the findbar the first time it's used. + * + * @param {Number} mode The find mode to be used, which is either + * FIND_NORMAL, FIND_TYPEAHEAD or FIND_LINKS. If not + * passed, we revert to the last find mode if any or + * FIND_NORMAL. + * @return {Promise} A promise that will be resolved when the findbar is + * fully opened. + */ + startFind(mode) { + let prefsvc = Services.prefs; + let userWantsPrefill = true; + this.open(mode); + + if (this._flashFindBar) { + this._flashFindBarTimeout = setInterval(() => this._flash(), 500); + prefsvc.setIntPref( + "accessibility.typeaheadfind.flashBar", + --this._flashFindBar + ); + } + + let { PromiseUtils } = ChromeUtils.import( + "resource://gre/modules/PromiseUtils.jsm" + ); + this._startFindDeferred = PromiseUtils.defer(); + let startFindPromise = this._startFindDeferred.promise; + + if (this.prefillWithSelection) { + userWantsPrefill = prefsvc.getBoolPref( + "accessibility.typeaheadfind.prefillwithselection" + ); + } + + if (this.prefillWithSelection && userWantsPrefill) { + this.browser.finder.getInitialSelection(); + + // NB: We have to focus this._findField here so tests that send + // key events can open and close the find bar synchronously. + this._findField.focus(); + + // (e10s) since we focus lets also select it, otherwise that would + // only happen in this.onCurrentSelection and, because it is async, + // there's a chance keypresses could come inbetween, leading to + // jumbled up queries. + this._findField.select(); + + return startFindPromise; + } + + // If userWantsPrefill is false but prefillWithSelection is true, + // then we might need to check the selection clipboard. Call + // onCurrentSelection to do so. + // Note: this.onCurrentSelection clears this._startFindDeferred. + this.onCurrentSelection("", true); + return startFindPromise; + } + + /** + * Convenient alias to startFind(gFindBar.FIND_NORMAL); + * + * You should generally map the window's find command to this method. + * e.g. <command name="cmd_find" oncommand="gFindBar.onFindCommand();"/> + */ + onFindCommand() { + return this.startFind(this.FIND_NORMAL); + } + + /** + * Stub for find-next and find-previous commands. + * + * @param {Boolean} findPrevious `true` for find-previous, `false` + * otherwise. + */ + onFindAgainCommand(findPrevious) { + if (findPrevious) { + Services.telemetry.scalarAdd("findbar.find_prev", 1); + } else { + Services.telemetry.scalarAdd("findbar.find_next", 1); + } + + let findString = + this._browser.finder.searchString || this._findField.value; + if (!findString) { + return this.startFind(); + } + + // We dispatch the findAgain event here instead of in _findAgain since + // if there is a find event handler that prevents the default then + // finder.searchString will never get updated which in turn means + // there would never be findAgain events because of the logic below. + if (!this._dispatchFindEvent("again", findPrevious)) { + return undefined; + } + + // user explicitly requested another search, so do it even if we think it'll fail + this._findFailedString = null; + + // Ensure the stored SearchString is in sync with what we want to find + if (this._findField.value != this._browser.finder.searchString) { + this._find(this._findField.value); + } else { + this._findAgain(findPrevious); + if (this._useModalHighlight) { + this.open(); + this._findField.focus(); + } + } + + return undefined; + } + + /** + * Fetches the currently selected text and sets that as the text to search + * next. This is a MacOS specific feature. + */ + onFindSelectionCommand() { + this.browser.finder.setSearchStringToSelection().then(searchInfo => { + if (searchInfo.selectedText) { + this._findField.value = searchInfo.selectedText; + } + }); + } + + _onAppActivateMac() { + const kPref = "accessibility.typeaheadfind.prefillwithselection"; + if (this.prefillWithSelection && Services.prefs.getBoolPref(kPref)) { + return; + } + + let clipboardSearchString = this._browser.finder.clipboardSearchString; + if ( + clipboardSearchString && + this._findField.value != clipboardSearchString && + !this._findField._willfullyDeleted + ) { + this._findField.value = clipboardSearchString; + this._findField._hadValue = true; + // Changing the search string makes the previous status invalid, so + // we better clear it here. + this._updateStatusUI(); + } + } + + /** + * This handles all the result changes for both type-ahead-find and + * highlighting. + * + * @param {Object} data A dictionary that holds the following properties: + * - {Number} result One of the FIND_* constants + * indicating the result of a search + * operation. + * - {Boolean} findBackwards If the search was done + * from the bottom to the + * top. This is used for + * status messages when + * reaching "the end of the + * page". + * - {String} linkURL When a link matched, then its + * URL. Always null when not in + * FIND_LINKS mode. + */ + onFindResult(data) { + if (data.result == Ci.nsITypeAheadFind.FIND_NOTFOUND) { + // If an explicit Find Again command fails, re-open the toolbar. + if (data.storeResult && this.open()) { + this._findField.select(); + this._findField.focus(); + } + this._findFailedString = data.searchString; + } else { + this._findFailedString = null; + } + + this._updateStatusUI(data.result, data.findBackwards); + this._updateStatusUIBar(data.linkURL); + + if (this.findMode != this.FIND_NORMAL) { + this._setFindCloseTimeout(); + } + } + + /** + * This handles all the result changes for matches counts. + * + * @param {Object} result Result Object, containing the total amount of + * matches and a vector of the current result. + * - {Number} total Total count number of matches found. + * - {Number} limit Current setting of the number of matches + * to hit to hit the limit. + * - {Number} current Vector of the current result. + */ + onMatchesCountResult(result) { + if (result.total !== 0) { + if (result.total == -1) { + this._foundMatches.value = LazyConstants.PluralForm.get( + result.limit, + this.strBundle.GetStringFromName("FoundMatchesCountLimit") + ).replace("#1", result.limit); + } else { + this._foundMatches.value = LazyConstants.PluralForm.get( + result.total, + this.strBundle.GetStringFromName("FoundMatches") + ) + .replace("#1", result.current) + .replace("#2", result.total); + } + this._foundMatches.hidden = false; + } else { + this._foundMatches.hidden = true; + this._foundMatches.value = ""; + } + } + + onHighlightFinished(result) { + // Noop. + } + + onCurrentSelection(selectionString, isInitialSelection) { + // Ignore the prefill if the user has already typed in the findbar, + // it would have been overwritten anyway. See bug 1198465. + if (isInitialSelection && !this._startFindDeferred) { + return; + } + + if ( + AppConstants.platform == "macosx" && + isInitialSelection && + !selectionString + ) { + let clipboardSearchString = this.browser.finder.clipboardSearchString; + if (clipboardSearchString) { + selectionString = clipboardSearchString; + } + } + + if (selectionString) { + this._findField.value = selectionString; + } + + if (isInitialSelection) { + this._enableFindButtons(!!this._findField.value); + this._findField.select(); + this._findField.focus(); + + this._startFindDeferred.resolve(); + this._startFindDeferred = null; + } + } + + /** + * This handler may cancel a request to focus content by returning |false| + * explicitly. + */ + shouldFocusContent() { + const fm = Services.focus; + if (fm.focusedWindow != window) { + return false; + } + + let focusedElement = fm.focusedElement; + if (!focusedElement) { + return false; + } + + let focusedParent = focusedElement.closest("findbar"); + if (focusedParent != this && focusedParent != this._findField) { + return false; + } + + return true; + } + + disconnectedCallback() { + // Empty the DOM. We will rebuild if reconnected. + while (this.lastChild) { + this.removeChild(this.lastChild); + } + this.destroy(); + } + } + + customElements.define("findbar", MozFindbar); +} diff --git a/toolkit/content/widgets/general.js b/toolkit/content/widgets/general.js new file mode 100644 index 0000000000..41ce769f4d --- /dev/null +++ b/toolkit/content/widgets/general.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +{ + class MozDeck extends MozXULElement { + set selectedIndex(val) { + if (this.selectedIndex == val) { + return val; + } + this.setAttribute("selectedIndex", val); + var event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + return val; + } + + get selectedIndex() { + return this.getAttribute("selectedIndex") || "0"; + } + + set selectedPanel(val) { + var selectedIndex = -1; + for ( + var panel = val; + panel != null; + panel = panel.previousElementSibling + ) { + ++selectedIndex; + } + this.selectedIndex = selectedIndex; + return val; + } + + get selectedPanel() { + return this.children[this.selectedIndex]; + } + } + + customElements.define("deck", MozDeck); + + class MozDropmarker extends MozXULElement { + constructor() { + super(); + let shadowRoot = this.attachShadow({ mode: "open" }); + let stylesheet = document.createElement("link"); + stylesheet.rel = "stylesheet"; + stylesheet.href = "chrome://global/skin/dropmarker.css"; + + let image = document.createXULElement("image"); + image.part = "icon"; + shadowRoot.append(stylesheet, image); + } + } + + customElements.define("dropmarker", MozDropmarker); + + class MozCommandSet extends MozXULElement { + connectedCallback() { + if (this.getAttribute("commandupdater") === "true") { + const events = this.getAttribute("events") || "*"; + const targets = this.getAttribute("targets") || "*"; + document.commandDispatcher.addCommandUpdater(this, events, targets); + } + } + + disconnectedCallback() { + if (this.getAttribute("commandupdater") === "true") { + document.commandDispatcher.removeCommandUpdater(this); + } + } + } + + customElements.define("commandset", MozCommandSet); +} diff --git a/toolkit/content/widgets/marquee.css b/toolkit/content/widgets/marquee.css new file mode 100644 index 0000000000..b898cd0dce --- /dev/null +++ b/toolkit/content/widgets/marquee.css @@ -0,0 +1,26 @@ +/* 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/. */ + +.outerDiv { + overflow: hidden; + width: -moz-available; +} + +.horizontal > .innerDiv { + width: max-content; + /* We want to create overflow of twice our available space. */ + padding: 0 100%; +} + +/* disable scrolling in contenteditable */ +:host(:read-write) .innerDiv { + padding: 0 !important; +} + +/* When printing or when the user doesn't want movement, we disable scrolling */ +@media print, (prefers-reduced-motion) { + .innerDiv { + padding: 0 !important; + } +} diff --git a/toolkit/content/widgets/marquee.js b/toolkit/content/widgets/marquee.js new file mode 100644 index 0000000000..34e9323ec7 --- /dev/null +++ b/toolkit/content/widgets/marquee.js @@ -0,0 +1,417 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* + * This is the class of entry. It will construct the actual implementation + * according to the value of the "direction" property. + */ +this.MarqueeWidget = class { + constructor(shadowRoot) { + this.shadowRoot = shadowRoot; + this.element = shadowRoot.host; + } + + /* + * Callback called by UAWidgets right after constructor. + */ + onsetup() { + this.switchImpl(); + } + + /* + * Callback called by UAWidgetsChild wheen the direction property + * changes. + */ + onchange() { + this.switchImpl(); + } + + switchImpl() { + let newImpl; + switch (this.element.direction) { + case "up": + case "down": + newImpl = MarqueeVerticalImplWidget; + break; + case "left": + case "right": + newImpl = MarqueeHorizontalImplWidget; + break; + } + + // Skip if we are asked to load the same implementation. + // This can happen if the property is set again w/o value change. + if (this.impl && this.impl.constructor == newImpl) { + return; + } + this.destructor(); + if (newImpl) { + this.impl = new newImpl(this.shadowRoot); + this.impl.onsetup(); + } + } + + destructor() { + if (!this.impl) { + return; + } + this.impl.destructor(); + this.shadowRoot.firstChild.remove(); + delete this.impl; + } +}; + +this.MarqueeBaseImplWidget = class { + constructor(shadowRoot) { + this.shadowRoot = shadowRoot; + this.element = shadowRoot.host; + this.document = this.element.ownerDocument; + this.window = this.document.defaultView; + } + + onsetup() { + this.generateContent(); + + // Set up state. + this._currentDirection = this.element.direction || "left"; + this._currentLoop = this.element.loop; + this.dirsign = 1; + this.startAt = 0; + this.stopAt = 0; + this.newPosition = 0; + this.runId = 0; + this.originalHeight = 0; + this.invalidateCache = true; + + this._mutationObserver = new this.window.MutationObserver(aMutations => + this._mutationActor(aMutations) + ); + this._mutationObserver.observe(this.element, { + attributes: true, + attributeOldValue: true, + attributeFilter: ["loop", "", "behavior", "direction", "width", "height"], + }); + + // init needs to be run after the page has loaded in order to calculate + // the correct height/width + if (this.document.readyState == "complete") { + this.init(); + } else { + this.window.addEventListener("load", this, { once: true }); + } + + this.shadowRoot.addEventListener("marquee-start", this); + this.shadowRoot.addEventListener("marquee-stop", this); + } + + destructor() { + this._mutationObserver.disconnect(); + this.window.clearTimeout(this.runId); + + this.window.removeEventListener("load", this); + this.shadowRoot.removeEventListener("marquee-start", this); + this.shadowRoot.removeEventListener("marquee-stop", this); + } + + handleEvent(aEvent) { + if (!aEvent.isTrusted) { + return; + } + + switch (aEvent.type) { + case "load": + this.init(); + break; + case "marquee-start": + this.doStart(); + break; + case "marquee-stop": + this.doStop(); + break; + } + } + + get outerDiv() { + return this.shadowRoot.firstChild; + } + + get innerDiv() { + return this.shadowRoot.getElementById("innerDiv"); + } + + get scrollDelayWithTruespeed() { + if (this.element.scrollDelay < 60 && !this.element.trueSpeed) { + return 60; + } + return this.element.scrollDelay; + } + + doStart() { + if (this.runId == 0) { + var lambda = () => this._doMove(false); + this.runId = this.window.setTimeout( + lambda, + this.scrollDelayWithTruespeed - this._deltaStartStop + ); + this._deltaStartStop = 0; + } + } + + doStop() { + if (this.runId != 0) { + this._deltaStartStop = Date.now() - this._lastMoveDate; + this.window.clearTimeout(this.runId); + } + + this.runId = 0; + } + + _fireEvent(aName, aBubbles, aCancelable) { + var e = this.document.createEvent("Events"); + e.initEvent(aName, aBubbles, aCancelable); + this.element.dispatchEvent(e); + } + + _doMove(aResetPosition) { + this._lastMoveDate = Date.now(); + + // invalidateCache is true at first load and whenever an attribute + // is changed + if (this.invalidateCache) { + this.invalidateCache = false; // we only want this to run once every scroll direction change + + var corrvalue = 0; + + switch (this._currentDirection) { + case "up": + case "down": { + let height = this.window.getComputedStyle(this.element).height; + this.outerDiv.style.height = height; + if (this.originalHeight > this.outerDiv.offsetHeight) { + corrvalue = this.originalHeight - this.outerDiv.offsetHeight; + } + this.innerDiv.style.padding = height + " 0"; + let isUp = this._currentDirection == "up"; + if (isUp) { + this.dirsign = 1; + this.startAt = + this.element.behavior == "alternate" + ? this.originalHeight - corrvalue + : 0; + this.stopAt = + this.element.behavior == "alternate" || + this.element.behavior == "slide" + ? parseInt(height) + corrvalue + : this.originalHeight + parseInt(height); + } else { + this.dirsign = -1; + this.startAt = + this.element.behavior == "alternate" + ? parseInt(height) + corrvalue + : this.originalHeight + parseInt(height); + this.stopAt = + this.element.behavior == "alternate" || + this.element.behavior == "slide" + ? this.originalHeight - corrvalue + : 0; + } + break; + } + case "left": + case "right": + default: { + let isRight = this._currentDirection == "right"; + // NOTE: It's important to use getComputedStyle() to not account for the padding. + let innerWidth = parseInt( + this.window.getComputedStyle(this.innerDiv).width + ); + if (innerWidth > this.outerDiv.offsetWidth) { + corrvalue = innerWidth - this.outerDiv.offsetWidth; + } + let rtl = + this.window.getComputedStyle(this.element).direction == "rtl"; + if (isRight != rtl) { + this.dirsign = -1; + this.stopAt = + this.element.behavior == "alternate" || + this.element.behavior == "slide" + ? innerWidth - corrvalue + : 0; + this.startAt = + this.outerDiv.offsetWidth + + (this.element.behavior == "alternate" + ? corrvalue + : innerWidth + this.stopAt); + } else { + this.dirsign = 1; + this.startAt = + this.element.behavior == "alternate" ? innerWidth - corrvalue : 0; + this.stopAt = + this.outerDiv.offsetWidth + + (this.element.behavior == "alternate" || + this.element.behavior == "slide" + ? corrvalue + : innerWidth + this.startAt); + } + if (rtl) { + this.startAt = -this.startAt; + this.stopAt = -this.stopAt; + this.dirsign = -this.dirsign; + } + break; + } + } + + if (aResetPosition) { + this.newPosition = this.startAt; + this._fireEvent("start", false, false); + } + } // end if + + this.newPosition = + this.newPosition + this.dirsign * this.element.scrollAmount; + + if ( + (this.dirsign == 1 && this.newPosition > this.stopAt) || + (this.dirsign == -1 && this.newPosition < this.stopAt) + ) { + switch (this.element.behavior) { + case "alternate": + // lets start afresh + this.invalidateCache = true; + + // swap direction + const swap = { left: "right", down: "up", up: "down", right: "left" }; + this._currentDirection = swap[this._currentDirection] || "left"; + this.newPosition = this.stopAt; + + if ( + this._currentDirection == "up" || + this._currentDirection == "down" + ) { + this.outerDiv.scrollTop = this.newPosition; + } else { + this.outerDiv.scrollLeft = this.newPosition; + } + + if (this._currentLoop != 1) { + this._fireEvent("bounce", false, true); + } + break; + + case "slide": + if (this._currentLoop > 1) { + this.newPosition = this.startAt; + } + break; + + default: + this.newPosition = this.startAt; + + if ( + this._currentDirection == "up" || + this._currentDirection == "down" + ) { + this.outerDiv.scrollTop = this.newPosition; + } else { + this.outerDiv.scrollLeft = this.newPosition; + } + + // dispatch start event, even when this._currentLoop == 1, comp. with IE6 + this._fireEvent("start", false, false); + } + + if (this._currentLoop > 1) { + this._currentLoop--; + } else if (this._currentLoop == 1) { + if ( + this._currentDirection == "up" || + this._currentDirection == "down" + ) { + this.outerDiv.scrollTop = this.stopAt; + } else { + this.outerDiv.scrollLeft = this.stopAt; + } + this.element.stop(); + this._fireEvent("finish", false, true); + return; + } + } else if ( + this._currentDirection == "up" || + this._currentDirection == "down" + ) { + this.outerDiv.scrollTop = this.newPosition; + } else { + this.outerDiv.scrollLeft = this.newPosition; + } + + var myThis = this; + var lambda = function myTimeOutFunction() { + myThis._doMove(false); + }; + this.runId = this.window.setTimeout(lambda, this.scrollDelayWithTruespeed); + } + + init() { + this.element.stop(); + + if (this._currentDirection == "up" || this._currentDirection == "down") { + // store the original height before we add padding + this.innerDiv.style.padding = 0; + this.originalHeight = this.innerDiv.offsetHeight; + } + + this._doMove(true); + } + + _mutationActor(aMutations) { + while (aMutations.length) { + var mutation = aMutations.shift(); + var attrName = mutation.attributeName.toLowerCase(); + var oldValue = mutation.oldValue; + var target = mutation.target; + var newValue = target.getAttribute(attrName); + + if (oldValue != newValue) { + this.invalidateCache = true; + switch (attrName) { + case "loop": + this._currentLoop = target.loop; + break; + case "direction": + this._currentDirection = target.direction; + break; + } + } + } + } +}; + +this.MarqueeHorizontalImplWidget = class extends MarqueeBaseImplWidget { + generateContent() { + // White-space isn't allowed because a marquee could be + // inside 'white-space: pre' + this.shadowRoot.innerHTML = `<div class="outerDiv horizontal" + ><link rel="stylesheet" href="chrome://global/content/elements/marquee.css" + /><div class="innerDiv" id="innerDiv" + ><slot + /></div + ></div>`; + } +}; + +this.MarqueeVerticalImplWidget = class extends MarqueeBaseImplWidget { + generateContent() { + // White-space isn't allowed because a marquee could be + // inside 'white-space: pre' + this.shadowRoot.innerHTML = `<div class="outerDiv vertical" + ><link rel="stylesheet" href="chrome://global/content/elements/marquee.css" + /><div class="innerDiv" id="innerDiv" + ><slot + /></div + ></div>`; + } +}; diff --git a/toolkit/content/widgets/menu.js b/toolkit/content/widgets/menu.js new file mode 100644 index 0000000000..07869317bb --- /dev/null +++ b/toolkit/content/widgets/menu.js @@ -0,0 +1,492 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + let imports = {}; + ChromeUtils.defineModuleGetter( + imports, + "ShortcutUtils", + "resource://gre/modules/ShortcutUtils.jsm" + ); + + const MozMenuItemBaseMixin = Base => { + class MozMenuItemBase extends MozElements.BaseTextMixin(Base) { + // nsIDOMXULSelectControlItemElement + set value(val) { + this.setAttribute("value", val); + } + get value() { + return this.getAttribute("value"); + } + + // nsIDOMXULSelectControlItemElement + get selected() { + return this.getAttribute("selected") == "true"; + } + + // nsIDOMXULSelectControlItemElement + get control() { + var parent = this.parentNode; + // Return the parent if it is a menu or menulist. + if (parent && parent.parentNode instanceof XULMenuElement) { + return parent.parentNode; + } + return null; + } + + // nsIDOMXULContainerItemElement + get parentContainer() { + for (var parent = this.parentNode; parent; parent = parent.parentNode) { + if (parent instanceof XULMenuElement) { + return parent; + } + } + return null; + } + } + MozXULElement.implementCustomInterface(MozMenuItemBase, [ + Ci.nsIDOMXULSelectControlItemElement, + Ci.nsIDOMXULContainerItemElement, + ]); + return MozMenuItemBase; + }; + + const MozMenuBaseMixin = Base => { + class MozMenuBase extends MozMenuItemBaseMixin(Base) { + set open(val) { + this.openMenu(val); + return val; + } + + get open() { + return this.hasAttribute("open"); + } + + get itemCount() { + var menupopup = this.menupopup; + return menupopup ? menupopup.children.length : 0; + } + + get menupopup() { + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + for ( + var child = this.firstElementChild; + child; + child = child.nextElementSibling + ) { + if (child.namespaceURI == XUL_NS && child.localName == "menupopup") { + return child; + } + } + return null; + } + + appendItem(aLabel, aValue) { + var menupopup = this.menupopup; + if (!menupopup) { + menupopup = this.ownerDocument.createXULElement("menupopup"); + this.appendChild(menupopup); + } + + var menuitem = this.ownerDocument.createXULElement("menuitem"); + menuitem.setAttribute("label", aLabel); + menuitem.setAttribute("value", aValue); + + return menupopup.appendChild(menuitem); + } + + getIndexOfItem(aItem) { + var menupopup = this.menupopup; + if (menupopup) { + var items = menupopup.children; + var length = items.length; + for (var index = 0; index < length; ++index) { + if (items[index] == aItem) { + return index; + } + } + } + return -1; + } + + getItemAtIndex(aIndex) { + var menupopup = this.menupopup; + if (!menupopup || aIndex < 0 || aIndex >= menupopup.children.length) { + return null; + } + + return menupopup.children[aIndex]; + } + } + MozXULElement.implementCustomInterface(MozMenuBase, [ + Ci.nsIDOMXULContainerElement, + ]); + return MozMenuBase; + }; + + // The <menucaption> element is used for rendering <html:optgroup> inside of <html:select>, + // See SelectParentHelper.jsm. + class MozMenuCaption extends MozMenuBaseMixin(MozXULElement) { + static get inheritedAttributes() { + return { + ".menu-iconic-left": "selected,disabled,checked", + ".menu-iconic-icon": "src=image,validate,src", + ".menu-iconic-text": "value=label,crop,highlightable", + ".menu-iconic-highlightable-text": "text=label,crop,highlightable", + }; + } + + connectedCallback() { + this.textContent = ""; + this.appendChild( + MozXULElement.parseXULToFragment(` + <hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true"> + <image class="menu-iconic-icon" aria-hidden="true"></image> + </hbox> + <label class="menu-iconic-text" flex="1" crop="right" aria-hidden="true"></label> + <label class="menu-iconic-highlightable-text" crop="right" aria-hidden="true"></label> + `) + ); + this.initializeAttributeInheritance(); + } + } + + customElements.define("menucaption", MozMenuCaption); + + // In general, wait to render menus and menuitems inside menupopups + // until they are going to be visible: + window.addEventListener( + "popupshowing", + e => { + if (e.originalTarget.ownerDocument != document) { + return; + } + e.originalTarget.setAttribute("hasbeenopened", "true"); + for (let el of e.originalTarget.querySelectorAll("menuitem, menu")) { + el.render(); + } + }, + { capture: true } + ); + + class MozMenuItem extends MozMenuItemBaseMixin(MozXULElement) { + static get observedAttributes() { + return super.observedAttributes.concat("acceltext", "key"); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name == "acceltext") { + if (this._ignoreAccelTextChange) { + this._ignoreAccelTextChange = false; + } else { + this._accelTextIsDerived = false; + this._computeAccelTextFromKeyIfNeeded(); + } + } + if (name == "key") { + this._computeAccelTextFromKeyIfNeeded(); + } + super.attributeChangedCallback(name, oldValue, newValue); + } + + static get inheritedAttributes() { + return { + ".menu-iconic-text": "value=label,crop,accesskey,highlightable", + ".menu-text": "value=label,crop,accesskey,highlightable", + ".menu-iconic-highlightable-text": + "text=label,crop,accesskey,highlightable", + ".menu-iconic-left": "selected,_moz-menuactive,disabled,checked", + ".menu-iconic-icon": + "src=image,validate,triggeringprincipal=iconloadingprincipal", + ".menu-iconic-accel": "value=acceltext", + ".menu-accel": "value=acceltext", + }; + } + + static get iconicNoAccelFragment() { + // Add aria-hidden="true" on all DOM, since XULMenuAccessible handles accessibility here. + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true"> + <image class="menu-iconic-icon"/> + </hbox> + <label class="menu-iconic-text" flex="1" crop="right" aria-hidden="true"/> + <label class="menu-iconic-highlightable-text" crop="right" aria-hidden="true"/> + `), + true + ); + Object.defineProperty(this, "iconicNoAccelFragment", { value: frag }); + return frag; + } + + static get iconicFragment() { + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true"> + <image class="menu-iconic-icon"/> + </hbox> + <label class="menu-iconic-text" flex="1" crop="right" aria-hidden="true"/> + <label class="menu-iconic-highlightable-text" crop="right" aria-hidden="true"/> + <hbox class="menu-accel-container" aria-hidden="true"> + <label class="menu-iconic-accel"/> + </hbox> + `), + true + ); + Object.defineProperty(this, "iconicFragment", { value: frag }); + return frag; + } + + static get plainFragment() { + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <label class="menu-text" crop="right" aria-hidden="true"/> + <hbox class="menu-accel-container" aria-hidden="true"> + <label class="menu-accel"/> + </hbox> + `), + true + ); + Object.defineProperty(this, "plainFragment", { value: frag }); + return frag; + } + + get isIconic() { + let type = this.getAttribute("type"); + return ( + type == "checkbox" || + type == "radio" || + this.classList.contains("menuitem-iconic") + ); + } + + get isMenulistChild() { + return this.matches("menulist > menupopup > menuitem"); + } + + get isInHiddenMenupopup() { + return this.matches("menupopup:not([hasbeenopened]) menuitem"); + } + + _computeAccelTextFromKeyIfNeeded() { + if (!this._accelTextIsDerived && this.getAttribute("acceltext")) { + return; + } + let accelText = (() => { + if (!document.contains(this)) { + return null; + } + let keyId = this.getAttribute("key"); + if (!keyId) { + return null; + } + let key = document.getElementById(keyId); + if (!key) { + Cu.reportError( + `Key ${keyId} of menuitem ${this.getAttribute("label")} ` + + `could not be found` + ); + return null; + } + return imports.ShortcutUtils.prettifyShortcut(key); + })(); + + this._accelTextIsDerived = true; + // We need to ignore the next attribute change callback for acceltext, in + // order to not reenter here. + this._ignoreAccelTextChange = true; + if (accelText) { + this.setAttribute("acceltext", accelText); + } else { + this.removeAttribute("acceltext"); + } + } + + render() { + if (this.renderedOnce) { + return; + } + this.renderedOnce = true; + this.textContent = ""; + if (this.isMenulistChild) { + this.append(this.constructor.iconicNoAccelFragment.cloneNode(true)); + } else if (this.isIconic) { + this.append(this.constructor.iconicFragment.cloneNode(true)); + } else { + this.append(this.constructor.plainFragment.cloneNode(true)); + } + + this._computeAccelTextFromKeyIfNeeded(); + this.initializeAttributeInheritance(); + } + + connectedCallback() { + if (this.renderedOnce) { + this._computeAccelTextFromKeyIfNeeded(); + } + // Eagerly render if we are being inserted into a menulist (since we likely need to + // size it), or into an already-opened menupopup (since we are already visible). + // Checking isConnectedAndReady is an optimization that will let us quickly skip + // non-menulists that are being connected during parse. + if ( + this.isMenulistChild || + (this.isConnectedAndReady && !this.isInHiddenMenupopup) + ) { + this.render(); + } + } + } + + customElements.define("menuitem", MozMenuItem); + + const isHiddenWindow = + document.documentURI == "chrome://browser/content/hiddenWindowMac.xhtml"; + + class MozMenu extends MozMenuBaseMixin( + MozElements.MozElementMixin(XULMenuElement) + ) { + static get inheritedAttributes() { + return { + ".menubar-text": "value=label,accesskey,crop", + ".menu-iconic-text": "value=label,accesskey,crop,highlightable", + ".menu-text": "value=label,accesskey,crop", + ".menu-iconic-highlightable-text": + "text=label,crop,accesskey,highlightable", + ".menubar-left": "src=image", + ".menu-iconic-icon": + "src=image,triggeringprincipal=iconloadingprincipal,validate", + ".menu-iconic-accel": "value=acceltext", + ".menu-right": "_moz-menuactive,disabled", + ".menu-accel": "value=acceltext", + }; + } + + get needsEagerRender() { + return ( + this.isMenubarChild || this.isMenulistChild || !this.isInHiddenMenupopup + ); + } + + get isMenubarChild() { + return this.matches("menubar > menu"); + } + + get isMenulistChild() { + return this.matches("menulist > menupopup > menu"); + } + + get isInHiddenMenupopup() { + return this.matches("menupopup:not([hasbeenopened]) menu"); + } + + get isIconic() { + return this.classList.contains("menu-iconic"); + } + + get fragment() { + let { isMenubarChild, isIconic } = this; + let fragment = null; + // Add aria-hidden="true" on all DOM, since XULMenuAccessible handles accessibility here. + if (isMenubarChild && isIconic) { + if (!MozMenu.menubarIconicFrag) { + MozMenu.menubarIconicFrag = MozXULElement.parseXULToFragment(` + <image class="menubar-left" aria-hidden="true"/> + <label class="menubar-text" crop="right" aria-hidden="true"/> + `); + } + fragment = document.importNode(MozMenu.menubarIconicFrag, true); + } + if (isMenubarChild && !isIconic) { + if (!MozMenu.menubarFrag) { + MozMenu.menubarFrag = MozXULElement.parseXULToFragment(` + <label class="menubar-text" crop="right" aria-hidden="true"/> + `); + } + fragment = document.importNode(MozMenu.menubarFrag, true); + } + if (!isMenubarChild && isIconic) { + if (!MozMenu.normalIconicFrag) { + MozMenu.normalIconicFrag = MozXULElement.parseXULToFragment(` + <hbox class="menu-iconic-left" align="center" pack="center" aria-hidden="true"> + <image class="menu-iconic-icon"/> + </hbox> + <label class="menu-iconic-text" flex="1" crop="right" aria-hidden="true"/> + <label class="menu-iconic-highlightable-text" crop="right" aria-hidden="true"/> + <hbox class="menu-accel-container" anonid="accel" aria-hidden="true"> + <label class="menu-iconic-accel"/> + </hbox> + <hbox align="center" class="menu-right" aria-hidden="true"> + <image/> + </hbox> + `); + } + + fragment = document.importNode(MozMenu.normalIconicFrag, true); + } + if (!isMenubarChild && !isIconic) { + if (!MozMenu.normalFrag) { + MozMenu.normalFrag = MozXULElement.parseXULToFragment(` + <label class="menu-text" crop="right" aria-hidden="true"/> + <hbox class="menu-accel-container" anonid="accel" aria-hidden="true"> + <label class="menu-accel"/> + </hbox> + <hbox align="center" class="menu-right" aria-hidden="true"> + <image/> + </hbox> + `); + } + + fragment = document.importNode(MozMenu.normalFrag, true); + } + return fragment; + } + + render() { + // There are 2 main types of menus: + // (1) direct descendant of a menubar + // (2) all other menus + // There is also an "iconic" variation of (1) and (2) based on the class. + // To make this as simple as possible, we don't support menus being changed from one + // of these types to another after the initial DOM connection. It'd be possible to make + // this work by keeping track of the markup we prepend and then removing / re-prepending + // during a change, but it's not a feature we use anywhere currently. + if (this.renderedOnce) { + return; + } + this.renderedOnce = true; + + // There will be a <menupopup /> already. Don't clear it out, just put our markup before it. + this.prepend(this.fragment); + this.initializeAttributeInheritance(); + } + + connectedCallback() { + // On OSX we will have a bunch of menus in the hidden window. They get converted + // into native menus based on the host attributes, so the inner DOM doesn't need + // to be created. + if (isHiddenWindow) { + return; + } + + if (this.delayConnectedCallback()) { + return; + } + + // Wait until we are going to be visible or required for sizing a popup. + if (!this.needsEagerRender) { + return; + } + + this.render(); + } + } + + customElements.define("menu", MozMenu); +} diff --git a/toolkit/content/widgets/menulist.js b/toolkit/content/widgets/menulist.js new file mode 100644 index 0000000000..e4565824c2 --- /dev/null +++ b/toolkit/content/widgets/menulist.js @@ -0,0 +1,416 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const MozXULMenuElement = MozElements.MozElementMixin(XULMenuElement); + const MenuBaseControl = MozElements.BaseControlMixin(MozXULMenuElement); + + class MozMenuList extends MenuBaseControl { + constructor() { + super(); + + this.addEventListener( + "command", + event => { + if (event.target.parentNode.parentNode == this) { + this.selectedItem = event.target; + } + }, + true + ); + + this.addEventListener("popupshowing", event => { + if (event.target.parentNode == this) { + this.activeChild = null; + if (this.selectedItem) { + // Not ready for auto-setting the active child in hierarchies yet. + // For now, only do this when the outermost menupopup opens. + this.activeChild = this.mSelectedInternal; + } + } + }); + + this.addEventListener( + "keypress", + event => { + if (event.altKey || event.ctrlKey || event.metaKey) { + return; + } + + if ( + !event.defaultPrevented && + (event.keyCode == KeyEvent.DOM_VK_UP || + event.keyCode == KeyEvent.DOM_VK_DOWN || + event.keyCode == KeyEvent.DOM_VK_PAGE_UP || + event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN || + event.keyCode == KeyEvent.DOM_VK_HOME || + event.keyCode == KeyEvent.DOM_VK_END || + event.keyCode == KeyEvent.DOM_VK_BACK_SPACE || + event.charCode > 0) + ) { + // Moving relative to an item: start from the currently selected item + this.activeChild = this.mSelectedInternal; + if (this.handleKeyPress(event)) { + this.activeChild.doCommand(); + event.preventDefault(); + } + } + }, + { mozSystemGroup: true } + ); + + this.attachShadow({ mode: "open" }); + } + + static get inheritedAttributes() { + return { + image: "src=image", + "#label": "value=label,crop,accesskey,highlightable", + "#highlightable-label": "text=label,crop,accesskey,highlightable", + dropmarker: "disabled,open", + }; + } + + static get markup() { + // Accessibility information of these nodes will be presented + // on XULComboboxAccessible generated from <menulist>; + // hide these nodes from the accessibility tree. + return ` + <html:link href="chrome://global/skin/menulist.css" rel="stylesheet"/> + <hbox id="label-box" part="label-box" flex="1" role="none"> + <image part="icon" role="none"/> + <label id="label" part="label" crop="right" flex="1" role="none"/> + <label id="highlightable-label" part="label" crop="right" flex="1" role="none"/> + </hbox> + <dropmarker part="dropmarker" exportparts="icon: dropmarker-icon" type="menu" role="none"/> + <html:slot/> + `; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + if (!this.hasAttribute("popuponly")) { + this.shadowRoot.appendChild(this.constructor.fragment); + this._labelBox = this.shadowRoot.getElementById("label-box"); + this._dropmarker = this.shadowRoot.querySelector("dropmarker"); + this.initializeAttributeInheritance(); + } else { + this.shadowRoot.appendChild(document.createElement("slot")); + } + + this.mSelectedInternal = null; + this.mAttributeObserver = null; + this.setInitialSelection(); + } + + // nsIDOMXULSelectControlElement + set value(val) { + // if the new value is null, we still need to remove the old value + if (val == null) { + return (this.selectedItem = val); + } + + var arr = null; + var popup = this.menupopup; + if (popup) { + arr = popup.getElementsByAttribute("value", val); + } + + if (arr && arr.item(0)) { + this.selectedItem = arr[0]; + } else { + this.selectedItem = null; + this.setAttribute("value", val); + } + + return val; + } + + // nsIDOMXULSelectControlElement + get value() { + return this.getAttribute("value"); + } + + // nsIDOMXULMenuListElement + set crop(val) { + this.setAttribute("crop", val); + } + + // nsIDOMXULMenuListElement + get crop() { + return this.getAttribute("crop"); + } + + // nsIDOMXULMenuListElement + set image(val) { + this.setAttribute("image", val); + return val; + } + + // nsIDOMXULMenuListElement + get image() { + return this.getAttribute("image"); + } + + // nsIDOMXULMenuListElement + get label() { + return this.getAttribute("label"); + } + + set description(val) { + this.setAttribute("description", val); + return val; + } + + get description() { + return this.getAttribute("description"); + } + + // nsIDOMXULMenuListElement + set open(val) { + this.openMenu(val); + return val; + } + + // nsIDOMXULMenuListElement + get open() { + return this.hasAttribute("open"); + } + + // nsIDOMXULSelectControlElement + get itemCount() { + return this.menupopup ? this.menupopup.children.length : 0; + } + + get menupopup() { + var popup = this.firstElementChild; + while (popup && popup.localName != "menupopup") { + popup = popup.nextElementSibling; + } + return popup; + } + + // nsIDOMXULSelectControlElement + set selectedIndex(val) { + var popup = this.menupopup; + if (popup && 0 <= val) { + if (val < popup.children.length) { + this.selectedItem = popup.children[val]; + } + } else { + this.selectedItem = null; + } + return val; + } + + // nsIDOMXULSelectControlElement + get selectedIndex() { + // Quick and dirty. We won't deal with hierarchical menulists yet. + if ( + !this.selectedItem || + !this.mSelectedInternal.parentNode || + this.mSelectedInternal.parentNode.parentNode != this + ) { + return -1; + } + + var children = this.mSelectedInternal.parentNode.children; + var i = children.length; + while (i--) { + if (children[i] == this.mSelectedInternal) { + break; + } + } + + return i; + } + + // nsIDOMXULSelectControlElement + set selectedItem(val) { + var oldval = this.mSelectedInternal; + if (oldval == val) { + return val; + } + + if (val && !this.contains(val)) { + return val; + } + + if (oldval) { + oldval.removeAttribute("selected"); + this.mAttributeObserver.disconnect(); + } + + this.mSelectedInternal = val; + let attributeFilter = ["value", "label", "image", "description"]; + if (val) { + val.setAttribute("selected", "true"); + for (let attr of attributeFilter) { + if (val.hasAttribute(attr)) { + this.setAttribute(attr, val.getAttribute(attr)); + } else { + this.removeAttribute(attr); + } + } + + this.mAttributeObserver = new MutationObserver( + this.handleMutation.bind(this) + ); + this.mAttributeObserver.observe(val, { attributeFilter }); + } else { + for (let attr of attributeFilter) { + this.removeAttribute(attr); + } + } + + var event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + + event = document.createEvent("Events"); + event.initEvent("ValueChange", true, true); + this.dispatchEvent(event); + + return val; + } + + // nsIDOMXULSelectControlElement + get selectedItem() { + return this.mSelectedInternal; + } + + setInitialSelection() { + var popup = this.menupopup; + if (popup) { + var arr = popup.getElementsByAttribute("selected", "true"); + + var editable = this.editable; + var value = this.value; + if (!arr.item(0) && value) { + arr = popup.getElementsByAttribute( + editable ? "label" : "value", + value + ); + } + + if (arr.item(0)) { + this.selectedItem = arr[0]; + } else if (!editable) { + this.selectedIndex = 0; + } + } + } + + contains(item) { + if (!item) { + return false; + } + + var parent = item.parentNode; + return parent && parent.parentNode == this; + } + + handleMutation(aRecords) { + for (let record of aRecords) { + let t = record.target; + if (t == this.mSelectedInternal) { + let attrName = record.attributeName; + switch (attrName) { + case "value": + case "label": + case "image": + case "description": + if (t.hasAttribute(attrName)) { + this.setAttribute(attrName, t.getAttribute(attrName)); + } else { + this.removeAttribute(attrName); + } + } + } + } + } + + // nsIDOMXULSelectControlElement + getIndexOfItem(item) { + var popup = this.menupopup; + if (popup) { + var children = popup.children; + var i = children.length; + while (i--) { + if (children[i] == item) { + return i; + } + } + } + return -1; + } + + // nsIDOMXULSelectControlElement + getItemAtIndex(index) { + var popup = this.menupopup; + if (popup) { + var children = popup.children; + if (index >= 0 && index < children.length) { + return children[index]; + } + } + return null; + } + + appendItem(label, value, description) { + if (!this.menupopup) { + this.appendChild(MozXULElement.parseXULToFragment(`<menupopup />`)); + } + + var popup = this.menupopup; + popup.appendChild(MozXULElement.parseXULToFragment(`<menuitem />`)); + + var item = popup.lastElementChild; + if (label !== undefined) { + item.setAttribute("label", label); + } + item.setAttribute("value", value); + if (description) { + item.setAttribute("description", description); + } + + return item; + } + + removeAllItems() { + this.selectedItem = null; + var popup = this.menupopup; + if (popup) { + this.removeChild(popup); + } + } + + disconnectedCallback() { + if (this.mAttributeObserver) { + this.mAttributeObserver.disconnect(); + } + + if (this._labelBox) { + this._labelBox.remove(); + this._dropmarker.remove(); + this._labelBox = null; + this._dropmarker = null; + } + } + } + + MenuBaseControl.implementCustomInterface(MozMenuList, [ + Ci.nsIDOMXULMenuListElement, + Ci.nsIDOMXULSelectControlElement, + ]); + + customElements.define("menulist", MozMenuList); +} diff --git a/toolkit/content/widgets/menupopup.js b/toolkit/content/widgets/menupopup.js new file mode 100644 index 0000000000..a8513050bd --- /dev/null +++ b/toolkit/content/widgets/menupopup.js @@ -0,0 +1,255 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + class MozMenuPopup extends MozElements.MozElementMixin(XULPopupElement) { + constructor() { + super(); + + this.AUTOSCROLL_INTERVAL = 25; + this.NOT_DRAGGING = 0; + this.DRAG_OVER_BUTTON = -1; + this.DRAG_OVER_POPUP = 1; + this._draggingState = this.NOT_DRAGGING; + this._scrollTimer = 0; + + this.addEventListener("popupshowing", event => { + if (event.target != this) { + return; + } + + // Make sure we generated shadow DOM to place menuitems into. + this.shadowRoot; + }); + + this.attachShadow({ mode: "open" }); + } + + connectedCallback() { + if (this.delayConnectedCallback() || this.hasConnected) { + return; + } + + this.hasConnected = true; + if (this.parentNode && this.parentNode.localName == "menulist") { + this._setUpMenulistPopup(); + } + } + + initShadowDOM() { + // Retarget events from shadow DOM arrowscrollbox to the host. + this.scrollBox.addEventListener("scroll", ev => + this.dispatchEvent(new Event("scroll")) + ); + this.scrollBox.addEventListener("overflow", ev => + this.dispatchEvent(new Event("overflow")) + ); + this.scrollBox.addEventListener("underflow", ev => + this.dispatchEvent(new Event("underflow")) + ); + } + + get shadowRoot() { + // We generate shadow DOM lazily on popupshowing event to avoid extra load + // on the system during browser startup. + if (!super.shadowRoot.firstElementChild) { + super.shadowRoot.appendChild(this.fragment); + this.initShadowDOM(); + } + return super.shadowRoot; + } + + get fragment() { + if (!this.constructor.hasOwnProperty("_fragment")) { + this.constructor._fragment = MozXULElement.parseXULToFragment( + this.markup + ); + } + return document.importNode(this.constructor._fragment, true); + } + + get markup() { + return ` + <html:link rel="stylesheet" href="chrome://global/skin/global.css"/> + <html:style>${this.styles}</html:style> + <arrowscrollbox class="menupopup-arrowscrollbox" + exportparts="scrollbox: arrowscrollbox-scrollbox" + flex="1" + orient="vertical" + smoothscroll="false"> + <html:slot></html:slot> + </arrowscrollbox> + `; + } + + get styles() { + let s = ` + :host(.in-menulist) arrowscrollbox::part(scrollbutton-up), + :host(.in-menulist) arrowscrollbox::part(scrollbutton-down) { + display: none; + } + :host(.in-menulist) arrowscrollbox::part(scrollbox) { + overflow: auto; + margin: 0; + } + :host(.in-menulist) arrowscrollbox::part(scrollbox-clip) { + overflow: visible; + } + `; + + switch (AppConstants.platform) { + case "macosx": + s += ` + :host(.in-menulist) arrowscrollbox { + padding: 0; + } + `; + break; + + default: + break; + } + + return s; + } + + get scrollBox() { + if (!this._scrollBox) { + this._scrollBox = this.shadowRoot.querySelector("arrowscrollbox"); + } + return this._scrollBox; + } + + /** + * Adds event listeners for a MozMenuPopup inside a menulist element. + */ + _setUpMenulistPopup() { + // Access shadow root to generate menupoup shadow DOMs. We do generate + // shadow DOM on popupshowing, but it doesn't work for HTML:selects, + // which are implemented via menulist elements living in the main process. + // So make them a special case then. + this.shadowRoot; + this.classList.add("in-menulist"); + + this.addEventListener("popupshown", () => { + // Enable drag scrolling even when the mouse wasn't used. The + // mousemove handler will remove it if the mouse isn't down. + this._enableDragScrolling(false); + }); + + this.addEventListener("popuphidden", () => { + this._draggingState = this.NOT_DRAGGING; + this._clearScrollTimer(); + this.releaseCapture(); + this.scrollBox.scrollbox.scrollTop = 0; + }); + + this.addEventListener("mousedown", event => { + if (event.button != 0) { + return; + } + + if ( + this.state == "open" && + (event.target.localName == "menuitem" || + event.target.localName == "menu" || + event.target.localName == "menucaption") + ) { + this._enableDragScrolling(true); + } + }); + + this.addEventListener("mouseup", event => { + if (event.button != 0) { + return; + } + + this._draggingState = this.NOT_DRAGGING; + this._clearScrollTimer(); + }); + + this.addEventListener("mousemove", event => { + if (!this._draggingState) { + return; + } + + this._clearScrollTimer(); + + // If the user released the mouse before the menupopup opens, we will + // still be capturing, so check that the button is still pressed. If + // not, release the capture and do nothing else. This also handles if + // the dropdown was opened via the keyboard. + if (!(event.buttons & 1)) { + this._draggingState = this.NOT_DRAGGING; + this.releaseCapture(); + return; + } + + // If dragging outside the top or bottom edge of the menupopup, but + // within the menupopup area horizontally, scroll the list in that + // direction. The _draggingState flag is used to ensure that scrolling + // does not start until the mouse has moved over the menupopup first, + // preventing scrolling while over the dropdown button. + let popupRect = this.getOuterScreenRect(); + if ( + event.screenX >= popupRect.left && + event.screenX <= popupRect.right + ) { + if (this._draggingState == this.DRAG_OVER_BUTTON) { + if ( + event.screenY > popupRect.top && + event.screenY < popupRect.bottom + ) { + this._draggingState = this.DRAG_OVER_POPUP; + } + } + + if ( + this._draggingState == this.DRAG_OVER_POPUP && + (event.screenY <= popupRect.top || + event.screenY >= popupRect.bottom) + ) { + let scrollAmount = event.screenY <= popupRect.top ? -1 : 1; + this.scrollBox.scrollByIndex(scrollAmount, true); + + let win = this.ownerGlobal; + this._scrollTimer = win.setInterval(() => { + this.scrollBox.scrollByIndex(scrollAmount, true); + }, this.AUTOSCROLL_INTERVAL); + } + } + }); + + this._menulistPopupIsSetUp = true; + } + + _enableDragScrolling(overItem) { + if (!this._draggingState) { + this.setCaptureAlways(); + this._draggingState = overItem + ? this.DRAG_OVER_POPUP + : this.DRAG_OVER_BUTTON; + } + } + + _clearScrollTimer() { + if (this._scrollTimer) { + this.ownerGlobal.clearInterval(this._scrollTimer); + this._scrollTimer = 0; + } + } + } + + customElements.define("menupopup", MozMenuPopup); + + MozElements.MozMenuPopup = MozMenuPopup; +} diff --git a/toolkit/content/widgets/moz-input-box.js b/toolkit/content/widgets/moz-input-box.js new file mode 100644 index 0000000000..be1bc4d560 --- /dev/null +++ b/toolkit/content/widgets/moz-input-box.js @@ -0,0 +1,233 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +{ + const cachedFragments = { + get entities() { + return ["chrome://global/locale/textcontext.dtd"]; + }, + get editMenuItems() { + return ` + <menuitem data-l10n-id="text-action-undo" cmd="cmd_undo"></menuitem> + <menuseparator></menuseparator> + <menuitem data-l10n-id="text-action-cut" cmd="cmd_cut"></menuitem> + <menuitem data-l10n-id="text-action-copy" cmd="cmd_copy"></menuitem> + <menuitem data-l10n-id="text-action-paste" cmd="cmd_paste"></menuitem> + <menuitem data-l10n-id="text-action-delete" cmd="cmd_delete"></menuitem> + <menuseparator></menuseparator> + <menuitem data-l10n-id="text-action-select-all" cmd="cmd_selectAll"></menuitem> + `; + }, + get normal() { + delete this.normal; + this.normal = MozXULElement.parseXULToFragment( + ` + <menupopup class="textbox-contextmenu"> + ${this.editMenuItems} + </menupopup> + ` + ); + MozXULElement.insertFTLIfNeeded("toolkit/global/textActions.ftl"); + return this.normal; + }, + get spellcheck() { + delete this.spellcheck; + this.spellcheck = MozXULElement.parseXULToFragment( + ` + <menupopup class="textbox-contextmenu"> + <menuitem label="&spellNoSuggestions.label;" anonid="spell-no-suggestions" disabled="true"></menuitem> + <menuitem label="&spellAddToDictionary.label;" accesskey="&spellAddToDictionary.accesskey;" anonid="spell-add-to-dictionary" oncommand="this.parentNode.parentNode.spellCheckerUI.addToDictionary();"></menuitem> + <menuitem label="&spellUndoAddToDictionary.label;" accesskey="&spellUndoAddToDictionary.accesskey;" anonid="spell-undo-add-to-dictionary" oncommand="this.parentNode.parentNode.spellCheckerUI.undoAddToDictionary();"></menuitem> + <menuseparator anonid="spell-suggestions-separator"></menuseparator> + ${this.editMenuItems} + <menuseparator anonid="spell-check-separator"></menuseparator> + <menuitem label="&spellCheckToggle.label;" type="checkbox" accesskey="&spellCheckToggle.accesskey;" anonid="spell-check-enabled" oncommand="this.parentNode.parentNode.spellCheckerUI.toggleEnabled();"></menuitem> + <menu label="&spellDictionaries.label;" accesskey="&spellDictionaries.accesskey;" anonid="spell-dictionaries"> + <menupopup anonid="spell-dictionaries-menu" onpopupshowing="event.stopPropagation();" onpopuphiding="event.stopPropagation();"></menupopup> + </menu> + </menupopup> + `, + this.entities + ); + return this.spellcheck; + }, + }; + + class MozInputBox extends MozXULElement { + static get observedAttributes() { + return ["spellcheck"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === "spellcheck" && oldValue != newValue) { + this._initUI(); + } + } + + connectedCallback() { + this._initUI(); + } + + _initUI() { + this.spellcheck = this.hasAttribute("spellcheck"); + if (this.menupopup) { + this.menupopup.remove(); + } + + this.setAttribute("context", "_child"); + this.appendChild( + this.spellcheck + ? cachedFragments.spellcheck.cloneNode(true) + : cachedFragments.normal.cloneNode(true) + ); + this.menupopup = this.querySelector(".textbox-contextmenu"); + + this.menupopup.addEventListener("popupshowing", event => { + let input = this._input; + if (document.commandDispatcher.focusedElement != input) { + input.focus(); + } + this._doPopupItemEnabling(event.target); + }); + + if (this.spellcheck) { + this.menupopup.addEventListener("popuphiding", event => { + if (this.spellCheckerUI) { + this.spellCheckerUI.clearSuggestionsFromMenu(); + this.spellCheckerUI.clearDictionaryListFromMenu(); + } + }); + } + + this.menupopup.addEventListener("command", event => { + var cmd = event.originalTarget.getAttribute("cmd"); + if (cmd) { + this.doCommand(cmd); + event.stopPropagation(); + } + }); + } + + _doPopupItemEnablingSpell(popupNode) { + var spellui = this.spellCheckerUI; + if (!spellui || !spellui.canSpellCheck) { + this._setMenuItemVisibility("spell-no-suggestions", false); + this._setMenuItemVisibility("spell-check-enabled", false); + this._setMenuItemVisibility("spell-check-separator", false); + this._setMenuItemVisibility("spell-add-to-dictionary", false); + this._setMenuItemVisibility("spell-undo-add-to-dictionary", false); + this._setMenuItemVisibility("spell-suggestions-separator", false); + this._setMenuItemVisibility("spell-dictionaries", false); + return; + } + + spellui.initFromEvent( + document.popupRangeParent, + document.popupRangeOffset + ); + + var enabled = spellui.enabled; + var showUndo = spellui.canSpellCheck && spellui.canUndo(); + + var enabledCheckbox = this.getMenuItem("spell-check-enabled"); + enabledCheckbox.setAttribute("checked", enabled); + + var overMisspelling = spellui.overMisspelling; + this._setMenuItemVisibility("spell-add-to-dictionary", overMisspelling); + this._setMenuItemVisibility("spell-undo-add-to-dictionary", showUndo); + this._setMenuItemVisibility( + "spell-suggestions-separator", + overMisspelling || showUndo + ); + + // suggestion list + var suggestionsSeparator = this.getMenuItem("spell-no-suggestions"); + var numsug = spellui.addSuggestionsToMenu( + popupNode, + suggestionsSeparator, + 5 + ); + this._setMenuItemVisibility( + "spell-no-suggestions", + overMisspelling && numsug == 0 + ); + + // dictionary list + var dictionariesMenu = this.getMenuItem("spell-dictionaries-menu"); + var numdicts = spellui.addDictionaryListToMenu(dictionariesMenu, null); + this._setMenuItemVisibility( + "spell-dictionaries", + enabled && numdicts > 1 + ); + } + + _doPopupItemEnabling(popupNode) { + if (this.spellcheck) { + this._doPopupItemEnablingSpell(popupNode); + } + + var children = popupNode.childNodes; + for (var i = 0; i < children.length; i++) { + var command = children[i].getAttribute("cmd"); + if (command) { + var controller = document.commandDispatcher.getControllerForCommand( + command + ); + var enabled = controller.isCommandEnabled(command); + if (enabled) { + children[i].removeAttribute("disabled"); + } else { + children[i].setAttribute("disabled", "true"); + } + } + } + } + + get spellCheckerUI() { + if (!this._spellCheckInitialized) { + this._spellCheckInitialized = true; + + try { + ChromeUtils.import( + "resource://gre/modules/InlineSpellChecker.jsm", + this + ); + this.InlineSpellCheckerUI = new this.InlineSpellChecker( + this._input.editor + ); + } catch (ex) {} + } + + return this.InlineSpellCheckerUI; + } + + getMenuItem(anonid) { + return this.querySelector(`[anonid="${anonid}"]`); + } + + _setMenuItemVisibility(anonid, visible) { + this.getMenuItem(anonid).hidden = !visible; + } + + doCommand(command) { + var controller = document.commandDispatcher.getControllerForCommand( + command + ); + controller.doCommand(command); + } + + get _input() { + return ( + this.getElementsByAttribute("anonid", "input")[0] || + this.querySelector(".textbox-input") + ); + } + } + + customElements.define("moz-input-box", MozInputBox); +} diff --git a/toolkit/content/widgets/notificationbox.js b/toolkit/content/widgets/notificationbox.js new file mode 100644 index 0000000000..ca1d9ba5ca --- /dev/null +++ b/toolkit/content/widgets/notificationbox.js @@ -0,0 +1,474 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. If you need to +// define globals, wrap in a block to prevent leaking onto `window`. +{ + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + MozElements.NotificationBox = class NotificationBox { + /** + * Creates a new class to handle a notification box, but does not add any + * elements to the DOM until a notification has to be displayed. + * + * @param insertElementFn + * Called with the "notification-stack" element as an argument when the + * first notification has to be displayed. + */ + constructor(insertElementFn) { + this._insertElementFn = insertElementFn; + this._animating = false; + this.currentNotification = null; + } + + get stack() { + if (!this._stack) { + let stack = document.createXULElement("legacy-stack"); + stack._notificationBox = this; + stack.className = "notificationbox-stack"; + stack.appendChild(document.createXULElement("spacer")); + stack.addEventListener("transitionend", event => { + if ( + event.target.localName == "notification" && + event.propertyName == "margin-top" + ) { + this._finishAnimation(); + } + }); + this._insertElementFn(stack); + this._stack = stack; + } + return this._stack; + } + + get _allowAnimation() { + return window.matchMedia("(prefers-reduced-motion: no-preference)") + .matches; + } + + get allNotifications() { + // Don't create any DOM if no new notification has been added yet. + if (!this._stack) { + return []; + } + + var closedNotification = this._closedNotification; + var notifications = this.stack.getElementsByTagName("notification"); + return Array.prototype.filter.call( + notifications, + n => n != closedNotification + ); + } + + getNotificationWithValue(aValue) { + var notifications = this.allNotifications; + for (var n = notifications.length - 1; n >= 0; n--) { + if (aValue == notifications[n].getAttribute("value")) { + return notifications[n]; + } + } + return null; + } + + /** + * Creates a <notification> element and shows it. The calling code can modify + * the element synchronously to add features to the notification. + * + * @param aLabel + * The main message text, or a DocumentFragment containing elements to + * add as children of the notification's main <description> element. + * @param aValue + * String identifier of the notification. + * @param aImage + * URL of the icon image to display. If not specified, a default icon + * based on the priority will be shown. + * @param aPriority + * One of the PRIORITY_ constants. These determine the appearance of + * the notification based on severity (using the "type" attribute), and + * only the notification with the highest priority is displayed. + * @param aButtons + * Array of objects defining action buttons: + * { + * label: + * Label of the <button> element. + * accessKey: + * Access key character for the <button> element. + * "l10n-id" + * Localization id for the <button>, to be used instead of + * specifying a separate label and access key. + * callback: + * When the button is used, this is called with the arguments: + * 1. The <notification> element. + * 2. This button object definition. + * 3. The <button> element. + * 4. The "command" event. + * popup: + * If specified, the button will open the popup element with this + * ID, anchored to the button. This is alternative to "callback". + * is: + * Defines a Custom Element name to use as the "is" value on + * button creation. + * } + * @param aEventCallback + * This may be called with the "removed", "dismissed" or "disconnected" + * parameter: + * removed - notification has been removed + * dismissed - user dismissed notification + * disconnected - notification removed in any way + * @param aNotificationIs + * Defines a Custom Element name to use as the "is" value on creation. + * This allows subclassing the created element. + * + * @return The <notification> element that is shown. + */ + appendNotification( + aLabel, + aValue, + aImage, + aPriority, + aButtons, + aEventCallback, + aNotificationIs + ) { + if ( + aPriority < this.PRIORITY_INFO_LOW || + aPriority > this.PRIORITY_CRITICAL_HIGH + ) { + throw new Error("Invalid notification priority " + aPriority); + } + + // check for where the notification should be inserted according to + // priority. If two are equal, the existing one appears on top. + var notifications = this.allNotifications; + var insertPos = null; + for (var n = notifications.length - 1; n >= 0; n--) { + if (notifications[n].priority < aPriority) { + break; + } + insertPos = notifications[n]; + } + + // Create the Custom Element and connect it to the document immediately. + var newitem = document.createXULElement( + "notification", + aNotificationIs ? { is: aNotificationIs } : {} + ); + this.stack.insertBefore(newitem, insertPos); + + // Custom notification classes may not have the messageText property. + if (newitem.messageText) { + // Can't use instanceof in case this was created from a different document: + if ( + aLabel && + typeof aLabel == "object" && + aLabel.nodeType && + aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE + ) { + newitem.messageText.appendChild(aLabel); + } else { + newitem.messageText.textContent = aLabel; + } + } + newitem.setAttribute("value", aValue); + if (aImage) { + newitem.messageImage.setAttribute("src", aImage); + } + newitem.eventCallback = aEventCallback; + + if (aButtons) { + for (var b = 0; b < aButtons.length; b++) { + var button = aButtons[b]; + var buttonElem = document.createXULElement( + "button", + button.is ? { is: button.is } : {} + ); + + if (button["l10n-id"]) { + buttonElem.setAttribute("data-l10n-id", button["l10n-id"]); + } else { + buttonElem.setAttribute("label", button.label); + if (typeof button.accessKey == "string") { + buttonElem.setAttribute("accesskey", button.accessKey); + } + } + + buttonElem.classList.add("notification-button"); + if (button.primary) { + buttonElem.classList.add("primary"); + } + + newitem.messageDetails.appendChild(buttonElem); + buttonElem.buttonInfo = button; + } + } + + newitem.priority = aPriority; + if (aPriority >= this.PRIORITY_CRITICAL_LOW) { + newitem.setAttribute("type", "critical"); + } else if (aPriority <= this.PRIORITY_INFO_HIGH) { + newitem.setAttribute("type", "info"); + } else { + newitem.setAttribute("type", "warning"); + } + + if (!insertPos) { + newitem.style.display = "block"; + newitem.style.position = "fixed"; + newitem.style.top = "100%"; + newitem.style.marginTop = "-15px"; + newitem.style.opacity = "0"; + this._showNotification(newitem, true); + } + + // Fire event for accessibility APIs + var event = document.createEvent("Events"); + event.initEvent("AlertActive", true, true); + newitem.dispatchEvent(event); + + return newitem; + } + + removeNotification(aItem, aSkipAnimation) { + if (!aItem.parentNode) { + return; + } + if (aItem == this.currentNotification) { + this.removeCurrentNotification(aSkipAnimation); + } else if (aItem != this._closedNotification) { + this._removeNotificationElement(aItem); + } + } + + _removeNotificationElement(aChild) { + if (aChild.eventCallback) { + aChild.eventCallback("removed"); + } + this.stack.removeChild(aChild); + + // make sure focus doesn't get lost (workaround for bug 570835) + if (!Services.focus.getFocusedElementForWindow(window, false, {})) { + Services.focus.moveFocus( + window, + this.stack, + Services.focus.MOVEFOCUS_FORWARD, + 0 + ); + } + } + + removeCurrentNotification(aSkipAnimation) { + this._showNotification(this.currentNotification, false, aSkipAnimation); + } + + removeAllNotifications(aImmediate) { + var notifications = this.allNotifications; + for (var n = notifications.length - 1; n >= 0; n--) { + if (aImmediate) { + this._removeNotificationElement(notifications[n]); + } else { + this.removeNotification(notifications[n]); + } + } + this.currentNotification = null; + + // Clean up any currently-animating notification; this is necessary + // if a notification was just opened and is still animating, but we + // want to close it *without* animating. This can even happen if + // animations get disabled (via prefers-reduced-motion) and this method + // is called immediately after an animated notification was displayed + // (although this case isn't very likely). + if (aImmediate || !this._allowAnimation) { + this._finishAnimation(); + } + } + + removeTransientNotifications() { + var notifications = this.allNotifications; + for (var n = notifications.length - 1; n >= 0; n--) { + var notification = notifications[n]; + if (notification.persistence) { + notification.persistence--; + } else if (Date.now() > notification.timeout) { + this.removeNotification(notification, true); + } + } + } + + _showNotification(aNotification, aSlideIn, aSkipAnimation) { + this._finishAnimation(); + + var height = aNotification.getBoundingClientRect().height; + var skipAnimation = + aSkipAnimation || height == 0 || !this._allowAnimation; + aNotification.classList.toggle("animated", !skipAnimation); + + if (aSlideIn) { + this.currentNotification = aNotification; + aNotification.style.removeProperty("display"); + aNotification.style.removeProperty("position"); + aNotification.style.removeProperty("top"); + aNotification.style.removeProperty("margin-top"); + aNotification.style.removeProperty("opacity"); + + if (skipAnimation) { + return; + } + } else { + this._closedNotification = aNotification; + var notifications = this.allNotifications; + var idx = notifications.length - 1; + this.currentNotification = idx >= 0 ? notifications[idx] : null; + + if (skipAnimation) { + this._removeNotificationElement(this._closedNotification); + delete this._closedNotification; + return; + } + + aNotification.style.marginTop = -height + "px"; + aNotification.style.opacity = 0; + } + + this._animating = true; + } + + _finishAnimation() { + if (this._animating) { + this._animating = false; + if (this._closedNotification) { + this._removeNotificationElement(this._closedNotification); + delete this._closedNotification; + } + } + } + }; + + // These are defined on the instance prototype for backwards compatibility. + Object.assign(MozElements.NotificationBox.prototype, { + PRIORITY_INFO_LOW: 1, + PRIORITY_INFO_MEDIUM: 2, + PRIORITY_INFO_HIGH: 3, + PRIORITY_WARNING_LOW: 4, + PRIORITY_WARNING_MEDIUM: 5, + PRIORITY_WARNING_HIGH: 6, + PRIORITY_CRITICAL_LOW: 7, + PRIORITY_CRITICAL_MEDIUM: 8, + PRIORITY_CRITICAL_HIGH: 9, + }); + + MozElements.Notification = class Notification extends MozXULElement { + static get markup() { + return ` + <hbox class="messageDetails" align="center" flex="1" + oncommand="this.parentNode._doButtonCommand(event);"> + <image class="messageImage"/> + <description class="messageText" flex="1"/> + <spacer flex="1"/> + </hbox> + <toolbarbutton ondblclick="event.stopPropagation();" + class="messageCloseButton close-icon tabbable" + tooltiptext="&closeNotification.tooltip;" + oncommand="this.parentNode.dismiss();"/> + `; + } + + static get entities() { + return ["chrome://global/locale/notification.dtd"]; + } + + constructor() { + super(); + this.persistence = 0; + this.priority = 0; + this.timeout = 0; + } + + connectedCallback() { + this.appendChild(this.constructor.fragment); + + for (let [propertyName, selector] of [ + ["messageDetails", ".messageDetails"], + ["messageImage", ".messageImage"], + ["messageText", ".messageText"], + ["spacer", "spacer"], + ]) { + this[propertyName] = this.querySelector(selector); + } + } + + disconnectedCallback() { + if (this.eventCallback) { + this.eventCallback("disconnected"); + } + } + + get control() { + return this.closest(".notificationbox-stack")._notificationBox; + } + + /** + * Changes the text of an existing notification. If the notification was + * created with a custom fragment, it will be overwritten with plain text. + */ + set label(value) { + this.messageText.textContent = value; + } + + /** + * This method should only be called when the user has manually closed the + * notification. If you want to programmatically close the notification, you + * should call close() instead. + */ + dismiss() { + if (this.eventCallback) { + this.eventCallback("dismissed"); + } + this.close(); + } + + close() { + if (!this.parentNode) { + return; + } + this.control.removeNotification(this); + } + + _doButtonCommand(event) { + if (!("buttonInfo" in event.target)) { + return; + } + + var button = event.target.buttonInfo; + if (button.popup) { + document + .getElementById(button.popup) + .openPopup( + event.originalTarget, + "after_start", + 0, + 0, + false, + false, + event + ); + event.stopPropagation(); + } else { + var callback = button.callback; + if (callback) { + var result = callback(this, button, event.target, event); + if (!result) { + this.close(); + } + event.stopPropagation(); + } + } + } + }; + + customElements.define("notification", MozElements.Notification); +} diff --git a/toolkit/content/widgets/panel.js b/toolkit/content/widgets/panel.js new file mode 100644 index 0000000000..19c516e46d --- /dev/null +++ b/toolkit/content/widgets/panel.js @@ -0,0 +1,305 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + class MozPanel extends MozElements.MozElementMixin(XULPopupElement) { + static get markup() { + return ` + <html:link rel="stylesheet" href="chrome://global/skin/global.css"/> + <vbox class="panel-arrowcontainer" flex="1"> + <box class="panel-arrowbox" part="arrowbox"> + <image class="panel-arrow" part="arrow"/> + </box> + <box class="panel-arrowcontent" flex="1" part="arrowcontent"><html:slot/></box> + </vbox> + `; + } + constructor() { + super(); + + this.attachShadow({ mode: "open" }); + + this._prevFocus = 0; + this._fadeTimer = null; + + this.addEventListener("popupshowing", this); + this.addEventListener("popupshown", this); + this.addEventListener("popuphiding", this); + this.addEventListener("popuphidden", this); + this.addEventListener("popuppositioned", this); + } + + connectedCallback() { + // Create shadow DOM lazily if a panel is hidden. It helps to reduce + // cycles on startup. + if (!this.hidden) { + this.initialize(); + } + + if (this.isArrowPanel) { + if (!this.hasAttribute("flip")) { + this.setAttribute("flip", "both"); + } + if (!this.hasAttribute("side")) { + this.setAttribute("side", "top"); + } + if (!this.hasAttribute("position")) { + this.setAttribute("position", "bottomcenter topleft"); + } + if (!this.hasAttribute("consumeoutsideclicks")) { + this.setAttribute("consumeoutsideclicks", "false"); + } + } + } + + initialize() { + // As an optimization, we don't slot contents if the panel is [hidden] in + // connecetedCallack this means we can avoid running this code at startup + // and only need to do it when a panel is about to be shown. + // We then override the `hidden` setter and `removeAttribute` and call this + // function if the node is about to be shown. + if (this.shadowRoot.firstChild) { + return; + } + + if (!this.isArrowPanel) { + this.shadowRoot.appendChild(document.createElement("slot")); + } else { + this.shadowRoot.appendChild(this.constructor.fragment); + } + } + + get hidden() { + return super.hidden; + } + set hidden(v) { + if (!v) { + this.initialize(); + } + return (super.hidden = v); + } + + removeAttribute(name) { + if (name == "hidden") { + this.initialize(); + } + super.removeAttribute(name); + } + + get isArrowPanel() { + return this.getAttribute("type") == "arrow"; + } + + adjustArrowPosition(event) { + if (!this.isArrowPanel || !this.isAnchored) { + return; + } + + var container = this.shadowRoot.querySelector(".panel-arrowcontainer"); + var arrowbox = this.shadowRoot.querySelector(".panel-arrowbox"); + + var position = event.alignmentPosition; + var offset = event.alignmentOffset; + + this.setAttribute("arrowposition", position); + + if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) { + container.setAttribute("orient", "horizontal"); + arrowbox.setAttribute("orient", "vertical"); + if (position.indexOf("_after") > 0) { + arrowbox.setAttribute("pack", "end"); + } else { + arrowbox.setAttribute("pack", "start"); + } + arrowbox.style.transform = "translate(0, " + -offset + "px)"; + + // The assigned side stays the same regardless of direction. + var isRTL = window.getComputedStyle(this).direction == "rtl"; + + if (position.indexOf("start_") == 0) { + container.style.MozBoxDirection = "reverse"; + this.setAttribute("side", isRTL ? "left" : "right"); + } else { + container.style.removeProperty("-moz-box-direction"); + this.setAttribute("side", isRTL ? "right" : "left"); + } + } else if ( + position.indexOf("before_") == 0 || + position.indexOf("after_") == 0 + ) { + container.removeAttribute("orient"); + arrowbox.removeAttribute("orient"); + if (position.indexOf("_end") > 0) { + arrowbox.setAttribute("pack", "end"); + } else { + arrowbox.setAttribute("pack", "start"); + } + arrowbox.style.transform = "translate(" + -offset + "px, 0)"; + + if (position.indexOf("before_") == 0) { + container.style.MozBoxDirection = "reverse"; + this.setAttribute("side", "bottom"); + } else { + container.style.removeProperty("-moz-box-direction"); + this.setAttribute("side", "top"); + } + } + } + + on_popupshowing(event) { + if (this.isArrowPanel && event.target == this) { + var arrow = this.shadowRoot.querySelector(".panel-arrow"); + arrow.hidden = !this.isAnchored; + this.shadowRoot + .querySelector(".panel-arrowbox") + .style.removeProperty("transform"); + + if (this.getAttribute("animate") != "false") { + this.setAttribute("animate", "open"); + // the animating attribute prevents user interaction during transition + // it is removed when popupshown fires + this.setAttribute("animating", "true"); + } + + // set fading + var fade = this.getAttribute("fade"); + var fadeDelay = 0; + if (fade == "fast") { + fadeDelay = 1; + } else if (fade == "slow") { + fadeDelay = 4000; + } + + if (fadeDelay != 0) { + this._fadeTimer = setTimeout( + () => this.hidePopup(true), + fadeDelay, + this + ); + } + } + + // Capture the previous focus before has a chance to get set inside the panel + try { + this._prevFocus = Cu.getWeakReference( + document.commandDispatcher.focusedElement + ); + if (!this._prevFocus.get()) { + this._prevFocus = Cu.getWeakReference(document.activeElement); + return; + } + } catch (ex) { + this._prevFocus = Cu.getWeakReference(document.activeElement); + } + } + + on_popupshown(event) { + if (this.isArrowPanel && event.target == this) { + this.removeAttribute("animating"); + this.setAttribute("panelopen", "true"); + } + + // Fire event for accessibility APIs + let alertEvent = document.createEvent("Events"); + alertEvent.initEvent("AlertActive", true, true); + this.dispatchEvent(alertEvent); + } + + on_popuphiding(event) { + if (this.isArrowPanel && event.target == this) { + let animate = this.getAttribute("animate") != "false"; + + if (this._fadeTimer) { + clearTimeout(this._fadeTimer); + if (animate) { + this.setAttribute("animate", "fade"); + } + } else if (animate) { + this.setAttribute("animate", "cancel"); + } + } + + try { + this._currentFocus = document.commandDispatcher.focusedElement; + } catch (e) { + this._currentFocus = document.activeElement; + } + } + + on_popuphidden(event) { + if (this.isArrowPanel && event.target == this) { + this.removeAttribute("panelopen"); + if (this.getAttribute("animate") != "false") { + this.removeAttribute("animate"); + } + } + + function doFocus() { + // Focus was set on an element inside this panel, + // so we need to move it back to where it was previously. + // Elements can use refocused-by-panel to change their focus behaviour + // when re-focused by a panel hiding. + prevFocus.setAttribute("refocused-by-panel", true); + try { + let fm = Services.focus; + fm.setFocus(prevFocus, fm.FLAG_NOSCROLL); + } catch (e) { + prevFocus.focus(); + } + prevFocus.removeAttribute("refocused-by-panel"); + } + var currentFocus = this._currentFocus; + var prevFocus = this._prevFocus ? this._prevFocus.get() : null; + this._currentFocus = null; + this._prevFocus = null; + + // Avoid changing focus if focus changed while we hide the popup + // (This can happen e.g. if the popup is hiding as a result of a + // click/keypress that focused something) + let nowFocus; + try { + nowFocus = document.commandDispatcher.focusedElement; + } catch (e) { + nowFocus = document.activeElement; + } + if (nowFocus && nowFocus != currentFocus) { + return; + } + + if (prevFocus && this.getAttribute("norestorefocus") != "true") { + // Try to restore focus + try { + if (document.commandDispatcher.focusedWindow != window) { + // Focus has already been set to a window outside of this panel + return; + } + } catch (ex) {} + + if (!currentFocus) { + doFocus(); + return; + } + while (currentFocus) { + if (currentFocus == this) { + doFocus(); + return; + } + currentFocus = currentFocus.parentNode; + } + } + } + + on_popuppositioned(event) { + if (event.target == this) { + this.adjustArrowPosition(event); + } + } + } + + customElements.define("panel", MozPanel); +} diff --git a/toolkit/content/widgets/pluginProblem.js b/toolkit/content/widgets/pluginProblem.js new file mode 100644 index 0000000000..d918acd25a --- /dev/null +++ b/toolkit/content/widgets/pluginProblem.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is a UA Widget. It runs in per-origin UA widget scope, +// to be loaded by UAWidgetsChild.jsm. +// This widget results in a hidden element that occupies room where the plugin +// would be if we still supported plugins. +this.PluginProblemWidget = class { + constructor(shadowRoot) { + this.shadowRoot = shadowRoot; + this.element = shadowRoot.host; + // ownerGlobal is chrome-only, not accessible to UA Widget script here. + this.window = this.element.ownerDocument.defaultView; // eslint-disable-line mozilla/use-ownerGlobal + } + + onsetup() { + const parser = new this.window.DOMParser(); + parser.forceEnableDTD(); + let parserDoc = parser.parseFromString( + ` + <!DOCTYPE bindings [ + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > + %globalDTD; + %brandDTD; + ]> + <div xmlns="http://www.w3.org/1999/xhtml" class="mainBox" id="main" chromedir="&locale.dir;"> + <link rel="stylesheet" type="text/css" href="chrome://pluginproblem/content/pluginProblemContent.css" /> + </div> + `, + "application/xml" + ); + this.shadowRoot.importNodeAndAppendChildAt( + this.shadowRoot, + parserDoc.documentElement, + true + ); + + // Notify browser-plugins.js that we were attached, on a delay because + // this binding doesn't complete layout until the constructor + // completes. + this.element.dispatchEvent( + new this.window.CustomEvent("PluginBindingAttached") + ); + } +}; diff --git a/toolkit/content/widgets/popupnotification.js b/toolkit/content/widgets/popupnotification.js new file mode 100644 index 0000000000..1ec4c6f5a5 --- /dev/null +++ b/toolkit/content/widgets/popupnotification.js @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + class MozPopupNotification extends MozXULElement { + static get inheritedAttributes() { + return { + ".popup-notification-icon": "popupid,src=icon,class=iconclass", + ".popup-notification-origin": "value=origin,tooltiptext=origin", + ".popup-notification-description": "popupid", + ".popup-notification-description > span:first-of-type": + "text=label,popupid", + ".popup-notification-description > b:first-of-type": + "text=name,popupid", + ".popup-notification-description > span:nth-of-type(2)": + "text=endlabel,popupid", + ".popup-notification-description > b:last-of-type": + "text=secondname,popupid", + ".popup-notification-description > span:last-of-type": + "text=secondendlabel,popupid", + ".popup-notification-hint-text": "text=hinttext", + ".popup-notification-closebutton": + "oncommand=closebuttoncommand,hidden=closebuttonhidden", + ".popup-notification-learnmore-link": + "onclick=learnmoreclick,href=learnmoreurl", + ".popup-notification-warning": "hidden=warninghidden,text=warninglabel", + ".popup-notification-button-container > .popup-notification-secondary-button": + "oncommand=secondarybuttoncommand,label=secondarybuttonlabel,accesskey=secondarybuttonaccesskey,hidden=secondarybuttonhidden", + ".popup-notification-button-container > toolbarseparator": + "hidden=dropmarkerhidden", + ".popup-notification-dropmarker": + "onpopupshown=dropmarkerpopupshown,hidden=dropmarkerhidden", + ".popup-notification-dropmarker > menupopup": "oncommand=menucommand", + ".popup-notification-primary-button": + "oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey,default=buttonhighlight,disabled=mainactiondisabled", + }; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (!this._hasSlotted) { + return; + } + + super.attributeChangedCallback(name, oldValue, newValue); + } + + show() { + this.slotContents(); + + if (this.checkboxState) { + this.checkbox.checked = this.checkboxState.checked; + this.checkbox.setAttribute("label", this.checkboxState.label); + this.checkbox.hidden = false; + } else { + this.checkbox.hidden = true; + // Reset checked state to avoid wrong using of previous value. + this.checkbox.checked = false; + } + + this.hidden = false; + } + + static get markup() { + return ` + <hbox class="popup-notification-header-container"></hbox> + <hbox align="start" class="popup-notification-body-container"> + <image class="popup-notification-icon"/> + <vbox flex="1" pack="start" class="popup-notification-body"> + <hbox align="start"> + <vbox flex="1"> + <label class="popup-notification-origin header" crop="center"></label> + <!-- These need to be on the same line to avoid creating + whitespace between them (whitespace is added in the + localization file, if necessary). --> + <description class="popup-notification-description"><html:span></html:span><html:b></html:b><html:span></html:span><html:b></html:b><html:span></html:span></description> + <description class="popup-notification-hint-text"></description> + </vbox> + <toolbarbutton class="messageCloseButton close-icon popup-notification-closebutton tabbable" tooltiptext="&closeNotification.tooltip;"></toolbarbutton> + </hbox> + <label class="popup-notification-learnmore-link" is="text-link">&learnMoreNoEllipsis;</label> + <checkbox class="popup-notification-checkbox" oncommand="PopupNotifications._onCheckboxCommand(event)"></checkbox> + <description class="popup-notification-warning"></description> + </vbox> + </hbox> + <hbox class="popup-notification-footer-container"></hbox> + <hbox class="popup-notification-button-container panel-footer"> + <button class="popup-notification-button popup-notification-secondary-button"></button> + <toolbarseparator></toolbarseparator> + <button type="menu" class="popup-notification-button popup-notification-dropmarker" aria-label="&moreActionsButton.accessibleLabel;"> + <menupopup position="after_end" aria-label="&moreActionsButton.accessibleLabel;"> + </menupopup> + </button> + <button class="popup-notification-button popup-notification-primary-button" label="&defaultButton.label;" accesskey="&defaultButton.accesskey;"></button> + </hbox> + `; + } + + static get entities() { + return ["chrome://global/locale/notification.dtd"]; + } + + slotContents() { + if (this._hasSlotted) { + return; + } + this._hasSlotted = true; + this.appendChild(this.constructor.fragment); + + this.button = this.querySelector(".popup-notification-primary-button"); + this.secondaryButton = this.querySelector( + ".popup-notification-secondary-button" + ); + this.checkbox = this.querySelector(".popup-notification-checkbox"); + this.closebutton = this.querySelector(".popup-notification-closebutton"); + this.menubutton = this.querySelector(".popup-notification-dropmarker"); + this.menupopup = this.menubutton.querySelector("menupopup"); + + let popupnotificationfooter = this.querySelector( + "popupnotificationfooter" + ); + if (popupnotificationfooter) { + this.querySelector(".popup-notification-footer-container").append( + popupnotificationfooter + ); + } + + let popupnotificationheader = this.querySelector( + "popupnotificationheader" + ); + if (popupnotificationheader) { + this.querySelector(".popup-notification-header-container").append( + popupnotificationheader + ); + } + + for (let popupnotificationcontent of this.querySelectorAll( + "popupnotificationcontent" + )) { + this.appendNotificationContent(popupnotificationcontent); + } + + this.initializeAttributeInheritance(); + } + + appendNotificationContent(el) { + let nextSibling = this.querySelector( + ".popup-notification-body > .popup-notification-learnmore-link" + ); + nextSibling.before(el); + } + } + + customElements.define("popupnotification", MozPopupNotification); +} diff --git a/toolkit/content/widgets/radio.js b/toolkit/content/widgets/radio.js new file mode 100644 index 0000000000..6066241ed7 --- /dev/null +++ b/toolkit/content/widgets/radio.js @@ -0,0 +1,573 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +(() => { + class MozRadiogroup extends MozElements.BaseControl { + constructor() { + super(); + + this.addEventListener("mousedown", event => { + if (this.disabled) { + event.preventDefault(); + } + }); + + /** + * keyboard navigation Here's how keyboard navigation works in radio groups on Windows: + * The group takes 'focus' + * The user is then free to navigate around inside the group + * using the arrow keys. Accessing previous or following radio buttons + * is done solely through the arrow keys and not the tab button. Tab + * takes you to the next widget in the tab order + */ + this.addEventListener("keypress", event => { + if (event.key != " " || event.originalTarget != this) { + return; + } + this.selectedItem = this.focusedItem; + this.selectedItem.doCommand(); + // Prevent page from scrolling on the space key. + event.preventDefault(); + }); + + this.addEventListener("keypress", event => { + if ( + event.keyCode != KeyEvent.DOM_VK_UP || + event.originalTarget != this + ) { + return; + } + this.checkAdjacentElement(false); + event.stopPropagation(); + event.preventDefault(); + }); + + this.addEventListener("keypress", event => { + if ( + event.keyCode != KeyEvent.DOM_VK_LEFT || + event.originalTarget != this + ) { + return; + } + // left arrow goes back when we are ltr, forward when we are rtl + this.checkAdjacentElement( + document.defaultView.getComputedStyle(this).direction == "rtl" + ); + event.stopPropagation(); + event.preventDefault(); + }); + + this.addEventListener("keypress", event => { + if ( + event.keyCode != KeyEvent.DOM_VK_DOWN || + event.originalTarget != this + ) { + return; + } + this.checkAdjacentElement(true); + event.stopPropagation(); + event.preventDefault(); + }); + + this.addEventListener("keypress", event => { + if ( + event.keyCode != KeyEvent.DOM_VK_RIGHT || + event.originalTarget != this + ) { + return; + } + // right arrow goes forward when we are ltr, back when we are rtl + this.checkAdjacentElement( + document.defaultView.getComputedStyle(this).direction == "ltr" + ); + event.stopPropagation(); + event.preventDefault(); + }); + + /** + * set a focused attribute on the selected item when the group + * receives focus so that we can style it as if it were focused even though + * it is not (Windows platform behaviour is for the group to receive focus, + * not the item + */ + this.addEventListener("focus", event => { + if (event.originalTarget != this) { + return; + } + this.setAttribute("focused", "true"); + if (this.focusedItem) { + return; + } + + var val = this.selectedItem; + if (!val || val.disabled || val.hidden || val.collapsed) { + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if ( + !children[i].hidden && + !children[i].collapsed && + !children[i].disabled + ) { + val = children[i]; + break; + } + } + } + this.focusedItem = val; + }); + + this.addEventListener("blur", event => { + if (event.originalTarget != this) { + return; + } + this.removeAttribute("focused"); + this.focusedItem = null; + }); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + // When this is called via `connectedCallback` there are two main variations: + // 1) The radiogroup and radio children are defined in markup. + // 2) We are appending a DocumentFragment + // In both cases, the <radiogroup> connectedCallback fires first. But in (2), + // the children <radio>s won't be upgraded yet, so r.control will be undefined. + // To avoid churn in this case where we would have to reinitialize the list as each + // child radio gets upgraded as a result of init(), ignore the resulting calls + // to radioAttached. + this.ignoreRadioChildConstruction = true; + this.init(); + this.ignoreRadioChildConstruction = false; + if (!this.value) { + this.selectedIndex = 0; + } + } + + init() { + this._radioChildren = null; + + if (this.getAttribute("disabled") == "true") { + this.disabled = true; + } + + var children = this._getRadioChildren(); + var length = children.length; + for (var i = 0; i < length; i++) { + if (children[i].getAttribute("selected") == "true") { + this.selectedIndex = i; + return; + } + } + + var value = this.value; + if (value) { + this.value = value; + } + } + + /** + * Called when a new <radio> gets added to an already connected radiogroup. + * This can happen due to DOM getting appended after the <radiogroup> is created. + * When this happens, reinitialize the UI if necessary to make sure the state is + * consistent. + * + * @param {DOMNode} child + * The <radio> element that got added + */ + radioAttached(child) { + if (this.ignoreRadioChildConstruction) { + return; + } + if (!this._radioChildren || !this._radioChildren.includes(child)) { + this.init(); + } + } + + /** + * Called when a new <radio> gets removed from a radio group. + * + * @param {DOMNode} child + * The <radio> element that got removed + */ + radioUnattached(child) { + // Just invalidate the cache, next time it's fetched it'll get rebuilt. + this._radioChildren = null; + } + + set value(val) { + this.setAttribute("value", val); + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; i++) { + if (String(children[i].value) == String(val)) { + this.selectedItem = children[i]; + break; + } + } + return val; + } + + get value() { + return this.getAttribute("value"); + } + + set disabled(val) { + if (val) { + this.setAttribute("disabled", "true"); + } else { + this.removeAttribute("disabled"); + } + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + children[i].disabled = val; + } + return val; + } + + get disabled() { + if (this.getAttribute("disabled") == "true") { + return true; + } + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if ( + !children[i].hidden && + !children[i].collapsed && + !children[i].disabled + ) { + return false; + } + } + return true; + } + + get itemCount() { + return this._getRadioChildren().length; + } + + set selectedIndex(val) { + this.selectedItem = this._getRadioChildren()[val]; + return val; + } + + get selectedIndex() { + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (children[i].selected) { + return i; + } + } + return -1; + } + + set selectedItem(val) { + var focused = this.getAttribute("focused") == "true"; + var alreadySelected = false; + + if (val) { + alreadySelected = val.getAttribute("selected") == "true"; + val.setAttribute("focused", focused); + val.setAttribute("selected", "true"); + this.setAttribute("value", val.value); + } else { + this.removeAttribute("value"); + } + + // uncheck all other group nodes + var children = this._getRadioChildren(); + var previousItem = null; + for (var i = 0; i < children.length; ++i) { + if (children[i] != val) { + if (children[i].getAttribute("selected") == "true") { + previousItem = children[i]; + } + + children[i].removeAttribute("selected"); + children[i].removeAttribute("focused"); + } + } + + var event = document.createEvent("Events"); + event.initEvent("select", false, true); + this.dispatchEvent(event); + + if (focused) { + if (alreadySelected) { + // Notify accessibility that this item got focus. + event = document.createEvent("Events"); + event.initEvent("DOMMenuItemActive", true, true); + val.dispatchEvent(event); + } else { + // Only report if actual change + if (val) { + // Accessibility will fire focus for this. + event = document.createEvent("Events"); + event.initEvent("RadioStateChange", true, true); + val.dispatchEvent(event); + } + + if (previousItem) { + event = document.createEvent("Events"); + event.initEvent("RadioStateChange", true, true); + previousItem.dispatchEvent(event); + } + } + } + + return val; + } + + get selectedItem() { + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (children[i].selected) { + return children[i]; + } + } + return null; + } + + set focusedItem(val) { + if (val) { + val.setAttribute("focused", "true"); + // Notify accessibility that this item got focus. + let event = document.createEvent("Events"); + event.initEvent("DOMMenuItemActive", true, true); + val.dispatchEvent(event); + } + + // unfocus all other group nodes + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (children[i] != val) { + children[i].removeAttribute("focused"); + } + } + return val; + } + + get focusedItem() { + var children = this._getRadioChildren(); + for (var i = 0; i < children.length; ++i) { + if (children[i].getAttribute("focused") == "true") { + return children[i]; + } + } + return null; + } + + checkAdjacentElement(aNextFlag) { + var currentElement = this.focusedItem || this.selectedItem; + var i; + var children = this._getRadioChildren(); + for (i = 0; i < children.length; ++i) { + if (children[i] == currentElement) { + break; + } + } + var index = i; + + if (aNextFlag) { + do { + if (++i == children.length) { + i = 0; + } + if (i == index) { + break; + } + } while ( + children[i].hidden || + children[i].collapsed || + children[i].disabled + ); + // XXX check for display/visibility props too + + this.selectedItem = children[i]; + children[i].doCommand(); + } else { + do { + if (i == 0) { + i = children.length; + } + if (--i == index) { + break; + } + } while ( + children[i].hidden || + children[i].collapsed || + children[i].disabled + ); + // XXX check for display/visibility props too + + this.selectedItem = children[i]; + children[i].doCommand(); + } + } + + _getRadioChildren() { + if (this._radioChildren) { + return this._radioChildren; + } + + let radioChildren = []; + if (this.hasChildNodes()) { + for (let radio of this.querySelectorAll("radio")) { + customElements.upgrade(radio); + if (radio.control == this) { + radioChildren.push(radio); + } + } + } else { + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + for (let radio of this.ownerDocument.getElementsByAttribute( + "group", + this.id + )) { + if (radio.namespaceURI == XUL_NS && radio.localName == "radio") { + customElements.upgrade(radio); + radioChildren.push(radio); + } + } + } + + return (this._radioChildren = radioChildren); + } + + getIndexOfItem(item) { + return this._getRadioChildren().indexOf(item); + } + + getItemAtIndex(index) { + var children = this._getRadioChildren(); + return index >= 0 && index < children.length ? children[index] : null; + } + + appendItem(label, value) { + var radio = document.createXULElement("radio"); + radio.setAttribute("label", label); + radio.setAttribute("value", value); + this.appendChild(radio); + return radio; + } + } + + MozXULElement.implementCustomInterface(MozRadiogroup, [ + Ci.nsIDOMXULSelectControlElement, + Ci.nsIDOMXULRadioGroupElement, + ]); + + customElements.define("radiogroup", MozRadiogroup); + + class MozRadio extends MozElements.BaseText { + static get markup() { + return ` + <image class="radio-check"></image> + <hbox class="radio-label-box" align="center" flex="1"> + <image class="radio-icon"></image> + <label class="radio-label" flex="1"></label> + </hbox> + `; + } + + static get inheritedAttributes() { + return { + ".radio-check": "disabled,selected", + ".radio-label": "text=label,accesskey,crop", + ".radio-icon": "src", + }; + } + + constructor() { + super(); + this.addEventListener("click", event => { + if (!this.disabled) { + this.control.selectedItem = this; + } + }); + + this.addEventListener("mousedown", event => { + if (!this.disabled) { + this.control.focusedItem = this; + } + }); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + if (!this.connectedOnce) { + this.connectedOnce = true; + // If the caller didn't provide custom content then append the default: + if (!this.firstElementChild) { + this.appendChild(this.constructor.fragment); + this.initializeAttributeInheritance(); + } + } + + var control = (this._control = this.control); + if (control) { + control.radioAttached(this); + } + } + + disconnectedCallback() { + if (this.control) { + this.control.radioUnattached(this); + } + this._control = null; + } + + set value(val) { + this.setAttribute("value", val); + } + + get value() { + return this.getAttribute("value"); + } + + get selected() { + return this.hasAttribute("selected"); + } + + get radioGroup() { + return this.control; + } + + get control() { + if (this._control) { + return this._control; + } + + var radiogroup = this.closest("radiogroup"); + if (radiogroup) { + return radiogroup; + } + + var group = this.getAttribute("group"); + if (!group) { + return null; + } + + var parent = this.ownerDocument.getElementById(group); + if (!parent || parent.localName != "radiogroup") { + parent = null; + } + return parent; + } + } + + MozXULElement.implementCustomInterface(MozRadio, [ + Ci.nsIDOMXULSelectControlItemElement, + ]); + customElements.define("radio", MozRadio); +})(); diff --git a/toolkit/content/widgets/richlistbox.js b/toolkit/content/widgets/richlistbox.js new file mode 100644 index 0000000000..e52c957a9b --- /dev/null +++ b/toolkit/content/widgets/richlistbox.js @@ -0,0 +1,1023 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + /** + * XUL:richlistbox element. + */ + MozElements.RichListBox = class RichListBox extends MozElements.BaseControl { + constructor() { + super(); + + this.selectedItems = new ChromeNodeList(); + this._currentIndex = null; + this._lastKeyTime = 0; + this._incrementalString = ""; + this._suppressOnSelect = false; + this._userSelecting = false; + this._selectTimeout = null; + this._currentItem = null; + this._selectionStart = null; + + this.addEventListener( + "keypress", + event => { + if (event.altKey || event.metaKey) { + return; + } + + switch (event.keyCode) { + case KeyEvent.DOM_VK_UP: + this._moveByOffsetFromUserEvent(-1, event); + break; + case KeyEvent.DOM_VK_DOWN: + this._moveByOffsetFromUserEvent(1, event); + break; + case KeyEvent.DOM_VK_HOME: + this._moveByOffsetFromUserEvent(-this.currentIndex, event); + break; + case KeyEvent.DOM_VK_END: + this._moveByOffsetFromUserEvent( + this.getRowCount() - this.currentIndex - 1, + event + ); + break; + case KeyEvent.DOM_VK_PAGE_UP: + this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event); + break; + case KeyEvent.DOM_VK_PAGE_DOWN: + this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event); + break; + } + }, + { mozSystemGroup: true } + ); + + this.addEventListener("keypress", event => { + if (event.target != this) { + return; + } + + if ( + event.key == " " && + event.ctrlKey && + !event.shiftKey && + !event.altKey && + !event.metaKey && + this.currentItem && + this.selType == "multiple" + ) { + this.toggleItemSelection(this.currentItem); + } + + if (!event.charCode || event.altKey || event.ctrlKey || event.metaKey) { + return; + } + + if (event.timeStamp - this._lastKeyTime > 1000) { + this._incrementalString = ""; + } + + var key = String.fromCharCode(event.charCode).toLowerCase(); + this._incrementalString += key; + this._lastKeyTime = event.timeStamp; + + // If all letters in the incremental string are the same, just + // try to match the first one + var incrementalString = /^(.)\1+$/.test(this._incrementalString) + ? RegExp.$1 + : this._incrementalString; + var length = incrementalString.length; + + var rowCount = this.getRowCount(); + var l = this.selectedItems.length; + var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1; + // start from the first element if none was selected or from the one + // following the selected one if it's a new or a repeated-letter search + if (start == -1 || length == 1) { + start++; + } + + for (var i = 0; i < rowCount; i++) { + var k = (start + i) % rowCount; + var listitem = this.getItemAtIndex(k); + if (!this._canUserSelect(listitem)) { + continue; + } + // allow richlistitems to specify the string being searched for + var searchText = + "searchLabel" in listitem + ? listitem.searchLabel + : listitem.getAttribute("label"); // (see also bug 250123) + searchText = searchText.substring(0, length).toLowerCase(); + if (searchText == incrementalString) { + this.ensureIndexIsVisible(k); + this.timedSelect(listitem, this._selectDelay); + break; + } + } + }); + + this.addEventListener("focus", event => { + if (this.getRowCount() > 0) { + if (this.currentIndex == -1) { + this.currentIndex = this.getIndexOfFirstVisibleRow(); + let currentItem = this.getItemAtIndex(this.currentIndex); + if (currentItem) { + this.selectItem(currentItem); + } + } else { + this._fireEvent(this.currentItem, "DOMMenuItemActive"); + } + } + this._lastKeyTime = 0; + }); + + this.addEventListener("click", event => { + // clicking into nothing should unselect multiple selections + if (event.originalTarget == this && this.selType == "multiple") { + this.clearSelection(); + this.currentItem = null; + } + }); + + this.addEventListener("MozSwipeGesture", event => { + // Only handle swipe gestures up and down + switch (event.direction) { + case event.DIRECTION_DOWN: + this.scrollTop = this.scrollHeight; + break; + case event.DIRECTION_UP: + this.scrollTop = 0; + break; + } + }); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.setAttribute("allowevents", "true"); + this._refreshSelection(); + } + + // nsIDOMXULSelectControlElement + set selectedItem(val) { + this.selectItem(val); + } + get selectedItem() { + return this.selectedItems.length ? this.selectedItems[0] : null; + } + + // nsIDOMXULSelectControlElement + set selectedIndex(val) { + if (val >= 0) { + // This is a micro-optimization so that a call to getIndexOfItem or + // getItemAtIndex caused by _fireOnSelect (especially for derived + // widgets) won't loop the children. + this._selecting = { + item: this.getItemAtIndex(val), + index: val, + }; + this.selectItem(this._selecting.item); + delete this._selecting; + } else { + this.clearSelection(); + this.currentItem = null; + } + } + get selectedIndex() { + if (this.selectedItems.length) { + return this.getIndexOfItem(this.selectedItems[0]); + } + return -1; + } + + // nsIDOMXULSelectControlElement + set value(val) { + var kids = this.getElementsByAttribute("value", val); + if (kids && kids.item(0)) { + this.selectItem(kids[0]); + } + return val; + } + get value() { + if (this.selectedItems.length) { + return this.selectedItem.value; + } + return null; + } + + // nsIDOMXULSelectControlElement + get itemCount() { + return this.itemChildren.length; + } + + // nsIDOMXULSelectControlElement + set selType(val) { + this.setAttribute("seltype", val); + return val; + } + get selType() { + return this.getAttribute("seltype"); + } + + // nsIDOMXULSelectControlElement + set currentItem(val) { + if (this._currentItem == val) { + return val; + } + + if (this._currentItem) { + this._currentItem.current = false; + if (!val && !this.suppressMenuItemEvent) { + // An item is losing focus and there is no new item to focus. + // Notify a11y that there is no focused item. + this._fireEvent(this._currentItem, "DOMMenuItemInactive"); + } + } + this._currentItem = val; + + if (val) { + val.current = true; + if (!this.suppressMenuItemEvent) { + // Notify a11y that this item got focus. + this._fireEvent(val, "DOMMenuItemActive"); + } + } + + return val; + } + get currentItem() { + return this._currentItem; + } + + // nsIDOMXULSelectControlElement + set currentIndex(val) { + if (val >= 0) { + this.currentItem = this.getItemAtIndex(val); + } else { + this.currentItem = null; + } + } + get currentIndex() { + return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1; + } + + // nsIDOMXULSelectControlElement + get selectedCount() { + return this.selectedItems.length; + } + + get itemChildren() { + let children = Array.from(this.children).filter( + node => node.localName == "richlistitem" + ); + return children; + } + + set suppressOnSelect(val) { + this.setAttribute("suppressonselect", val); + } + get suppressOnSelect() { + return this.getAttribute("suppressonselect") == "true"; + } + + set _selectDelay(val) { + this.setAttribute("_selectDelay", val); + } + get _selectDelay() { + return this.getAttribute("_selectDelay") || 50; + } + + _fireOnSelect() { + // make sure not to modify last-selected when suppressing select events + // (otherwise we'll lose the selection when a template gets rebuilt) + if (this._suppressOnSelect || this.suppressOnSelect) { + return; + } + + // remember the current item and all selected items with IDs + var state = this.currentItem ? this.currentItem.id : ""; + if (this.selType == "multiple" && this.selectedCount) { + let getId = function getId(aItem) { + return aItem.id; + }; + state += + " " + + [...this.selectedItems] + .filter(getId) + .map(getId) + .join(" "); + } + if (state) { + this.setAttribute("last-selected", state); + } else { + this.removeAttribute("last-selected"); + } + + // preserve the index just in case no IDs are available + if (this.currentIndex > -1) { + this._currentIndex = this.currentIndex + 1; + } + + var event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + + // always call this (allows a commandupdater without controller) + document.commandDispatcher.updateCommands("richlistbox-select"); + } + + getNextItem(aStartItem, aDelta) { + while (aStartItem) { + aStartItem = aStartItem.nextSibling; + if ( + aStartItem && + aStartItem.localName == "richlistitem" && + (!this._userSelecting || this._canUserSelect(aStartItem)) + ) { + --aDelta; + if (aDelta == 0) { + return aStartItem; + } + } + } + return null; + } + + getPreviousItem(aStartItem, aDelta) { + while (aStartItem) { + aStartItem = aStartItem.previousSibling; + if ( + aStartItem && + aStartItem.localName == "richlistitem" && + (!this._userSelecting || this._canUserSelect(aStartItem)) + ) { + --aDelta; + if (aDelta == 0) { + return aStartItem; + } + } + } + return null; + } + + appendItem(aLabel, aValue) { + var item = this.ownerDocument.createXULElement("richlistitem"); + item.setAttribute("value", aValue); + + var label = this.ownerDocument.createXULElement("label"); + label.setAttribute("value", aLabel); + label.setAttribute("flex", "1"); + label.setAttribute("crop", "end"); + item.appendChild(label); + + this.appendChild(item); + + return item; + } + + // nsIDOMXULSelectControlElement + getIndexOfItem(aItem) { + // don't search the children, if we're looking for none of them + if (aItem == null) { + return -1; + } + if (this._selecting && this._selecting.item == aItem) { + return this._selecting.index; + } + return this.itemChildren.indexOf(aItem); + } + + // nsIDOMXULSelectControlElement + getItemAtIndex(aIndex) { + if (this._selecting && this._selecting.index == aIndex) { + return this._selecting.item; + } + return this.itemChildren[aIndex] || null; + } + + // nsIDOMXULMultiSelectControlElement + addItemToSelection(aItem) { + if (this.selType != "multiple" && this.selectedCount) { + return; + } + + if (aItem.selected) { + return; + } + + this.selectedItems.append(aItem); + aItem.selected = true; + + this._fireOnSelect(); + } + + // nsIDOMXULMultiSelectControlElement + removeItemFromSelection(aItem) { + if (!aItem.selected) { + return; + } + + this.selectedItems.remove(aItem); + aItem.selected = false; + this._fireOnSelect(); + } + + // nsIDOMXULMultiSelectControlElement + toggleItemSelection(aItem) { + if (aItem.selected) { + this.removeItemFromSelection(aItem); + } else { + this.addItemToSelection(aItem); + } + } + + // nsIDOMXULMultiSelectControlElement + selectItem(aItem) { + if (!aItem || aItem.disabled) { + return; + } + + if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem) { + return; + } + + this._selectionStart = null; + + var suppress = this._suppressOnSelect; + this._suppressOnSelect = true; + + this.clearSelection(); + this.addItemToSelection(aItem); + this.currentItem = aItem; + + this._suppressOnSelect = suppress; + this._fireOnSelect(); + } + + // nsIDOMXULMultiSelectControlElement + selectItemRange(aStartItem, aEndItem) { + if (this.selType != "multiple") { + return; + } + + if (!aStartItem) { + aStartItem = this._selectionStart + ? this._selectionStart + : this.currentItem; + } + + if (!aStartItem) { + aStartItem = aEndItem; + } + + var suppressSelect = this._suppressOnSelect; + this._suppressOnSelect = true; + + this._selectionStart = aStartItem; + + var currentItem; + var startIndex = this.getIndexOfItem(aStartItem); + var endIndex = this.getIndexOfItem(aEndItem); + if (endIndex < startIndex) { + currentItem = aEndItem; + aEndItem = aStartItem; + aStartItem = currentItem; + } else { + currentItem = aStartItem; + } + + while (currentItem) { + this.addItemToSelection(currentItem); + if (currentItem == aEndItem) { + currentItem = this.getNextItem(currentItem, 1); + break; + } + currentItem = this.getNextItem(currentItem, 1); + } + + // Clear around new selection + // Don't use clearSelection() because it causes a lot of noise + // with respect to selection removed notifications used by the + // accessibility API support. + var userSelecting = this._userSelecting; + this._userSelecting = false; // that's US automatically unselecting + for (; currentItem; currentItem = this.getNextItem(currentItem, 1)) { + this.removeItemFromSelection(currentItem); + } + + for ( + currentItem = this.getItemAtIndex(0); + currentItem != aStartItem; + currentItem = this.getNextItem(currentItem, 1) + ) { + this.removeItemFromSelection(currentItem); + } + this._userSelecting = userSelecting; + + this._suppressOnSelect = suppressSelect; + + this._fireOnSelect(); + } + + // nsIDOMXULMultiSelectControlElement + selectAll() { + this._selectionStart = null; + + var suppress = this._suppressOnSelect; + this._suppressOnSelect = true; + + var item = this.getItemAtIndex(0); + while (item) { + this.addItemToSelection(item); + item = this.getNextItem(item, 1); + } + + this._suppressOnSelect = suppress; + this._fireOnSelect(); + } + + // nsIDOMXULMultiSelectControlElement + clearSelection() { + if (this.selectedItems) { + while (this.selectedItems.length) { + let item = this.selectedItems[0]; + item.selected = false; + this.selectedItems.remove(item); + } + } + + this._selectionStart = null; + this._fireOnSelect(); + } + + // nsIDOMXULMultiSelectControlElement + getSelectedItem(aIndex) { + return aIndex < this.selectedItems.length + ? this.selectedItems[aIndex] + : null; + } + + ensureIndexIsVisible(aIndex) { + return this.ensureElementIsVisible(this.getItemAtIndex(aIndex)); + } + + ensureElementIsVisible(aElement, aAlignToTop) { + if (!aElement) { + return; + } + + // These calculations assume that there is no padding on the + // "richlistbox" element, although there might be a margin. + var targetRect = aElement.getBoundingClientRect(); + var scrollRect = this.getBoundingClientRect(); + var offset = targetRect.top - scrollRect.top; + if (!aAlignToTop && offset >= 0) { + // scrollRect.bottom wouldn't take a horizontal scroll bar into account + let scrollRectBottom = scrollRect.top + this.clientHeight; + offset = targetRect.bottom - scrollRectBottom; + if (offset <= 0) { + return; + } + } + this.scrollTop += offset; + } + + getIndexOfFirstVisibleRow() { + var children = this.itemChildren; + + for (var ix = 0; ix < children.length; ix++) { + if (this._isItemVisible(children[ix])) { + return ix; + } + } + + return -1; + } + + getRowCount() { + return this.itemChildren.length; + } + + scrollOnePage(aDirection) { + var children = this.itemChildren; + + if (!children.length) { + return 0; + } + + // If nothing is selected, we just select the first element + // at the extreme we're moving away from + if (!this.currentItem) { + return aDirection == -1 ? children.length : 0; + } + + // If the current item is visible, scroll by one page so that + // the new current item is at approximately the same position as + // the existing current item. + let height = this.getBoundingClientRect().height; + if (this._isItemVisible(this.currentItem)) { + this.scrollBy(0, height * aDirection); + } + + // Figure out, how many items fully fit into the view port + // (including the currently selected one), and determine + // the index of the first one lying (partially) outside + let currentItemRect = this.currentItem.getBoundingClientRect(); + var startBorder = currentItemRect.y; + if (aDirection == -1) { + startBorder += currentItemRect.height; + } + + var index = this.currentIndex; + for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) { + let childRect = children[ix].getBoundingClientRect(); + if (childRect.height == 0) { + continue; // hidden children have a y of 0 + } + var endBorder = childRect.y + (aDirection == -1 ? childRect.height : 0); + if ((endBorder - startBorder) * aDirection > height) { + break; // we've reached the desired distance + } + index = ix; + } + + return index != this.currentIndex + ? index - this.currentIndex + : aDirection; + } + + _refreshSelection() { + // when this method is called, we know that either the currentItem + // and selectedItems we have are null (ctor) or a reference to an + // element no longer in the DOM (template). + + // first look for the last-selected attribute + var state = this.getAttribute("last-selected"); + if (state) { + var ids = state.split(" "); + + var suppressSelect = this._suppressOnSelect; + this._suppressOnSelect = true; + this.clearSelection(); + for (let i = 1; i < ids.length; i++) { + var selectedItem = document.getElementById(ids[i]); + if (selectedItem) { + this.addItemToSelection(selectedItem); + } + } + + var currentItem = document.getElementById(ids[0]); + if (!currentItem && this._currentIndex) { + currentItem = this.getItemAtIndex( + Math.min(this._currentIndex - 1, this.getRowCount()) + ); + } + if (currentItem) { + this.currentItem = currentItem; + if (this.selType != "multiple" && this.selectedCount == 0) { + this.selectedItem = currentItem; + } + + if (this.getBoundingClientRect().height) { + this.ensureElementIsVisible(currentItem); + } else { + // XXX hack around a bug in ensureElementIsVisible as it will + // scroll beyond the last element, bug 493645. + this.ensureElementIsVisible(currentItem.previousElementSibling); + } + } + this._suppressOnSelect = suppressSelect; + // XXX actually it's just a refresh, but at least + // the Extensions manager expects this: + this._fireOnSelect(); + return; + } + + // try to restore the selected items according to their IDs + // (applies after a template rebuild, if last-selected was not set) + if (this.selectedItems) { + let itemIds = []; + for (let i = this.selectedCount - 1; i >= 0; i--) { + let selectedItem = this.selectedItems[i]; + itemIds.push(selectedItem.id); + this.selectedItems.remove(selectedItem); + } + for (let i = 0; i < itemIds.length; i++) { + let selectedItem = document.getElementById(itemIds[i]); + if (selectedItem) { + this.selectedItems.append(selectedItem); + } + } + } + if (this.currentItem && this.currentItem.id) { + this.currentItem = document.getElementById(this.currentItem.id); + } else { + this.currentItem = null; + } + + // if we have no previously current item or if the above check fails to + // find the previous nodes (which causes it to clear selection) + if (!this.currentItem && this.selectedCount == 0) { + this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0; + + // cf. listbox constructor: + // select items according to their attributes + var children = this.itemChildren; + for (let i = 0; i < children.length; ++i) { + if (children[i].getAttribute("selected") == "true") { + this.selectedItems.append(children[i]); + } + } + } + + if (this.selType != "multiple" && this.selectedCount == 0) { + this.selectedItem = this.currentItem; + } + } + + _isItemVisible(aItem) { + if (!aItem) { + return false; + } + + var y = this.getBoundingClientRect().y; + + // Partially visible items are also considered visible + let itemRect = aItem.getBoundingClientRect(); + return ( + itemRect.y + itemRect.height > y && itemRect.y < y + this.clientHeight + ); + } + + moveByOffset(aOffset, aIsSelecting, aIsSelectingRange) { + if ((aIsSelectingRange || !aIsSelecting) && this.selType != "multiple") { + return; + } + + var newIndex = this.currentIndex + aOffset; + if (newIndex < 0) { + newIndex = 0; + } + + var numItems = this.getRowCount(); + if (newIndex > numItems - 1) { + newIndex = numItems - 1; + } + + var newItem = this.getItemAtIndex(newIndex); + // make sure that the item is actually visible/selectable + if (this._userSelecting && newItem && !this._canUserSelect(newItem)) { + newItem = + aOffset > 0 + ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) + : this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1); + } + if (newItem) { + this.ensureIndexIsVisible(this.getIndexOfItem(newItem)); + if (aIsSelectingRange) { + this.selectItemRange(null, newItem); + } else if (aIsSelecting) { + this.selectItem(newItem); + } + + this.currentItem = newItem; + } + } + + _moveByOffsetFromUserEvent(aOffset, aEvent) { + if (!aEvent.defaultPrevented) { + this._userSelecting = true; + this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey); + this._userSelecting = false; + aEvent.preventDefault(); + } + } + + _canUserSelect(aItem) { + var style = document.defaultView.getComputedStyle(aItem); + return ( + style.display != "none" && + style.visibility == "visible" && + style.MozUserInput != "none" + ); + } + + _selectTimeoutHandler(aMe) { + aMe._fireOnSelect(); + aMe._selectTimeout = null; + } + + timedSelect(aItem, aTimeout) { + var suppress = this._suppressOnSelect; + if (aTimeout != -1) { + this._suppressOnSelect = true; + } + + this.selectItem(aItem); + + this._suppressOnSelect = suppress; + + if (aTimeout != -1) { + if (this._selectTimeout) { + window.clearTimeout(this._selectTimeout); + } + this._selectTimeout = window.setTimeout( + this._selectTimeoutHandler, + aTimeout, + this + ); + } + } + + /** + * For backwards-compatibility and for convenience. + * Use ensureElementIsVisible instead + */ + ensureSelectedElementIsVisible() { + return this.ensureElementIsVisible(this.selectedItem); + } + + _fireEvent(aTarget, aName) { + let event = document.createEvent("Events"); + event.initEvent(aName, true, true); + aTarget.dispatchEvent(event); + } + }; + + MozXULElement.implementCustomInterface(MozElements.RichListBox, [ + Ci.nsIDOMXULSelectControlElement, + Ci.nsIDOMXULMultiSelectControlElement, + ]); + + customElements.define("richlistbox", MozElements.RichListBox); + + /** + * XUL:richlistitem element. + */ + MozElements.MozRichlistitem = class MozRichlistitem extends MozElements.BaseText { + constructor() { + super(); + + this.selectedByMouseOver = false; + + /** + * If there is no modifier key, we select on mousedown, not + * click, so that drags work correctly. + */ + this.addEventListener("mousedown", event => { + var control = this.control; + if (!control || control.disabled) { + return; + } + if ( + (!event.ctrlKey || + (AppConstants.platform == "macosx" && event.button == 2)) && + !event.shiftKey && + !event.metaKey + ) { + if (!this.selected) { + control.selectItem(this); + } + control.currentItem = this; + } + }); + + /** + * On a click (up+down on the same item), deselect everything + * except this item. + */ + this.addEventListener("click", event => { + if (event.button != 0) { + return; + } + + var control = this.control; + if (!control || control.disabled) { + return; + } + control._userSelecting = true; + if (control.selType != "multiple") { + control.selectItem(this); + } else if (event.ctrlKey || event.metaKey) { + control.toggleItemSelection(this); + control.currentItem = this; + } else if (event.shiftKey) { + control.selectItemRange(null, this); + control.currentItem = this; + } else { + /* We want to deselect all the selected items except what was + clicked, UNLESS it was a right-click. We have to do this + in click rather than mousedown so that you can drag a + selected group of items */ + + // use selectItemRange instead of selectItem, because this + // doesn't de- and reselect this item if it is selected + control.selectItemRange(this, this); + } + control._userSelecting = false; + }); + } + + /** + * nsIDOMXULSelectControlItemElement + */ + get label() { + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + return Array.from( + this.getElementsByTagNameNS(XUL_NS, "label"), + label => label.value + ).join(" "); + } + + set searchLabel(val) { + if (val !== null) { + this.setAttribute("searchlabel", val); + } + // fall back to the label property (default value) + else { + this.removeAttribute("searchlabel"); + } + return val; + } + + get searchLabel() { + return this.hasAttribute("searchlabel") + ? this.getAttribute("searchlabel") + : this.label; + } + /** + * nsIDOMXULSelectControlItemElement + */ + set value(val) { + this.setAttribute("value", val); + return val; + } + + get value() { + return this.getAttribute("value"); + } + + /** + * nsIDOMXULSelectControlItemElement + */ + set selected(val) { + if (val) { + this.setAttribute("selected", "true"); + } else { + this.removeAttribute("selected"); + } + + return val; + } + + get selected() { + return this.getAttribute("selected") == "true"; + } + /** + * nsIDOMXULSelectControlItemElement + */ + get control() { + var parent = this.parentNode; + while (parent) { + if (parent.localName == "richlistbox") { + return parent; + } + parent = parent.parentNode; + } + return null; + } + + set current(val) { + if (val) { + this.setAttribute("current", "true"); + } else { + this.removeAttribute("current"); + } + return val; + } + + get current() { + return this.getAttribute("current") == "true"; + } + }; + + MozXULElement.implementCustomInterface(MozElements.MozRichlistitem, [ + Ci.nsIDOMXULSelectControlItemElement, + ]); + + customElements.define("richlistitem", MozElements.MozRichlistitem); +} diff --git a/toolkit/content/widgets/search-textbox.js b/toolkit/content/widgets/search-textbox.js new file mode 100644 index 0000000000..6a9ad2f261 --- /dev/null +++ b/toolkit/content/widgets/search-textbox.js @@ -0,0 +1,264 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + class MozSearchTextbox extends MozXULElement { + constructor() { + super(); + + this.inputField = document.createElement("input"); + + const METHODS = [ + "focus", + "blur", + "select", + "setUserInput", + "setSelectionRange", + ]; + for (const method of METHODS) { + this[method] = (...args) => this.inputField[method](...args); + } + + const READ_WRITE_PROPERTIES = [ + "defaultValue", + "placeholder", + "readOnly", + "size", + "selectionStart", + "selectionEnd", + ]; + for (const property of READ_WRITE_PROPERTIES) { + Object.defineProperty(this, property, { + enumerable: true, + get() { + return this.inputField[property]; + }, + set(val) { + return (this.inputField[property] = val); + }, + }); + } + + this.attachShadow({ mode: "open" }); + this.addEventListener("input", this); + this.addEventListener("keypress", this); + this.addEventListener("mousedown", this); + } + + static get inheritedAttributes() { + return { + input: + "value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint,spellcheck", + ".textbox-search-icon": "label=searchbuttonlabel,disabled", + ".textbox-search-clear": "disabled", + }; + } + + static get markup() { + // TODO: Bug 1534799 - Convert string to Fluent and use manual DOM construction + return ` + <image class="textbox-search-clear" label="&searchTextBox.clear.label;"/> + `; + } + + static get entities() { + return ["chrome://global/locale/textcontext.dtd"]; + } + + connectedCallback() { + if (this.delayConnectedCallback() || this.connected) { + return; + } + this.connected = true; + this.textContent = ""; + + const stylesheet = document.createElement("link"); + stylesheet.rel = "stylesheet"; + stylesheet.href = "chrome://global/skin/search-textbox.css"; + + const textboxSign = document.createXULElement("image"); + textboxSign.className = "textbox-search-sign"; + textboxSign.part = "search-sign"; + + const input = this.inputField; + input.setAttribute("mozactionhint", "search"); + input.addEventListener("focus", this); + input.addEventListener("blur", this); + + const searchBtn = (this._searchButtonIcon = document.createXULElement( + "image" + )); + searchBtn.className = "textbox-search-icon"; + searchBtn.addEventListener("click", e => this._iconClick(e)); + + let clearBtn = this.constructor.fragment; + clearBtn = this._searchClearIcon = clearBtn.querySelector( + ".textbox-search-clear" + ); + clearBtn.addEventListener("click", () => this._clearSearch()); + + const deck = (this._searchIcons = document.createXULElement("deck")); + deck.className = "textbox-search-icons"; + deck.append(searchBtn, clearBtn); + this.shadowRoot.append(stylesheet, textboxSign, input, deck); + + this._timer = null; + + // Ensure the button state is up to date: + // eslint-disable-next-line no-self-assign + this.searchButton = this.searchButton; + + this.initializeAttributeInheritance(); + } + + set timeout(val) { + this.setAttribute("timeout", val); + return val; + } + + get timeout() { + return parseInt(this.getAttribute("timeout")) || 500; + } + + set searchButton(val) { + if (val) { + this.setAttribute("searchbutton", "true"); + this.removeAttribute("aria-autocomplete"); + this._searchButtonIcon.setAttribute("role", "button"); + } else { + this.removeAttribute("searchbutton"); + this.setAttribute("aria-autocomplete", "list"); + this._searchButtonIcon.setAttribute("role", "none"); + } + return val; + } + + get searchButton() { + return this.getAttribute("searchbutton") == "true"; + } + + set value(val) { + this.inputField.value = val; + + if (val) { + this._searchIcons.selectedIndex = this.searchButton ? 0 : 1; + } else { + this._searchIcons.selectedIndex = 0; + } + + if (this._timer) { + clearTimeout(this._timer); + } + return val; + } + + get value() { + return this.inputField.value; + } + + get editor() { + return this.inputField.editor; + } + + set disabled(val) { + this.inputField.disabled = val; + if (val) { + this.setAttribute("disabled", "true"); + } else { + this.removeAttribute("disabled"); + } + return val; + } + + get disabled() { + return this.inputField.disabled; + } + + on_blur() { + this.removeAttribute("focused"); + } + + on_focus() { + this.setAttribute("focused", "true"); + } + + on_input() { + if (this.searchButton) { + this._searchIcons.selectedIndex = 0; + return; + } + if (this._timer) { + clearTimeout(this._timer); + } + this._timer = + this.timeout && setTimeout(this._fireCommand, this.timeout, this); + this._searchIcons.selectedIndex = this.value ? 1 : 0; + } + + on_keypress(event) { + switch (event.keyCode) { + case KeyEvent.DOM_VK_ESCAPE: + if (this._clearSearch()) { + event.preventDefault(); + event.stopPropagation(); + } + break; + case KeyEvent.DOM_VK_RETURN: + this._enterSearch(); + event.preventDefault(); + event.stopPropagation(); + break; + } + } + + on_mousedown(event) { + if (!this.hasAttribute("focused")) { + this.setSelectionRange(0, 0); + this.focus(); + } + } + + _fireCommand(me) { + if (me._timer) { + clearTimeout(me._timer); + } + me._timer = null; + me.doCommand(); + } + + _iconClick() { + if (this.searchButton) { + this._enterSearch(); + } else { + this.focus(); + } + } + + _enterSearch() { + if (this.disabled) { + return; + } + if (this.searchButton && this.value && !this.readOnly) { + this._searchIcons.selectedIndex = 1; + } + this._fireCommand(this); + } + + _clearSearch() { + if (!this.disabled && !this.readOnly && this.value) { + this.value = ""; + this._fireCommand(this); + this._searchIcons.selectedIndex = 0; + return true; + } + return false; + } + } + + customElements.define("search-textbox", MozSearchTextbox); +} diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js new file mode 100644 index 0000000000..56bdbfd8a7 --- /dev/null +++ b/toolkit/content/widgets/spinner.js @@ -0,0 +1,538 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* + * The spinner is responsible for displaying the items, and does + * not care what the values represent. The setValue function is called + * when it detects a change in value triggered by scroll event. + * Supports scrolling, clicking on up or down, clicking on item, and + * dragging. + */ + +function Spinner(props, context) { + this.context = context; + this._init(props); +} + +{ + const ITEM_HEIGHT = 2.5, + VIEWPORT_SIZE = 7, + VIEWPORT_COUNT = 5; + + Spinner.prototype = { + /** + * Initializes a spinner. Set the default states and properties, cache + * element references, create the HTML markup, and add event listeners. + * + * @param {Object} props [Properties passed in from parent] + * { + * {Function} setValue: Takes a value and set the state to + * the parent component. + * {Function} getDisplayString: Takes a value, and output it + * as localized strings. + * {Number} viewportSize [optional]: Number of items in a + * viewport. + * {Boolean} hideButtons [optional]: Hide up & down buttons + * {Number} rootFontSize [optional]: Used to support zoom in/out + * } + */ + _init(props) { + const { + id, + setValue, + getDisplayString, + hideButtons, + rootFontSize = 10, + } = props; + + const spinnerTemplate = document.getElementById("spinner-template"); + const spinnerElement = document.importNode(spinnerTemplate.content, true); + + // Make sure viewportSize is an odd number because we want to have the selected + // item in the center. If it's an even number, use the default size instead. + const viewportSize = + props.viewportSize % 2 ? props.viewportSize : VIEWPORT_SIZE; + + this.state = { + items: [], + isScrolling: false, + }; + this.props = { + setValue, + getDisplayString, + viewportSize, + rootFontSize, + // We can assume that the viewportSize is an odd number. Calculate how many + // items we need to insert on top of the spinner so that the selected is at + // the center. Ex: if viewportSize is 5, we need 2 items on top. + viewportTopOffset: (viewportSize - 1) / 2, + }; + this.elements = { + container: spinnerElement.querySelector(".spinner-container"), + spinner: spinnerElement.querySelector(".spinner"), + up: spinnerElement.querySelector(".up"), + down: spinnerElement.querySelector(".down"), + itemsViewElements: [], + }; + + this.elements.spinner.style.height = ITEM_HEIGHT * viewportSize + "rem"; + + if (id) { + this.elements.container.id = id; + } + if (hideButtons) { + this.elements.container.classList.add("hide-buttons"); + } + + this.context.appendChild(spinnerElement); + this._attachEventListeners(); + }, + + /** + * Only the parent component calls setState on the spinner. + * It checks if the items have changed and updates the spinner. + * If only the value has changed, smooth scrolls to the new value. + * + * @param {Object} newState [The new spinner state] + * { + * {Number/String} value: The centered value + * {Array} items: The list of items for display + * {Boolean} isInfiniteScroll: Whether or not the spinner should + * have infinite scroll capability + * {Boolean} isValueSet: true if user has selected a value + * } + */ + setState(newState) { + const { value, items } = this.state; + const { + value: newValue, + items: newItems, + isValueSet, + isInvalid, + smoothScroll = true, + } = newState; + + if (this._isArrayDiff(newItems, items)) { + this.state = Object.assign(this.state, newState); + this._updateItems(); + this._scrollTo(newValue, true); + } else if (newValue != value) { + this.state = Object.assign(this.state, newState); + if (smoothScroll) { + this._smoothScrollTo(newValue, true); + } else { + this._scrollTo(newValue, true); + } + } + + if (isValueSet && !isInvalid) { + this._updateSelection(); + } else { + this._removeSelection(); + } + }, + + /** + * Whenever scroll event is detected: + * - Update the index state + * - If the value has changed, update the [value] state and call [setValue] + * - If infinite scrolling is on, reset the scrolling position if necessary + */ + _onScroll() { + const { items, itemsView, isInfiniteScroll } = this.state; + const { viewportSize, viewportTopOffset } = this.props; + const { spinner } = this.elements; + + this.state.index = this._getIndexByOffset(spinner.scrollTop); + + const value = itemsView[this.state.index + viewportTopOffset].value; + + // Call setValue if value has changed + if (this.state.value != value) { + this.state.value = value; + this.props.setValue(value); + } + + // Do infinite scroll when items length is bigger or equal to viewport + // and isInfiniteScroll is not false. + if (items.length >= viewportSize && isInfiniteScroll) { + // If the scroll position is near the top or bottom, jump back to the middle + // so user can keep scrolling up or down. + if ( + this.state.index < viewportSize || + this.state.index > itemsView.length - viewportSize + ) { + this._scrollTo(this.state.value, true); + } + } + + this.elements.spinner.classList.add("scrolling"); + }, + + /** + * Remove the "scrolling" state on scrollend. + */ + _onScrollend() { + this.elements.spinner.classList.remove("scrolling"); + }, + + /** + * Updates the spinner items to the current states. + */ + _updateItems() { + const { viewportSize, viewportTopOffset } = this.props; + const { items, isInfiniteScroll } = this.state; + + // Prepends null elements so the selected value is centered in spinner + let itemsView = new Array(viewportTopOffset).fill({}).concat(items); + + if (items.length >= viewportSize && isInfiniteScroll) { + // To achieve infinite scroll, we move the scroll position back to the + // center when it is near the top or bottom. The scroll momentum could + // be lost in the process, so to minimize that, we need at least 2 sets + // of items to act as buffer: one for the top and one for the bottom. + // But if the number of items is small ( < viewportSize * viewport count) + // we should add more sets. + let count = + Math.ceil((viewportSize * VIEWPORT_COUNT) / items.length) * 2; + for (let i = 0; i < count; i += 1) { + itemsView.push(...items); + } + } + + // Reuse existing DOM nodes when possible. Create or remove + // nodes based on how big itemsView is. + this._prepareNodes(itemsView.length, this.elements.spinner); + // Once DOM nodes are ready, set display strings using textContent + this._setDisplayStringAndClass( + itemsView, + this.elements.itemsViewElements + ); + + this.state.itemsView = itemsView; + }, + + /** + * Make sure the number or child elements is the same as length + * and keep the elements' references for updating textContent + * + * @param {Number} length [The number of child elements] + * @param {DOMElement} parent [The parent element reference] + */ + _prepareNodes(length, parent) { + const diff = length - parent.childElementCount; + + if (!diff) { + return; + } + + if (diff > 0) { + // Add more elements if length is greater than current + let frag = document.createDocumentFragment(); + + // Remove margin bottom on the last element before appending + if (parent.lastChild) { + parent.lastChild.style.marginBottom = ""; + } + + for (let i = 0; i < diff; i++) { + let el = document.createElement("div"); + frag.appendChild(el); + this.elements.itemsViewElements.push(el); + } + parent.appendChild(frag); + } else if (diff < 0) { + // Remove elements if length is less than current + for (let i = 0; i < Math.abs(diff); i++) { + parent.removeChild(parent.lastChild); + } + this.elements.itemsViewElements.splice(diff); + } + + parent.lastChild.style.marginBottom = + ITEM_HEIGHT * this.props.viewportTopOffset + "rem"; + }, + + /** + * Set the display string and class name to the elements. + * + * @param {Array<Object>} items + * [{ + * {Number/String} value: The value in its original form + * {Boolean} enabled: Whether or not the item is enabled + * }] + * @param {Array<DOMElement>} elements + */ + _setDisplayStringAndClass(items, elements) { + const { getDisplayString } = this.props; + + items.forEach((item, index) => { + elements[index].textContent = + item.value != undefined ? getDisplayString(item.value) : ""; + elements[index].className = item.enabled ? "" : "disabled"; + }); + }, + + /** + * Attach event listeners to the spinner and buttons. + */ + _attachEventListeners() { + const { spinner, container } = this.elements; + + spinner.addEventListener("scroll", this, { passive: true }); + spinner.addEventListener("scrollend", this, { passive: true }); + container.addEventListener("mouseup", this, { passive: true }); + container.addEventListener("mousedown", this, { passive: true }); + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + const { mouseState = {}, index, itemsView } = this.state; + const { viewportTopOffset, setValue } = this.props; + const { spinner, up, down } = this.elements; + + switch (event.type) { + case "scroll": { + this._onScroll(); + break; + } + case "scrollend": { + this._onScrollend(); + break; + } + case "mousedown": { + this.state.mouseState = { + down: true, + layerX: event.layerX, + layerY: event.layerY, + }; + if (event.target == up) { + // An "active" class is needed to simulate :active pseudo-class + // because element is not focused. + event.target.classList.add("active"); + this._smoothScrollToIndex(index - 1); + } + if (event.target == down) { + event.target.classList.add("active"); + this._smoothScrollToIndex(index + 1); + } + if (event.target.parentNode == spinner) { + // Listen to dragging events + spinner.addEventListener("mousemove", this, { passive: true }); + spinner.addEventListener("mouseleave", this, { passive: true }); + } + break; + } + case "mouseup": { + this.state.mouseState.down = false; + if (event.target == up || event.target == down) { + event.target.classList.remove("active"); + } + if (event.target.parentNode == spinner) { + // Check if user clicks or drags, scroll to the item if clicked, + // otherwise get the current index and smooth scroll there. + if ( + event.layerX == mouseState.layerX && + event.layerY == mouseState.layerY + ) { + const newIndex = + this._getIndexByOffset(event.target.offsetTop) - + viewportTopOffset; + if (index == newIndex) { + // Set value manually if the clicked element is already centered. + // This happens when the picker first opens, and user pick the + // default value. + setValue(itemsView[index + viewportTopOffset].value); + } else { + this._smoothScrollToIndex(newIndex); + } + } else { + this._smoothScrollToIndex( + this._getIndexByOffset(spinner.scrollTop) + ); + } + // Stop listening to dragging + spinner.removeEventListener("mousemove", this, { passive: true }); + spinner.removeEventListener("mouseleave", this, { passive: true }); + } + break; + } + case "mouseleave": { + if (event.target == spinner) { + // Stop listening to drag event if mouse is out of the spinner + this._smoothScrollToIndex( + this._getIndexByOffset(spinner.scrollTop) + ); + spinner.removeEventListener("mousemove", this, { passive: true }); + spinner.removeEventListener("mouseleave", this, { passive: true }); + } + break; + } + case "mousemove": { + // Change spinner position on drag + spinner.scrollTop -= event.movementY; + break; + } + } + }, + + /** + * Find the index by offset + * @param {Number} offset: Offset value in pixel. + * @return {Number} Index number + */ + _getIndexByOffset(offset) { + return Math.round(offset / (ITEM_HEIGHT * this.props.rootFontSize)); + }, + + /** + * Find the index of a value that is the closest to the current position. + * If centering is true, find the index closest to the center. + * + * @param {Number/String} value: The value to find + * @param {Boolean} centering: Whether or not to find the value closest to center + * @return {Number} index of the value, returns -1 if value is not found + */ + _getScrollIndex(value, centering) { + const { itemsView } = this.state; + const { viewportTopOffset } = this.props; + + // If index doesn't exist, or centering is true, start from the middle point + let currentIndex = + centering || this.state.index == undefined + ? Math.round((itemsView.length - viewportTopOffset) / 2) + : this.state.index; + let closestIndex = itemsView.length; + let indexes = []; + let diff = closestIndex; + let isValueFound = false; + + // Find indexes of items match the value + itemsView.forEach((item, index) => { + if (item.value == value) { + indexes.push(index); + } + }); + + // Find the index closest to currentIndex + indexes.forEach(index => { + let d = Math.abs(index - currentIndex); + if (d < diff) { + diff = d; + closestIndex = index; + isValueFound = true; + } + }); + + return isValueFound ? closestIndex - viewportTopOffset : -1; + }, + + /** + * Scroll to a value. + * + * @param {Number/String} value: Value to scroll to + * @param {Boolean} centering: Whether or not to scroll to center location + */ + _scrollTo(value, centering) { + const index = this._getScrollIndex(value, centering); + // Do nothing if the value is not found + if (index > -1) { + this.state.index = index; + this.elements.spinner.scrollTop = + this.state.index * ITEM_HEIGHT * this.props.rootFontSize; + } + }, + + /** + * Smooth scroll to a value. + * + * @param {Number/String} value: Value to scroll to + */ + _smoothScrollTo(value) { + const index = this._getScrollIndex(value); + // Do nothing if the value is not found + if (index > -1) { + this.state.index = index; + this._smoothScrollToIndex(this.state.index); + } + }, + + /** + * Smooth scroll to a value based on the index + * + * @param {Number} index: Index number + */ + _smoothScrollToIndex(index) { + const element = this.elements.spinner.children[index]; + if (element) { + element.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } + }, + + /** + * Update the selection state. + */ + _updateSelection() { + const { itemsViewElements, selected } = this.elements; + const { itemsView, index } = this.state; + const { viewportTopOffset } = this.props; + const currentItemIndex = index + viewportTopOffset; + + if (selected && selected != itemsViewElements[currentItemIndex]) { + this._removeSelection(); + } + + this.elements.selected = itemsViewElements[currentItemIndex]; + if (itemsView[currentItemIndex] && itemsView[currentItemIndex].enabled) { + this.elements.selected.classList.add("selection"); + } + }, + + /** + * Remove selection if selected exists and different from current + */ + _removeSelection() { + const { selected } = this.elements; + if (selected) { + selected.classList.remove("selection"); + } + }, + + /** + * Compares arrays of objects. It assumes the structure is an array of + * objects, and objects in a and b have the same number of properties. + * + * @param {Array<Object>} a + * @param {Array<Object>} b + * @return {Boolean} Returns true if a and b are different + */ + _isArrayDiff(a, b) { + // Check reference first, exit early if reference is the same. + if (a == b) { + return false; + } + + if (a.length != b.length) { + return true; + } + + for (let i = 0; i < a.length; i++) { + for (let prop in a[i]) { + if (a[i][prop] != b[i][prop]) { + return true; + } + } + } + return false; + }, + }; +} diff --git a/toolkit/content/widgets/stringbundle.js b/toolkit/content/widgets/stringbundle.js new file mode 100644 index 0000000000..c780950f16 --- /dev/null +++ b/toolkit/content/widgets/stringbundle.js @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +{ + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + class MozStringbundle extends MozXULElement { + get stringBundle() { + if (!this._bundle) { + try { + this._bundle = Services.strings.createBundle(this.src); + } catch (e) { + dump("Failed to get stringbundle:\n"); + dump(e + "\n"); + } + } + return this._bundle; + } + + set src(val) { + this._bundle = null; + this.setAttribute("src", val); + return val; + } + + get src() { + return this.getAttribute("src"); + } + + get strings() { + // Note: this is a sucky method name! Should be: + // readonly attribute nsISimpleEnumerator strings; + return this.stringBundle.getSimpleEnumeration(); + } + + getString(aStringKey) { + try { + return this.stringBundle.GetStringFromName(aStringKey); + } catch (e) { + dump( + "*** Failed to get string " + + aStringKey + + " in bundle: " + + this.src + + "\n" + ); + throw e; + } + } + + getFormattedString(aStringKey, aStringsArray) { + try { + return this.stringBundle.formatStringFromName( + aStringKey, + aStringsArray + ); + } catch (e) { + dump( + "*** Failed to format string " + + aStringKey + + " in bundle: " + + this.src + + "\n" + ); + throw e; + } + } + } + + customElements.define("stringbundle", MozStringbundle); +} diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox.js new file mode 100644 index 0000000000..f09ac29995 --- /dev/null +++ b/toolkit/content/widgets/tabbox.js @@ -0,0 +1,831 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +{ + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + let imports = {}; + ChromeUtils.defineModuleGetter( + imports, + "ShortcutUtils", + "resource://gre/modules/ShortcutUtils.jsm" + ); + + class MozTabbox extends MozXULElement { + constructor() { + super(); + this._handleMetaAltArrows = AppConstants.platform == "macosx"; + this.disconnectedCallback = this.disconnectedCallback.bind(this); + } + + connectedCallback() { + Services.els.addSystemEventListener(document, "keydown", this, false); + window.addEventListener("unload", this.disconnectedCallback, { + once: true, + }); + } + + disconnectedCallback() { + window.removeEventListener("unload", this.disconnectedCallback); + Services.els.removeSystemEventListener(document, "keydown", this, false); + } + + set handleCtrlTab(val) { + this.setAttribute("handleCtrlTab", val); + return val; + } + + get handleCtrlTab() { + return this.getAttribute("handleCtrlTab") != "false"; + } + + get tabs() { + if (this.hasAttribute("tabcontainer")) { + return document.getElementById(this.getAttribute("tabcontainer")); + } + return this.getElementsByTagNameNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "tabs" + ).item(0); + } + + get tabpanels() { + return this.getElementsByTagNameNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "tabpanels" + ).item(0); + } + + set selectedIndex(val) { + let tabs = this.tabs; + if (tabs) { + tabs.selectedIndex = val; + } + this.setAttribute("selectedIndex", val); + return val; + } + + get selectedIndex() { + let tabs = this.tabs; + return tabs ? tabs.selectedIndex : -1; + } + + set selectedTab(val) { + if (val) { + let tabs = this.tabs; + if (tabs) { + tabs.selectedItem = val; + } + } + return val; + } + + get selectedTab() { + let tabs = this.tabs; + return tabs && tabs.selectedItem; + } + + set selectedPanel(val) { + if (val) { + let tabpanels = this.tabpanels; + if (tabpanels) { + tabpanels.selectedPanel = val; + } + } + return val; + } + + get selectedPanel() { + let tabpanels = this.tabpanels; + return tabpanels && tabpanels.selectedPanel; + } + + handleEvent(event) { + if (!event.isTrusted) { + // Don't let untrusted events mess with tabs. + return; + } + + // Skip this only if something has explicitly cancelled it. + if (event.defaultCancelled) { + return; + } + + // Don't check if the event was already consumed because tab + // navigation should always work for better user experience. + + const { ShortcutUtils } = imports; + + switch (ShortcutUtils.getSystemActionForEvent(event)) { + case ShortcutUtils.CYCLE_TABS: + Services.telemetry.keyedScalarAdd( + "browser.ui.interaction.keyboard", + "ctrl-tab", + 1 + ); + Services.prefs.setBoolPref( + "browser.engagement.ctrlTab.has-used", + true + ); + if (this.tabs && this.handleCtrlTab) { + this.tabs.advanceSelectedTab(event.shiftKey ? -1 : 1, true); + event.preventDefault(); + } + break; + case ShortcutUtils.PREVIOUS_TAB: + if (this.tabs) { + this.tabs.advanceSelectedTab(-1, true); + event.preventDefault(); + } + break; + case ShortcutUtils.NEXT_TAB: + if (this.tabs) { + this.tabs.advanceSelectedTab(1, true); + event.preventDefault(); + } + break; + } + } + } + + customElements.define("tabbox", MozTabbox); + + class MozTabpanels extends MozXULElement { + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this._tabbox = null; + this._selectedPanel = this.children.item(this.selectedIndex); + } + + get tabbox() { + // Memoize the result rather than replacing this getter, so that + // it can be reset if the parent changes. + if (this._tabbox) { + return this._tabbox; + } + + let parent = this.parentNode; + while (parent) { + if (parent.localName == "tabbox") { + break; + } + parent = parent.parentNode; + } + + return (this._tabbox = parent); + } + + set selectedIndex(val) { + if (val < 0 || val >= this.children.length) { + return val; + } + + let panel = this._selectedPanel; + this._selectedPanel = this.children[val]; + + if (this.getAttribute("async") != "true") { + this.setAttribute("selectedIndex", val); + } + + if (this._selectedPanel != panel) { + let event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + } + return val; + } + + get selectedIndex() { + let indexStr = this.getAttribute("selectedIndex"); + return indexStr ? parseInt(indexStr) : -1; + } + + set selectedPanel(val) { + let selectedIndex = -1; + for ( + let panel = val; + panel != null; + panel = panel.previousElementSibling + ) { + ++selectedIndex; + } + this.selectedIndex = selectedIndex; + return val; + } + + get selectedPanel() { + return this._selectedPanel; + } + + /** + * nsIDOMXULRelatedElement + */ + getRelatedElement(aTabPanelElm) { + if (!aTabPanelElm) { + return null; + } + + let tabboxElm = this.tabbox; + if (!tabboxElm) { + return null; + } + + let tabsElm = tabboxElm.tabs; + if (!tabsElm) { + return null; + } + + // Return tab element having 'linkedpanel' attribute equal to the id + // of the tab panel or the same index as the tab panel element. + let tabpanelIdx = Array.prototype.indexOf.call( + this.children, + aTabPanelElm + ); + if (tabpanelIdx == -1) { + return null; + } + + let tabElms = tabsElm.allTabs; + let tabElmFromIndex = tabElms[tabpanelIdx]; + + let tabpanelId = aTabPanelElm.id; + if (tabpanelId) { + for (let idx = 0; idx < tabElms.length; idx++) { + let tabElm = tabElms[idx]; + if (tabElm.linkedPanel == tabpanelId) { + return tabElm; + } + } + } + + return tabElmFromIndex; + } + } + + MozXULElement.implementCustomInterface(MozTabpanels, [ + Ci.nsIDOMXULRelatedElement, + ]); + customElements.define("tabpanels", MozTabpanels); + + MozElements.MozTab = class MozTab extends MozElements.BaseText { + static get markup() { + return ` + <hbox class="tab-middle box-inherit" flex="1"> + <image class="tab-icon" role="presentation"></image> + <label class="tab-text" flex="1" role="presentation"></label> + </hbox> + `; + } + + constructor() { + super(); + + this.addEventListener("mousedown", this); + this.addEventListener("keydown", this); + + this.arrowKeysShouldWrap = AppConstants.platform == "macosx"; + } + + static get inheritedAttributes() { + return { + ".tab-middle": "align,dir,pack,orient,selected,visuallyselected", + ".tab-icon": "validate,src=image", + ".tab-text": "value=label,accesskey,crop,disabled", + }; + } + + connectedCallback() { + if (!this._initialized) { + this.textContent = ""; + this.appendChild(this.constructor.fragment); + this.initializeAttributeInheritance(); + this._initialized = true; + } + } + + on_mousedown(event) { + if (event.button != 0 || this.disabled) { + return; + } + + this.parentNode.ariaFocusedItem = null; + + if (this == this.parentNode.selectedItem) { + // This tab is already selected and we will fall + // through to mousedown behavior which sets focus on the current tab, + // Only a click on an already selected tab should focus the tab itself. + return; + } + + let stopwatchid = this.parentNode.getAttribute("stopwatchid"); + if (stopwatchid) { + TelemetryStopwatch.start(stopwatchid); + } + + // Call this before setting the 'ignorefocus' attribute because this + // will pass on focus if the formerly selected tab was focused as well. + this.closest("tabs")._selectNewTab(this); + + var isTabFocused = false; + try { + isTabFocused = document.commandDispatcher.focusedElement == this; + } catch (e) {} + + // Set '-moz-user-focus' to 'ignore' so that PostHandleEvent() can't + // focus the tab; we only want tabs to be focusable by the mouse if + // they are already focused. After a short timeout we'll reset + // '-moz-user-focus' so that tabs can be focused by keyboard again. + if (!isTabFocused) { + this.setAttribute("ignorefocus", "true"); + setTimeout(tab => tab.removeAttribute("ignorefocus"), 0, this); + } + + if (stopwatchid) { + TelemetryStopwatch.finish(stopwatchid); + } + } + + on_keydown(event) { + if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) { + return; + } + switch (event.keyCode) { + case KeyEvent.DOM_VK_LEFT: { + let direction = window.getComputedStyle(this.parentNode).direction; + this.container.advanceSelectedTab( + direction == "ltr" ? -1 : 1, + this.arrowKeysShouldWrap + ); + event.preventDefault(); + break; + } + + case KeyEvent.DOM_VK_RIGHT: { + let direction = window.getComputedStyle(this.parentNode).direction; + this.container.advanceSelectedTab( + direction == "ltr" ? 1 : -1, + this.arrowKeysShouldWrap + ); + event.preventDefault(); + break; + } + + case KeyEvent.DOM_VK_UP: + this.container.advanceSelectedTab(-1, this.arrowKeysShouldWrap); + event.preventDefault(); + break; + + case KeyEvent.DOM_VK_DOWN: + this.container.advanceSelectedTab(1, this.arrowKeysShouldWrap); + event.preventDefault(); + break; + + case KeyEvent.DOM_VK_HOME: + this.container._selectNewTab(this.container.allTabs[0]); + event.preventDefault(); + break; + + case KeyEvent.DOM_VK_END: { + let { allTabs } = this.container; + this.container._selectNewTab(allTabs[allTabs.length - 1], -1); + event.preventDefault(); + break; + } + } + } + + set value(val) { + this.setAttribute("value", val); + return val; + } + + get value() { + return this.getAttribute("value"); + } + + get control() { + var parent = this.parentNode; + return parent.localName == "tabs" ? parent : null; + } + + get selected() { + return this.getAttribute("selected") == "true"; + } + + set _selected(val) { + if (val) { + this.setAttribute("selected", "true"); + this.setAttribute("visuallyselected", "true"); + } else { + this.removeAttribute("selected"); + this.removeAttribute("visuallyselected"); + } + + return val; + } + + set linkedPanel(val) { + this.setAttribute("linkedpanel", val); + } + + get linkedPanel() { + return this.getAttribute("linkedpanel"); + } + }; + + MozXULElement.implementCustomInterface(MozElements.MozTab, [ + Ci.nsIDOMXULSelectControlItemElement, + ]); + customElements.define("tab", MozElements.MozTab); + + class TabsBase extends MozElements.BaseControl { + constructor() { + super(); + + this.addEventListener("DOMMouseScroll", event => { + if (Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling")) { + if (event.detail > 0) { + this.advanceSelectedTab(1, false); + } else { + this.advanceSelectedTab(-1, false); + } + event.stopPropagation(); + } + }); + } + + // to be called from derived class connectedCallback + baseConnect() { + this._tabbox = null; + this.ACTIVE_DESCENDANT_ID = + "keyboard-focused-tab-" + Math.trunc(Math.random() * 1000000); + + if (!this.hasAttribute("orient")) { + this.setAttribute("orient", "horizontal"); + } + + if (this.tabbox && this.tabbox.hasAttribute("selectedIndex")) { + let selectedIndex = parseInt(this.tabbox.getAttribute("selectedIndex")); + this.selectedIndex = selectedIndex > 0 ? selectedIndex : 0; + return; + } + + let children = this.allTabs; + let length = children.length; + for (var i = 0; i < length; i++) { + if (children[i].getAttribute("selected") == "true") { + this.selectedIndex = i; + return; + } + } + + var value = this.value; + if (value) { + this.value = value; + } else { + this.selectedIndex = 0; + } + } + + /** + * nsIDOMXULSelectControlElement + */ + get itemCount() { + return this.allTabs.length; + } + + set value(val) { + this.setAttribute("value", val); + var children = this.allTabs; + for (var c = children.length - 1; c >= 0; c--) { + if (children[c].value == val) { + this.selectedIndex = c; + break; + } + } + return val; + } + + get value() { + return this.getAttribute("value"); + } + + get tabbox() { + if (!this._tabbox) { + // Memoize the result in a field rather than replacing this property, + // so that it can be reset along with the binding. + this._tabbox = this.closest("tabbox"); + } + + return this._tabbox; + } + + set selectedIndex(val) { + var tab = this.getItemAtIndex(val); + if (tab) { + for (let otherTab of this.allTabs) { + if (otherTab != tab && otherTab.selected) { + otherTab._selected = false; + } + } + tab._selected = true; + + this.setAttribute("value", tab.value); + + let linkedPanel = this.getRelatedElement(tab); + if (linkedPanel) { + this.tabbox.setAttribute("selectedIndex", val); + + // This will cause an onselect event to fire for the tabpanel + // element. + this.tabbox.tabpanels.selectedPanel = linkedPanel; + } + } + return val; + } + + get selectedIndex() { + const tabs = this.allTabs; + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].selected) { + return i; + } + } + return -1; + } + + set selectedItem(val) { + if (val && !val.selected) { + // The selectedIndex setter ignores invalid values + // such as -1 if |val| isn't one of our child nodes. + this.selectedIndex = this.getIndexOfItem(val); + } + return val; + } + + get selectedItem() { + const tabs = this.allTabs; + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].selected) { + return tabs[i]; + } + } + return null; + } + + get ariaFocusedIndex() { + const tabs = this.allTabs; + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].id == this.ACTIVE_DESCENDANT_ID) { + return i; + } + } + return -1; + } + + set ariaFocusedItem(val) { + let setNewItem = val && this.getIndexOfItem(val) != -1; + let clearExistingItem = this.ariaFocusedItem && (!val || setNewItem); + if (clearExistingItem) { + let ariaFocusedItem = this.ariaFocusedItem; + ariaFocusedItem.classList.remove("keyboard-focused-tab"); + ariaFocusedItem.id = ""; + this.selectedItem.removeAttribute("aria-activedescendant"); + let evt = new CustomEvent("AriaFocus"); + this.selectedItem.dispatchEvent(evt); + } + + if (setNewItem) { + this.ariaFocusedItem = null; + val.id = this.ACTIVE_DESCENDANT_ID; + val.classList.add("keyboard-focused-tab"); + this.selectedItem.setAttribute( + "aria-activedescendant", + this.ACTIVE_DESCENDANT_ID + ); + let evt = new CustomEvent("AriaFocus"); + val.dispatchEvent(evt); + } + + return val; + } + + get ariaFocusedItem() { + return document.getElementById(this.ACTIVE_DESCENDANT_ID); + } + + /** + * nsIDOMXULRelatedElement + */ + getRelatedElement(aTabElm) { + if (!aTabElm) { + return null; + } + + let tabboxElm = this.tabbox; + if (!tabboxElm) { + return null; + } + + let tabpanelsElm = tabboxElm.tabpanels; + if (!tabpanelsElm) { + return null; + } + + // Get linked tab panel by 'linkedpanel' attribute on the given tab + // element. + let linkedPanelId = aTabElm.linkedPanel; + if (linkedPanelId) { + return this.ownerDocument.getElementById(linkedPanelId); + } + + // otherwise linked tabpanel element has the same index as the given + // tab element. + let tabElmIdx = this.getIndexOfItem(aTabElm); + return tabpanelsElm.children[tabElmIdx]; + } + + getIndexOfItem(item) { + return Array.prototype.indexOf.call(this.allTabs, item); + } + + getItemAtIndex(index) { + return this.allTabs[index] || null; + } + + /** + * Find an adjacent tab. + * + * @param {Node} startTab A <tab> element to start searching from. + * @param {Number} opts.direction 1 to search forward, -1 to search backward. + * @param {Boolean} opts.wrap If true, wrap around if the search reaches + * the end (or beginning) of the tab strip. + * @param {Boolean} opts.startWithAdjacent + * If true (which is the default), start + * searching from the next tab after (or + * before) startTab. If false, startTab may + * be returned if it passes the filter. + * @param {Boolean} opts.advance If false, start searching with startTab. If + * true, start searching with an adjacent tab. + * @param {Function} opts.filter A function to select which tabs to return. + * + * @return {Node | null} The next <tab> element or, if none exists, null. + */ + findNextTab(startTab, opts = {}) { + let { + direction = 1, + wrap = false, + startWithAdjacent = true, + filter = tab => true, + } = opts; + + let tab = startTab; + if (!startWithAdjacent && filter(tab)) { + return tab; + } + + let children = this.allTabs; + let i = children.indexOf(tab); + if (i < 0) { + return null; + } + + while (true) { + i += direction; + if (wrap) { + if (i < 0) { + i = children.length - 1; + } else if (i >= children.length) { + i = 0; + } + } else if (i < 0 || i >= children.length) { + return null; + } + + tab = children[i]; + if (tab == startTab) { + return null; + } + if (filter(tab)) { + return tab; + } + } + } + + _selectNewTab(aNewTab, aFallbackDir, aWrap) { + this.ariaFocusedItem = null; + + aNewTab = this.findNextTab(aNewTab, { + direction: aFallbackDir, + wrap: aWrap, + startWithAdjacent: false, + filter: tab => + !tab.hidden && !tab.disabled && this._canAdvanceToTab(tab), + }); + + var isTabFocused = false; + try { + isTabFocused = + document.commandDispatcher.focusedElement == this.selectedItem; + } catch (e) {} + this.selectedItem = aNewTab; + if (isTabFocused) { + aNewTab.focus(); + } else if (this.getAttribute("setfocus") != "false") { + let selectedPanel = this.tabbox.selectedPanel; + document.commandDispatcher.advanceFocusIntoSubtree(selectedPanel); + + // Make sure that the focus doesn't move outside the tabbox + if (this.tabbox) { + try { + let el = document.commandDispatcher.focusedElement; + while (el && el != this.tabbox.tabpanels) { + if (el == this.tabbox || el == selectedPanel) { + return; + } + el = el.parentNode; + } + aNewTab.focus(); + } catch (e) {} + } + } + } + + _canAdvanceToTab(aTab) { + return true; + } + + advanceSelectedTab(aDir, aWrap) { + let startTab = this.ariaFocusedItem || this.selectedItem; + let newTab = this.findNextTab(startTab, { + direction: aDir, + wrap: aWrap, + }); + if (newTab && newTab != startTab) { + this._selectNewTab(newTab, aDir, aWrap); + } + } + + appendItem(label, value) { + var tab = document.createXULElement("tab"); + tab.setAttribute("label", label); + tab.setAttribute("value", value); + this.appendChild(tab); + return tab; + } + } + + MozXULElement.implementCustomInterface(TabsBase, [ + Ci.nsIDOMXULSelectControlElement, + Ci.nsIDOMXULRelatedElement, + ]); + + MozElements.TabsBase = TabsBase; + + class MozTabs extends TabsBase { + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + let start = MozXULElement.parseXULToFragment( + `<spacer class="tabs-left"/>` + ); + this.insertBefore(start, this.firstChild); + + let end = MozXULElement.parseXULToFragment( + `<spacer class="tabs-right" flex="1"/>` + ); + this.insertBefore(end, null); + + this.baseConnect(); + } + + // Accessor for tabs. This element has spacers as the first and + // last elements and <tab>s are everything in between. + get allTabs() { + let children = Array.from(this.children); + return children.splice(1, children.length - 2); + } + + appendChild(tab) { + // insert before the end spacer. + this.insertBefore(tab, this.lastChild); + } + } + + customElements.define("tabs", MozTabs); +} diff --git a/toolkit/content/widgets/text.js b/toolkit/content/widgets/text.js new file mode 100644 index 0000000000..402c08b284 --- /dev/null +++ b/toolkit/content/widgets/text.js @@ -0,0 +1,394 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + const MozXULTextElement = MozElements.MozElementMixin(XULTextElement); + + let gInsertSeparator = false; + let gAlwaysAppendAccessKey = false; + let gUnderlineAccesskey = + Services.prefs.getIntPref("ui.key.menuAccessKey") != 0; + if (gUnderlineAccesskey) { + try { + const nsIPrefLocalizedString = Ci.nsIPrefLocalizedString; + const prefNameInsertSeparator = + "intl.menuitems.insertseparatorbeforeaccesskeys"; + const prefNameAlwaysAppendAccessKey = + "intl.menuitems.alwaysappendaccesskeys"; + + let val = Services.prefs.getComplexValue( + prefNameInsertSeparator, + nsIPrefLocalizedString + ).data; + gInsertSeparator = val == "true"; + + val = Services.prefs.getComplexValue( + prefNameAlwaysAppendAccessKey, + nsIPrefLocalizedString + ).data; + gAlwaysAppendAccessKey = val == "true"; + } catch (e) { + gInsertSeparator = gAlwaysAppendAccessKey = true; + } + } + + class MozTextLabel extends MozXULTextElement { + constructor() { + super(); + this._lastFormattedAccessKey = null; + this.addEventListener("click", this._onClick); + } + + static get observedAttributes() { + return ["accesskey"]; + } + + set textContent(val) { + super.textContent = val; + this._lastFormattedAccessKey = null; + this.formatAccessKey(); + } + + get textContent() { + return super.textContent; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (!this.isConnectedAndReady || oldValue == newValue) { + return; + } + + // Note that this is only happening when "accesskey" attribute change: + this.formatAccessKey(); + } + + _onClick(event) { + let controlElement = this.labeledControlElement; + if (!controlElement || this.disabled) { + return; + } + controlElement.focus(); + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + if (controlElement.namespaceURI != XUL_NS) { + return; + } + + if ( + (controlElement.localName == "checkbox" || + controlElement.localName == "radio") && + controlElement.getAttribute("disabled") == "true" + ) { + return; + } + + if (controlElement.localName == "checkbox") { + controlElement.checked = !controlElement.checked; + } else if (controlElement.localName == "radio") { + controlElement.control.selectedItem = controlElement; + } + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.formatAccessKey(); + } + + set accessKey(val) { + this.setAttribute("accesskey", val); + var control = this.labeledControlElement; + if (control) { + control.setAttribute("accesskey", val); + } + } + + get accessKey() { + let accessKey = this.getAttribute("accesskey"); + return accessKey ? accessKey[0] : null; + } + + get labeledControlElement() { + let control = this.control; + return control ? document.getElementById(control) : null; + } + + set control(val) { + this.setAttribute("control", val); + } + + get control() { + return this.getAttribute("control"); + } + + // This is used to match the rendering of accesskeys from nsTextBoxFrame.cpp (i.e. when the + // label uses [value]). So this is just for when we have textContent. + formatAccessKey() { + // Skip doing any DOM manipulation whenever possible: + let accessKey = this.accessKey; + if ( + !gUnderlineAccesskey || + !this.isConnectedAndReady || + this._lastFormattedAccessKey == accessKey || + !this.textContent + ) { + return; + } + this._lastFormattedAccessKey = accessKey; + if (this.accessKeySpan) { + // Clear old accesskey + mergeElement(this.accessKeySpan); + this.accessKeySpan = null; + } + + if (this.hiddenColon) { + mergeElement(this.hiddenColon); + this.hiddenColon = null; + } + + if (this.accessKeyParens) { + this.accessKeyParens.remove(); + this.accessKeyParens = null; + } + + // If we used to have an accessKey but not anymore, we're done here + if (!accessKey) { + return; + } + + let labelText = this.textContent; + let accessKeyIndex = -1; + if (!gAlwaysAppendAccessKey) { + accessKeyIndex = labelText.indexOf(accessKey); + if (accessKeyIndex < 0) { + // Try again in upper case + accessKeyIndex = labelText + .toUpperCase() + .indexOf(accessKey.toUpperCase()); + } + } else if (labelText.endsWith(`(${accessKey.toUpperCase()})`)) { + accessKeyIndex = labelText.length - (1 + accessKey.length); // = index of accessKey. + } + + const HTML_NS = "http://www.w3.org/1999/xhtml"; + this.accessKeySpan = document.createElementNS(HTML_NS, "span"); + this.accessKeySpan.className = "accesskey"; + + // Note that if you change the following code, see the comment of + // nsTextBoxFrame::UpdateAccessTitle. + + // If accesskey is in the string, underline it: + if (accessKeyIndex >= 0) { + wrapChar(this, this.accessKeySpan, accessKeyIndex); + return; + } + + // If accesskey is not in string, append in parentheses + // If end is colon, we should insert before colon. + // i.e., "label:" -> "label(X):" + let colonHidden = false; + if (/:$/.test(labelText)) { + labelText = labelText.slice(0, -1); + this.hiddenColon = document.createElementNS(HTML_NS, "span"); + this.hiddenColon.className = "hiddenColon"; + this.hiddenColon.style.display = "none"; + // Hide the last colon by using span element. + // I.e., label<span style="display:none;">:</span> + wrapChar(this, this.hiddenColon, labelText.length); + colonHidden = true; + } + // If end is space(U+20), + // we should not add space before parentheses. + let endIsSpace = false; + if (/ $/.test(labelText)) { + endIsSpace = true; + } + + this.accessKeyParens = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "span" + ); + this.appendChild(this.accessKeyParens); + if (gInsertSeparator && !endIsSpace) { + this.accessKeyParens.textContent = " ("; + } else { + this.accessKeyParens.textContent = "("; + } + this.accessKeySpan.textContent = accessKey.toUpperCase(); + this.accessKeyParens.appendChild(this.accessKeySpan); + if (!colonHidden) { + this.accessKeyParens.appendChild(document.createTextNode(")")); + } else { + this.accessKeyParens.appendChild(document.createTextNode("):")); + } + } + } + + customElements.define("label", MozTextLabel); + + function mergeElement(element) { + // If the element has been removed already, return: + if (!element.isConnected) { + return; + } + if (element.previousSibling instanceof Text) { + element.previousSibling.appendData(element.textContent); + } else { + element.parentNode.insertBefore(element.firstChild, element); + } + element.remove(); + } + + function wrapChar(parent, element, index) { + let treeWalker = document.createNodeIterator( + parent, + NodeFilter.SHOW_TEXT, + null + ); + let node = treeWalker.nextNode(); + while (index >= node.length) { + index -= node.length; + node = treeWalker.nextNode(); + } + if (index) { + node = node.splitText(index); + } + + node.parentNode.insertBefore(element, node); + if (node.length > 1) { + node.splitText(1); + } + element.appendChild(node); + } + + class MozTextLink extends MozXULTextElement { + constructor() { + super(); + + this.addEventListener( + "click", + event => { + if (event.button == 0 || event.button == 1) { + this.open(event); + } + }, + true + ); + + this.addEventListener("keypress", event => { + if (event.keyCode != KeyEvent.DOM_VK_RETURN) { + return; + } + this.click(); + }); + } + + connectedCallback() { + this.classList.add("text-link"); + } + + set href(val) { + this.setAttribute("href", val); + return val; + } + + get href() { + return this.getAttribute("href"); + } + + open(aEvent) { + var href = this.href; + if (!href || this.disabled || aEvent.defaultPrevented) { + return; + } + + var uri = null; + try { + const nsISSM = Ci.nsIScriptSecurityManager; + const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + nsISSM + ); + + uri = Services.io.newURI(href); + + let principal; + if (this.getAttribute("useoriginprincipal") == "true") { + principal = this.nodePrincipal; + } else { + principal = secMan.createNullPrincipal({}); + } + try { + secMan.checkLoadURIWithPrincipal( + principal, + uri, + nsISSM.DISALLOW_INHERIT_PRINCIPAL + ); + } catch (ex) { + var msg = + "Error: Cannot open a " + + uri.scheme + + ": link using \ + the text-link binding."; + Cu.reportError(msg); + return; + } + + const cID = "@mozilla.org/uriloader/external-protocol-service;1"; + const nsIEPS = Ci.nsIExternalProtocolService; + var protocolSvc = Cc[cID].getService(nsIEPS); + + // if the scheme is not an exposed protocol, then opening this link + // should be deferred to the system's external protocol handler + if (!protocolSvc.isExposedProtocol(uri.scheme)) { + protocolSvc.loadURI(uri, principal); + aEvent.preventDefault(); + return; + } + } catch (ex) { + Cu.reportError(ex); + } + + aEvent.preventDefault(); + href = uri ? uri.spec : href; + + // Try handing off the link to the host application, e.g. for + // opening it in a tabbed browser. + var linkHandled = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + linkHandled.data = false; + let { shiftKey, ctrlKey, metaKey, altKey, button } = aEvent; + let data = { shiftKey, ctrlKey, metaKey, altKey, button, href }; + Services.obs.notifyObservers( + linkHandled, + "handle-xul-text-link", + JSON.stringify(data) + ); + if (linkHandled.data) { + return; + } + + // otherwise, fall back to opening the anchor directly + var win = window; + if (window.isChromeWindow) { + while (win.opener && !win.opener.closed) { + win = win.opener; + } + } + win.open(href, "_blank", "noopener"); + } + } + + customElements.define("text-link", MozTextLink, { extends: "label" }); +} diff --git a/toolkit/content/widgets/timekeeper.js b/toolkit/content/widgets/timekeeper.js new file mode 100644 index 0000000000..9e53ce602f --- /dev/null +++ b/toolkit/content/widgets/timekeeper.js @@ -0,0 +1,445 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * TimeKeeper keeps track of the time states. Given min, max, step, and + * format (12/24hr), TimeKeeper will determine the ranges of possible + * selections, and whether or not the current time state is out of range + * or off step. + * + * @param {Object} props + * { + * {Date} min + * {Date} max + * {Number} step + * {String} format: Either "12" or "24" + * } + */ +function TimeKeeper(props) { + this.props = props; + this.state = { time: new Date(0), ranges: {} }; +} + +{ + const DAY_PERIOD_IN_HOURS = 12, + SECOND_IN_MS = 1000, + MINUTE_IN_MS = 60000, + HOUR_IN_MS = 3600000, + DAY_PERIOD_IN_MS = 43200000, + DAY_IN_MS = 86400000, + TIME_FORMAT_24 = "24"; + + TimeKeeper.prototype = { + /** + * Getters for different time units. + * @return {Number} + */ + get hour() { + return this.state.time.getUTCHours(); + }, + get minute() { + return this.state.time.getUTCMinutes(); + }, + get second() { + return this.state.time.getUTCSeconds(); + }, + get millisecond() { + return this.state.time.getUTCMilliseconds(); + }, + get dayPeriod() { + // 0 stands for AM and 12 for PM + return this.state.time.getUTCHours() < DAY_PERIOD_IN_HOURS + ? 0 + : DAY_PERIOD_IN_HOURS; + }, + + /** + * Get the ranges of different time units. + * @return {Object} + * { + * {Array<Number>} dayPeriod + * {Array<Number>} hours + * {Array<Number>} minutes + * {Array<Number>} seconds + * {Array<Number>} milliseconds + * } + */ + get ranges() { + return this.state.ranges; + }, + + /** + * Set new time, check if the current state is valid, and set ranges. + * + * @param {Object} timeState: The new time + * { + * {Number} hour [optional] + * {Number} minute [optional] + * {Number} second [optional] + * {Number} millisecond [optional] + * } + */ + setState(timeState) { + const { min, max } = this.props; + const { hour, minute, second, millisecond } = timeState; + + if (hour != undefined) { + this.state.time.setUTCHours(hour); + } + if (minute != undefined) { + this.state.time.setUTCMinutes(minute); + } + if (second != undefined) { + this.state.time.setUTCSeconds(second); + } + if (millisecond != undefined) { + this.state.time.setUTCMilliseconds(millisecond); + } + + this.state.isOffStep = this._isOffStep(this.state.time); + this.state.isOutOfRange = this.state.time < min || this.state.time > max; + this.state.isInvalid = this.state.isOutOfRange || this.state.isOffStep; + + this._setRanges(this.dayPeriod, this.hour, this.minute, this.second); + }, + + /** + * Set day-period (AM/PM) + * @param {Number} dayPeriod: 0 as AM, 12 as PM + */ + setDayPeriod(dayPeriod) { + if (dayPeriod == this.dayPeriod) { + return; + } + + if (dayPeriod == 0) { + this.setState({ hour: this.hour - DAY_PERIOD_IN_HOURS }); + } else { + this.setState({ hour: this.hour + DAY_PERIOD_IN_HOURS }); + } + }, + + /** + * Set hour in 24hr format (0 ~ 23) + * @param {Number} hour + */ + setHour(hour) { + this.setState({ hour }); + }, + + /** + * Set minute (0 ~ 59) + * @param {Number} minute + */ + setMinute(minute) { + this.setState({ minute }); + }, + + /** + * Set second (0 ~ 59) + * @param {Number} second + */ + setSecond(second) { + this.setState({ second }); + }, + + /** + * Set millisecond (0 ~ 999) + * @param {Number} millisecond + */ + setMillisecond(millisecond) { + this.setState({ millisecond }); + }, + + /** + * Calculate the range of possible choices for each time unit. + * Reuse the old result if the input has not changed. + * + * @param {Number} dayPeriod + * @param {Number} hour + * @param {Number} minute + * @param {Number} second + */ + _setRanges(dayPeriod, hour, minute, second) { + this.state.ranges.dayPeriod = + this.state.ranges.dayPeriod || this._getDayPeriodRange(); + + if (this.state.dayPeriod != dayPeriod) { + this.state.ranges.hours = this._getHoursRange(dayPeriod); + } + + if (this.state.hour != hour) { + this.state.ranges.minutes = this._getMinutesRange(hour); + } + + if (this.state.hour != hour || this.state.minute != minute) { + this.state.ranges.seconds = this._getSecondsRange(hour, minute); + } + + if ( + this.state.hour != hour || + this.state.minute != minute || + this.state.second != second + ) { + this.state.ranges.milliseconds = this._getMillisecondsRange( + hour, + minute, + second + ); + } + + // Save the time states for comparison. + this.state.dayPeriod = dayPeriod; + this.state.hour = hour; + this.state.minute = minute; + this.state.second = second; + }, + + /** + * Get the AM/PM range. Return an empty array if in 24hr mode. + * + * @return {Array<Number>} + */ + _getDayPeriodRange() { + if (this.props.format == TIME_FORMAT_24) { + return []; + } + + const start = 0; + const end = DAY_IN_MS - 1; + const minStep = DAY_PERIOD_IN_MS; + const formatter = time => + new Date(time).getUTCHours() < DAY_PERIOD_IN_HOURS + ? 0 + : DAY_PERIOD_IN_HOURS; + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Get the hours range. + * + * @param {Number} dayPeriod + * @return {Array<Number>} + */ + _getHoursRange(dayPeriod) { + const { format } = this.props; + const start = format == "24" ? 0 : dayPeriod * HOUR_IN_MS; + const end = format == "24" ? DAY_IN_MS - 1 : start + DAY_PERIOD_IN_MS - 1; + const minStep = HOUR_IN_MS; + const formatter = time => new Date(time).getUTCHours(); + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Get the minutes range + * + * @param {Number} hour + * @return {Array<Number>} + */ + _getMinutesRange(hour) { + const start = hour * HOUR_IN_MS; + const end = start + HOUR_IN_MS - 1; + const minStep = MINUTE_IN_MS; + const formatter = time => new Date(time).getUTCMinutes(); + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Get the seconds range + * + * @param {Number} hour + * @param {Number} minute + * @return {Array<Number>} + */ + _getSecondsRange(hour, minute) { + const start = hour * HOUR_IN_MS + minute * MINUTE_IN_MS; + const end = start + MINUTE_IN_MS - 1; + const minStep = SECOND_IN_MS; + const formatter = time => new Date(time).getUTCSeconds(); + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Get the milliseconds range + * @param {Number} hour + * @param {Number} minute + * @param {Number} second + * @return {Array<Number>} + */ + _getMillisecondsRange(hour, minute, second) { + const start = + hour * HOUR_IN_MS + minute * MINUTE_IN_MS + second * SECOND_IN_MS; + const end = start + SECOND_IN_MS - 1; + const minStep = 1; + const formatter = time => new Date(time).getUTCMilliseconds(); + + return this._getSteps(start, end, minStep, formatter); + }, + + /** + * Calculate the range of possible steps. + * + * @param {Number} startValue: Start time in ms + * @param {Number} endValue: End time in ms + * @param {Number} minStep: Smallest step in ms for the time unit + * @param {Function} formatter: Outputs time in a particular format + * @return {Array<Object>} + * { + * {Number} value + * {Boolean} enabled + * } + */ + _getSteps(startValue, endValue, minStep, formatter) { + const { min, max, step } = this.props; + // The timeStep should be big enough so that there won't be + // duplications. Ex: minimum step for minute should be 60000ms, + // if smaller than that, next step might return the same minute. + const timeStep = Math.max(minStep, step); + + // Make sure the starting point and end point is not off step + let time = + min.valueOf() + + Math.ceil((startValue - min.valueOf()) / timeStep) * timeStep; + let maxValue = + min.valueOf() + + Math.floor((max.valueOf() - min.valueOf()) / step) * step; + let steps = []; + + // Increment by timeStep until reaching the end of the range. + while (time <= endValue) { + steps.push({ + value: formatter(time), + // Check if the value is within the min and max. If it's out of range, + // also check for the case when minStep is too large, and has stepped out + // of range when it should be enabled. + enabled: + (time >= min.valueOf() && time <= max.valueOf()) || + (time > maxValue && + startValue <= maxValue && + endValue >= maxValue && + formatter(time) == formatter(maxValue)), + }); + time += timeStep; + } + + return steps; + }, + + /** + * A generic function for stepping up or down from a value of a range. + * It stops at the upper and lower limits. + * + * @param {Number} current: The current value + * @param {Number} offset: The offset relative to current value + * @param {Array<Object>} range: List of possible steps + * @return {Number} The new value + */ + _step(current, offset, range) { + const index = range.findIndex(step => step.value == current); + const newIndex = + offset > 0 + ? Math.min(index + offset, range.length - 1) + : Math.max(index + offset, 0); + return range[newIndex].value; + }, + + /** + * Step up or down AM/PM + * + * @param {Number} offset + */ + stepDayPeriodBy(offset) { + const current = this.dayPeriod; + const dayPeriod = this._step( + current, + offset, + this.state.ranges.dayPeriod + ); + + if (current != dayPeriod) { + this.hour < DAY_PERIOD_IN_HOURS + ? this.setState({ hour: this.hour + DAY_PERIOD_IN_HOURS }) + : this.setState({ hour: this.hour - DAY_PERIOD_IN_HOURS }); + } + }, + + /** + * Step up or down hours + * + * @param {Number} offset + */ + stepHourBy(offset) { + const current = this.hour; + const hour = this._step(current, offset, this.state.ranges.hours); + + if (current != hour) { + this.setState({ hour }); + } + }, + + /** + * Step up or down minutes + * + * @param {Number} offset + */ + stepMinuteBy(offset) { + const current = this.minute; + const minute = this._step(current, offset, this.state.ranges.minutes); + + if (current != minute) { + this.setState({ minute }); + } + }, + + /** + * Step up or down seconds + * + * @param {Number} offset + */ + stepSecondBy(offset) { + const current = this.second; + const second = this._step(current, offset, this.state.ranges.seconds); + + if (current != second) { + this.setState({ second }); + } + }, + + /** + * Step up or down milliseconds + * + * @param {Number} offset + */ + stepMillisecondBy(offset) { + const current = this.milliseconds; + const millisecond = this._step( + current, + offset, + this.state.ranges.millisecond + ); + + if (current != millisecond) { + this.setState({ millisecond }); + } + }, + + /** + * Checks if the time state is off step. + * + * @param {Date} time + * @return {Boolean} + */ + _isOffStep(time) { + const { min, step } = this.props; + + return (time.valueOf() - min.valueOf()) % step != 0; + }, + }; +} diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js new file mode 100644 index 0000000000..645391ae73 --- /dev/null +++ b/toolkit/content/widgets/timepicker.js @@ -0,0 +1,288 @@ +/* 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/. */ + +/* import-globals-from timekeeper.js */ +/* import-globals-from spinner.js */ + +"use strict"; + +function TimePicker(context) { + this.context = context; + this._attachEventListeners(); +} + +{ + const DAY_PERIOD_IN_HOURS = 12, + DAY_IN_MS = 86400000; + + TimePicker.prototype = { + /** + * Initializes the time picker. Set the default states and properties. + * @param {Object} props + * { + * {Number} hour [optional]: Hour in 24 hours format (0~23), default is current hour + * {Number} minute [optional]: Minute (0~59), default is current minute + * {Number} min: Minimum time, in ms + * {Number} max: Maximum time, in ms + * {Number} step: Step size in ms + * {String} format [optional]: "12" for 12 hours, "24" for 24 hours format + * {String} locale [optional]: User preferred locale + * } + */ + init(props) { + this.props = props || {}; + this._setDefaultState(); + this._createComponents(); + this._setComponentStates(); + }, + + /* + * Set initial time states. If there's no hour & minute, it will + * use the current time. The Time module keeps track of the time states, + * and calculates the valid options given the time, min, max, step, + * and format (12 or 24). + */ + _setDefaultState() { + const { hour, minute, min, max, step, format } = this.props; + const now = new Date(); + + let timerHour = hour == undefined ? now.getHours() : hour; + let timerMinute = minute == undefined ? now.getMinutes() : minute; + let timeKeeper = new TimeKeeper({ + min: new Date(Number.isNaN(min) ? 0 : min), + max: new Date(Number.isNaN(max) ? DAY_IN_MS - 1 : max), + step, + format: format || "12", + }); + timeKeeper.setState({ hour: timerHour, minute: timerMinute }); + + this.state = { timeKeeper }; + }, + + /** + * Initalize the spinner components. + */ + _createComponents() { + const { locale, format } = this.props; + const { timeKeeper } = this.state; + + const wrapSetValueFn = setTimeFunction => { + return value => { + setTimeFunction(value); + this._setComponentStates(); + this._dispatchState(); + }; + }; + const numberFormat = new Intl.NumberFormat(locale).format; + + this.components = { + hour: new Spinner( + { + setValue: wrapSetValueFn(value => { + timeKeeper.setHour(value); + this.state.isHourSet = true; + }), + getDisplayString: hour => { + if (format == "24") { + return numberFormat(hour); + } + // Hour 0 in 12 hour format is displayed as 12. + const hourIn12 = hour % DAY_PERIOD_IN_HOURS; + return hourIn12 == 0 ? numberFormat(12) : numberFormat(hourIn12); + }, + }, + this.context + ), + minute: new Spinner( + { + setValue: wrapSetValueFn(value => { + timeKeeper.setMinute(value); + this.state.isMinuteSet = true; + }), + getDisplayString: minute => numberFormat(minute), + }, + this.context + ), + }; + + this._insertLayoutElement({ + tag: "div", + textContent: ":", + className: "colon", + insertBefore: this.components.minute.elements.container, + }); + + // The AM/PM spinner is only available in 12hr mode + // TODO: Replace AM & PM string with localized string + if (format == "12") { + this.components.dayPeriod = new Spinner( + { + setValue: wrapSetValueFn(value => { + timeKeeper.setDayPeriod(value); + this.state.isDayPeriodSet = true; + }), + getDisplayString: dayPeriod => (dayPeriod == 0 ? "AM" : "PM"), + hideButtons: true, + }, + this.context + ); + + this._insertLayoutElement({ + tag: "div", + className: "spacer", + insertBefore: this.components.dayPeriod.elements.container, + }); + } + }, + + /** + * Insert element for layout purposes. + * + * @param {Object} + * { + * {String} tag: The tag to create + * {DOMElement} insertBefore: The DOM node to insert before + * {String} className [optional]: Class name + * {String} textContent [optional]: Text content + * } + */ + _insertLayoutElement({ tag, insertBefore, className, textContent }) { + let el = document.createElement(tag); + el.textContent = textContent; + el.className = className; + this.context.insertBefore(el, insertBefore); + }, + + /** + * Set component states. + */ + _setComponentStates() { + const { timeKeeper, isHourSet, isMinuteSet, isDayPeriodSet } = this.state; + const isInvalid = timeKeeper.state.isInvalid; + // Value is set to min if it's first opened and time state is invalid + const setToMinValue = + !isHourSet && !isMinuteSet && !isDayPeriodSet && isInvalid; + + this.components.hour.setState({ + value: setToMinValue + ? timeKeeper.ranges.hours[0].value + : timeKeeper.hour, + items: timeKeeper.ranges.hours, + isInfiniteScroll: true, + isValueSet: isHourSet, + isInvalid, + }); + + this.components.minute.setState({ + value: setToMinValue + ? timeKeeper.ranges.minutes[0].value + : timeKeeper.minute, + items: timeKeeper.ranges.minutes, + isInfiniteScroll: true, + isValueSet: isMinuteSet, + isInvalid, + }); + + // The AM/PM spinner is only available in 12hr mode + if (this.props.format == "12") { + this.components.dayPeriod.setState({ + value: setToMinValue + ? timeKeeper.ranges.dayPeriod[0].value + : timeKeeper.dayPeriod, + items: timeKeeper.ranges.dayPeriod, + isInfiniteScroll: false, + isValueSet: isDayPeriodSet, + isInvalid, + }); + } + }, + + /** + * Dispatch CustomEvent to pass the state of picker to the panel. + */ + _dispatchState() { + const { hour, minute } = this.state.timeKeeper; + const { isHourSet, isMinuteSet, isDayPeriodSet } = this.state; + // The panel is listening to window for postMessage event, so we + // do postMessage to itself to send data to input boxes. + window.postMessage( + { + name: "PickerPopupChanged", + detail: { + hour, + minute, + isHourSet, + isMinuteSet, + isDayPeriodSet, + }, + }, + "*" + ); + }, + _attachEventListeners() { + window.addEventListener("message", this); + document.addEventListener("mousedown", this); + }, + + /** + * Handle events. + * + * @param {Event} event + */ + handleEvent(event) { + switch (event.type) { + case "message": { + this.handleMessage(event); + break; + } + case "mousedown": { + // Use preventDefault to keep focus on input boxes + event.preventDefault(); + event.target.setCapture(); + break; + } + } + }, + + /** + * Handle postMessage events. + * + * @param {Event} event + */ + handleMessage(event) { + switch (event.data.name) { + case "PickerSetValue": { + this.set(event.data.detail); + break; + } + case "PickerInit": { + this.init(event.data.detail); + break; + } + } + }, + + /** + * Set the time state and update the components with the new state. + * + * @param {Object} timeState + * { + * {Number} hour [optional] + * {Number} minute [optional] + * {Number} second [optional] + * {Number} millisecond [optional] + * } + */ + set(timeState) { + if (timeState.hour != undefined) { + this.state.isHourSet = true; + } + if (timeState.minute != undefined) { + this.state.isMinuteSet = true; + } + this.state.timeKeeper.setState(timeState); + this._setComponentStates(); + }, + }; +} diff --git a/toolkit/content/widgets/toolbarbutton.js b/toolkit/content/widgets/toolbarbutton.js new file mode 100644 index 0000000000..6b4f5449e5 --- /dev/null +++ b/toolkit/content/widgets/toolbarbutton.js @@ -0,0 +1,227 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const KEEP_CHILDREN = new Set([ + "observes", + "template", + "menupopup", + "panel", + "tooltip", + ]); + + window.addEventListener( + "popupshowing", + e => { + if (e.originalTarget.ownerDocument != document) { + return; + } + + e.originalTarget.setAttribute("hasbeenopened", "true"); + for (let el of e.originalTarget.querySelectorAll("toolbarbutton")) { + el.render(); + } + }, + { capture: true } + ); + + class MozToolbarbutton extends MozElements.ButtonBase { + static get inheritedAttributes() { + // Note: if you remove 'wrap' or 'label' from the inherited attributes, + // you'll need to add them to observedAttributes. + return { + ".toolbarbutton-icon": + "validate,src=image,label,type,consumeanchor,triggeringprincipal=iconloadingprincipal", + ".toolbarbutton-text": "accesskey,crop,dragover-top,wrap", + ".toolbarbutton-menu-dropmarker": "disabled,label", + + ".toolbarbutton-badge": "text=badge,style=badgeStyle", + }; + } + + static get fragment() { + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <image class="toolbarbutton-icon"></image> + <label class="toolbarbutton-text" crop="right" flex="1"></label> + `), + true + ); + Object.defineProperty(this, "fragment", { value: frag }); + return frag; + } + + static get badgedFragment() { + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <stack class="toolbarbutton-badge-stack"> + <image class="toolbarbutton-icon"/> + <html:label class="toolbarbutton-badge"/> + </stack> + <label class="toolbarbutton-text" crop="right" flex="1"/> + `), + true + ); + Object.defineProperty(this, "badgedFragment", { value: frag }); + return frag; + } + + static get dropmarkerFragment() { + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <dropmarker type="menu" class="toolbarbutton-menu-dropmarker"></dropmarker> + `), + true + ); + Object.defineProperty(this, "dropmarkerFragment", { value: frag }); + return frag; + } + + get _hasRendered() { + return this.querySelector(":scope > .toolbarbutton-text") != null; + } + + get _textNode() { + let node = this.getElementForAttrInheritance(".toolbarbutton-text"); + if (node) { + Object.defineProperty(this, "_textNode", { value: node }); + } + return node; + } + + _setLabel() { + let label = this.getAttribute("label") || ""; + let hasLabel = this.hasAttribute("label"); + if (this.getAttribute("wrap") == "true") { + this._textNode.removeAttribute("value"); + this._textNode.textContent = label; + } else { + this._textNode.textContent = ""; + if (hasLabel) { + this._textNode.setAttribute("value", label); + } else { + this._textNode.removeAttribute("value"); + } + } + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue === newValue || !this.initializedAttributeInheritance) { + return; + } + // Deal with single/multiline label inheritance: + if (name == "label" || name == "wrap") { + this._setLabel(); + } + // The normal implementation will deal with everything else. + super.attributeChangedCallback(name, oldValue, newValue); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + // Defer creating DOM elements for content inside popups. + // These will be added in the popupshown handler above. + let panel = this.closest("panel"); + if (panel && !panel.hasAttribute("hasbeenopened")) { + return; + } + + this.render(); + } + + render() { + if (this._hasRendered) { + return; + } + + let badged = this.getAttribute("badged") == "true"; + + if (badged) { + let moveChildren = []; + for (let child of this.children) { + if (!KEEP_CHILDREN.has(child.tagName)) { + moveChildren.push(child); + } + } + + this.appendChild(this.constructor.badgedFragment.cloneNode(true)); + + if (this.hasAttribute("wantdropmarker")) { + this.appendChild(this.constructor.dropmarkerFragment.cloneNode(true)); + } + + if (moveChildren.length) { + let { badgeStack, icon } = this; + for (let child of moveChildren) { + badgeStack.insertBefore(child, icon); + } + } + } else { + let moveChildren = []; + for (let child of this.children) { + if (!KEEP_CHILDREN.has(child.tagName) && child.tagName != "box") { + // XBL toolbarbutton doesn't insert any anonymous content + // if it has a child of any other type + return; + } + + if (child.tagName == "box") { + moveChildren.push(child); + } + } + + this.appendChild(this.constructor.fragment.cloneNode(true)); + + if (this.hasAttribute("wantdropmarker")) { + this.appendChild(this.constructor.dropmarkerFragment.cloneNode(true)); + } + + // XBL toolbarbutton explicitly places any <box> children + // right before the menu marker. + for (let child of moveChildren) { + this.insertBefore(child, this.lastChild); + } + } + + this.initializeAttributeInheritance(); + this._setLabel(); + } + + get icon() { + return this.querySelector(".toolbarbutton-icon"); + } + + get badgeLabel() { + return this.querySelector(".toolbarbutton-badge"); + } + + get badgeStack() { + return this.querySelector(".toolbarbutton-badge-stack"); + } + + get multilineLabel() { + if (this.getAttribute("wrap") == "true") { + return this._textNode; + } + return null; + } + + get dropmarker() { + return this.querySelector(".toolbarbutton-menu-dropmarker"); + } + + get menupopup() { + return this.querySelector("menupopup"); + } + } + + customElements.define("toolbarbutton", MozToolbarbutton); +} diff --git a/toolkit/content/widgets/tree.js b/toolkit/content/widgets/tree.js new file mode 100644 index 0000000000..a8639f8029 --- /dev/null +++ b/toolkit/content/widgets/tree.js @@ -0,0 +1,1644 @@ +/* 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/. */ + +/* globals XULTreeElement */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + class MozTreeChildren extends MozElements.BaseControl { + constructor() { + super(); + + /** + * If there is no modifier key, we select on mousedown, not + * click, so that drags work correctly. + */ + this.addEventListener("mousedown", event => { + if (this.parentNode.disabled) { + return; + } + if ( + ((!event.getModifierState("Accel") || + !this.parentNode.pageUpOrDownMovesSelection) && + !event.shiftKey && + !event.metaKey) || + this.parentNode.view.selection.single + ) { + var b = this.parentNode; + var cell = b.getCellAt(event.clientX, event.clientY); + var view = this.parentNode.view; + + // save off the last selected row + this._lastSelectedRow = cell.row; + + if (cell.row == -1) { + return; + } + + if (cell.childElt == "twisty") { + return; + } + + if (cell.col && event.button == 0) { + if (cell.col.cycler) { + view.cycleCell(cell.row, cell.col); + return; + } else if (cell.col.type == window.TreeColumn.TYPE_CHECKBOX) { + if ( + this.parentNode.editable && + cell.col.editable && + view.isEditable(cell.row, cell.col) + ) { + var value = view.getCellValue(cell.row, cell.col); + value = value == "true" ? "false" : "true"; + view.setCellValue(cell.row, cell.col, value); + return; + } + } + } + + if (!view.selection.isSelected(cell.row)) { + view.selection.select(cell.row); + b.ensureRowIsVisible(cell.row); + } + } + }); + + /** + * On a click (up+down on the same item), deselect everything + * except this item. + */ + this.addEventListener("click", event => { + if (event.button != 0) { + return; + } + if (this.parentNode.disabled) { + return; + } + var b = this.parentNode; + var cell = b.getCellAt(event.clientX, event.clientY); + var view = this.parentNode.view; + + if (cell.row == -1) { + return; + } + + if (cell.childElt == "twisty") { + if ( + view.selection.currentIndex >= 0 && + view.isContainerOpen(cell.row) + ) { + var parentIndex = view.getParentIndex(view.selection.currentIndex); + while (parentIndex >= 0 && parentIndex != cell.row) { + parentIndex = view.getParentIndex(parentIndex); + } + if (parentIndex == cell.row) { + var parentSelectable = true; + if (parentSelectable) { + view.selection.select(parentIndex); + } + } + } + this.parentNode.changeOpenState(cell.row); + return; + } + + if (!view.selection.single) { + var augment = event.getModifierState("Accel"); + if (event.shiftKey) { + view.selection.rangedSelect(-1, cell.row, augment); + b.ensureRowIsVisible(cell.row); + return; + } + if (augment) { + view.selection.toggleSelect(cell.row); + b.ensureRowIsVisible(cell.row); + view.selection.currentIndex = cell.row; + return; + } + } + + /* We want to deselect all the selected items except what was + clicked, UNLESS it was a right-click. We have to do this + in click rather than mousedown so that you can drag a + selected group of items */ + + if (!cell.col) { + return; + } + + // if the last row has changed in between the time we + // mousedown and the time we click, don't fire the select handler. + // see bug #92366 + if ( + !cell.col.cycler && + this._lastSelectedRow == cell.row && + cell.col.type != window.TreeColumn.TYPE_CHECKBOX + ) { + view.selection.select(cell.row); + b.ensureRowIsVisible(cell.row); + } + }); + + /** + * double-click + */ + this.addEventListener("dblclick", event => { + if (this.parentNode.disabled) { + return; + } + var tree = this.parentNode; + var view = this.parentNode.view; + var row = view.selection.currentIndex; + + if (row == -1) { + return; + } + + var cell = tree.getCellAt(event.clientX, event.clientY); + + if (cell.childElt != "twisty") { + this.parentNode.startEditing(row, cell.col); + } + + if (this.parentNode._editingColumn || !view.isContainer(row)) { + return; + } + + // Cyclers and twisties respond to single clicks, not double clicks + if (cell.col && !cell.col.cycler && cell.childElt != "twisty") { + this.parentNode.changeOpenState(row); + } + }); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.setAttribute("slot", "treechildren"); + + this._lastSelectedRow = -1; + + if ("_ensureColumnOrder" in this.parentNode) { + this.parentNode._ensureColumnOrder(); + } + } + } + + customElements.define("treechildren", MozTreeChildren); + + class MozTreecolPicker extends MozElements.BaseControl { + static get entities() { + return ["chrome://global/locale/tree.dtd"]; + } + + static get markup() { + return ` + <image class="tree-columnpicker-icon"></image> + <menupopup anonid="popup"> + <menuseparator anonid="menuseparator"></menuseparator> + <menuitem anonid="menuitem" label="&restoreColumnOrder.label;"></menuitem> + </menupopup> + `; + } + constructor() { + super(); + + this.addEventListener("command", event => { + if (event.originalTarget == this) { + var popup = this.querySelector('[anonid="popup"]'); + this.buildPopup(popup); + popup.openPopup(this, "after_end"); + } else { + var tree = this.parentNode.parentNode; + tree.stopEditing(true); + var menuitem = this.querySelector('[anonid="menuitem"]'); + if (event.originalTarget == menuitem) { + this.style.MozBoxOrdinalGroup = ""; + tree._ensureColumnOrder(tree.NATURAL_ORDER); + } else { + var colindex = event.originalTarget.getAttribute("colindex"); + var column = tree.columns[colindex]; + if (column) { + var element = column.element; + if (element.getAttribute("hidden") == "true") { + element.setAttribute("hidden", "false"); + } else { + element.setAttribute("hidden", "true"); + } + } + } + } + }); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.textContent = ""; + this.appendChild(this.constructor.fragment); + } + + buildPopup(aPopup) { + // We no longer cache the picker content, remove the old content related to + // the cols - menuitem and separator should stay. + aPopup.querySelectorAll("[colindex]").forEach(e => { + e.remove(); + }); + + var refChild = aPopup.firstChild; + + var tree = this.parentNode.parentNode; + for ( + var currCol = tree.columns.getFirstColumn(); + currCol; + currCol = currCol.getNext() + ) { + // Construct an entry for each column in the row, unless + // it is not being shown. + var currElement = currCol.element; + if (!currElement.hasAttribute("ignoreincolumnpicker")) { + var popupChild = document.createXULElement("menuitem"); + popupChild.setAttribute("type", "checkbox"); + var columnName = + currElement.getAttribute("display") || + currElement.getAttribute("label"); + popupChild.setAttribute("label", columnName); + popupChild.setAttribute("colindex", currCol.index); + if (currElement.getAttribute("hidden") != "true") { + popupChild.setAttribute("checked", "true"); + } + if (currCol.primary) { + popupChild.setAttribute("disabled", "true"); + } + if (currElement.hasAttribute("closemenu")) { + popupChild.setAttribute( + "closemenu", + currElement.getAttribute("closemenu") + ); + } + aPopup.insertBefore(popupChild, refChild); + } + } + + var hidden = !tree.enableColumnDrag; + aPopup.querySelectorAll(":scope > :not([colindex])").forEach(e => { + e.hidden = hidden; + }); + } + } + + customElements.define("treecolpicker", MozTreecolPicker); + + class MozTreecol extends MozElements.BaseControl { + static get inheritedAttributes() { + return { + ".treecol-sortdirection": "sortdirection,hidden=hideheader", + ".treecol-text": "value=label,crop", + }; + } + + static get markup() { + return ` + <label class="treecol-text" flex="1" crop="right"></label> + <image class="treecol-sortdirection"></image> + `; + } + + constructor() { + super(); + + this.addEventListener("mousedown", event => { + if (event.button != 0) { + return; + } + if (this.parentNode.parentNode.enableColumnDrag) { + var XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var cols = this.parentNode.getElementsByTagNameNS(XUL_NS, "treecol"); + + // only start column drag operation if there are at least 2 visible columns + var visible = 0; + for (var i = 0; i < cols.length; ++i) { + if (cols[i].getBoundingClientRect().width > 0) { + ++visible; + } + } + + if (visible > 1) { + window.addEventListener("mousemove", this._onDragMouseMove, true); + window.addEventListener("mouseup", this._onDragMouseUp, true); + document.treecolDragging = this; + this.mDragGesturing = true; + this.mStartDragX = event.clientX; + this.mStartDragY = event.clientY; + } + } + }); + + this.addEventListener("click", event => { + if (event.button != 0) { + return; + } + if (event.target != event.originalTarget) { + return; + } + + // On Windows multiple clicking on tree columns only cycles one time + // every 2 clicks. + if (AppConstants.platform == "win" && event.detail % 2 == 0) { + return; + } + + var tree = this.parentNode.parentNode; + if (tree.columns) { + tree.view.cycleHeader(tree.columns.getColumnFor(this)); + } + }); + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.textContent = ""; + this.appendChild(this.constructor.fragment); + this.initializeAttributeInheritance(); + if (this.hasAttribute("ordinal")) { + this.style.MozBoxOrdinalGroup = this.getAttribute("ordinal"); + } + } + + set ordinal(val) { + this.style.MozBoxOrdinalGroup = val; + this.setAttribute("ordinal", val); + return val; + } + + get ordinal() { + var val = this.style.MozBoxOrdinalGroup; + if (val == "") { + return "1"; + } + + return "" + (val == "0" ? 0 : parseInt(val)); + } + + get _previousVisibleColumn() { + var tree = this.parentNode.parentNode; + let sib = tree.columns.getColumnFor(this).previousColumn; + while (sib) { + if (sib.element && sib.element.getBoundingClientRect().width > 0) { + return sib.element; + } + + sib = sib.previousColumn; + } + + return null; + } + + _onDragMouseMove(aEvent) { + var col = document.treecolDragging; + if (!col) { + return; + } + + // determine if we have moved the mouse far enough + // to initiate a drag + if (col.mDragGesturing) { + if ( + Math.abs(aEvent.clientX - col.mStartDragX) < 5 && + Math.abs(aEvent.clientY - col.mStartDragY) < 5 + ) { + return; + } + col.mDragGesturing = false; + col.setAttribute("dragging", "true"); + window.addEventListener("click", col._onDragMouseClick, true); + } + + var pos = {}; + var targetCol = col.parentNode.parentNode._getColumnAtX( + aEvent.clientX, + 0.5, + pos + ); + + // bail if we haven't mousemoved to a different column + if (col.mTargetCol == targetCol && col.mTargetDir == pos.value) { + return; + } + + var tree = col.parentNode.parentNode; + var sib; + var column; + if (col.mTargetCol) { + // remove previous insertbefore/after attributes + col.mTargetCol.removeAttribute("insertbefore"); + col.mTargetCol.removeAttribute("insertafter"); + column = tree.columns.getColumnFor(col.mTargetCol); + tree.invalidateColumn(column); + sib = col.mTargetCol._previousVisibleColumn; + if (sib) { + sib.removeAttribute("insertafter"); + column = tree.columns.getColumnFor(sib); + tree.invalidateColumn(column); + } + col.mTargetCol = null; + col.mTargetDir = null; + } + + if (targetCol) { + // set insertbefore/after attributes + if (pos.value == "after") { + targetCol.setAttribute("insertafter", "true"); + } else { + targetCol.setAttribute("insertbefore", "true"); + sib = targetCol._previousVisibleColumn; + if (sib) { + sib.setAttribute("insertafter", "true"); + column = tree.columns.getColumnFor(sib); + tree.invalidateColumn(column); + } + } + column = tree.columns.getColumnFor(targetCol); + tree.invalidateColumn(column); + col.mTargetCol = targetCol; + col.mTargetDir = pos.value; + } + } + + _onDragMouseUp(aEvent) { + var col = document.treecolDragging; + if (!col) { + return; + } + + if (!col.mDragGesturing) { + if (col.mTargetCol) { + // remove insertbefore/after attributes + var before = col.mTargetCol.hasAttribute("insertbefore"); + col.mTargetCol.removeAttribute( + before ? "insertbefore" : "insertafter" + ); + + var sib = col.mTargetCol._previousVisibleColumn; + if (before && sib) { + sib.removeAttribute("insertafter"); + } + + // Move the column only if it will result in a different column + // ordering + var move = true; + + // If this is a before move and the previous visible column is + // the same as the column we're moving, don't move + if (before && col == sib) { + move = false; + } else if (!before && col == col.mTargetCol) { + // If this is an after move and the column we're moving is + // the same as the target column, don't move. + move = false; + } + + if (move) { + col.parentNode.parentNode._reorderColumn( + col, + col.mTargetCol, + before + ); + } + + // repaint to remove lines + col.parentNode.parentNode.invalidate(); + + col.mTargetCol = null; + } + } else { + col.mDragGesturing = false; + } + + document.treecolDragging = null; + col.removeAttribute("dragging"); + + window.removeEventListener("mousemove", col._onDragMouseMove, true); + window.removeEventListener("mouseup", col._onDragMouseUp, true); + // we have to wait for the click event to fire before removing + // cancelling handler + var clickHandler = function(handler) { + window.removeEventListener("click", handler, true); + }; + window.setTimeout(clickHandler, 0, col._onDragMouseClick); + } + + _onDragMouseClick(aEvent) { + // prevent click event from firing after column drag and drop + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + } + + customElements.define("treecol", MozTreecol); + + class MozTreecols extends MozElements.BaseControl { + static get inheritedAttributes() { + return { + treecolpicker: "tooltiptext=pickertooltiptext", + }; + } + + static get markup() { + return ` + <treecolpicker class="treecol-image" fixed="true"></treecolpicker> + `; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.setAttribute("slot", "treecols"); + + if (!this.querySelector("treecolpicker")) { + this.appendChild(this.constructor.fragment); + this.initializeAttributeInheritance(); + } + + // Set resizeafter="farthest" on the splitters if nothing else has been + // specified. + for (let splitter of this.getElementsByTagName("splitter")) { + if (!splitter.hasAttribute("resizeafter")) { + splitter.setAttribute("resizeafter", "farthest"); + } + } + } + } + + customElements.define("treecols", MozTreecols); + + class MozTree extends MozElements.BaseControlMixin( + MozElements.MozElementMixin(XULTreeElement) + ) { + static get markup() { + return ` + <html:link rel="stylesheet" href="chrome://global/content/widgets.css" /> + <html:slot name="treecols"></html:slot> + <stack class="tree-stack" flex="1"> + <hbox class="tree-rows" flex="1"> + <hbox flex="1" class="tree-bodybox"> + <html:slot name="treechildren"></html:slot> + </hbox> + <scrollbar height="0" minwidth="0" minheight="0" orient="vertical" + class="hidevscroll-scrollbar scrollbar-topmost" + ></scrollbar> + </hbox> + <html:input class="tree-input" type="text" hidden="true"/> + </stack> + <hbox class="hidehscroll-box"> + <scrollbar orient="horizontal" flex="1" increment="16" class="scrollbar-topmost" ></scrollbar> + <scrollcorner class="hidevscroll-scrollcorner"></scrollcorner> + </hbox> + `; + } + + constructor() { + super(); + + // These enumerated constants are used as the first argument to + // _ensureColumnOrder to specify what column ordering should be used. + this.CURRENT_ORDER = 0; + this.NATURAL_ORDER = 1; // The original order, which is the DOM ordering + + this.attachShadow({ mode: "open" }); + let handledElements = this.constructor.fragment.querySelectorAll( + "scrollbar,scrollcorner" + ); + let stopAndPrevent = e => { + e.stopPropagation(); + e.preventDefault(); + }; + let stopProp = e => e.stopPropagation(); + for (let el of handledElements) { + el.addEventListener("click", stopAndPrevent); + el.addEventListener("contextmenu", stopAndPrevent); + el.addEventListener("dblclick", stopProp); + el.addEventListener("command", stopProp); + } + this.shadowRoot.appendChild(this.constructor.fragment); + } + + static get inheritedAttributes() { + return { + ".hidehscroll-box": "collapsed=hidehscroll", + ".hidevscroll-scrollbar": "collapsed=hidevscroll", + ".hidevscroll-scrollcorner": "collapsed=hidevscroll", + }; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + if (!this._eventListenersSetup) { + this._eventListenersSetup = true; + this.setupEventListeners(); + } + + this.setAttribute("hidevscroll", "true"); + this.setAttribute("hidehscroll", "true"); + this.setAttribute("clickthrough", "never"); + + this.initializeAttributeInheritance(); + + this.pageUpOrDownMovesSelection = AppConstants.platform != "macosx"; + + this._inputField = null; + + this._editingRow = -1; + + this._editingColumn = null; + + this._columnsDirty = true; + + this._lastKeyTime = 0; + + this._incrementalString = ""; + + this._touchY = -1; + } + + setupEventListeners() { + this.addEventListener("underflow", event => { + // Scrollport event orientation + // 0: vertical + // 1: horizontal + // 2: both (not used) + if (event.target.tagName != "treechildren") { + return; + } + if (event.detail == 1) { + this.setAttribute("hidehscroll", "true"); + } else if (event.detail == 0) { + this.setAttribute("hidevscroll", "true"); + } + event.stopPropagation(); + }); + + this.addEventListener("overflow", event => { + if (event.target.tagName != "treechildren") { + return; + } + if (event.detail == 1) { + this.removeAttribute("hidehscroll"); + } else if (event.detail == 0) { + this.removeAttribute("hidevscroll"); + } + event.stopPropagation(); + }); + + this.addEventListener("touchstart", event => { + function isScrollbarElement(target) { + return ( + (target.localName == "thumb" || target.localName == "slider") && + target.namespaceURI == + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + ); + } + if ( + event.touches.length > 1 || + isScrollbarElement(event.touches[0].target) + ) { + // Multiple touch points detected, abort. In particular this aborts + // the panning gesture when the user puts a second finger down after + // already panning with one finger. Aborting at this point prevents + // the pan gesture from being resumed until all fingers are lifted + // (as opposed to when the user is back down to one finger). + // Additionally, if the user lands on the scrollbar don't use this + // code for scrolling, instead allow gecko to handle scrollbar + // interaction normally. + this._touchY = -1; + } else { + this._touchY = event.touches[0].screenY; + } + }); + + this.addEventListener("touchmove", event => { + if (event.touches.length == 1 && this._touchY >= 0) { + var deltaY = this._touchY - event.touches[0].screenY; + var lines = Math.trunc(deltaY / this.rowHeight); + if (Math.abs(lines) > 0) { + this.scrollByLines(lines); + deltaY -= lines * this.rowHeight; + this._touchY = event.touches[0].screenY + deltaY; + } + event.preventDefault(); + } + }); + + this.addEventListener("touchend", event => { + this._touchY = -1; + }); + + // This event doesn't retarget, so listen on the shadow DOM directly + this.shadowRoot.addEventListener("MozMousePixelScroll", event => { + if ( + !( + this.getAttribute("allowunderflowscroll") == "true" && + this.getAttribute("hidevscroll") == "true" + ) + ) { + event.preventDefault(); + } + }); + + // This event doesn't retarget, so listen on the shadow DOM directly + this.shadowRoot.addEventListener("DOMMouseScroll", event => { + if ( + !( + this.getAttribute("allowunderflowscroll") == "true" && + this.getAttribute("hidevscroll") == "true" + ) + ) { + event.preventDefault(); + } + + if (this._editingColumn) { + return; + } + if (event.axis == event.HORIZONTAL_AXIS) { + return; + } + + var rows = event.detail; + if (rows == UIEvent.SCROLL_PAGE_UP) { + this.scrollByPages(-1); + } else if (rows == UIEvent.SCROLL_PAGE_DOWN) { + this.scrollByPages(1); + } else { + this.scrollByLines(rows); + } + }); + + this.addEventListener("MozSwipeGesture", event => { + // Figure out which row to show + let targetRow = 0; + + // Only handle swipe gestures up and down + switch (event.direction) { + case event.DIRECTION_DOWN: + targetRow = this.view.rowCount - 1; + // Fall through for actual action + case event.DIRECTION_UP: + this.ensureRowIsVisible(targetRow); + break; + } + }); + + this.addEventListener("select", event => { + if (event.originalTarget == this) { + this.stopEditing(true); + } + }); + + this.addEventListener("focus", event => { + this.focused = true; + if (this.currentIndex == -1 && this.view.rowCount > 0) { + this.currentIndex = this.getFirstVisibleRow(); + } + }); + + this.addEventListener( + "blur", + event => { + this.focused = false; + if (event.target == this.inputField) { + this.stopEditing(true); + } + }, + true + ); + + this.addEventListener("keydown", event => { + if (event.altKey) { + return; + } + + let toggleClose = () => { + if (this._editingColumn) { + return; + } + + let row = this.currentIndex; + if (row < 0) { + return; + } + + if (this.changeOpenState(this.currentIndex, false)) { + event.preventDefault(); + return; + } + + let parentIndex = this.view.getParentIndex(this.currentIndex); + if (parentIndex >= 0) { + this.view.selection.select(parentIndex); + this.ensureRowIsVisible(parentIndex); + event.preventDefault(); + } + }; + + let toggleOpen = () => { + if (this._editingColumn) { + return; + } + + let row = this.currentIndex; + if (row < 0) { + return; + } + + if (this.changeOpenState(row, true)) { + event.preventDefault(); + return; + } + let c = row + 1; + let view = this.view; + if (c < view.rowCount && view.getParentIndex(c) == row) { + // If already opened, select the first child. + // The getParentIndex test above ensures that the children + // are already populated and ready. + this.view.selection.timedSelect(c, this._selectDelay); + this.ensureRowIsVisible(c); + event.preventDefault(); + } + }; + + switch (event.keyCode) { + case KeyEvent.DOM_VK_RETURN: { + if (this._handleEnter(event)) { + event.stopPropagation(); + event.preventDefault(); + } + break; + } + case KeyEvent.DOM_VK_ESCAPE: { + if (this._editingColumn) { + this.stopEditing(false); + this.focus(); + event.stopPropagation(); + event.preventDefault(); + } + break; + } + case KeyEvent.DOM_VK_LEFT: { + if (!this.isRTL) { + toggleClose(); + } else { + toggleOpen(); + } + break; + } + case KeyEvent.DOM_VK_RIGHT: { + if (!this.isRTL) { + toggleOpen(); + } else { + toggleClose(); + } + break; + } + case KeyEvent.DOM_VK_UP: { + if (this._editingColumn) { + return; + } + + if (event.getModifierState("Shift")) { + this._moveByOffsetShift(-1, 0, event); + } else { + this._moveByOffset(-1, 0, event); + } + break; + } + case KeyEvent.DOM_VK_DOWN: { + if (this._editingColumn) { + return; + } + if (event.getModifierState("Shift")) { + this._moveByOffsetShift(1, this.view.rowCount - 1, event); + } else { + this._moveByOffset(1, this.view.rowCount - 1, event); + } + break; + } + case KeyEvent.DOM_VK_PAGE_UP: { + if (this._editingColumn) { + return; + } + + if (event.getModifierState("Shift")) { + this._moveByPageShift(-1, 0, event); + } else { + this._moveByPage(-1, 0, event); + } + break; + } + case KeyEvent.DOM_VK_PAGE_DOWN: { + if (this._editingColumn) { + return; + } + + if (event.getModifierState("Shift")) { + this._moveByPageShift(1, this.view.rowCount - 1, event); + } else { + this._moveByPage(1, this.view.rowCount - 1, event); + } + break; + } + case KeyEvent.DOM_VK_HOME: { + if (this._editingColumn) { + return; + } + + if (event.getModifierState("Shift")) { + this._moveToEdgeShift(0, event); + } else { + this._moveToEdge(0, event); + } + break; + } + case KeyEvent.DOM_VK_END: { + if (this._editingColumn) { + return; + } + + if (event.getModifierState("Shift")) { + this._moveToEdgeShift(this.view.rowCount - 1, event); + } else { + this._moveToEdge(this.view.rowCount - 1, event); + } + break; + } + } + }); + + this.addEventListener("keypress", event => { + if (this._editingColumn) { + return; + } + + if (event.charCode == " ".charCodeAt(0)) { + var c = this.currentIndex; + if ( + !this.view.selection.isSelected(c) || + (!this.view.selection.single && event.getModifierState("Accel")) + ) { + this.view.selection.toggleSelect(c); + event.preventDefault(); + } + } else if ( + !this.disableKeyNavigation && + event.charCode > 0 && + !event.altKey && + !event.getModifierState("Accel") && + !event.metaKey && + !event.ctrlKey + ) { + var l = this._keyNavigate(event); + if (l >= 0) { + this.view.selection.timedSelect(l, this._selectDelay); + this.ensureRowIsVisible(l); + } + event.preventDefault(); + } + }); + } + + get body() { + return this.treeBody; + } + + get isRTL() { + return document.defaultView.getComputedStyle(this).direction == "rtl"; + } + + set editable(val) { + if (val) { + this.setAttribute("editable", "true"); + } else { + this.removeAttribute("editable"); + } + return val; + } + + get editable() { + return this.getAttribute("editable") == "true"; + } + /** + * ///////////////// nsIDOMXULSelectControlElement ///////////////// ///////////////// nsIDOMXULMultiSelectControlElement ///////////////// + */ + set selType(val) { + this.setAttribute("seltype", val); + return val; + } + + get selType() { + return this.getAttribute("seltype"); + } + + set currentIndex(val) { + if (this.view) { + return (this.view.selection.currentIndex = val); + } + return val; + } + + get currentIndex() { + if (this.view && this.view.selection) { + return this.view.selection.currentIndex; + } + return -1; + } + + set keepCurrentInView(val) { + if (val) { + this.setAttribute("keepcurrentinview", "true"); + } else { + this.removeAttribute("keepcurrentinview"); + } + return val; + } + + get keepCurrentInView() { + return this.getAttribute("keepcurrentinview") == "true"; + } + + set enableColumnDrag(val) { + if (val) { + this.setAttribute("enableColumnDrag", "true"); + } else { + this.removeAttribute("enableColumnDrag"); + } + return val; + } + + get enableColumnDrag() { + return this.hasAttribute("enableColumnDrag"); + } + + get inputField() { + if (!this._inputField) { + this._inputField = this.shadowRoot.querySelector(".tree-input"); + this._inputField.addEventListener("blur", () => this.stopEditing(true)); + } + return this._inputField; + } + + set disableKeyNavigation(val) { + if (val) { + this.setAttribute("disableKeyNavigation", "true"); + } else { + this.removeAttribute("disableKeyNavigation"); + } + return val; + } + + get disableKeyNavigation() { + return this.hasAttribute("disableKeyNavigation"); + } + + get editingRow() { + return this._editingRow; + } + + get editingColumn() { + return this._editingColumn; + } + + set _selectDelay(val) { + this.setAttribute("_selectDelay", val); + } + + get _selectDelay() { + return this.getAttribute("_selectDelay") || 50; + } + + // The first argument (order) can be either one of these constants: + // this.CURRENT_ORDER + // this.NATURAL_ORDER + _ensureColumnOrder(order = this.CURRENT_ORDER) { + if (this.columns) { + // update the ordinal position of each column to assure that it is + // an odd number and 2 positions above its next sibling + var cols = []; + + if (order == this.CURRENT_ORDER) { + for ( + let col = this.columns.getFirstColumn(); + col; + col = col.getNext() + ) { + cols.push(col.element); + } + } else { + // order == this.NATURAL_ORDER + cols = this.getElementsByTagName("treecol"); + } + + for (let i = 0; i < cols.length; ++i) { + cols[i].ordinal = i * 2 + 1; + } + // update the ordinal positions of splitters to even numbers, so that + // they are in between columns + var splitters = this.getElementsByTagName("splitter"); + for (let i = 0; i < splitters.length; ++i) { + splitters[i].style.MozBoxOrdinalGroup = (i + 1) * 2; + } + } + } + + _reorderColumn(aColMove, aColBefore, aBefore) { + this._ensureColumnOrder(); + + var i; + var cols = []; + var col = this.columns.getColumnFor(aColBefore); + if (parseInt(aColBefore.ordinal) < parseInt(aColMove.ordinal)) { + if (aBefore) { + cols.push(aColBefore); + } + for ( + col = col.getNext(); + col.element != aColMove; + col = col.getNext() + ) { + cols.push(col.element); + } + + aColMove.ordinal = cols[0].ordinal; + for (i = 0; i < cols.length; ++i) { + cols[i].ordinal = parseInt(cols[i].ordinal) + 2; + } + } else if (aColBefore.ordinal != aColMove.ordinal) { + if (!aBefore) { + cols.push(aColBefore); + } + for ( + col = col.getPrevious(); + col.element != aColMove; + col = col.getPrevious() + ) { + cols.push(col.element); + } + + aColMove.ordinal = cols[0].ordinal; + for (i = 0; i < cols.length; ++i) { + cols[i].ordinal = parseInt(cols[i].ordinal) - 2; + } + } + } + + _getColumnAtX(aX, aThresh, aPos) { + let isRTL = this.isRTL; + + if (aPos) { + aPos.value = isRTL ? "after" : "before"; + } + + var columns = []; + var col = this.columns.getFirstColumn(); + while (col) { + columns.push(col); + col = col.getNext(); + } + if (isRTL) { + columns.reverse(); + } + var currentX = this.getBoundingClientRect().x; + var adjustedX = aX + this.horizontalPosition; + for (var i = 0; i < columns.length; ++i) { + col = columns[i]; + var cw = col.element.getBoundingClientRect().width; + if (cw > 0) { + currentX += cw; + if (currentX - cw * aThresh > adjustedX) { + return col.element; + } + } + } + + if (aPos) { + aPos.value = isRTL ? "before" : "after"; + } + return columns.pop().element; + } + + changeOpenState(row, openState) { + if (row < 0 || !this.view.isContainer(row)) { + return false; + } + + if (this.view.isContainerOpen(row) != openState) { + this.view.toggleOpenState(row); + if (row == this.currentIndex) { + // Only fire event when current row is expanded or collapsed + // because that's all the assistive technology really cares about. + var event = document.createEvent("Events"); + event.initEvent("OpenStateChange", true, true); + this.dispatchEvent(event); + } + return true; + } + return false; + } + + _keyNavigate(event) { + var key = String.fromCharCode(event.charCode).toLowerCase(); + if (event.timeStamp - this._lastKeyTime > 1000) { + this._incrementalString = key; + } else { + this._incrementalString += key; + } + this._lastKeyTime = event.timeStamp; + + var length = this._incrementalString.length; + var incrementalString = this._incrementalString; + var charIndex = 1; + while ( + charIndex < length && + incrementalString[charIndex] == incrementalString[charIndex - 1] + ) { + charIndex++; + } + // If all letters in incremental string are same, just try to match the first one + if (charIndex == length) { + length = 1; + incrementalString = incrementalString.substring(0, length); + } + + var keyCol = this.columns.getKeyColumn(); + var rowCount = this.view.rowCount; + var start = 1; + + var c = this.currentIndex; + if (length > 1) { + start = 0; + if (c < 0) { + c = 0; + } + } + + for (var i = 0; i < rowCount; i++) { + var l = (i + start + c) % rowCount; + var cellText = this.view.getCellText(l, keyCol); + cellText = cellText.substring(0, length).toLowerCase(); + if (cellText == incrementalString) { + return l; + } + } + return -1; + } + + startEditing(row, column) { + if (!this.editable) { + return false; + } + if (row < 0 || row >= this.view.rowCount || !column) { + return false; + } + if (column.type !== window.TreeColumn.TYPE_TEXT) { + return false; + } + if (column.cycler || !this.view.isEditable(row, column)) { + return false; + } + + // Beyond this point, we are going to edit the cell. + if (this._editingColumn) { + this.stopEditing(); + } + + var input = this.inputField; + + this.ensureCellIsVisible(row, column); + + // Get the coordinates of the text inside the cell. + var textRect = this.getCoordsForCellItem(row, column, "text"); + + // Get the coordinates of the cell itself. + var cellRect = this.getCoordsForCellItem(row, column, "cell"); + + // Calculate the top offset of the textbox. + var style = window.getComputedStyle(input); + var topadj = parseInt(style.borderTopWidth) + parseInt(style.paddingTop); + input.style.top = `${textRect.y - topadj}px`; + + // The leftside of the textbox is aligned to the left side of the text + // in LTR mode, and left side of the cell in RTL mode. + var left, widthdiff; + if (style.direction == "rtl") { + left = cellRect.x; + widthdiff = cellRect.x - textRect.x; + } else { + left = textRect.x; + widthdiff = textRect.x - cellRect.x; + } + + input.style.left = `${left}px`; + input.style.height = `${textRect.height + + topadj + + parseInt(style.borderBottomWidth) + + parseInt(style.paddingBottom)}px`; + input.style.width = `${cellRect.width - widthdiff}px`; + input.hidden = false; + + input.value = this.view.getCellText(row, column); + + input.select(); + input.focus(); + + this._editingRow = row; + this._editingColumn = column; + this.setAttribute("editing", "true"); + + this.invalidateCell(row, column); + return true; + } + + stopEditing(accept) { + if (!this._editingColumn) { + return; + } + + var input = this.inputField; + var editingRow = this._editingRow; + var editingColumn = this._editingColumn; + this._editingRow = -1; + this._editingColumn = null; + + // `this.view` could be null if the tree was hidden before we were called. + if (accept && this.view) { + var value = input.value; + this.view.setCellText(editingRow, editingColumn, value); + } + input.hidden = true; + input.value = ""; + this.removeAttribute("editing"); + } + + _moveByOffset(offset, edge, event) { + event.preventDefault(); + + if (this.view.rowCount == 0) { + return; + } + + if (event.getModifierState("Accel") && this.view.selection.single) { + this.scrollByLines(offset); + return; + } + + var c = this.currentIndex + offset; + if (offset > 0 ? c > edge : c < edge) { + if ( + this.view.selection.isSelected(edge) && + this.view.selection.count <= 1 + ) { + return; + } + c = edge; + } + + if (!event.getModifierState("Accel")) { + this.view.selection.timedSelect(c, this._selectDelay); + } + // Ctrl+Up/Down moves the anchor without selecting + else { + this.currentIndex = c; + } + this.ensureRowIsVisible(c); + } + + _moveByOffsetShift(offset, edge, event) { + event.preventDefault(); + + if (this.view.rowCount == 0) { + return; + } + + if (this.view.selection.single) { + this.scrollByLines(offset); + return; + } + + if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) { + this.view.selection.timedSelect(0, this._selectDelay); + return; + } + + var c = this.currentIndex; + if (c == -1) { + c = 0; + } + + if (c == edge) { + if (this.view.selection.isSelected(c)) { + return; + } + } + + // Extend the selection from the existing pivot, if any + this.view.selection.rangedSelect( + -1, + c + offset, + event.getModifierState("Accel") + ); + this.ensureRowIsVisible(c + offset); + } + + _moveByPage(offset, edge, event) { + event.preventDefault(); + + if (this.view.rowCount == 0) { + return; + } + + if (this.pageUpOrDownMovesSelection == event.getModifierState("Accel")) { + this.scrollByPages(offset); + return; + } + + if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) { + this.view.selection.timedSelect(0, this._selectDelay); + return; + } + + var c = this.currentIndex; + if (c == -1) { + return; + } + + if (c == edge && this.view.selection.isSelected(c)) { + this.ensureRowIsVisible(c); + return; + } + var i = this.getFirstVisibleRow(); + var p = this.getPageLength(); + + if (offset > 0) { + i += p - 1; + if (c >= i) { + i = c + p; + this.ensureRowIsVisible(i > edge ? edge : i); + } + i = i > edge ? edge : i; + } else if (c <= i) { + i = c <= p ? 0 : c - p; + this.ensureRowIsVisible(i); + } + this.view.selection.timedSelect(i, this._selectDelay); + } + + _moveByPageShift(offset, edge, event) { + event.preventDefault(); + + if (this.view.rowCount == 0) { + return; + } + + if ( + this.view.rowCount == 1 && + !this.view.selection.isSelected(0) && + !(this.pageUpOrDownMovesSelection == event.getModifierState("Accel")) + ) { + this.view.selection.timedSelect(0, this._selectDelay); + return; + } + + if (this.view.selection.single) { + return; + } + + var c = this.currentIndex; + if (c == -1) { + return; + } + if (c == edge && this.view.selection.isSelected(c)) { + this.ensureRowIsVisible(edge); + return; + } + var i = this.getFirstVisibleRow(); + var p = this.getPageLength(); + + if (offset > 0) { + i += p - 1; + if (c >= i) { + i = c + p; + this.ensureRowIsVisible(i > edge ? edge : i); + } + // Extend the selection from the existing pivot, if any + this.view.selection.rangedSelect( + -1, + i > edge ? edge : i, + event.getModifierState("Accel") + ); + } else { + if (c <= i) { + i = c <= p ? 0 : c - p; + this.ensureRowIsVisible(i); + } + // Extend the selection from the existing pivot, if any + this.view.selection.rangedSelect( + -1, + i, + event.getModifierState("Accel") + ); + } + } + + _moveToEdge(edge, event) { + event.preventDefault(); + + if (this.view.rowCount == 0) { + return; + } + + if ( + this.view.selection.isSelected(edge) && + this.view.selection.count == 1 + ) { + this.currentIndex = edge; + return; + } + + // Normal behaviour is to select the first/last row + if (!event.getModifierState("Accel")) { + this.view.selection.timedSelect(edge, this._selectDelay); + } + // In a multiselect tree Ctrl+Home/End moves the anchor + else if (!this.view.selection.single) { + this.currentIndex = edge; + } + + this.ensureRowIsVisible(edge); + } + + _moveToEdgeShift(edge, event) { + event.preventDefault(); + + if (this.view.rowCount == 0) { + return; + } + + if (this.view.rowCount == 1 && !this.view.selection.isSelected(0)) { + this.view.selection.timedSelect(0, this._selectDelay); + return; + } + + if ( + this.view.selection.single || + (this.view.selection.isSelected(edge) && + this.view.selection.isSelected(this.currentIndex)) + ) { + return; + } + + // Extend the selection from the existing pivot, if any. + // -1 doesn't work here, so using currentIndex instead + this.view.selection.rangedSelect( + this.currentIndex, + edge, + event.getModifierState("Accel") + ); + + this.ensureRowIsVisible(edge); + } + + _handleEnter(event) { + if (this._editingColumn) { + this.stopEditing(true); + this.focus(); + return true; + } + + return this.changeOpenState(this.currentIndex); + } + } + + MozXULElement.implementCustomInterface(MozTree, [ + Ci.nsIDOMXULMultiSelectControlElement, + ]); + customElements.define("tree", MozTree); +} diff --git a/toolkit/content/widgets/videocontrols.js b/toolkit/content/widgets/videocontrols.js new file mode 100644 index 0000000000..c34f4d082a --- /dev/null +++ b/toolkit/content/widgets/videocontrols.js @@ -0,0 +1,3339 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is a UA widget. It runs in per-origin UA widget scope, +// to be loaded by UAWidgetsChild.jsm. + +/* + * This is the class of entry. It will construct the actual implementation + * according to the value of the "controls" property. + */ +this.VideoControlsWidget = class { + constructor(shadowRoot, prefs) { + this.shadowRoot = shadowRoot; + this.prefs = prefs; + this.element = shadowRoot.host; + this.document = this.element.ownerDocument; + this.window = this.document.defaultView; + + this.isMobile = this.window.navigator.appVersion.includes("Android"); + } + + /* + * Callback called by UAWidgets right after constructor. + */ + onsetup() { + this.switchImpl(); + } + + /* + * Callback called by UAWidgets when the "controls" property changes. + */ + onchange() { + this.switchImpl(); + } + + /* + * Actually switch the implementation. + * - With "controls" set, the VideoControlsImplWidget controls should load. + * - Without it, on mobile, the NoControlsMobileImplWidget should load, so + * the user could see the click-to-play button when the video/audio is blocked. + * - Without it, on desktop, the NoControlsPictureInPictureImpleWidget should load + * if the video is being viewed in Picture-in-Picture. + */ + switchImpl() { + let newImpl; + let pageURI = this.document.documentURI; + if (this.element.controls) { + newImpl = VideoControlsImplWidget; + } else if (this.isMobile) { + newImpl = NoControlsMobileImplWidget; + } else if (VideoControlsWidget.isPictureInPictureVideo(this.element)) { + newImpl = NoControlsPictureInPictureImplWidget; + } else if ( + pageURI.startsWith("http://") || + pageURI.startsWith("https://") + ) { + newImpl = NoControlsDesktopImplWidget; + } + + // Skip if we are asked to load the same implementation, and + // the underlying element state hasn't changed in ways that we + // care about. This can happen if the property is set again + // without a value change. + if ( + this.impl && + this.impl.constructor == newImpl && + this.impl.elementStateMatches(this.element) + ) { + return; + } + if (this.impl) { + this.impl.destructor(); + this.shadowRoot.firstChild.remove(); + } + if (newImpl) { + this.impl = new newImpl(this.shadowRoot, this.prefs); + + this.mDirection = "ltr"; + let intlUtils = this.window.intlUtils; + if (intlUtils) { + this.mDirection = intlUtils.isAppLocaleRTL() ? "rtl" : "ltr"; + } + + this.impl.onsetup(this.mDirection); + } else { + this.impl = undefined; + } + } + + destructor() { + if (!this.impl) { + return; + } + this.impl.destructor(); + this.shadowRoot.firstChild.remove(); + delete this.impl; + } + + onPrefChange(prefName, prefValue) { + this.prefs[prefName] = prefValue; + + if (!this.impl) { + return; + } + + this.impl.onPrefChange(prefName, prefValue); + } + + static isPictureInPictureVideo(someVideo) { + return someVideo.isCloningElementVisually; + } + + /** + * Returns true if a <video> meets the requirements to show the Picture-in-Picture + * toggle. Those requirements currently are: + * + * 1. The video must be 45 seconds in length or longer. + * 2. Neither the width or the height of the video can be less than 140px. + * 3. The video must have audio. + * 4. The video must not a MediaStream video (Bug 1592539) + * + * This can be overridden via the + * media.videocontrols.picture-in-picture.video-toggle.always-show pref, which + * is mostly used for testing. + * + * @param {Object} prefs + * The preferences set that was passed to the UAWidget. + * @param {Element} someVideo + * The <video> to test. + * @param {Object} reflowedDimensions + * An object representing the reflowed dimensions of the <video>. Properties + * are: + * + * videoWidth (Number): + * The width of the video in pixels. + * + * videoHeight (Number): + * The height of the video in pixels. + * + * @return {Boolean} + */ + static shouldShowPictureInPictureToggle( + prefs, + someVideo, + reflowedDimensions + ) { + if ( + prefs["media.videocontrols.picture-in-picture.video-toggle.always-show"] + ) { + return true; + } + + const MIN_VIDEO_LENGTH = + prefs[ + "media.videocontrols.picture-in-picture.video-toggle.min-video-secs" + ]; + + if (someVideo.duration < MIN_VIDEO_LENGTH) { + return false; + } + + const MIN_VIDEO_DIMENSION = 140; // pixels + if ( + reflowedDimensions.videoWidth < MIN_VIDEO_DIMENSION || + reflowedDimensions.videoHeight < MIN_VIDEO_DIMENSION + ) { + return false; + } + + if (!someVideo.mozHasAudio) { + return false; + } + + return true; + } + + /** + * Some variations on the Picture-in-Picture toggle are being experimented with. + * These variations have slightly different setup parameters from the currently + * shipping toggle, so this method sets up the experimental toggles in the event + * that they're being used. It also will enable the appropriate stylesheet for + * the preferred toggle experiment. + * + * @param {Object} prefs + * The preferences set that was passed to the UAWidget. + * @param {ShadowRoot} shadowRoot + * The shadowRoot of the <video> element where the video controls are. + * @param {Element} toggle + * The toggle element. + * @param {Object} reflowedDimensions + * An object representing the reflowed dimensions of the <video>. Properties + * are: + * + * videoWidth (Number): + * The width of the video in pixels. + * + * videoHeight (Number): + * The height of the video in pixels. + */ + static setupToggle(prefs, toggle, reflowedDimensions) { + // These thresholds are all in pixels + const SMALL_VIDEO_WIDTH_MAX = 320; + const MEDIUM_VIDEO_WIDTH_MAX = 720; + + let isSmall = reflowedDimensions.videoWidth <= SMALL_VIDEO_WIDTH_MAX; + toggle.toggleAttribute("small-video", isSmall); + toggle.toggleAttribute( + "medium-video", + !isSmall && reflowedDimensions.videoWidth <= MEDIUM_VIDEO_WIDTH_MAX + ); + + toggle.setAttribute( + "position", + prefs["media.videocontrols.picture-in-picture.video-toggle.position"] + ); + toggle.toggleAttribute( + "has-used", + prefs["media.videocontrols.picture-in-picture.video-toggle.has-used"] + ); + } +}; + +this.VideoControlsImplWidget = class { + constructor(shadowRoot, prefs) { + this.shadowRoot = shadowRoot; + this.prefs = prefs; + this.element = shadowRoot.host; + this.document = this.element.ownerDocument; + this.window = this.document.defaultView; + } + + onsetup(direction) { + this.generateContent(); + + this.shadowRoot.firstElementChild.setAttribute("localedir", direction); + + this.Utils = { + debug: false, + video: null, + videocontrols: null, + controlBar: null, + playButton: null, + muteButton: null, + volumeControl: null, + durationLabel: null, + positionLabel: null, + scrubber: null, + progressBar: null, + bufferBar: null, + statusOverlay: null, + controlsSpacer: null, + clickToPlay: null, + controlsOverlay: null, + fullscreenButton: null, + layoutControls: null, + isShowingPictureInPictureMessage: false, + + textTracksCount: 0, + videoEvents: [ + "play", + "pause", + "ended", + "volumechange", + "loadeddata", + "loadstart", + "timeupdate", + "progress", + "playing", + "waiting", + "canplay", + "canplaythrough", + "seeking", + "seeked", + "emptied", + "loadedmetadata", + "error", + "suspend", + "stalled", + "mozvideoonlyseekbegin", + "mozvideoonlyseekcompleted", + "durationchange", + ], + + showHours: false, + firstFrameShown: false, + timeUpdateCount: 0, + maxCurrentTimeSeen: 0, + isPausedByDragging: false, + _isAudioOnly: false, + + get isAudioOnly() { + return this._isAudioOnly; + }, + set isAudioOnly(val) { + this._isAudioOnly = val; + this.setFullscreenButtonState(); + this.updatePictureInPictureToggleDisplay(); + + if (!this.isTopLevelSyntheticDocument) { + return; + } + if (this._isAudioOnly) { + this.video.style.height = this.controlBarMinHeight + "px"; + this.video.style.width = "66%"; + } else { + this.video.style.removeProperty("height"); + this.video.style.removeProperty("width"); + } + }, + + suppressError: false, + + setupStatusFader(immediate) { + // Since the play button will be showing, we don't want to + // show the throbber behind it. The throbber here will + // only show if needed after the play button has been pressed. + if (!this.clickToPlay.hidden) { + this.startFadeOut(this.statusOverlay, true); + return; + } + + var show = false; + if ( + this.video.seeking || + (this.video.error && !this.suppressError) || + this.video.networkState == this.video.NETWORK_NO_SOURCE || + (this.video.networkState == this.video.NETWORK_LOADING && + (this.video.paused || this.video.ended + ? this.video.readyState < this.video.HAVE_CURRENT_DATA + : this.video.readyState < this.video.HAVE_FUTURE_DATA)) || + (this.timeUpdateCount <= 1 && + !this.video.ended && + this.video.readyState < this.video.HAVE_FUTURE_DATA && + this.video.networkState == this.video.NETWORK_LOADING) + ) { + show = true; + } + + // Explicitly hide the status fader if this + // is audio only until bug 619421 is fixed. + if (this.isAudioOnly) { + show = false; + } + + if (this._showThrobberTimer) { + show = true; + } + + this.log( + "Status overlay: seeking=" + + this.video.seeking + + " error=" + + this.video.error + + " readyState=" + + this.video.readyState + + " paused=" + + this.video.paused + + " ended=" + + this.video.ended + + " networkState=" + + this.video.networkState + + " timeUpdateCount=" + + this.timeUpdateCount + + " _showThrobberTimer=" + + this._showThrobberTimer + + " --> " + + (show ? "SHOW" : "HIDE") + ); + this.startFade(this.statusOverlay, show, immediate); + }, + + /* + * Set the initial state of the controls. The UA widget is normally created along + * with video element, but could be attached at any point (eg, if the video is + * removed from the document and then reinserted). Thus, some one-time events may + * have already fired, and so we'll need to explicitly check the initial state. + */ + setupInitialState() { + this.setPlayButtonState(this.video.paused); + + this.setFullscreenButtonState(); + + var duration = Math.round(this.video.duration * 1000); // in ms + var currentTime = Math.round(this.video.currentTime * 1000); // in ms + this.log( + "Initial playback position is at " + currentTime + " of " + duration + ); + // It would be nice to retain maxCurrentTimeSeen, but it would be difficult + // to determine if the media source changed while we were detached. + this.initPositionDurationBox(); + this.maxCurrentTimeSeen = currentTime; + this.showPosition(currentTime, duration); + + // If we have metadata, check if this is a <video> without + // video data, or a video with no audio track. + if (this.video.readyState >= this.video.HAVE_METADATA) { + if ( + this.video.localName == "video" && + (this.video.videoWidth == 0 || this.video.videoHeight == 0) + ) { + this.isAudioOnly = true; + } + + // We have to check again if the media has audio here. + if (!this.isAudioOnly && !this.video.mozHasAudio) { + this.muteButton.setAttribute("noAudio", "true"); + this.muteButton.disabled = true; + } + } + + // We should lock the orientation if we are already in + // fullscreen. + this.updateOrientationState(this.isVideoInFullScreen); + + // The video itself might not be fullscreen, but part of the + // document might be, in which case we set this attribute to + // apply any styles for the DOM fullscreen case. + if (this.document.fullscreenElement) { + this.videocontrols.setAttribute("inDOMFullscreen", true); + } + + if (this.isAudioOnly) { + this.startFadeOut(this.clickToPlay, true); + } + + // If the first frame hasn't loaded, kick off a throbber fade-in. + if (this.video.readyState >= this.video.HAVE_CURRENT_DATA) { + this.firstFrameShown = true; + } + + // We can't determine the exact buffering status, but do know if it's + // fully loaded. (If it's still loading, it will fire a progress event + // and we'll figure out the exact state then.) + this.bufferBar.max = 100; + if (this.video.readyState >= this.video.HAVE_METADATA) { + this.showBuffered(); + } else { + this.bufferBar.value = 0; + } + + // Set the current status icon. + if (this.hasError()) { + this.startFadeOut(this.clickToPlay, true); + this.statusIcon.setAttribute("type", "error"); + this.updateErrorText(); + this.setupStatusFader(true); + } else if (VideoControlsWidget.isPictureInPictureVideo(this.video)) { + this.setShowPictureInPictureMessage(true); + } + + if (this.video.readyState >= this.video.HAVE_METADATA) { + // According to the spec[1], at the HAVE_METADATA (or later) state, we know + // the video duration and dimensions, which means we can calculate whether or + // not to show the Picture-in-Picture toggle now. + // + // [1]: https://www.w3.org/TR/html50/embedded-content-0.html#dom-media-have_metadata + this.updatePictureInPictureToggleDisplay(); + } + + let adjustableControls = [ + ...this.prioritizedControls, + this.controlBar, + this.clickToPlay, + ]; + + let throwOnGet = { + get() { + throw new Error("Please don't trigger reflow. See bug 1493525."); + }, + }; + + for (let control of adjustableControls) { + if (!control) { + break; + } + + Object.defineProperties(control, { + // We should directly access CSSOM to get pre-defined style instead of + // retrieving computed dimensions from layout. + minWidth: { + get: () => { + let controlId = control.id; + let propertyName = `--${controlId}-width`; + if (control.modifier) { + propertyName += "-" + control.modifier; + } + let preDefinedSize = this.controlBarComputedStyles.getPropertyValue( + propertyName + ); + + // The stylesheet from <link> might not be loaded if the + // element was inserted into a hidden iframe. + // We can safely return 0 here for now, given that the controls + // will be resized again, by the resizevideocontrols event, + // from nsVideoFrame, when the element is visible. + if (!preDefinedSize) { + return 0; + } + + return parseInt(preDefinedSize, 10); + }, + }, + offsetLeft: throwOnGet, + offsetTop: throwOnGet, + offsetWidth: throwOnGet, + offsetHeight: throwOnGet, + offsetParent: throwOnGet, + clientLeft: throwOnGet, + clientTop: throwOnGet, + clientWidth: throwOnGet, + clientHeight: throwOnGet, + getClientRects: throwOnGet, + getBoundingClientRect: throwOnGet, + isAdjustableControl: { + value: true, + }, + modifier: { + value: "", + writable: true, + }, + isWanted: { + value: true, + writable: true, + }, + hidden: { + set: v => { + control._isHiddenExplicitly = v; + control._updateHiddenAttribute(); + }, + get: () => { + return ( + control.hasAttribute("hidden") || + control.classList.contains("fadeout") + ); + }, + }, + hiddenByAdjustment: { + set: v => { + control._isHiddenByAdjustment = v; + control._updateHiddenAttribute(); + }, + get: () => control._isHiddenByAdjustment, + }, + _isHiddenByAdjustment: { + value: false, + writable: true, + }, + _isHiddenExplicitly: { + value: false, + writable: true, + }, + _updateHiddenAttribute: { + value: () => { + if ( + control._isHiddenExplicitly || + control._isHiddenByAdjustment + ) { + control.setAttribute("hidden", ""); + } else { + control.removeAttribute("hidden"); + } + }, + }, + }); + } + this.adjustControlSize(); + + // Can only update the volume controls once we've computed + // _volumeControlWidth, since the volume slider implementation + // depends on it. + this.updateVolumeControls(); + }, + + updatePictureInPictureToggleDisplay() { + if (this.isAudioOnly) { + this.pictureInPictureToggle.setAttribute("hidden", true); + return; + } + + if ( + this.pipToggleEnabled && + !this.isShowingPictureInPictureMessage && + VideoControlsWidget.shouldShowPictureInPictureToggle( + this.prefs, + this.video, + this.reflowedDimensions + ) + ) { + this.pictureInPictureToggle.removeAttribute("hidden"); + VideoControlsWidget.setupToggle( + this.prefs, + this.pictureInPictureToggle, + this.reflowedDimensions + ); + } else { + this.pictureInPictureToggle.setAttribute("hidden", true); + } + }, + + setupNewLoadState() { + // For videos with |autoplay| set, we'll leave the controls initially hidden, + // so that they don't get in the way of the playing video. Otherwise we'll + // go ahead and reveal the controls now, so they're an obvious user cue. + var shouldShow = + !this.dynamicControls || (this.video.paused && !this.video.autoplay); + // Hide the overlay if the video time is non-zero or if an error occurred to workaround bug 718107. + let shouldClickToPlayShow = + shouldShow && + !this.isAudioOnly && + this.video.currentTime == 0 && + !this.hasError() && + !this.isShowingPictureInPictureMessage; + this.startFade(this.clickToPlay, shouldClickToPlayShow, true); + this.startFade(this.controlBar, shouldShow, true); + }, + + get dynamicControls() { + // Don't fade controls for <audio> elements. + var enabled = !this.isAudioOnly; + + // Allow tests to explicitly suppress the fading of controls. + if (this.video.hasAttribute("mozNoDynamicControls")) { + enabled = false; + } + + // If the video hits an error, suppress controls if it + // hasn't managed to do anything else yet. + if (!this.firstFrameShown && this.hasError()) { + enabled = false; + } + + return enabled; + }, + + updateVolume() { + const volume = this.volumeControl.value; + this.setVolume(volume / 100); + }, + + updateVolumeControls() { + var volume = this.video.muted ? 0 : this.video.volume; + var volumePercentage = Math.round(volume * 100); + this.updateMuteButtonState(); + this.volumeControl.value = volumePercentage; + }, + + /* + * We suspend a video element's video decoder if the video + * element is invisible. However, resuming the video decoder + * takes time and we show the throbber UI if it takes more than + * 250 ms. + * + * When an already-suspended video element becomes visible, we + * resume its video decoder immediately and queue a video-only seek + * task to seek the resumed video decoder to the current position; + * meanwhile, we also file a "mozvideoonlyseekbegin" event which + * we used to start the timer here. + * + * Once the queued seek operation is done, we dispatch a + * "canplay" event which indicates that the resuming operation + * is completed. + */ + SHOW_THROBBER_TIMEOUT_MS: 250, + _showThrobberTimer: null, + _delayShowThrobberWhileResumingVideoDecoder() { + this._showThrobberTimer = this.window.setTimeout(() => { + this.statusIcon.setAttribute("type", "throbber"); + // Show the throbber immediately since we have waited for SHOW_THROBBER_TIMEOUT_MS. + // We don't want to wait for another animation delay(750ms) and the + // animation duration(300ms). + this.setupStatusFader(true); + }, this.SHOW_THROBBER_TIMEOUT_MS); + }, + _cancelShowThrobberWhileResumingVideoDecoder() { + if (this._showThrobberTimer) { + this.window.clearTimeout(this._showThrobberTimer); + this._showThrobberTimer = null; + } + }, + + handleEvent(aEvent) { + if (!aEvent.isTrusted) { + this.log("Drop untrusted event ----> " + aEvent.type); + return; + } + + this.log("Got event ----> " + aEvent.type); + + if (this.videoEvents.includes(aEvent.type)) { + this.handleVideoEvent(aEvent); + } else { + this.handleControlEvent(aEvent); + } + }, + + handleVideoEvent(aEvent) { + switch (aEvent.type) { + case "play": + this.setPlayButtonState(false); + this.setupStatusFader(); + if ( + !this._triggeredByControls && + this.dynamicControls && + this.isTouchControls + ) { + this.startFadeOut(this.controlBar); + } + if (!this._triggeredByControls) { + this.startFadeOut(this.clickToPlay, true); + } + this._triggeredByControls = false; + break; + case "pause": + // Little white lie: if we've internally paused the video + // while dragging the scrubber, don't change the button state. + if (!this.scrubber.isDragging) { + this.setPlayButtonState(true); + } + this.setupStatusFader(); + break; + case "ended": + this.setPlayButtonState(true); + // We throttle timechange events, so the thumb might not be + // exactly at the end when the video finishes. + this.showPosition( + Math.round(this.video.currentTime * 1000), + Math.round(this.video.duration * 1000) + ); + this.startFadeIn(this.controlBar); + this.setupStatusFader(); + break; + case "volumechange": + this.updateVolumeControls(); + // Show the controls to highlight the changing volume, + // but only if the click-to-play overlay has already + // been hidden (we don't hide controls when the overlay is visible). + if (this.clickToPlay.hidden && !this.isAudioOnly) { + this.startFadeIn(this.controlBar); + this.window.clearTimeout(this._hideControlsTimeout); + this._hideControlsTimeout = this.window.setTimeout( + () => this._hideControlsFn(), + this.HIDE_CONTROLS_TIMEOUT_MS + ); + } + break; + case "loadedmetadata": + // If a <video> doesn't have any video data, treat it as <audio> + // and show the controls (they won't fade back out) + if ( + this.video.localName == "video" && + (this.video.videoWidth == 0 || this.video.videoHeight == 0) + ) { + this.isAudioOnly = true; + this.startFadeOut(this.clickToPlay, true); + this.startFadeIn(this.controlBar); + this.setFullscreenButtonState(); + } + this.showPosition( + Math.round(this.video.currentTime * 1000), + Math.round(this.video.duration * 1000) + ); + if (!this.isAudioOnly && !this.video.mozHasAudio) { + this.muteButton.setAttribute("noAudio", "true"); + this.muteButton.disabled = true; + } + this.adjustControlSize(); + this.updatePictureInPictureToggleDisplay(); + break; + case "durationchange": + this.updatePictureInPictureToggleDisplay(); + break; + case "loadeddata": + this.firstFrameShown = true; + this.setupStatusFader(); + break; + case "loadstart": + this.maxCurrentTimeSeen = 0; + this.controlsSpacer.removeAttribute("aria-label"); + this.statusOverlay.removeAttribute("status"); + this.statusIcon.setAttribute("type", "throbber"); + this.isAudioOnly = this.video.localName == "audio"; + this.setPlayButtonState(true); + this.setupNewLoadState(); + this.setupStatusFader(); + break; + case "progress": + this.statusIcon.removeAttribute("stalled"); + this.showBuffered(); + this.setupStatusFader(); + break; + case "stalled": + this.statusIcon.setAttribute("stalled", "true"); + this.statusIcon.setAttribute("type", "throbber"); + this.setupStatusFader(); + break; + case "suspend": + this.setupStatusFader(); + break; + case "timeupdate": + var currentTime = Math.round(this.video.currentTime * 1000); // in ms + var duration = Math.round(this.video.duration * 1000); // in ms + + // If playing/seeking after the video ended, we won't get a "play" + // event, so update the button state here. + if (!this.video.paused) { + this.setPlayButtonState(false); + } + + this.timeUpdateCount++; + // Whether we show the statusOverlay sometimes depends + // on whether we've seen more than one timeupdate + // event (if we haven't, there hasn't been any + // "playback activity" and we may wish to show the + // statusOverlay while we wait for HAVE_ENOUGH_DATA). + // If we've seen more than 2 timeupdate events, + // the count is no longer relevant to setupStatusFader. + if (this.timeUpdateCount <= 2) { + this.setupStatusFader(); + } + + // If the user is dragging the scrubber ignore the delayed seek + // responses (don't yank the thumb away from the user) + if (this.scrubber.isDragging) { + return; + } + this.showPosition(currentTime, duration); + this.showBuffered(); + break; + case "emptied": + this.bufferBar.value = 0; + this.showPosition(0, 0); + break; + case "seeking": + this.showBuffered(); + this.statusIcon.setAttribute("type", "throbber"); + this.setupStatusFader(); + break; + case "waiting": + this.statusIcon.setAttribute("type", "throbber"); + this.setupStatusFader(); + break; + case "seeked": + case "playing": + case "canplay": + case "canplaythrough": + this.setupStatusFader(); + break; + case "error": + // We'll show the error status icon when we receive an error event + // under either of the following conditions: + // 1. The video has its error attribute set; this means we're loading + // from our src attribute, and the load failed, or we we're loading + // from source children and the decode or playback failed after we + // determined our selected resource was playable. + // 2. The video's networkState is NETWORK_NO_SOURCE. This means we we're + // loading from child source elements, but we were unable to select + // any of the child elements for playback during resource selection. + if (this.hasError()) { + this.suppressError = false; + this.startFadeOut(this.clickToPlay, true); + this.statusIcon.setAttribute("type", "error"); + this.updateErrorText(); + this.setupStatusFader(true); + // If video hasn't shown anything yet, disable the controls. + if (!this.firstFrameShown && !this.isAudioOnly) { + this.startFadeOut(this.controlBar); + } + this.controlsSpacer.removeAttribute("hideCursor"); + } + break; + case "mozvideoonlyseekbegin": + this._delayShowThrobberWhileResumingVideoDecoder(); + break; + case "mozvideoonlyseekcompleted": + this._cancelShowThrobberWhileResumingVideoDecoder(); + this.setupStatusFader(); + break; + default: + this.log("!!! media event " + aEvent.type + " not handled!"); + } + }, + + handleControlEvent(aEvent) { + switch (aEvent.type) { + case "click": + switch (aEvent.currentTarget) { + case this.muteButton: + this.toggleMute(); + break; + case this.castingButton: + this.toggleCasting(); + break; + case this.closedCaptionButton: + this.toggleClosedCaption(); + break; + case this.fullscreenButton: + this.toggleFullscreen(); + break; + case this.playButton: + case this.clickToPlay: + case this.controlsSpacer: + this.clickToPlayClickHandler(aEvent); + break; + case this.textTrackList: + const index = +aEvent.originalTarget.getAttribute("index"); + this.changeTextTrack(index); + break; + case this.videocontrols: + // Prevent any click event within media controls from dispatching through to video. + aEvent.stopPropagation(); + break; + } + break; + case "dblclick": + this.toggleFullscreen(); + break; + case "resizevideocontrols": + // Since this event come from the layout, this is the only place + // we are sure of that probing into layout won't trigger or force + // reflow. + this.reflowTriggeringCallValidator.isReflowTriggeringPropsAllowed = true; + this.updateReflowedDimensions(); + this.reflowTriggeringCallValidator.isReflowTriggeringPropsAllowed = false; + this.adjustControlSize(); + this.updatePictureInPictureToggleDisplay(); + break; + case "fullscreenchange": + this.onFullscreenChange(); + break; + case "keypress": + this.keyHandler(aEvent); + break; + case "dragstart": + aEvent.preventDefault(); // prevent dragging of controls image (bug 517114) + break; + case "input": + switch (aEvent.currentTarget) { + case this.scrubber: + this.onScrubberInput(aEvent); + break; + case this.volumeControl: + this.updateVolume(); + break; + } + break; + case "change": + switch (aEvent.currentTarget) { + case this.scrubber: + this.onScrubberChange(aEvent); + break; + case this.video.textTracks: + this.setClosedCaptionButtonState(); + break; + } + break; + case "mouseup": + // add mouseup listener additionally to handle the case that `change` event + // isn't fired when the input value before/after dragging are the same. (bug 1328061) + this.onScrubberChange(aEvent); + break; + case "addtrack": + this.onTextTrackAdd(aEvent); + break; + case "removetrack": + this.onTextTrackRemove(aEvent); + break; + case "media-videoCasting": + this.updateCasting(aEvent.detail); + break; + case "focusin": + // Show the controls to highlight the focused control, but only + // under certain conditions: + if ( + this.prefs["media.videocontrols.keyboard-tab-to-all-controls"] && + // The click-to-play overlay must already be hidden (we don't + // hide controls when the overlay is visible). + this.clickToPlay.hidden && + // Don't do this if the controls are static. + this.dynamicControls && + // If the mouse is hovering over the control bar, the controls + // are already showing and they shouldn't hide, so don't mess + // with them. + // We use "div:hover" instead of just ":hover" so this works in + // quirks mode documents. See + // https://quirks.spec.whatwg.org/#the-active-and-hover-quirk + !this.controlBar.matches("div:hover") + ) { + this.startFadeIn(this.controlBar); + this.window.clearTimeout(this._hideControlsTimeout); + this._hideControlsTimeout = this.window.setTimeout( + () => this._hideControlsFn(), + this.HIDE_CONTROLS_TIMEOUT_MS + ); + } + break; + case "mousedown": + // We only listen for mousedown on sliders. + // If this slider isn't focused already, mousedown will focus it. + // We don't want that because it will then handle additional keys. + // For example, we don't want the up/down arrow keys to seek after + // the scrubber is clicked. To prevent that, we need to redirect + // focus. However, dragging only works while the slider is focused, + // so we must redirect focus after mouseup. + if ( + this.prefs["media.videocontrols.keyboard-tab-to-all-controls"] && + !aEvent.currentTarget.matches(":focus") + ) { + aEvent.currentTarget.addEventListener( + "mouseup", + aEvent => { + if (aEvent.currentTarget.matches(":focus")) { + // We can't use target.blur() because that will blur the + // video element as well. + this.video.focus(); + } + }, + { once: true } + ); + } + break; + default: + this.log("!!! control event " + aEvent.type + " not handled!"); + } + }, + + terminate() { + if (this.videoEvents) { + for (let event of this.videoEvents) { + try { + this.video.removeEventListener(event, this, { + capture: true, + mozSystemGroup: true, + }); + } catch (ex) {} + } + } + + try { + for (let { el, type, capture = false } of this.controlsEvents) { + el.removeEventListener(type, this, { + mozSystemGroup: true, + capture, + }); + } + } catch (ex) {} + + this.window.clearTimeout(this._showControlsTimeout); + this.window.clearTimeout(this._hideControlsTimeout); + this._cancelShowThrobberWhileResumingVideoDecoder(); + + this.log("--- videocontrols terminated ---"); + }, + + hasError() { + // We either have an explicit error, or the resource selection + // algorithm is running and we've tried to load something and failed. + // Note: we don't consider the case where we've tried to load but + // there's no sources to load as an error condition, as sites may + // do this intentionally to work around requires-user-interaction to + // play restrictions, and we don't want to display a debug message + // if that's the case. + return ( + this.video.error != null || + (this.video.networkState == this.video.NETWORK_NO_SOURCE && + this.hasSources()) + ); + }, + + setShowPictureInPictureMessage(showMessage) { + if (showMessage) { + this.pictureInPictureOverlay.removeAttribute("hidden"); + } else { + this.pictureInPictureOverlay.setAttribute("hidden", true); + } + + this.isShowingPictureInPictureMessage = showMessage; + }, + + hasSources() { + if ( + this.video.hasAttribute("src") && + this.video.getAttribute("src") !== "" + ) { + return true; + } + for ( + var child = this.video.firstChild; + child !== null; + child = child.nextElementSibling + ) { + if (child instanceof this.window.HTMLSourceElement) { + return true; + } + } + return false; + }, + + updateErrorText() { + let error; + let v = this.video; + // It is possible to have both v.networkState == NETWORK_NO_SOURCE + // as well as v.error being non-null. In this case, we will show + // the v.error.code instead of the v.networkState error. + if (v.error) { + switch (v.error.code) { + case v.error.MEDIA_ERR_ABORTED: + error = "errorAborted"; + break; + case v.error.MEDIA_ERR_NETWORK: + error = "errorNetwork"; + break; + case v.error.MEDIA_ERR_DECODE: + error = "errorDecode"; + break; + case v.error.MEDIA_ERR_SRC_NOT_SUPPORTED: + error = + v.networkState == v.NETWORK_NO_SOURCE + ? "errorNoSource" + : "errorSrcNotSupported"; + break; + default: + error = "errorGeneric"; + break; + } + } else if (v.networkState == v.NETWORK_NO_SOURCE) { + error = "errorNoSource"; + } else { + return; // No error found. + } + + let label = this.shadowRoot.getElementById(error); + this.controlsSpacer.setAttribute("aria-label", label.textContent); + this.statusOverlay.setAttribute("status", error); + }, + + formatTime(aTime, showHours = false) { + // Format the duration as "h:mm:ss" or "m:ss" + aTime = Math.round(aTime / 1000); + let hours = Math.floor(aTime / 3600); + let mins = Math.floor((aTime % 3600) / 60); + let secs = Math.floor(aTime % 60); + let timeString; + if (secs < 10) { + secs = "0" + secs; + } + if (hours || showHours) { + if (mins < 10) { + mins = "0" + mins; + } + timeString = hours + ":" + mins + ":" + secs; + } else { + timeString = mins + ":" + secs; + } + return timeString; + }, + + initPositionDurationBox() { + const positionTextNode = Array.prototype.find.call( + this.positionDurationBox.childNodes, + n => !!~n.textContent.search("#1") + ); + const durationSpan = this.durationSpan; + const durationFormat = durationSpan.textContent; + const positionFormat = positionTextNode.textContent; + + durationSpan.classList.add("duration"); + durationSpan.setAttribute("role", "none"); + durationSpan.id = "durationSpan"; + + Object.defineProperties(this.positionDurationBox, { + durationSpan: { + value: durationSpan, + }, + position: { + set: v => { + positionTextNode.textContent = positionFormat.replace("#1", v); + }, + }, + duration: { + set: v => { + durationSpan.textContent = v + ? durationFormat.replace("#2", v) + : ""; + }, + }, + }); + }, + + showDuration(duration) { + let isInfinite = duration == Infinity; + this.log("Duration is " + duration + "ms.\n"); + + if (isNaN(duration) || isInfinite) { + duration = this.maxCurrentTimeSeen; + } + + // If the duration is over an hour, thumb should show h:mm:ss instead of mm:ss + this.showHours = duration >= 3600000; + + // Format the duration as "h:mm:ss" or "m:ss" + let timeString = isInfinite ? "" : this.formatTime(duration); + this.positionDurationBox.duration = timeString; + + if (this.showHours) { + this.positionDurationBox.modifier = "long"; + this.durationSpan.modifier = "long"; + } + + this.scrubber.max = duration; + }, + + pauseVideoDuringDragging() { + if ( + !this.video.paused && + !this.isPausedByDragging && + this.scrubber.isDragging + ) { + this.isPausedByDragging = true; + this.video.pause(); + } + }, + + onScrubberInput(e) { + const duration = Math.round(this.video.duration * 1000); // in ms + let time = this.scrubber.value; + + this.seekToPosition(time); + this.showPosition(time, duration); + + this.scrubber.isDragging = true; + this.pauseVideoDuringDragging(); + }, + + onScrubberChange(e) { + this.scrubber.isDragging = false; + + if (this.isPausedByDragging) { + this.video.play(); + this.isPausedByDragging = false; + } + }, + + updateScrubberProgress() { + const positionPercent = (this.scrubber.value / this.scrubber.max) * 100; + + if (!isNaN(positionPercent) && positionPercent != Infinity) { + this.progressBar.value = positionPercent; + } else { + this.progressBar.value = 0; + } + }, + + seekToPosition(newPosition) { + newPosition /= 1000; // convert from ms + this.log("+++ seeking to " + newPosition); + this.video.currentTime = newPosition; + }, + + setVolume(newVolume) { + this.log("*** setting volume to " + newVolume); + this.video.volume = newVolume; + this.video.muted = false; + }, + + showPosition(currentTime, duration) { + // If the duration is unknown (because the server didn't provide + // it, or the video is a stream), then we want to fudge the duration + // by using the maximum playback position that's been seen. + if (currentTime > this.maxCurrentTimeSeen) { + this.maxCurrentTimeSeen = currentTime; + } + this.showDuration(duration); + + this.log("time update @ " + currentTime + "ms of " + duration + "ms"); + + let positionTime = this.formatTime(currentTime, this.showHours); + + this.scrubber.value = currentTime; + this.positionDurationBox.position = positionTime; + this.scrubber.setAttribute( + "aria-valuetext", + this.positionDurationBox.textContent.trim() + ); + this.updateScrubberProgress(); + }, + + showBuffered() { + function bsearch(haystack, needle, cmp) { + var length = haystack.length; + var low = 0; + var high = length; + while (low < high) { + var probe = low + ((high - low) >> 1); + var r = cmp(haystack, probe, needle); + if (r == 0) { + return probe; + } else if (r > 0) { + low = probe + 1; + } else { + high = probe; + } + } + return -1; + } + + function bufferedCompare(buffered, i, time) { + if (time > buffered.end(i)) { + return 1; + } else if (time >= buffered.start(i)) { + return 0; + } + return -1; + } + + var duration = Math.round(this.video.duration * 1000); + if (isNaN(duration) || duration == Infinity) { + duration = this.maxCurrentTimeSeen; + } + + // Find the range that the current play position is in and use that + // range for bufferBar. At some point we may support multiple ranges + // displayed in the bar. + var currentTime = this.video.currentTime; + var buffered = this.video.buffered; + var index = bsearch(buffered, currentTime, bufferedCompare); + var endTime = 0; + if (index >= 0) { + endTime = Math.round(buffered.end(index) * 1000); + } + this.bufferBar.max = duration; + this.bufferBar.value = endTime; + // Progress bars are automatically reported by screen readers even when + // they aren't focused, which intrudes on the audio being played. + // Ideally, we'd just change the a11y role of bufferBar, but there's + // no role which will let us just expose text via an ARIA attribute. + // Therefore, we hide bufferBar for a11y and expose the info as + // off-screen text. + this.bufferA11yVal.textContent = + (this.bufferBar.position * 100).toFixed() + "%"; + }, + + _controlsHiddenByTimeout: false, + _showControlsTimeout: 0, + SHOW_CONTROLS_TIMEOUT_MS: 500, + _showControlsFn() { + if (this.video.matches("video:hover")) { + this.startFadeIn(this.controlBar, false); + this._showControlsTimeout = 0; + this._controlsHiddenByTimeout = false; + } + }, + + _hideControlsTimeout: 0, + _hideControlsFn() { + if (!this.scrubber.isDragging) { + this.startFade(this.controlBar, false); + this._hideControlsTimeout = 0; + this._controlsHiddenByTimeout = true; + } + }, + HIDE_CONTROLS_TIMEOUT_MS: 2000, + + // By "Video" we actually mean the video controls container, + // because we don't want to consider the padding of <video> added + // by the web content. + isMouseOverVideo(event) { + // XXX: this triggers reflow too, but the layout should only be dirty + // if the web content touches it while the mouse is moving. + let el = this.shadowRoot.elementFromPoint(event.clientX, event.clientY); + + // As long as this is not null, the cursor is over something within our + // Shadow DOM. + return !!el; + }, + + isMouseOverControlBar(event) { + // XXX: this triggers reflow too, but the layout should only be dirty + // if the web content touches it while the mouse is moving. + let el = this.shadowRoot.elementFromPoint(event.clientX, event.clientY); + while (el && el !== this.shadowRoot) { + if (el == this.controlBar) { + return true; + } + el = el.parentNode; + } + return false; + }, + + onMouseMove(event) { + // If the controls are static, don't change anything. + if (!this.dynamicControls) { + return; + } + + this.window.clearTimeout(this._hideControlsTimeout); + + // Suppress fading out the controls until the video has rendered + // its first frame. But since autoplay videos start off with no + // controls, let them fade-out so the controls don't get stuck on. + if (!this.firstFrameShown && !this.video.autoplay) { + return; + } + + if (this._controlsHiddenByTimeout) { + this._showControlsTimeout = this.window.setTimeout( + () => this._showControlsFn(), + this.SHOW_CONTROLS_TIMEOUT_MS + ); + } else { + this.startFade(this.controlBar, true); + } + + // Hide the controls if the mouse cursor is left on top of the video + // but above the control bar and if the click-to-play overlay is hidden. + if ( + (this._controlsHiddenByTimeout || + !this.isMouseOverControlBar(event)) && + this.clickToPlay.hidden + ) { + this._hideControlsTimeout = this.window.setTimeout( + () => this._hideControlsFn(), + this.HIDE_CONTROLS_TIMEOUT_MS + ); + } + }, + + onMouseInOut(event) { + // If the controls are static, don't change anything. + if (!this.dynamicControls) { + return; + } + + this.window.clearTimeout(this._hideControlsTimeout); + + let isMouseOverVideo = this.isMouseOverVideo(event); + + // Suppress fading out the controls until the video has rendered + // its first frame. But since autoplay videos start off with no + // controls, let them fade-out so the controls don't get stuck on. + if ( + !this.firstFrameShown && + !isMouseOverVideo && + !this.video.autoplay + ) { + return; + } + + if (!isMouseOverVideo && !this.isMouseOverControlBar(event)) { + this.adjustControlSize(); + + // Keep the controls visible if the click-to-play is visible. + if (!this.clickToPlay.hidden) { + return; + } + + this.startFadeOut(this.controlBar, false); + this.textTrackListContainer.hidden = true; + this.window.clearTimeout(this._showControlsTimeout); + this._controlsHiddenByTimeout = false; + } + }, + + startFadeIn(element, immediate) { + this.startFade(element, true, immediate); + }, + + startFadeOut(element, immediate) { + this.startFade(element, false, immediate); + }, + + animationMap: new WeakMap(), + + animationProps: { + clickToPlay: { + keyframes: [ + { transform: "scale(3)", opacity: 0 }, + { transform: "scale(1)", opacity: 0.55 }, + ], + options: { + easing: "ease", + duration: 400, + // The fill mode here and below is a workaround to avoid flicker + // due to bug 1495350. + fill: "both", + }, + }, + controlBar: { + keyframes: [{ opacity: 0 }, { opacity: 1 }], + options: { + easing: "ease", + duration: 200, + fill: "both", + }, + }, + statusOverlay: { + keyframes: [ + { opacity: 0 }, + { opacity: 0, offset: 0.72 }, // ~750ms into animation + { opacity: 1 }, + ], + options: { + duration: 1050, + fill: "both", + }, + }, + }, + + startFade(element, fadeIn, immediate = false) { + let animationProp = this.animationProps[element.id]; + if (!animationProp) { + throw new Error( + "Element " + + element.id + + " has no transition. Toggle the hidden property directly." + ); + } + + let animation = this.animationMap.get(element); + if (!animation) { + animation = new this.window.Animation( + new this.window.KeyframeEffect( + element, + animationProp.keyframes, + animationProp.options + ) + ); + + this.animationMap.set(element, animation); + } + + if (fadeIn) { + if (element == this.controlBar) { + this.controlsSpacer.removeAttribute("hideCursor"); + // Ensure the Full Screen button is in the tab order. + this.fullscreenButton.removeAttribute("tabindex"); + } + + // hidden state should be controlled by adjustControlSize + if (element.isAdjustableControl && element.hiddenByAdjustment) { + return; + } + + // No need to fade in again if the hidden property returns false + // (not hidden and not fading out.) + if (!element.hidden) { + return; + } + + // Unhide + element.hidden = false; + } else { + if (element == this.controlBar) { + if (!this.hasError() && this.isVideoInFullScreen) { + this.controlsSpacer.setAttribute("hideCursor", true); + } + if ( + !this.prefs["media.videocontrols.keyboard-tab-to-all-controls"] + ) { + // The Full Screen button is currently the only tabbable button + // when the controls are shown. Remove it from the tab order when + // visually hidden to prevent visual confusion. + this.fullscreenButton.setAttribute("tabindex", "-1"); + } + } + + // No need to fade out if the hidden property returns true + // (hidden or is fading out) + if (element.hidden) { + return; + } + } + + element.classList.toggle("fadeout", !fadeIn); + element.classList.toggle("fadein", fadeIn); + let finishedPromise; + if (!immediate) { + // At this point, if there is a pending animation, we just stop it to avoid it happening. + // If there is a running animation, we reverse it, to have it rewind to the beginning. + // If there is an idle/finished animation, we schedule a new one that reverses the finished one. + if (animation.pending) { + // Animation is running but pending. + // Just cancel the pending animation to stop its effect. + animation.cancel(); + finishedPromise = Promise.resolve(); + } else { + switch (animation.playState) { + case "idle": + case "finished": + // There is no animation currently playing. + // Schedule a new animation with the desired playback direction. + animation.playbackRate = fadeIn ? 1 : -1; + animation.play(); + break; + case "running": + // Allow the animation to play from its current position in + // reverse to finish. + animation.reverse(); + break; + case "pause": + throw new Error("Animation should never reach pause state."); + default: + throw new Error( + "Unknown Animation playState: " + animation.playState + ); + } + finishedPromise = animation.finished; + } + } else { + // immediate + animation.cancel(); + finishedPromise = Promise.resolve(); + } + finishedPromise.then( + animation => { + if (element == this.controlBar) { + this.onControlBarAnimationFinished(); + } + element.classList.remove(fadeIn ? "fadein" : "fadeout"); + if (!fadeIn) { + element.hidden = true; + } + if (animation) { + // Explicitly clear the animation effect so that filling animations + // stop overwriting stylesheet styles. Remove when bug 1495350 is + // fixed and animations are no longer filling animations. + // This also stops them from accumulating (See bug 1253476). + animation.cancel(); + } + }, + () => { + /* Do nothing on rejection */ + } + ); + }, + + _triggeredByControls: false, + + startPlay() { + this._triggeredByControls = true; + this.hideClickToPlay(); + this.video.play(); + }, + + togglePause() { + if (this.video.paused || this.video.ended) { + this.startPlay(); + } else { + this.video.pause(); + } + + // We'll handle style changes in the event listener for + // the "play" and "pause" events, same as if content + // script was controlling video playback. + }, + + get isVideoWithoutAudioTrack() { + return ( + this.video.readyState >= this.video.HAVE_METADATA && + !this.isAudioOnly && + !this.video.mozHasAudio + ); + }, + + toggleMute() { + if (this.isVideoWithoutAudioTrack) { + return; + } + this.video.muted = !this.isEffectivelyMuted; + if (this.video.volume === 0) { + this.video.volume = 0.5; + } + + // We'll handle style changes in the event listener for + // the "volumechange" event, same as if content script was + // controlling volume. + }, + + get isVideoInFullScreen() { + return this.video.isSameNode( + this.video.getRootNode().fullscreenElement + ); + }, + + toggleFullscreen() { + this.isVideoInFullScreen + ? this.document.exitFullscreen() + : this.video.requestFullscreen(); + }, + + setFullscreenButtonState() { + if (this.isAudioOnly || !this.document.fullscreenEnabled) { + this.controlBar.setAttribute("fullscreen-unavailable", true); + this.adjustControlSize(); + return; + } + this.controlBar.removeAttribute("fullscreen-unavailable"); + this.adjustControlSize(); + + var attrName = this.isVideoInFullScreen + ? "exitfullscreenlabel" + : "enterfullscreenlabel"; + var value = this.fullscreenButton.getAttribute(attrName); + this.fullscreenButton.setAttribute("aria-label", value); + + if (this.isVideoInFullScreen) { + this.fullscreenButton.setAttribute("fullscreened", "true"); + } else { + this.fullscreenButton.removeAttribute("fullscreened"); + } + }, + + onFullscreenChange() { + if (this.document.fullscreenElement) { + this.videocontrols.setAttribute("inDOMFullscreen", true); + } else { + this.videocontrols.removeAttribute("inDOMFullscreen"); + } + + this.updateOrientationState(this.isVideoInFullScreen); + + if (this.isVideoInFullScreen) { + this.startFadeOut(this.controlBar, true); + } + + this.setFullscreenButtonState(); + }, + + updateOrientationState(lock) { + if (!this.video.mozOrientationLockEnabled) { + return; + } + if (lock) { + if (this.video.mozIsOrientationLocked) { + return; + } + let dimenDiff = this.video.videoWidth - this.video.videoHeight; + if (dimenDiff > 0) { + this.video.mozIsOrientationLocked = this.window.screen.mozLockOrientation( + "landscape" + ); + } else if (dimenDiff < 0) { + this.video.mozIsOrientationLocked = this.window.screen.mozLockOrientation( + "portrait" + ); + } else { + this.video.mozIsOrientationLocked = this.window.screen.mozLockOrientation( + this.window.screen.orientation + ); + } + } else { + if (!this.video.mozIsOrientationLocked) { + return; + } + this.window.screen.mozUnlockOrientation(); + this.video.mozIsOrientationLocked = false; + } + }, + + clickToPlayClickHandler(e) { + if (e.button != 0) { + return; + } + if (this.hasError() && !this.suppressError) { + // Errors that can be dismissed should be placed here as we discover them. + if (this.video.error.code != this.video.error.MEDIA_ERR_ABORTED) { + return; + } + this.startFadeOut(this.statusOverlay, true); + this.suppressError = true; + return; + } + if (e.defaultPrevented) { + return; + } + if (this.playButton.hasAttribute("paused")) { + this.startPlay(); + } else { + this.video.pause(); + } + }, + hideClickToPlay() { + let videoHeight = this.reflowedDimensions.videoHeight; + let videoWidth = this.reflowedDimensions.videoWidth; + + // The play button will animate to 3x its size. This + // shows the animation unless the video is too small + // to show 2/3 of the animation. + let animationScale = 2; + let animationMinSize = this.clickToPlay.minWidth * animationScale; + + let immediate = + animationMinSize > videoWidth || + animationMinSize > videoHeight - this.controlBarMinHeight; + this.startFadeOut(this.clickToPlay, immediate); + }, + + setPlayButtonState(aPaused) { + if (aPaused) { + this.playButton.setAttribute("paused", "true"); + } else { + this.playButton.removeAttribute("paused"); + } + + var attrName = aPaused ? "playlabel" : "pauselabel"; + var value = this.playButton.getAttribute(attrName); + this.playButton.setAttribute("aria-label", value); + }, + + get isEffectivelyMuted() { + return this.video.muted || !this.video.volume; + }, + + updateMuteButtonState() { + var muted = this.isEffectivelyMuted; + + if (muted) { + this.muteButton.setAttribute("muted", "true"); + } else { + this.muteButton.removeAttribute("muted"); + } + + var attrName = muted ? "unmutelabel" : "mutelabel"; + var value = this.muteButton.getAttribute(attrName); + this.muteButton.setAttribute("aria-label", value); + }, + + keyboardVolumeDecrease() { + const oldval = this.video.volume; + this.video.volume = oldval < 0.1 ? 0 : oldval - 0.1; + this.video.muted = false; + }, + + keyboardVolumeIncrease() { + const oldval = this.video.volume; + this.video.volume = oldval > 0.9 ? 1 : oldval + 0.1; + this.video.muted = false; + }, + + keyboardSeekBack(tenPercent) { + const oldval = this.video.currentTime; + let newval; + if (tenPercent) { + newval = + oldval - + (this.video.duration || this.maxCurrentTimeSeen / 1000) / 10; + } else { + newval = oldval - 15; + } + this.video.currentTime = Math.max(0, newval); + }, + + keyboardSeekForward(tenPercent) { + const oldval = this.video.currentTime; + const maxtime = this.video.duration || this.maxCurrentTimeSeen / 1000; + let newval; + if (tenPercent) { + newval = oldval + maxtime / 10; + } else { + newval = oldval + 15; + } + this.video.currentTime = Math.min(newval, maxtime); + }, + + keyHandler(event) { + // Ignore keys when content might be providing its own. + if (!this.video.hasAttribute("controls")) { + return; + } + + let keystroke = ""; + if (event.altKey) { + keystroke += "alt-"; + } + if (event.shiftKey) { + keystroke += "shift-"; + } + if (this.window.navigator.platform.startsWith("Mac")) { + if (event.metaKey) { + keystroke += "accel-"; + } + if (event.ctrlKey) { + keystroke += "control-"; + } + } else { + if (event.metaKey) { + keystroke += "meta-"; + } + if (event.ctrlKey) { + keystroke += "accel-"; + } + } + if (event.key == " ") { + keystroke += "Space"; + } else { + keystroke += event.key; + } + + this.log("Got keystroke: " + keystroke); + + // If unmodified cursor keys are pressed when a slider is focused, we + // should act on that slider. For example, if we're focused on the + // volume slider, rightArrow should increase the volume, not seek. + // Normally, we'd just pass the keys through to the slider in this case. + // However, the native adjustment is too small, so we override it. + try { + const target = event.originalTarget; + const allTabbable = this.prefs[ + "media.videocontrols.keyboard-tab-to-all-controls" + ]; + switch (keystroke) { + case "Space" /* Play */: + if (target.localName === "button" && !target.disabled) { + break; + } + this.togglePause(); + break; + case "ArrowDown" /* Volume decrease */: + if (allTabbable && target == this.scrubber) { + this.keyboardSeekBack(/* tenPercent */ false); + } else { + this.keyboardVolumeDecrease(); + } + break; + case "ArrowUp" /* Volume increase */: + if (allTabbable && target == this.scrubber) { + this.keyboardSeekForward(/* tenPercent */ false); + } else { + this.keyboardVolumeIncrease(); + } + break; + case "accel-ArrowDown" /* Mute */: + this.video.muted = true; + break; + case "accel-ArrowUp" /* Unmute */: + this.video.muted = false; + break; + case "ArrowLeft" /* Seek back 15 seconds */: + if (allTabbable && target == this.volumeControl) { + this.keyboardVolumeDecrease(); + } else { + this.keyboardSeekBack(/* tenPercent */ false); + } + break; + case "accel-ArrowLeft" /* Seek back 10% */: + this.keyboardSeekBack(/* tenPercent */ true); + break; + case "ArrowRight" /* Seek forward 15 seconds */: + if (allTabbable && target == this.volumeControl) { + this.keyboardVolumeIncrease(); + } else { + this.keyboardSeekForward(/* tenPercent */ false); + } + break; + case "accel-ArrowRight" /* Seek forward 10% */: + this.keyboardSeekForward(/* tenPercent */ true); + break; + case "Home" /* Seek to beginning */: + this.video.currentTime = 0; + break; + case "End" /* Seek to end */: + if (this.video.currentTime != this.video.duration) { + this.video.currentTime = + this.video.duration || this.maxCurrentTimeSeen / 1000; + } + break; + default: + return; + } + } catch (e) { + /* ignore any exception from setting .currentTime */ + } + + event.preventDefault(); // Prevent page scrolling + }, + + checkTextTrackSupport(textTrack) { + return textTrack.kind == "subtitles" || textTrack.kind == "captions"; + }, + + get isCastingAvailable() { + return !this.isAudioOnly && this.video.mozAllowCasting; + }, + + get isClosedCaptionAvailable() { + // There is no rendering area, no need to show the caption. + if (this.isAudioOnly) { + return false; + } + return this.overlayableTextTracks.length; + }, + + get overlayableTextTracks() { + return Array.prototype.filter.call( + this.video.textTracks, + this.checkTextTrackSupport + ); + }, + + get currentTextTrackIndex() { + const showingTT = this.overlayableTextTracks.find( + tt => tt.mode == "showing" + ); + + // fallback to off button if there's no showing track. + return showingTT ? showingTT.index : 0; + }, + + get isCastingOn() { + return this.isCastingAvailable && this.video.mozIsCasting; + }, + + setCastingButtonState() { + if (this.isCastingOn) { + this.castingButton.setAttribute("enabled", "true"); + } else { + this.castingButton.removeAttribute("enabled"); + } + + this.adjustControlSize(); + }, + + updateCasting(eventDetail) { + let castingData = JSON.parse(eventDetail); + if ("allow" in castingData) { + this.video.mozAllowCasting = !!castingData.allow; + } + + if ("active" in castingData) { + this.video.mozIsCasting = !!castingData.active; + } + this.setCastingButtonState(); + }, + + get isClosedCaptionOn() { + for (let tt of this.overlayableTextTracks) { + if (tt.mode === "showing") { + return true; + } + } + + return false; + }, + + setClosedCaptionButtonState() { + if (this.isClosedCaptionOn) { + this.closedCaptionButton.setAttribute("enabled", "true"); + } else { + this.closedCaptionButton.removeAttribute("enabled"); + } + + let ttItems = this.textTrackList.childNodes; + + for (let tti of ttItems) { + const idx = +tti.getAttribute("index"); + + if (idx == this.currentTextTrackIndex) { + tti.setAttribute("on", "true"); + } else { + tti.removeAttribute("on"); + } + } + + this.adjustControlSize(); + }, + + addNewTextTrack(tt) { + if (!this.checkTextTrackSupport(tt)) { + return; + } + + if (tt.index && tt.index < this.textTracksCount) { + // Don't create items for initialized tracks. However, we + // still need to care about mode since TextTrackManager would + // turn on the first available track automatically. + if (tt.mode === "showing") { + this.changeTextTrack(tt.index); + } + return; + } + + tt.index = this.textTracksCount++; + + const ttBtn = this.shadowRoot.createElementAndAppendChildAt( + this.textTrackList, + "button" + ); + ttBtn.textContent = tt.label || ""; + + ttBtn.classList.add("textTrackItem"); + ttBtn.setAttribute("index", tt.index); + + if (tt.mode === "showing" && tt.index) { + this.changeTextTrack(tt.index); + } + }, + + changeTextTrack(index) { + for (let tt of this.overlayableTextTracks) { + if (tt.index === index) { + tt.mode = "showing"; + } else { + tt.mode = "disabled"; + } + } + + if (!this.textTrackListContainer.hidden) { + this.toggleClosedCaption(); + } + }, + + onControlBarAnimationFinished() { + this.textTrackListContainer.hidden = true; + this.video.dispatchEvent( + new this.window.CustomEvent("controlbarchange") + ); + this.adjustControlSize(); + }, + + toggleCasting() { + this.videocontrols.dispatchEvent( + new this.window.CustomEvent("VideoBindingCast") + ); + }, + + toggleClosedCaption() { + if (this.textTrackListContainer.hidden) { + this.textTrackListContainer.hidden = false; + if (this.prefs["media.videocontrols.keyboard-tab-to-all-controls"]) { + // If we're about to hide the controls after focus, prevent that, as + // that will dismiss the CC menu before the user can use it. + this.window.clearTimeout(this._hideControlsTimeout); + this._hideControlsTimeout = 0; + } + } else { + this.textTrackListContainer.hidden = true; + // If the CC menu was shown via the keyboard, we may have prevented + // the controls from hiding. We can now hide them. + if ( + this.prefs["media.videocontrols.keyboard-tab-to-all-controls"] && + !this.controlBar.hidden && + // The click-to-play overlay must already be hidden (we don't + // hide controls when the overlay is visible). + this.clickToPlay.hidden && + // Don't do this if the controls are static. + this.dynamicControls && + // If the mouse is hovering over the control bar, the controls + // shouldn't hide. + // We use "div:hover" instead of just ":hover" so this works in + // quirks mode documents. See + // https://quirks.spec.whatwg.org/#the-active-and-hover-quirk + !this.controlBar.matches("div:hover") + ) { + this.window.clearTimeout(this._hideControlsTimeout); + this._hideControlsTimeout = this.window.setTimeout( + () => this._hideControlsFn(), + this.HIDE_CONTROLS_TIMEOUT_MS + ); + } + } + }, + + onTextTrackAdd(trackEvent) { + this.addNewTextTrack(trackEvent.track); + this.setClosedCaptionButtonState(); + }, + + onTextTrackRemove(trackEvent) { + const toRemoveIndex = trackEvent.track.index; + const ttItems = this.textTrackList.childNodes; + + if (!ttItems) { + return; + } + + for (let tti of ttItems) { + const idx = +tti.getAttribute("index"); + + if (idx === toRemoveIndex) { + tti.remove(); + this.textTracksCount--; + } + + this.video.dispatchEvent( + new this.window.CustomEvent("texttrackchange") + ); + } + + this.setClosedCaptionButtonState(); + }, + + initTextTracks() { + // add 'off' button anyway as new text track might be + // dynamically added after initialization. + const offLabel = this.textTrackList.getAttribute("offlabel"); + this.addNewTextTrack({ + label: offLabel, + kind: "subtitles", + }); + + for (let tt of this.overlayableTextTracks) { + this.addNewTextTrack(tt); + } + + this.setClosedCaptionButtonState(); + }, + + log(msg) { + if (this.debug) { + this.window.console.log("videoctl: " + msg + "\n"); + } + }, + + get isTopLevelSyntheticDocument() { + return ( + this.document.mozSyntheticDocument && this.window === this.window.top + ); + }, + + controlBarMinHeight: 40, + controlBarMinVisibleHeight: 28, + + reflowTriggeringCallValidator: { + isReflowTriggeringPropsAllowed: false, + reflowTriggeringProps: Object.freeze([ + "offsetLeft", + "offsetTop", + "offsetWidth", + "offsetHeight", + "offsetParent", + "clientLeft", + "clientTop", + "clientWidth", + "clientHeight", + "getClientRects", + "getBoundingClientRect", + ]), + get(obj, prop) { + if ( + !this.isReflowTriggeringPropsAllowed && + this.reflowTriggeringProps.includes(prop) + ) { + throw new Error("Please don't trigger reflow. See bug 1493525."); + } + let val = obj[prop]; + if (typeof val == "function") { + return function() { + return val.apply(obj, arguments); + }; + } + return val; + }, + + set(obj, prop, value) { + return Reflect.set(obj, prop, value); + }, + }, + + installReflowCallValidator(element) { + return new Proxy(element, this.reflowTriggeringCallValidator); + }, + + reflowedDimensions: { + // Set the dimensions to intrinsic <video> dimensions before the first + // update. + // These values are not picked up by <audio> in adjustControlSize() + // (except for the fact that they are non-zero), + // it takes controlBarMinHeight and the value below instead. + videoHeight: 150, + videoWidth: 300, + + // <audio> takes this width to grow/shrink controls. + // The initial value has to be smaller than the calculated minRequiredWidth + // so that we don't run into bug 1495821 (see comment on adjustControlSize() + // below) + videocontrolsWidth: 0, + }, + + updateReflowedDimensions() { + this.reflowedDimensions.videoHeight = this.video.clientHeight; + this.reflowedDimensions.videoWidth = this.video.clientWidth; + this.reflowedDimensions.videocontrolsWidth = this.videocontrols.clientWidth; + }, + + /** + * adjustControlSize() considers outer dimensions of the <video>/<audio> element + * from layout, and accordingly, sets/hides the controls, and adjusts + * the width/height of the control bar. + * + * It's important to remember that for <audio>, layout (specifically, + * nsVideoFrame) rely on us to expose the intrinsic dimensions of the + * control bar to properly size the <audio> element. We interact with layout + * by: + * + * 1) When the element has a non-zero height, explicitly set the height + * of the control bar to a size between controlBarMinHeight and + * controlBarMinVisibleHeight in response. + * Note: the logic here is flawed and had caused the end height to be + * depend on its previous state, see bug 1495817. + * 2) When the element has a outer width smaller or equal to minControlBarPaddingWidth, + * explicitly set the control bar to minRequiredWidth, so that when the + * outer width is unset, the audio element could go back to minRequiredWidth. + * Otherwise, set the width of the control bar to be the current outer width. + * Note: the logic here is also flawed; when the control bar is set to + * the current outer width, it never go back when the width is unset, + * see bug 1495821. + */ + adjustControlSize() { + const minControlBarPaddingWidth = 18; + + this.fullscreenButton.isWanted = !this.controlBar.hasAttribute( + "fullscreen-unavailable" + ); + this.castingButton.isWanted = this.isCastingAvailable; + this.closedCaptionButton.isWanted = this.isClosedCaptionAvailable; + this.volumeStack.isWanted = !this.muteButton.hasAttribute("noAudio"); + + let minRequiredWidth = this.prioritizedControls + .filter(control => control && control.isWanted) + .reduce( + (accWidth, cc) => accWidth + cc.minWidth, + minControlBarPaddingWidth + ); + // Skip the adjustment in case the stylesheets haven't been loaded yet. + if (!minRequiredWidth) { + return; + } + + let givenHeight = this.reflowedDimensions.videoHeight; + let videoWidth = + (this.isAudioOnly + ? this.reflowedDimensions.videocontrolsWidth + : this.reflowedDimensions.videoWidth) || minRequiredWidth; + let videoHeight = this.isAudioOnly + ? this.controlBarMinHeight + : givenHeight; + let videocontrolsWidth = this.reflowedDimensions.videocontrolsWidth; + + let widthUsed = minControlBarPaddingWidth; + let preventAppendControl = false; + + for (let control of this.prioritizedControls) { + if (!control.isWanted) { + control.hiddenByAdjustment = true; + continue; + } + + control.hiddenByAdjustment = + preventAppendControl || widthUsed + control.minWidth > videoWidth; + + if (control.hiddenByAdjustment) { + preventAppendControl = true; + } else { + widthUsed += control.minWidth; + } + } + + // Use flexible spacer to separate controls when scrubber is hidden. + // As long as muteButton hidden, which means only play button presents, + // hide spacer and make playButton centered. + this.controlBarSpacer.hidden = + !this.scrubberStack.hidden || this.muteButton.hidden; + + // Since the size of videocontrols is expanded with controlBar in <audio>, we + // should fix the dimensions in order not to recursively trigger reflow afterwards. + if (this.video.localName == "audio") { + if (givenHeight) { + // The height of controlBar should be capped with the bounds between controlBarMinHeight + // and controlBarMinVisibleHeight. + let controlBarHeight = Math.max( + Math.min(givenHeight, this.controlBarMinHeight), + this.controlBarMinVisibleHeight + ); + this.controlBar.style.height = `${controlBarHeight}px`; + } + // Bug 1367875: Set minimum required width to controlBar if the given size is smaller than padding. + // This can help us expand the control and restore to the default size the next time we need + // to adjust the sizing. + if (videocontrolsWidth <= minControlBarPaddingWidth) { + this.controlBar.style.width = `${minRequiredWidth}px`; + } else { + this.controlBar.style.width = `${videoWidth}px`; + } + return; + } + + if ( + videoHeight < this.controlBarMinHeight || + widthUsed === minControlBarPaddingWidth + ) { + this.controlBar.setAttribute("size", "hidden"); + this.controlBar.hiddenByAdjustment = true; + } else { + this.controlBar.removeAttribute("size"); + this.controlBar.hiddenByAdjustment = false; + } + + // Adjust clickToPlayButton size. + const minVideoSideLength = Math.min(videoWidth, videoHeight); + const clickToPlayViewRatio = 0.15; + const clickToPlayScaledSize = Math.max( + this.clickToPlay.minWidth, + minVideoSideLength * clickToPlayViewRatio + ); + + if ( + clickToPlayScaledSize >= videoWidth || + clickToPlayScaledSize + this.controlBarMinHeight / 2 >= + videoHeight / 2 + ) { + this.clickToPlay.hiddenByAdjustment = true; + } else { + if ( + this.clickToPlay.hidden && + !this.video.played.length && + this.video.paused + ) { + this.clickToPlay.hiddenByAdjustment = false; + } + this.clickToPlay.style.width = `${clickToPlayScaledSize}px`; + this.clickToPlay.style.height = `${clickToPlayScaledSize}px`; + } + }, + + get pipToggleEnabled() { + return this.prefs[ + "media.videocontrols.picture-in-picture.video-toggle.enabled" + ]; + }, + + init(shadowRoot, prefs) { + this.shadowRoot = shadowRoot; + this.video = this.installReflowCallValidator(shadowRoot.host); + this.videocontrols = this.installReflowCallValidator( + shadowRoot.firstChild + ); + this.document = this.videocontrols.ownerDocument; + this.window = this.document.defaultView; + this.shadowRoot = shadowRoot; + this.prefs = prefs; + + this.controlsContainer = this.shadowRoot.getElementById( + "controlsContainer" + ); + this.statusIcon = this.shadowRoot.getElementById("statusIcon"); + this.controlBar = this.shadowRoot.getElementById("controlBar"); + this.playButton = this.shadowRoot.getElementById("playButton"); + this.controlBarSpacer = this.shadowRoot.getElementById( + "controlBarSpacer" + ); + this.muteButton = this.shadowRoot.getElementById("muteButton"); + this.volumeStack = this.shadowRoot.getElementById("volumeStack"); + this.volumeControl = this.shadowRoot.getElementById("volumeControl"); + this.progressBar = this.shadowRoot.getElementById("progressBar"); + this.bufferBar = this.shadowRoot.getElementById("bufferBar"); + this.bufferA11yVal = this.shadowRoot.getElementById("bufferA11yVal"); + this.scrubberStack = this.shadowRoot.getElementById("scrubberStack"); + this.scrubber = this.shadowRoot.getElementById("scrubber"); + this.durationLabel = this.shadowRoot.getElementById("durationLabel"); + this.positionLabel = this.shadowRoot.getElementById("positionLabel"); + this.positionDurationBox = this.shadowRoot.getElementById( + "positionDurationBox" + ); + this.statusOverlay = this.shadowRoot.getElementById("statusOverlay"); + this.controlsOverlay = this.shadowRoot.getElementById( + "controlsOverlay" + ); + this.pictureInPictureOverlay = this.shadowRoot.getElementById( + "pictureInPictureOverlay" + ); + this.controlsSpacer = this.shadowRoot.getElementById("controlsSpacer"); + this.clickToPlay = this.shadowRoot.getElementById("clickToPlay"); + this.fullscreenButton = this.shadowRoot.getElementById( + "fullscreenButton" + ); + this.castingButton = this.shadowRoot.getElementById("castingButton"); + this.closedCaptionButton = this.shadowRoot.getElementById( + "closedCaptionButton" + ); + this.textTrackList = this.shadowRoot.getElementById("textTrackList"); + this.textTrackListContainer = this.shadowRoot.getElementById( + "textTrackListContainer" + ); + this.pictureInPictureToggle = this.shadowRoot.getElementById( + "pictureInPictureToggle" + ); + + if (this.positionDurationBox) { + this.durationSpan = this.positionDurationBox.getElementsByTagName( + "span" + )[0]; + } + + let isMobile = this.window.navigator.appVersion.includes("Android"); + if (isMobile) { + this.controlsContainer.classList.add("mobile"); + } + + // TODO: Switch to touch controls on touch-based desktops (bug 1447547) + this.isTouchControls = isMobile; + if (this.isTouchControls) { + this.controlsContainer.classList.add("touch"); + } + + // XXX: Calling getComputedStyle() here by itself doesn't cause any reflow, + // but there is no guard proventing accessing any properties and methods + // of this saved CSSStyleDeclaration instance that could trigger reflow. + this.controlBarComputedStyles = this.window.getComputedStyle( + this.controlBar + ); + + // Hide and show control in certain order. + this.prioritizedControls = [ + this.playButton, + this.muteButton, + this.fullscreenButton, + this.castingButton, + this.closedCaptionButton, + this.positionDurationBox, + this.scrubberStack, + this.durationSpan, + this.volumeStack, + ]; + + this.isAudioOnly = this.video.localName == "audio"; + this.setupInitialState(); + this.setupNewLoadState(); + this.initTextTracks(); + + // Use the handleEvent() callback for all media events. + // Only the "error" event listener must capture, so that it can trap error + // events from <source> children, which don't bubble. But we use capture + // for all events in order to simplify the event listener add/remove. + for (let event of this.videoEvents) { + this.video.addEventListener(event, this, { + capture: true, + mozSystemGroup: true, + }); + } + + this.controlsEvents = [ + { el: this.muteButton, type: "click" }, + { el: this.castingButton, type: "click" }, + { el: this.closedCaptionButton, type: "click" }, + { el: this.fullscreenButton, type: "click" }, + { el: this.playButton, type: "click" }, + { el: this.clickToPlay, type: "click" }, + + // On touch videocontrols, tapping controlsSpacer should show/hide + // the control bar, instead of playing the video or toggle fullscreen. + { el: this.controlsSpacer, type: "click", nonTouchOnly: true }, + { el: this.controlsSpacer, type: "dblclick", nonTouchOnly: true }, + + { el: this.textTrackList, type: "click" }, + + { el: this.videocontrols, type: "resizevideocontrols" }, + + { el: this.document, type: "fullscreenchange" }, + { el: this.video, type: "keypress", capture: true }, + + // Prevent any click event within media controls from dispatching through to video. + { el: this.videocontrols, type: "click", mozSystemGroup: false }, + + // prevent dragging of controls image (bug 517114) + { el: this.videocontrols, type: "dragstart" }, + + { el: this.scrubber, type: "input" }, + { el: this.scrubber, type: "change" }, + // add mouseup listener additionally to handle the case that `change` event + // isn't fired when the input value before/after dragging are the same. (bug 1328061) + { el: this.scrubber, type: "mouseup" }, + { el: this.volumeControl, type: "input" }, + { el: this.video.textTracks, type: "addtrack" }, + { el: this.video.textTracks, type: "removetrack" }, + { el: this.video.textTracks, type: "change" }, + + { el: this.video, type: "media-videoCasting", touchOnly: true }, + + { el: this.controlBar, type: "focusin" }, + { el: this.scrubber, type: "mousedown" }, + { el: this.volumeControl, type: "mousedown" }, + ]; + + for (let { + el, + type, + nonTouchOnly = false, + touchOnly = false, + mozSystemGroup = true, + capture = false, + } of this.controlsEvents) { + if ( + (this.isTouchControls && nonTouchOnly) || + (!this.isTouchControls && touchOnly) + ) { + continue; + } + el.addEventListener(type, this, { mozSystemGroup, capture }); + } + + this.log("--- videocontrols initialized ---"); + }, + }; + + this.TouchUtils = { + videocontrols: null, + video: null, + controlsTimer: null, + controlsTimeout: 5000, + + get visible() { + return ( + !this.Utils.controlBar.hasAttribute("fadeout") && + !this.Utils.controlBar.hidden + ); + }, + + firstShow: false, + + toggleControls() { + if (!this.Utils.dynamicControls || !this.visible) { + this.showControls(); + } else { + this.delayHideControls(0); + } + }, + + showControls() { + if (this.Utils.dynamicControls) { + this.Utils.startFadeIn(this.Utils.controlBar); + this.delayHideControls(this.controlsTimeout); + } + }, + + clearTimer() { + if (this.controlsTimer) { + this.window.clearTimeout(this.controlsTimer); + this.controlsTimer = null; + } + }, + + delayHideControls(aTimeout) { + this.clearTimer(); + this.controlsTimer = this.window.setTimeout( + () => this.hideControls(), + aTimeout + ); + }, + + hideControls() { + if (!this.Utils.dynamicControls) { + return; + } + this.Utils.startFadeOut(this.Utils.controlBar); + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "click": + switch (aEvent.currentTarget) { + case this.Utils.playButton: + if (!this.video.paused) { + this.delayHideControls(0); + } else { + this.showControls(); + } + break; + case this.Utils.muteButton: + this.delayHideControls(this.controlsTimeout); + break; + } + break; + case "touchstart": + this.clearTimer(); + break; + case "touchend": + this.delayHideControls(this.controlsTimeout); + break; + case "mouseup": + if (aEvent.originalTarget == this.Utils.controlsSpacer) { + if (this.firstShow) { + this.Utils.video.play(); + this.firstShow = false; + } + this.toggleControls(); + } + + break; + } + }, + + terminate() { + try { + for (let { el, type, mozSystemGroup = true } of this.controlsEvents) { + el.removeEventListener(type, this, { mozSystemGroup }); + } + } catch (ex) {} + + this.clearTimer(); + }, + + init(shadowRoot, utils) { + this.Utils = utils; + this.videocontrols = this.Utils.videocontrols; + this.video = this.Utils.video; + this.document = this.videocontrols.ownerDocument; + this.window = this.document.defaultView; + this.shadowRoot = shadowRoot; + + this.controlsEvents = [ + { el: this.Utils.playButton, type: "click" }, + { el: this.Utils.scrubber, type: "touchstart" }, + { el: this.Utils.scrubber, type: "touchend" }, + { el: this.Utils.muteButton, type: "click" }, + { el: this.Utils.controlsSpacer, type: "mouseup" }, + ]; + + for (let { el, type, mozSystemGroup = true } of this.controlsEvents) { + el.addEventListener(type, this, { mozSystemGroup }); + } + + // The first time the controls appear we want to just display + // a play button that does not fade away. The firstShow property + // makes that happen. But because of bug 718107 this init() method + // may be called again when we switch in or out of fullscreen + // mode. So we only set firstShow if we're not autoplaying and + // if we are at the beginning of the video and not already playing + if ( + !this.video.autoplay && + this.Utils.dynamicControls && + this.video.paused && + this.video.currentTime === 0 + ) { + this.firstShow = true; + } + + // If the video is not at the start, then we probably just + // transitioned into or out of fullscreen mode, and we don't want + // the controls to remain visible. this.controlsTimeout is a full + // 5s, which feels too long after the transition. + if (this.video.currentTime !== 0) { + this.delayHideControls(this.Utils.HIDE_CONTROLS_TIMEOUT_MS); + } + }, + }; + + this.Utils.init(this.shadowRoot, this.prefs); + if (this.Utils.isTouchControls) { + this.TouchUtils.init(this.shadowRoot, this.Utils); + } + this.shadowRoot.firstChild.dispatchEvent( + new this.window.CustomEvent("VideoBindingAttached") + ); + + this._setupEventListeners(); + } + + generateContent() { + /* + * Pass the markup through XML parser purely for the reason of loading the localization DTD. + * Remove it when migrate to Fluent. + */ + const parser = new this.window.DOMParser(); + parser.forceEnableDTD(); + let parserDoc = parser.parseFromString( + `<!DOCTYPE bindings [ + <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> + %videocontrolsDTD; + ]> + <div class="videocontrols" xmlns="http://www.w3.org/1999/xhtml" role="none"> + <link rel="stylesheet" href="chrome://global/skin/media/videocontrols.css" /> + + <div id="controlsContainer" class="controlsContainer" role="none"> + <div id="statusOverlay" class="statusOverlay stackItem" hidden="true"> + <div id="statusIcon" class="statusIcon"></div> + <bdi class="statusLabel" id="errorAborted">&error.aborted;</bdi> + <bdi class="statusLabel" id="errorNetwork">&error.network;</bdi> + <bdi class="statusLabel" id="errorDecode">&error.decode;</bdi> + <bdi class="statusLabel" id="errorSrcNotSupported">&error.srcNotSupported;</bdi> + <bdi class="statusLabel" id="errorNoSource">&error.noSource2;</bdi> + <bdi class="statusLabel" id="errorGeneric">&error.generic;</bdi> + </div> + + <div id="pictureInPictureOverlay" class="pictureInPictureOverlay stackItem" status="pictureInPicture" hidden="true"> + <div class="statusIcon" type="pictureInPicture"></div> + <bdi class="statusLabel" id="pictureInPicture">&status.pictureInPicture;</bdi> + </div> + + <div id="controlsOverlay" class="controlsOverlay stackItem" role="none"> + <div class="controlsSpacerStack"> + <div id="controlsSpacer" class="controlsSpacer stackItem" role="none"></div> + <div id="clickToPlay" class="clickToPlay" hidden="true"></div> + </div> + + <button id="pictureInPictureToggle" class="pip-wrapper" position="left" hidden="true"> + <div class="pip-small clickable"></div> + <div class="pip-expanded clickable"> + <span class="pip-icon-label clickable"> + <span class="pip-icon"></span> + <span class="pip-label">&pictureInPictureToggle.label;</span> + </span> + <div class="pip-explainer clickable"> + &pictureInPictureExplainer; + </div> + </div> + <div class="pip-icon clickable"></div> + </button> + + <div id="controlBar" class="controlBar" role="none" hidden="true"> + <button id="playButton" + class="button playButton" + playlabel="&playButton.playLabel;" + pauselabel="&playButton.pauseLabel;" + tabindex="-1"/> + <div id="scrubberStack" class="scrubberStack progressContainer" role="none"> + <div class="progressBackgroundBar stackItem" role="none"> + <div class="progressStack" role="none"> + <progress id="bufferBar" class="bufferBar" value="0" max="100" aria-hidden="true"></progress> + <span class="a11y-only" role="status" aria-live="off"> + <span data-l10n-id="videocontrols-buffer-bar-label"></span> + <span id="bufferA11yVal"></span> + </span> + <progress id="progressBar" class="progressBar" value="0" max="100" aria-hidden="true"></progress> + </div> + </div> + <input type="range" id="scrubber" class="scrubber" tabindex="-1" data-l10n-id="videocontrols-scrubber"/> + </div> + <bdi id="positionLabel" class="positionLabel" role="presentation"></bdi> + <bdi id="durationLabel" class="durationLabel" role="presentation"></bdi> + <bdi id="positionDurationBox" class="positionDurationBox" aria-hidden="true"> + &positionAndDuration.nameFormat; + </bdi> + <div id="controlBarSpacer" class="controlBarSpacer" hidden="true" role="none"></div> + <button id="muteButton" + class="button muteButton" + mutelabel="&muteButton.muteLabel;" + unmutelabel="&muteButton.unmuteLabel;" + tabindex="-1"/> + <div id="volumeStack" class="volumeStack progressContainer" role="none"> + <input type="range" id="volumeControl" class="volumeControl" min="0" max="100" step="1" tabindex="-1" + data-l10n-id="videocontrols-volume-control"/> + </div> + <button id="castingButton" class="button castingButton" + aria-label="&castingButton.castingLabel;"/> + <button id="closedCaptionButton" class="button closedCaptionButton" + data-l10n-id="videocontrols-closed-caption-button"/> + <button id="fullscreenButton" + class="button fullscreenButton" + enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;" + exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/> + </div> + <div id="textTrackListContainer" class="textTrackListContainer" hidden="true"> + <div id="textTrackList" class="textTrackList" offlabel="&closedCaption.off;"></div> + </div> + </div> + </div> + </div>`, + "application/xml" + ); + this.l10n = new this.window.DOMLocalization([ + "toolkit/global/videocontrols.ftl", + ]); + this.l10n.connectRoot(this.shadowRoot); + if (this.prefs["media.videocontrols.keyboard-tab-to-all-controls"]) { + // Make all of the individual controls tabbable. + for (const el of parserDoc.documentElement.querySelectorAll( + '[tabindex="-1"]' + )) { + el.removeAttribute("tabindex"); + } + } + this.shadowRoot.importNodeAndAppendChildAt( + this.shadowRoot, + parserDoc.documentElement, + true + ); + } + + elementStateMatches(element) { + let elementInPiP = VideoControlsWidget.isPictureInPictureVideo(element); + return this.isShowingPictureInPictureMessage == elementInPiP; + } + + destructor() { + this.Utils.terminate(); + this.TouchUtils.terminate(); + this.Utils.updateOrientationState(false); + this.l10n.disconnectRoot(this.shadowRoot); + this.l10n = null; + } + + onPrefChange(prefName, prefValue) { + this.prefs[prefName] = prefValue; + this.Utils.updatePictureInPictureToggleDisplay(); + } + + _setupEventListeners() { + this.shadowRoot.firstChild.addEventListener("mouseover", event => { + if (!this.Utils.isTouchControls) { + this.Utils.onMouseInOut(event); + } + }); + + this.shadowRoot.firstChild.addEventListener("mouseout", event => { + if (!this.Utils.isTouchControls) { + this.Utils.onMouseInOut(event); + } + }); + + this.shadowRoot.firstChild.addEventListener("mousemove", event => { + if (!this.Utils.isTouchControls) { + this.Utils.onMouseMove(event); + } + }); + } +}; + +this.NoControlsMobileImplWidget = class { + constructor(shadowRoot) { + this.shadowRoot = shadowRoot; + this.element = shadowRoot.host; + this.document = this.element.ownerDocument; + this.window = this.document.defaultView; + } + + onsetup(direction) { + this.generateContent(); + + this.shadowRoot.firstElementChild.setAttribute("localedir", direction); + + this.Utils = { + videoEvents: ["play", "playing"], + videoControlEvents: ["MozNoControlsBlockedVideo"], + terminate() { + for (let event of this.videoEvents) { + try { + this.video.removeEventListener(event, this, { + capture: true, + mozSystemGroup: true, + }); + } catch (ex) {} + } + + for (let event of this.videoControlEvents) { + try { + this.videocontrols.removeEventListener(event, this); + } catch (ex) {} + } + + try { + this.clickToPlay.removeEventListener("click", this, { + mozSystemGroup: true, + }); + } catch (ex) {} + }, + + hasError() { + return ( + this.video.error != null || + this.video.networkState == this.video.NETWORK_NO_SOURCE + ); + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "play": + this.noControlsOverlay.hidden = true; + break; + case "playing": + this.noControlsOverlay.hidden = true; + break; + case "MozNoControlsBlockedVideo": + this.blockedVideoHandler(); + break; + case "click": + this.clickToPlayClickHandler(aEvent); + break; + } + }, + + blockedVideoHandler() { + if (this.hasError()) { + this.noControlsOverlay.hidden = true; + return; + } + this.noControlsOverlay.hidden = false; + }, + + clickToPlayClickHandler(e) { + if (e.button != 0) { + return; + } + + this.noControlsOverlay.hidden = true; + this.video.play(); + }, + + init(shadowRoot) { + this.shadowRoot = shadowRoot; + this.video = shadowRoot.host; + this.videocontrols = shadowRoot.firstChild; + this.document = this.videocontrols.ownerDocument; + this.window = this.document.defaultView; + this.shadowRoot = shadowRoot; + + this.controlsContainer = this.shadowRoot.getElementById( + "controlsContainer" + ); + this.clickToPlay = this.shadowRoot.getElementById("clickToPlay"); + this.noControlsOverlay = this.shadowRoot.getElementById( + "controlsContainer" + ); + + let isMobile = this.window.navigator.appVersion.includes("Android"); + if (isMobile) { + this.controlsContainer.classList.add("mobile"); + } + + // TODO: Switch to touch controls on touch-based desktops (bug 1447547) + this.isTouchControls = isMobile; + if (this.isTouchControls) { + this.controlsContainer.classList.add("touch"); + } + + this.clickToPlay.addEventListener("click", this, { + mozSystemGroup: true, + }); + + for (let event of this.videoEvents) { + this.video.addEventListener(event, this, { + capture: true, + mozSystemGroup: true, + }); + } + + for (let event of this.videoControlEvents) { + this.videocontrols.addEventListener(event, this); + } + }, + }; + this.Utils.init(this.shadowRoot); + this.Utils.video.dispatchEvent( + new this.window.CustomEvent("MozNoControlsVideoBindingAttached") + ); + } + + elementStateMatches(element) { + return true; + } + + destructor() { + this.Utils.terminate(); + } + + onPrefChange(prefName, prefValue) { + this.prefs[prefName] = prefValue; + } + + generateContent() { + /* + * Pass the markup through XML parser purely for the reason of loading the localization DTD. + * Remove it when migrate to Fluent. + */ + const parser = new this.window.DOMParser(); + parser.forceEnableDTD(); + let parserDoc = parser.parseFromString( + `<!DOCTYPE bindings [ + <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> + %videocontrolsDTD; + ]> + <div class="videocontrols" xmlns="http://www.w3.org/1999/xhtml" role="none"> + <link rel="stylesheet" href="chrome://global/skin/media/videocontrols.css" /> + <div id="controlsContainer" class="controlsContainer" role="none" hidden="true"> + <div class="controlsOverlay stackItem"> + <div class="controlsSpacerStack"> + <div id="clickToPlay" class="clickToPlay"></div> + </div> + </div> + </div> + </div>`, + "application/xml" + ); + this.shadowRoot.importNodeAndAppendChildAt( + this.shadowRoot, + parserDoc.documentElement, + true + ); + } +}; + +this.NoControlsPictureInPictureImplWidget = class { + constructor(shadowRoot, prefs) { + this.shadowRoot = shadowRoot; + this.prefs = prefs; + this.element = shadowRoot.host; + this.document = this.element.ownerDocument; + this.window = this.document.defaultView; + } + + onsetup(direction) { + this.generateContent(); + + this.shadowRoot.firstElementChild.setAttribute("localedir", direction); + } + + elementStateMatches(element) { + return true; + } + + destructor() {} + + onPrefChange(prefName, prefValue) { + this.prefs[prefName] = prefValue; + } + + generateContent() { + /* + * Pass the markup through XML parser purely for the reason of loading the localization DTD. + * Remove it when migrate to Fluent. + */ + const parser = new this.window.DOMParser(); + parser.forceEnableDTD(); + let parserDoc = parser.parseFromString( + `<!DOCTYPE bindings [ + <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> + %videocontrolsDTD; + ]> + <div class="videocontrols" xmlns="http://www.w3.org/1999/xhtml" role="none"> + <link rel="stylesheet" href="chrome://global/skin/media/videocontrols.css" /> + <div id="controlsContainer" class="controlsContainer" role="none"> + <div class="pictureInPictureOverlay stackItem" status="pictureInPicture"> + <div id="statusIcon" class="statusIcon" type="pictureInPicture"></div> + <bdi class="statusLabel" id="pictureInPicture">&status.pictureInPicture;</bdi> + </div> + <div class="controlsOverlay stackItem"></div> + </div> + </div>`, + "application/xml" + ); + this.shadowRoot.importNodeAndAppendChildAt( + this.shadowRoot, + parserDoc.documentElement, + true + ); + } +}; + +this.NoControlsDesktopImplWidget = class { + constructor(shadowRoot, prefs) { + this.shadowRoot = shadowRoot; + this.element = shadowRoot.host; + this.document = this.element.ownerDocument; + this.window = this.document.defaultView; + this.prefs = prefs; + } + + onsetup(direction) { + this.generateContent(); + + this.shadowRoot.firstElementChild.setAttribute("localedir", direction); + + this.Utils = { + handleEvent(event) { + switch (event.type) { + case "fullscreenchange": { + if (this.document.fullscreenElement) { + this.videocontrols.setAttribute("inDOMFullscreen", true); + } else { + this.videocontrols.removeAttribute("inDOMFullscreen"); + } + break; + } + case "resizevideocontrols": { + this.updateReflowedDimensions(); + this.updatePictureInPictureToggleDisplay(); + break; + } + case "durationchange": + // Intentional fall-through + case "emptied": + // Intentional fall-through + case "loadedmetadata": { + this.updatePictureInPictureToggleDisplay(); + break; + } + } + }, + + updatePictureInPictureToggleDisplay() { + if ( + this.pipToggleEnabled && + VideoControlsWidget.shouldShowPictureInPictureToggle( + this.prefs, + this.video, + this.reflowedDimensions + ) + ) { + this.pictureInPictureToggle.removeAttribute("hidden"); + VideoControlsWidget.setupToggle( + this.prefs, + this.pictureInPictureToggle, + this.reflowedDimensions + ); + } else { + this.pictureInPictureToggle.setAttribute("hidden", true); + } + }, + + init(shadowRoot, prefs) { + this.shadowRoot = shadowRoot; + this.prefs = prefs; + this.video = shadowRoot.host; + this.videocontrols = shadowRoot.firstChild; + this.document = this.videocontrols.ownerDocument; + this.window = this.document.defaultView; + this.shadowRoot = shadowRoot; + + this.pictureInPictureToggle = this.shadowRoot.getElementById( + "pictureInPictureToggle" + ); + + if (this.document.fullscreenElement) { + this.videocontrols.setAttribute("inDOMFullscreen", true); + } + + // Default the Picture-in-Picture toggle button to being hidden. We might unhide it + // later if we determine that this video is qualified to show it. + this.pictureInPictureToggle.setAttribute("hidden", true); + + if (this.video.readyState >= this.video.HAVE_METADATA) { + // According to the spec[1], at the HAVE_METADATA (or later) state, we know + // the video duration and dimensions, which means we can calculate whether or + // not to show the Picture-in-Picture toggle now. + // + // [1]: https://www.w3.org/TR/html50/embedded-content-0.html#dom-media-have_metadata + this.updatePictureInPictureToggleDisplay(); + } + + this.document.addEventListener("fullscreenchange", this, { + capture: true, + }); + + this.video.addEventListener("emptied", this); + this.video.addEventListener("loadedmetadata", this); + this.video.addEventListener("durationchange", this); + this.videocontrols.addEventListener("resizevideocontrols", this); + }, + + terminate() { + this.document.removeEventListener("fullscreenchange", this, { + capture: true, + }); + + this.video.removeEventListener("emptied", this); + this.video.removeEventListener("loadedmetadata", this); + this.video.removeEventListener("durationchange", this); + this.videocontrols.removeEventListener("resizevideocontrols", this); + }, + + updateReflowedDimensions() { + this.reflowedDimensions.videoHeight = this.video.clientHeight; + this.reflowedDimensions.videoWidth = this.video.clientWidth; + this.reflowedDimensions.videocontrolsWidth = this.videocontrols.clientWidth; + }, + + reflowedDimensions: { + // Set the dimensions to intrinsic <video> dimensions before the first + // update. + videoHeight: 150, + videoWidth: 300, + videocontrolsWidth: 0, + }, + + get pipToggleEnabled() { + return this.prefs[ + "media.videocontrols.picture-in-picture.video-toggle.enabled" + ]; + }, + }; + this.Utils.init(this.shadowRoot, this.prefs); + } + + elementStateMatches(element) { + return true; + } + + destructor() { + this.Utils.terminate(); + } + + onPrefChange(prefName, prefValue) { + this.prefs[prefName] = prefValue; + this.Utils.updatePictureInPictureToggleDisplay(); + } + + generateContent() { + /* + * Pass the markup through XML parser purely for the reason of loading the localization DTD. + * Remove it when migrate to Fluent. + */ + const parser = new this.window.DOMParser(); + parser.forceEnableDTD(); + let parserDoc = parser.parseFromString( + `<!DOCTYPE bindings [ + <!ENTITY % videocontrolsDTD SYSTEM "chrome://global/locale/videocontrols.dtd"> + %videocontrolsDTD; + ]> + <div class="videocontrols" xmlns="http://www.w3.org/1999/xhtml" role="none"> + <link rel="stylesheet" href="chrome://global/skin/media/videocontrols.css" /> + + <div id="controlsContainer" class="controlsContainer" role="none"> + <div class="controlsOverlay stackItem"> + <button id="pictureInPictureToggle" class="pip-wrapper" position="left" hidden="true"> + <div class="pip-small clickable"></div> + <div class="pip-expanded clickable"> + <span class="pip-icon-label clickable"> + <span class="pip-icon"></span> + <span class="pip-label">&pictureInPictureToggle.label;</span> + </span> + <div class="pip-explainer clickable"> + &pictureInPictureExplainer; + </div> + </div> + <div class="pip-icon"></div> + </button> + </div> + </div> + </div>`, + "application/xml" + ); + this.shadowRoot.importNodeAndAppendChildAt( + this.shadowRoot, + parserDoc.documentElement, + true + ); + } +}; diff --git a/toolkit/content/widgets/wizard.js b/toolkit/content/widgets/wizard.js new file mode 100644 index 0000000000..a029d29f3b --- /dev/null +++ b/toolkit/content/widgets/wizard.js @@ -0,0 +1,668 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +{ + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + // Note: MozWizard currently supports adding, but not removing MozWizardPage + // children. + class MozWizard extends MozXULElement { + constructor() { + super(); + + // About this._accessMethod: + // There are two possible access methods: "sequential" and "random". + // "sequential" causes the MozWizardPage's to be displayed in the order + // that they are added to the DOM. + // The "random" method name is a bit misleading since the pages aren't + // displayed in a random order. Instead, each MozWizardPage must have + // a "next" attribute containing the id of the MozWizardPage that should + // be loaded next. + this._accessMethod = null; + this._currentPage = null; + this._canAdvance = true; + this._canRewind = false; + this._hasLoaded = false; + this._hasStarted = false; // Whether any MozWizardPage has been shown yet + this._wizardButtonsReady = false; + this.pageCount = 0; + this._pageStack = []; + + this._bundle = Services.strings.createBundle( + "chrome://global/locale/wizard.properties" + ); + + this.addEventListener( + "keypress", + event => { + if (event.keyCode == KeyEvent.DOM_VK_RETURN) { + this._hitEnter(event); + } else if ( + event.keyCode == KeyEvent.DOM_VK_ESCAPE && + !event.defaultPrevented + ) { + this.cancel(); + } + }, + { mozSystemGroup: true } + ); + + /* + XXX(ntim): We import button.css here for the wizard-buttons children + This won't be needed after bug 1624888. + */ + this.attachShadow({ mode: "open" }).appendChild( + MozXULElement.parseXULToFragment(` + <html:link rel="stylesheet" href="chrome://global/skin/button.css"/> + <html:link rel="stylesheet" href="chrome://global/skin/wizard.css"/> + <hbox class="wizard-header"></hbox> + <deck class="wizard-page-box" flex="1"> + <html:slot name="wizardpage"/> + </deck> + <html:slot/> + <wizard-buttons class="wizard-buttons"></wizard-buttons> + `) + ); + this.initializeAttributeInheritance(); + + this._deck = this.shadowRoot.querySelector(".wizard-page-box"); + this._wizardButtons = this.shadowRoot.querySelector(".wizard-buttons"); + + this._wizardHeader = this.shadowRoot.querySelector(".wizard-header"); + this._wizardHeader.appendChild( + MozXULElement.parseXULToFragment( + AppConstants.platform == "macosx" + ? `<stack class="wizard-header-stack" flex="1"> + <vbox class="wizard-header-box-1"> + <vbox class="wizard-header-box-text"> + <label class="wizard-header-label"/> + </vbox> + </vbox> + <hbox class="wizard-header-box-icon"> + <spacer flex="1"/> + <image class="wizard-header-icon"/> + </hbox> + </stack>` + : `<hbox class="wizard-header-box-1" flex="1"> + <vbox class="wizard-header-box-text" flex="1"> + <label class="wizard-header-label"/> + <label class="wizard-header-description"/> + </vbox> + <image class="wizard-header-icon"/> + </hbox>` + ) + ); + } + + static get inheritedAttributes() { + return { + ".wizard-buttons": "pagestep,firstpage,lastpage", + }; + } + + connectedCallback() { + if (document.l10n) { + document.l10n.connectRoot(this.shadowRoot); + } + document.documentElement.setAttribute("role", "dialog"); + this._maybeStartWizard(); + + window.addEventListener("close", event => { + if (this.cancel()) { + event.preventDefault(); + } + }); + + // Give focus to the first focusable element in the wizard, do it after + // onload completes, see bug 103197. + window.addEventListener("load", () => + window.setTimeout(() => { + this._hasLoaded = true; + if (!document.commandDispatcher.focusedElement) { + document.commandDispatcher.advanceFocusIntoSubtree(this); + } + try { + let button = this._wizardButtons.defaultButton; + if (button) { + window.notifyDefaultButtonLoaded(button); + } + } catch (e) {} + }, 0) + ); + } + + set title(val) { + return (document.title = val); + } + + get title() { + return document.title; + } + + set canAdvance(val) { + this.getButton("next").disabled = !val; + return (this._canAdvance = val); + } + + get canAdvance() { + return this._canAdvance; + } + + set canRewind(val) { + this.getButton("back").disabled = !val; + return (this._canRewind = val); + } + + get canRewind() { + return this._canRewind; + } + + get pageStep() { + return this._pageStack.length; + } + + get wizardPages() { + return this.getElementsByTagNameNS(XUL_NS, "wizardpage"); + } + + set currentPage(val) { + if (!val) { + return val; + } + + this._currentPage = val; + + // Setting this attribute allows wizard's clients to dynamically + // change the styles of each page based on purpose of the page. + this.setAttribute("currentpageid", val.pageid); + + this._initCurrentPage(); + + this._deck.setAttribute("selectedIndex", val.pageIndex); + this._advanceFocusToPage(val); + + this._fireEvent(val, "pageshow"); + + return val; + } + + get currentPage() { + return this._currentPage; + } + + set pageIndex(val) { + if (val < 0 || val >= this.pageCount) { + return val; + } + + var page = this.wizardPages[val]; + this._pageStack[this._pageStack.length - 1] = page; + this.currentPage = page; + + return val; + } + + get pageIndex() { + return this._currentPage ? this._currentPage.pageIndex : -1; + } + + get onFirstPage() { + return this._pageStack.length == 1; + } + + get onLastPage() { + var cp = this.currentPage; + return ( + cp && + ((this._accessMethod == "sequential" && + cp.pageIndex == this.pageCount - 1) || + (this._accessMethod == "random" && cp.next == "")) + ); + } + + getButton(aDlgType) { + return this._wizardButtons.getButton(aDlgType); + } + + getPageById(aPageId) { + var els = this.getElementsByAttribute("pageid", aPageId); + return els.item(0); + } + + extra1() { + if (this.currentPage) { + this._fireEvent(this.currentPage, "extra1"); + } + } + + extra2() { + if (this.currentPage) { + this._fireEvent(this.currentPage, "extra2"); + } + } + + rewind() { + if (!this.canRewind) { + return; + } + + if (this.currentPage && !this._fireEvent(this.currentPage, "pagehide")) { + return; + } + + if ( + this.currentPage && + !this._fireEvent(this.currentPage, "pagerewound") + ) { + return; + } + + if (!this._fireEvent(this, "wizardback")) { + return; + } + + this._pageStack.pop(); + this.currentPage = this._pageStack[this._pageStack.length - 1]; + this.setAttribute("pagestep", this._pageStack.length); + } + + advance(aPageId) { + if (!this.canAdvance) { + return; + } + + if (this.currentPage && !this._fireEvent(this.currentPage, "pagehide")) { + return; + } + + if ( + this.currentPage && + !this._fireEvent(this.currentPage, "pageadvanced") + ) { + return; + } + + if (this.onLastPage && !aPageId) { + if (this._fireEvent(this, "wizardfinish")) { + window.setTimeout(function() { + window.close(); + }, 1); + } + } else { + if (!this._fireEvent(this, "wizardnext")) { + return; + } + + let page; + if (aPageId) { + page = this.getPageById(aPageId); + } else if (this.currentPage) { + if (this._accessMethod == "random") { + page = this.getPageById(this.currentPage.next); + } else { + page = this.wizardPages[this.currentPage.pageIndex + 1]; + } + } else { + page = this.wizardPages[0]; + } + + if (page) { + this._pageStack.push(page); + this.setAttribute("pagestep", this._pageStack.length); + + this.currentPage = page; + } + } + } + + goTo(aPageId) { + var page = this.getPageById(aPageId); + if (page) { + this._pageStack[this._pageStack.length - 1] = page; + this.currentPage = page; + } + } + + cancel() { + if (!this._fireEvent(this, "wizardcancel")) { + return true; + } + + window.close(); + window.setTimeout(function() { + window.close(); + }, 1); + return false; + } + + _initCurrentPage() { + if (this.onFirstPage) { + this.canRewind = false; + this.setAttribute("firstpage", "true"); + if (AppConstants.platform == "linux") { + this.getButton("back").setAttribute("hidden", "true"); + } + } else { + this.canRewind = true; + this.setAttribute("firstpage", "false"); + if (AppConstants.platform == "linux") { + this.getButton("back").setAttribute("hidden", "false"); + } + } + + if (this.onLastPage) { + this.canAdvance = true; + this.setAttribute("lastpage", "true"); + } else { + this.setAttribute("lastpage", "false"); + } + + this._adjustWizardHeader(); + this._wizardButtons.onPageChange(); + } + + _advanceFocusToPage(aPage) { + if (!this._hasLoaded) { + return; + } + + // XXX: it'd be correct to advance focus into the panel, however we can't do + // it until bug 1558990 is fixed, so moving the focus into a wizard itsef + // as a workaround - it's same behavior but less optimal. + document.commandDispatcher.advanceFocusIntoSubtree(this); + + // if advanceFocusIntoSubtree tries to focus one of our + // dialog buttons, then remove it and put it on the root + var focused = document.commandDispatcher.focusedElement; + if (focused && focused.hasAttribute("dlgtype")) { + this.focus(); + } + } + + _registerPage(aPage) { + aPage.pageIndex = this.pageCount; + this.pageCount += 1; + if (!this._accessMethod) { + this._accessMethod = aPage.next == "" ? "sequential" : "random"; + } + if (!this._maybeStartWizard() && this._hasStarted) { + // If the wizard has already started, adding a page might require + // updating elements to reflect that (ex: changing the Finish button to + // the Next button). + this._initCurrentPage(); + } + } + + _onWizardButtonsReady() { + this._wizardButtonsReady = true; + this._maybeStartWizard(); + } + + _maybeStartWizard(aIsConnected) { + if ( + !this._hasStarted && + this.isConnected && + this._wizardButtonsReady && + this.pageCount > 0 + ) { + this._hasStarted = true; + this.advance(); + return true; + } + return false; + } + + _adjustWizardHeader() { + let labelElement = this._wizardHeader.querySelector( + ".wizard-header-label" + ); + // First deal with fluent. Ideally, we'd stop supporting anything else, + // but right now the migration wizard still uses non-fluent l10n + // (fixing is bug 1518234), as do some comm-central consumers + // (bug 1627049). Removing the DTD support is bug 1627051. + if (this.currentPage.hasAttribute("data-header-label-id")) { + let id = this.currentPage.getAttribute("data-header-label-id"); + document.l10n.setAttributes(labelElement, id); + } else { + // Otherwise, make sure we remove any fluent IDs leftover: + if (labelElement.hasAttribute("data-l10n-id")) { + labelElement.removeAttribute("data-l10n-id"); + } + // And use the label attribute or the default: + var label = this.currentPage.getAttribute("label") || ""; + if (!label && this.onFirstPage && this._bundle) { + if (AppConstants.platform == "macosx") { + label = this._bundle.GetStringFromName("default-first-title-mac"); + } else { + label = this._bundle.formatStringFromName("default-first-title", [ + this.title, + ]); + } + } else if (!label && this.onLastPage && this._bundle) { + if (AppConstants.platform == "macosx") { + label = this._bundle.GetStringFromName("default-last-title-mac"); + } else { + label = this._bundle.formatStringFromName("default-last-title", [ + this.title, + ]); + } + } + labelElement.textContent = label; + } + let headerDescEl = this._wizardHeader.querySelector( + ".wizard-header-description" + ); + if (headerDescEl) { + headerDescEl.textContent = this.currentPage.getAttribute("description"); + } + } + + _hitEnter(evt) { + if (!evt.defaultPrevented) { + this.advance(); + } + } + + _fireEvent(aTarget, aType) { + var event = document.createEvent("Events"); + event.initEvent(aType, true, true); + + // handle dom event handlers + return aTarget.dispatchEvent(event); + } + } + + customElements.define("wizard", MozWizard); + + class MozWizardPage extends MozXULElement { + constructor() { + super(); + this.pageIndex = -1; + } + connectedCallback() { + this.setAttribute("slot", "wizardpage"); + + let wizard = this.closest("wizard"); + if (wizard) { + wizard._registerPage(this); + } + } + get pageid() { + return this.getAttribute("pageid"); + } + set pageid(val) { + this.setAttribute("pageid", val); + } + get next() { + return this.getAttribute("next"); + } + set next(val) { + this.setAttribute("next", val); + this.parentNode._accessMethod = "random"; + return val; + } + } + + customElements.define("wizardpage", MozWizardPage); + + class MozWizardButtons extends MozXULElement { + connectedCallback() { + this._wizard = this.getRootNode().host; + + this.textContent = ""; + this.appendChild(this.constructor.fragment); + + MozXULElement.insertFTLIfNeeded("toolkit/global/wizard.ftl"); + + this._wizardButtonDeck = this.querySelector(".wizard-next-deck"); + + this.initializeAttributeInheritance(); + + const listeners = [ + ["back", () => this._wizard.rewind()], + ["next", () => this._wizard.advance()], + ["finish", () => this._wizard.advance()], + ["cancel", () => this._wizard.cancel()], + ["extra1", () => this._wizard.extra1()], + ["extra2", () => this._wizard.extra2()], + ]; + for (let [name, listener] of listeners) { + let btn = this.getButton(name); + if (btn) { + btn.addEventListener("command", listener); + } + } + + this._wizard._onWizardButtonsReady(); + } + + static get inheritedAttributes() { + return AppConstants.platform == "macosx" + ? { + "[dlgtype='next']": "hidden=lastpage", + } + : null; + } + + static get markup() { + if (AppConstants.platform == "macosx") { + return ` + <vbox flex="1"> + <hbox class="wizard-buttons-btm"> + <button class="wizard-button" dlgtype="extra1" hidden="true"/> + <button class="wizard-button" dlgtype="extra2" hidden="true"/> + <button data-l10n-id="wizard-macos-button-cancel" + class="wizard-button" dlgtype="cancel"/> + <spacer flex="1"/> + <button data-l10n-id="wizard-macos-button-back" + class="wizard-button wizard-nav-button" dlgtype="back"/> + <button data-l10n-id="wizard-macos-button-next" + class="wizard-button wizard-nav-button" dlgtype="next" + default="true" /> + <button data-l10n-id="wizard-macos-button-finish" class="wizard-button" + dlgtype="finish" default="true" /> + </hbox> + </vbox>`; + } + + let buttons = + AppConstants.platform == "linux" + ? ` + <button data-l10n-id="wizard-linux-button-cancel" + class="wizard-button" + dlgtype="cancel"/> + <spacer style="width: 24px;"/> + <button data-l10n-id="wizard-linux-button-back" + class="wizard-button" dlgtype="back"/> + <deck class="wizard-next-deck"> + <hbox> + <button data-l10n-id="wizard-linux-button-finish" + class="wizard-button" + dlgtype="finish" default="true" flex="1"/> + </hbox> + <hbox> + <button data-l10n-id="wizard-linux-button-next" + class="wizard-button" dlgtype="next" + default="true" flex="1"/> + </hbox> + </deck>` + : ` + <button data-l10n-id="wizard-win-button-back" + class="wizard-button" dlgtype="back"/> + <deck class="wizard-next-deck"> + <hbox> + <button data-l10n-id="wizard-win-button-finish" + class="wizard-button" + dlgtype="finish" default="true" flex="1"/> + </hbox> + <hbox> + <button data-l10n-id="wizard-win-button-next" + class="wizard-button" dlgtype="next" + default="true" flex="1"/> + </hbox> + </deck> + <button data-l10n-id="wizard-win-button-cancel" + class="wizard-button" + dlgtype="cancel"/>`; + + return ` + <vbox class="wizard-buttons-box-1" flex="1"> + <separator class="wizard-buttons-separator groove"/> + <hbox class="wizard-buttons-box-2"> + <button class="wizard-button" dlgtype="extra1" hidden="true"/> + <button class="wizard-button" dlgtype="extra2" hidden="true"/> + <spacer flex="1" anonid="spacer"/> + ${buttons} + </hbox> + </vbox>`; + } + + onPageChange() { + if (AppConstants.platform == "macosx") { + this.getButton("finish").hidden = !( + this.getAttribute("lastpage") == "true" + ); + } else if (this.getAttribute("lastpage") == "true") { + this._wizardButtonDeck.setAttribute("selectedIndex", 0); + } else { + this._wizardButtonDeck.setAttribute("selectedIndex", 1); + } + } + + getButton(type) { + return this.querySelector(`[dlgtype="${type}"]`); + } + + get defaultButton() { + let buttons = this._wizardButtonDeck.selectedPanel.getElementsByTagNameNS( + XUL_NS, + "button" + ); + for (let i = 0; i < buttons.length; i++) { + if ( + buttons[i].getAttribute("default") == "true" && + !buttons[i].hidden && + !buttons[i].disabled + ) { + return buttons[i]; + } + } + return null; + } + } + + customElements.define("wizard-buttons", MozWizardButtons); +} diff --git a/toolkit/content/xul.css b/toolkit/content/xul.css new file mode 100644 index 0000000000..73b4447dbc --- /dev/null +++ b/toolkit/content/xul.css @@ -0,0 +1,617 @@ +/* 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/. */ + +/** + * A minimal set of rules for the XUL elements that may be implicitly created + * as part of HTML/SVG documents (e.g. scrollbars) can be found over in + * minimal-xul.css. Rules for everything else related to XUL can be found in + * this file. Make sure you choose the correct style sheet when adding new + * rules. (This split of the XUL rules is to minimize memory use and improve + * performance in HTML/SVG documents.) + * + * This file should also not contain any app specific styling. Defaults for + * widgets of a particular application should be in that application's style + * sheet. For example, style definitions for browser can be found in + * browser.css. + */ + +@import url("chrome://global/skin/tooltip.css"); + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ +@namespace html url("http://www.w3.org/1999/xhtml"); /* namespace for HTML elements */ + +/* TODO: investigate unifying these two root selectors + * https://bugzilla.mozilla.org/show_bug.cgi?id=1592344 + */ +*|*:root { + --animation-easing-function: cubic-bezier(.07, .95, 0, 1); +} + +:root { + text-rendering: optimizeLegibility; + -moz-control-character-visibility: visible; +} + +:root:-moz-locale-dir(rtl) { + direction: rtl; +} + +/* + * Native anonymous popups and tooltips in html are document-level, which means + * that they don't inherit from the root, so this is needed. + */ +popupgroup:-moz-native-anonymous:-moz-locale-dir(rtl), +tooltip:-moz-native-anonymous:-moz-locale-dir(rtl) { + direction: rtl; +} + +/* :::::::::: + :: Rules for 'hiding' portions of the chrome for special + :: kinds of windows (not JUST browser windows) with toolbars + ::::: */ + +*|*:root[chromehidden~="menubar"] .chromeclass-menubar, +*|*:root[chromehidden~="directories"] .chromeclass-directories, +*|*:root[chromehidden~="status"] .chromeclass-status, +*|*:root[chromehidden~="extrachrome"] .chromeclass-extrachrome, +*|*:root[chromehidden~="location"] .chromeclass-location, +*|*:root[chromehidden~="location"][chromehidden~="toolbar"] .chromeclass-toolbar, +*|*:root[chromehidden~="toolbar"] .chromeclass-toolbar-additional { + display: none; +} + +/* :::::::::: + :: Rules for forcing direction for entry and display of URIs + :: or URI elements + ::::: */ + +.uri-element { + direction: ltr !important; +} + +/****** elements that have no visual representation ******/ + +script, data, commandset, command, +broadcasterset, broadcaster, observes, +keyset, key, toolbarpalette, template, +treeitem, treeseparator, treerow, treecell { + display: none; +} + +/********** focus rules **********/ + +button, +checkbox, +menulist, +radiogroup, +tree, +browser, +editor, +iframe { + -moz-user-focus: normal; +} + +/******** window & page ******/ + +window { + overflow: clip; + -moz-box-orient: vertical; +} + +/******** box *******/ + +vbox { + -moz-box-orient: vertical; +} + +/********** label **********/ +label.text-link, label[onclick] { + -moz-user-focus: normal; +} + +label html|span.accesskey { + text-decoration: underline; + text-decoration-skip-ink: none; +} + +/********** toolbarbutton **********/ + +toolbar[mode="icons"] .toolbarbutton-text, +toolbar[mode="text"] .toolbarbutton-icon, +html|label.toolbarbutton-badge:empty { + display: none; +} + +/********** button **********/ + +button { + -moz-default-appearance: button; + appearance: auto; +} + +/******** browser, editor, iframe ********/ + +browser, +editor, +iframe { + display: inline; +} + +@supports -moz-bool-pref("layout.css.emulate-moz-box-with-flex") { + browser, + editor, + iframe { + display: block; + } +} + +/* Allow the browser to shrink below its intrinsic size, to match legacy + * behavior */ +browser { + align-self: stretch; + justify-self: stretch; + min-height: 0; + min-width: 0; +} + +/*********** popup notification ************/ +popupnotification { + -moz-box-orient: vertical; +} + +.popup-notification-menubutton:not([label]) { + display: none; +} + +/********** radio **********/ + +radiogroup { + -moz-box-orient: vertical; +} + +/******** groupbox *********/ + +groupbox { + -moz-box-orient: vertical; +} + +/******** draggable elements *********/ + +%ifndef MOZ_WIDGET_GTK +titlebar, +toolbar:not([nowindowdrag="true"], [customizing="true"]) { + -moz-window-dragging: drag; +} +%endif + +/* The list below is non-comprehensive and will probably need some tweaking. */ +toolbaritem, +toolbarbutton, +toolbarseparator, +button, +search-textbox, +html|input, +tab, +radio, +splitter, +menulist { + -moz-window-dragging: no-drag; +} + +titlebar { + pointer-events: auto !important; +} + +/******* toolbar *******/ + +toolbox { + -moz-box-orient: vertical; +} + +%ifdef XP_MACOSX +toolbar[type="menubar"] { + min-height: 0 !important; + border: 0 !important; +} +%endif + +toolbarspring { + -moz-box-flex: 1000; +} + +/********* menu ***********/ + +menubar > menu:empty { + visibility: collapse; +} + +.menu-text { + -moz-box-flex: 1; +} + +/********* menupopup, panel, & tooltip ***********/ + +menupopup, +panel { + -moz-box-orient: vertical; +} + +menupopup, +panel, +tooltip { + display: -moz-popup; + z-index: 2147483647; + text-shadow: none; +} + +tooltip { + -moz-box-orient: vertical; + white-space: pre-wrap; + margin-top: 21px; +} + +% excluding Android here to work around bug 1637975: +%ifndef ANDROID +@supports -moz-bool-pref("xul.panel-animations.enabled") { +@media (prefers-reduced-motion: no-preference) { +%ifdef MOZ_WIDGET_COCOA + /* On Mac, use the properties "-moz-window-transform" and "-moz-window-opacity" + instead of "transform" and "opacity" for these animations. + The -moz-window* properties apply to the whole window including the window's + shadow, and they don't affect the window's "shape", so the system doesn't + have to recompute the shadow shape during the animation. This makes them a + lot faster. In fact, Gecko no longer triggers shadow shape recomputations + for repaints. + These properties are not implemented on other platforms. */ + panel[type="arrow"]:not([animate="false"]) { + transition-property: -moz-window-transform, -moz-window-opacity; + transition-duration: 0.18s, 0.18s; + transition-timing-function: + var(--animation-easing-function), ease-out; + } + + /* Only do the fade-in animation on pre-Big Sur to avoid missing shadows on + * Big Sur, see bug 1672091. */ + @media (-moz-mac-big-sur-theme: 0) { + panel[type="arrow"]:not([animate="false"]) { + -moz-window-opacity: 0; + -moz-window-transform: translateY(-70px); + } + + panel[type="arrow"][side="bottom"]:not([animate="false"]) { + -moz-window-transform: translateY(70px); + } + } + + /* [animate] is here only so that this rule has greater specificity than the + * rule right above */ + panel[type="arrow"][animate][animate="open"] { + -moz-window-opacity: 1.0; + transition-duration: 0.18s, 0.18s; + -moz-window-transform: none; + transition-timing-function: + var(--animation-easing-function), ease-in-out; + } + + panel[type="arrow"][animate][animate="cancel"] { + -moz-window-opacity: 0; + -moz-window-transform: none; + } +%else + panel[type="arrow"]:not([animate="false"]) { + opacity: 0; + transform: translateY(-70px); + transition-property: transform, opacity; + transition-duration: 0.18s, 0.18s; + transition-timing-function: + var(--animation-easing-function), ease-out; + } + + panel[type="arrow"][side="bottom"]:not([animate="false"]) { + transform: translateY(70px); + } + + /* [animate] is here only so that this rule has greater specificity than the + * rule right above */ + panel[type="arrow"][animate][animate="open"] { + opacity: 1.0; + transition-duration: 0.18s, 0.18s; + transform: none; + transition-timing-function: + var(--animation-easing-function), ease-in-out; + } + + panel[type="arrow"][animate][animate="cancel"] { + transform: none; + } +%endif +} +} +%endif + +panel[type="arrow"][animating] { + pointer-events: none; +} + +/******** tree ******/ + +treecolpicker { + -moz-box-ordinal-group: 2147483646; +} + +treechildren { + display: -moz-box; + user-select: none; + -moz-box-flex: 1; +} + +tree { + -moz-box-orient: vertical; + width: 10px; + height: 10px; +} + +tree[hidecolumnpicker="true"] treecolpicker { + display: none; +} + +treecol { + min-width: 16px; +} + +treecol[hidden="true"] { + visibility: collapse; + display: -moz-box; +} + +/* ::::: lines connecting cells ::::: */ +tree:not([treelines="true"]) treechildren::-moz-tree-line { + visibility: hidden; +} + +treechildren::-moz-tree-cell(ltr) { + direction: ltr !important; +} + +/********** deck & stack *********/ + +deck { + display: -moz-deck; +} + +stack { + display: grid; + position: relative; +} + +/* We shouldn't style native anonymous children like scrollbars or what not */ +stack > *|*:not(:-moz-native-anonymous) { + grid-area: 1 / 1; + z-index: 0; + + /* + The default `min-height: auto` value makes grid items refuse to be smaller + than their content. This doesn't match the traditional behavior of XUL stack, + which often shoehorns tall content into a smaller stack and allows the content + to decide how to handle overflow (e.g. by scaling down if it's an image, or + by adding scrollbars if it's scrollable). + */ + min-height: 0; +} + +legacy-stack { + display: -moz-stack; +} + +/********** tabbox *********/ + +tabbox { + -moz-box-orient: vertical; +} + +tabs { + -moz-box-orient: horizontal; +} + +tab { + -moz-box-align: center; + -moz-box-pack: center; +} + +tab[selected="true"]:not([ignorefocus="true"]) { + -moz-user-focus: normal; +} + +tabpanels { + display: -moz-deck; +} + +/********** tooltip *********/ + +tooltip[titletip="true"] { + /* The width of the tooltip isn't limited on cropped <tree> cells. */ + max-width: none; +} + +/********** basic rule for anonymous content that needs to pass box properties through + ********** to an insertion point parent that holds the real kids **************/ + +.box-inherit { + -moz-box-orient: inherit; + -moz-box-pack: inherit; + -moz-box-align: inherit; + -moz-box-direction: inherit; +} + +/********** textbox **********/ + +search-textbox { + user-select: text; + text-shadow: none; +} + +/* Prefix with (xul|*):root to workaround HTML tests loading xul.css */ +:root html|textarea:not([resizable="true"]) { + resize: none; +} + +@supports -moz-bool-pref("layout.css.emulate-moz-box-with-flex") { + html|*.textbox-input { + /* Be block-level, so that -moz-box-flex can take effect, when we are an item + in a -moz-box being emulated by modified modern flex. */ + display: block; + } +} + +/********** autocomplete textbox **********/ + +/* SeaMonkey uses its own autocomplete and the toolkit's autocomplete widget */ +%ifndef MOZ_SUITE + +.autocomplete-richlistbox { + -moz-user-focus: ignore; + overflow-x: hidden !important; +} + +.autocomplete-richlistitem { + -moz-box-orient: vertical; + -moz-box-align: center; + overflow: clip; +} + +%endif + +/* The following rule is here to fix bug 96899 (and now 117952). + Somehow trees create a situation + in which a popupset flows itself as if its popup child is directly within it + instead of the placeholder child that should actually be inside the popupset. + This is a stopgap measure, and it does not address the real bug. */ +.autocomplete-result-popupset { + max-width: 0px; + width: 0 !important; + min-width: 0%; + min-height: 0%; +} + +/********** menulist **********/ + +menulist[popuponly] { + appearance: none !important; + margin: 0 !important; + height: 0 !important; + min-height: 0 !important; + border: 0 !important; +} + +/********** splitter **********/ + +.tree-splitter { + width: 0px; + max-width: 0px; + min-width: 0% ! important; + min-height: 0% ! important; + -moz-box-ordinal-group: 2147483646; +} + +/******** scrollbar ********/ + +slider { + /* This is a hint to layerization that the scrollbar thumb can never leave + the scrollbar track. */ + overflow: hidden; +} + +/******** scrollbox ********/ + +scrollbox { + /* This makes it scrollable! */ + overflow: hidden; +} + +scrollbox[smoothscroll=true] { + scroll-behavior: smooth; +} + +/********** stringbundle **********/ + +stringbundle, +stringbundleset { + display: none; +} + +/********** dialog **********/ + +dialog { + -moz-box-flex: 1; + -moz-box-orient: vertical; +} + +/********** wizard **********/ + +wizard { + -moz-box-flex: 1; + -moz-box-orient: vertical; + width: 40em; + height: 30em; +} + +wizardpage { + -moz-box-orient: vertical; + overflow: auto; +} + +/********** Rich Listbox ********/ + +richlistbox { + -moz-user-focus: normal; + -moz-box-orient: vertical; +} + +/*********** findbar ************/ +findbar { + overflow-x: hidden; +} + +/* Some elements that in HTML blocks should be inline-level by default */ +label, button, image { + display: -moz-inline-box; +} + +.menu-iconic-highlightable-text:not([highlightable="true"]), +.menu-iconic-text[highlightable="true"] { + display: none; +} + +[orient="vertical"] { -moz-box-orient: vertical !important; } +[orient="horizontal"] { -moz-box-orient: horizontal !important; } + +[align="start"] { -moz-box-align: start !important; } +[align="center"] { -moz-box-align: center !important; } +[align="end"] { -moz-box-align: end !important; } +[align="baseline"] { -moz-box-align: baseline !important; } +[align="stretch"] { -moz-box-align: stretch !important; } + +[pack="start"] { -moz-box-pack: start !important; } +[pack="center"] { -moz-box-pack: center !important; } +[pack="end"] { -moz-box-pack: end !important; } + +@supports -moz-bool-pref("layout.css.emulate-moz-box-with-flex") { + /* This isn't a real solution for [flex] and [ordinal], but it covers enough + cases to render the browser chrome for us to test emulated flex mode without + mass-changing existing markup and CSS. + If we get attr() in Bug 435426 this could work for all cases. */ + [flex="1"] { -moz-box-flex: 1; } + [flex="2"] { -moz-box-flex: 2; } + [flex="3"] { -moz-box-flex: 3; } + [flex="4"] { -moz-box-flex: 4; } + [flex="5"] { -moz-box-flex: 5; } + [flex="6"] { -moz-box-flex: 6; } + [flex="7"] { -moz-box-flex: 7; } + [flex="8"] { -moz-box-flex: 8; } + [flex="9"] { -moz-box-flex: 9; } + [flex="100"] { -moz-box-flex: 100; } + [flex="400"] { -moz-box-flex: 400; } + [flex="1000"] { -moz-box-flex: 1000; } + [flex="10000"] { -moz-box-flex: 10000; } +} |