forked from mirrors/gecko-dev
Merge autoland to mozilla-central. a=merge
This commit is contained in:
commit
2010f3a375
121 changed files with 3293 additions and 2974 deletions
|
|
@ -78,6 +78,8 @@ class LinkHandlerChild extends ActorChild {
|
|||
if (this._iconLoader) {
|
||||
this._iconLoader.onPageHide();
|
||||
}
|
||||
|
||||
this.seenTabIcon = false;
|
||||
}
|
||||
|
||||
onLinkEvent(event) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<blocklist lastupdate="1533550294490" xmlns="http://www.mozilla.org/2006/addons-blocklist">
|
||||
<blocklist lastupdate="1534336165428" xmlns="http://www.mozilla.org/2006/addons-blocklist">
|
||||
<emItems>
|
||||
<emItem blockID="i334" id="{0F827075-B026-42F3-885D-98981EE7B1AE}">
|
||||
<prefs/>
|
||||
|
|
@ -2308,6 +2308,18 @@
|
|||
<prefs/>
|
||||
<versionRange minVersion="0" maxVersion="*" severity="3"/>
|
||||
</emItem>
|
||||
<emItem blockID="cee5c2ab-1059-4b15-a78c-1203116552c4" id="/^(({0f9e469e-4245-43f8-a7a8-7e730f80d284})|({117ca2f3-df4c-4e17-a5c5-b49077e9c731})|({11db147a-a1cb-43dd-9c05-0d11683483e1})|({1ed2af70-9e89-42db-a9e8-17ae594003ac})|({24ed6bdc-3085-413b-a62e-dc5dd30272f4})|({2aa19a7a-2a43-4e0d-a3dc-abb33fa7e2b6})|({3d6fbbb3-6c80-47bb-af20-56fcaebcb9ca})|({42f4c194-8929-42b9-a9a3-afa56dd0913b})|({46740fa0-896d-4f2e-a240-9478865c47c2})|({4718da68-a373-4a03-a77b-0f49b8bb40ee})|({4d41e0b8-bf7e-45ab-bd90-c426b420e3ee})|({50957a38-c15d-42da-94f5-325bc74a554c})|({5650fc63-a7c5-4627-8d0a-99b20dcbd94b})|({5c5c38ec-08bf-493a-9352-6ccf25d60c08})|({67ecb446-9ccd-4193-a27f-7bd1521bd03c})|({71f01ffe-226d-4634-9b21-968f5ce9f8f5})|({72f31855-2412-4998-a6ff-978f89bba0c3})|({7b3c1e86-2599-4e1a-ad98-767ae38286c8})|({7c37463c-001e-4f58-9e88-aaab2a624551})|({7de64f18-8e6b-4c41-9b05-d8872b418026})|({82dcf841-c7e1-4764-bb47-caa28909e447})|({872f20ea-196e-4d11-8835-1cc4c877b1b8})|({8efee317-546f-418d-82d3-60cc5187acf5})|({93deeba1-0126-43f7-a94d-4eecfce53b33})|({9cc12446-16da-4200-b284-d5fc18670825})|({9cd27996-6068-4597-8e97-bb63f783a224})|({9fdcedc7-ffde-44c3-94f6-4196b1e0d9fc})|({a191563e-ac30-4c5a-af3d-85bb9e9f9286})|({a4cb0430-c92e-44c6-9427-6a6629c4c5f6})|({a87f1b9b-8817-4bff-80fd-db96020c56c8})|({ae29a313-c6a9-48be-918d-1e4c67ba642f})|({b2cea58a-845d-4394-9b02-8a31cfbb4873})|({b420e2be-df31-4bea-83f4-103fe0aa558c})|({b77afcab-0971-4c50-9486-f6f54845a273})|({b868c6f4-5841-4c14-86ee-d60bbfd1cec1})|({b99ae7b1-aabb-4674-ba8f-14ed32d04e76})|({b9bb8009-3716-4d0c-bcb4-35f9874e931e})|({c53c4cbc-04a7-4771-9e97-c08c85871e1e})|({ce0d1384-b99b-478e-850a-fa6dfbe5a2d4})|({cf8e8789-e75d-4823-939f-c49a9ae7fba2})|({d0f67c53-42b5-4650-b343-d9664c04c838})|({dfa77d38-f67b-4c41-80d5-96470d804d09})|({e20c916e-12ea-445b-b6f6-a42ec801b9f8})|({e2a4966f-919d-4afc-a94f-5bd6e0606711})|({e7d03b09-24b3-4d99-8e1b-c510f5d13612})|({fa8141ba-fa56-414e-91c0-898135c74c9d})|({fc99b961-5878-46b4-b091-6d2f507bf44d})|(firedocs@mozilla\.com)|(firetasks@mozilla\.com)|(getta@mozilla\.com)|(javideo@mozilla\.com)|(javideo2@mozilla\.com)|(javideos@mozilla\.com)|(javideosz@mozilla\.com)|(search_free@mozilla\.com)|(search-unlisted@mozilla\.com)|(search-unlisted101125511@mozilla\.com)|(search-unlisted10155511@mozilla\.com)|(search-unlisted1025525511@mozilla\.com)|(search-unlisted1099120071@mozilla\.com)|(search-unlisted1099125511@mozilla\.com)|(search-unlisted109925511@mozilla\.com)|(search-unlisted11@mozilla\.com)|(search-unlisted111@mozilla\.com)|(search-unlisted12@mozilla\.com)|(search-unlisted14400770034@mozilla\.com)|(search-unlisted144007741154@mozilla\.com)|(search-unlisted144436110034@mozilla\.com)|(search-unlisted14454@mozilla\.com)|(search-unlisted1570124111@mozilla\.com)|(search-unlisted1570254441111@mozilla\.com)|(search-unlisted15721239034@mozilla\.com)|(search-unlisted157441@mozilla\.com)|(search-unlisted15757771@mozilla\.com)|(search-unlisted1577122001@mozilla\.com)|(search-unlisted15777441001@mozilla\.com)|(search-unlisted15788120036001@mozilla\.com)|(search-unlisted157881200361111@mozilla\.com)|(search-unlisted1578899961111@mozilla\.com)|(search-unlisted157999658@mozilla\.com)|(search-unlisted158436561@mozilla\.com)|(search-unlisted158440374111@mozilla\.com)|(search-unlisted15874111@mozilla\.com)|(search-unlisted1741395551@mozilla\.com)|(search-unlisted17441000051@mozilla\.com)|(search-unlisted174410000522777441@mozilla\.com)|(search-unlisted1768fdgfdg@mozilla\.com)|(search-unlisted180000411@mozilla\.com)|(search-unlisted18000411@mozilla\.com)|(search-unlisted1800411@mozilla\.com)|(search-unlisted18011888@mozilla\.com)|(search-unlisted1801668@mozilla\.com)|(search-unlisted18033411@mozilla\.com)|(search-unlisted180888@mozilla\.com)|(search-unlisted181438@mozilla\.com)|(search-unlisted18411@mozilla\.com)|(search-unlisted18922544@mozilla\.com)|(search-unlisted1955511@mozilla\.com)|(search-unlisted2@mozilla\.com)|(search-unlisted3@mozilla\.com)|(search-unlisted4@mozilla\.com)|(search-unlisted400@mozilla\.com)|(search-unlisted40110@mozilla\.com)|(search-unlisted5@mozilla\.com)|(search-unlisted55@mozilla\.com)|(search@mozilla\.com)|(searchazsd@mozilla\.com)|(smart246@mozilla\.com)|(smarter1@mozilla\.com)|(smarters1@mozilla\.com)|(stream@mozilla\.com)|(tahdith@mozilla\.com)|(therill@mozilla\.com)|(Updates@mozilla\.com))$/">
|
||||
<prefs/>
|
||||
<versionRange minVersion="0" maxVersion="*" severity="3"/>
|
||||
</emItem>
|
||||
<emItem blockID="2734325e-143b-4962-98bf-4b18c77407e2" id="/^((Timemetric@tmetric)|(textMarkertool@underFlyingBirches\.org)|(youpanel@jetpack)|({6f13489d-b274-45b6-80fa-e9daa140e1a4})|({568db771-c718-4587-bcd0-e3728ee53550})|({829827cd-03be-4fed-af96-dd5997806fb4})|({9077390b-89a9-41ad-998f-ab973e37f26f})|({8e7269ac-a171-4d9f-9c0a-c504848fd52f})|({aaaffe20-3306-4c64-9fe5-66986ebb248e})|({bf153de7-cdf2-4554-af46-29dabfb2aa2d})|({c579191c-6bb8-4795-adca-d1bf180b512d})|({e2a4966f-919d-4afc-a94f-5bd6e0606711})|({ee97f92d-1bfe-4e9d-816c-0dfcd63a6206}))$/">
|
||||
<prefs/>
|
||||
<versionRange minVersion="0" maxVersion="*" severity="3"/>
|
||||
</emItem>
|
||||
<emItem blockID="d9892a76-b22e-40bd-8073-89b0f8110ec7" id="/^(({1a3fb414-0945-405c-a62a-9fe5e1a50c69})|({1a45f6aa-d80a-4317-84d2-0ce43671b08a})|({2d52a462-8bec-4708-9cd1-894b682bdc78})|({3f841cfc-de5a-421f-8bd7-2bf1d943b02a})|({5c7601bf-522b-47e5-b0f0-ea0e706af443})|({7ebe580f-71c9-4ef8-8073-f38deaeb9dfb})|({8b2188fd-1daf-4851-b387-28d964014353})|({8cee42ac-f1fe-40ae-aed6-24e3b76b2f77})|({8d13c4a9-5e8c-47a6-b583-681c83164ac9})|({9b1d775a-1877-45c9-ad48-d6fcfa4fff39})|({9efdbe5f-6e51-4a35-a41b-71dc939e6221})|({23f63efb-156e-440b-a96c-118bebc21057})|({026dfc8c-ecc8-41ba-b45f-70ffbd5cc672})|({34aa433c-27e9-4c87-a662-9f82f99eb9af})|({36f34d69-f22f-47c3-b4cd-4f37b7676107})|({39bd8607-0af4-4d6b-bd69-9a63c1825d3c})|({48c6ad6d-297c-4074-8fef-ca5f07683859})|({54aa688d-9504-481d-ba75-cfee421b98e0})|({59f59748-e6a8-4b41-87b5-9baadd75ddef})|({61d99407-1231-4edc-acc8-ab96cbbcf151})|({68ca8e3a-397a-4135-a3af-b6e4068a1eae})|({71beafd6-779b-4b7d-a78b-18a107277b59})|({83ed90f8-b07e-4c45-ba6b-ba2fe12cebb6})|({231dfb44-98e0-4bc4-b6ee-1dac4a836b08})|({273f0bce-33f4-45f6-ae03-df67df3864c2})|({392f4252-c731-4715-9f8d-d5815f766abb})|({484ec5d0-4cfd-4d96-88d0-a349bfc33780})|({569dbf47-cc10-41c4-8fd5-5f6cf4a833c7})|({578cad7a-57d5-404d-8dda-4d30de33b0c2})|({986b2c3f-e335-4b39-b3ad-46caf809d3aa})|({1091c11f-5983-410e-a715-0968754cff54})|({2330eb8a-e3fe-4b2e-9f17-9ddbfb96e6f5})|({5920b042-0af1-4658-97c1-602315d3b93d})|({6331a47f-8aae-490c-a9ad-eae786b4349f})|({6698b988-c3ef-4e1f-8740-08d52719eab5})|({30516f71-88d4-489b-a27f-d00a63ad459f})|({12089699-5570-4bf6-890f-07e7f674aa6e})|({84887738-92bf-4903-a5e8-695fd078c657})|({8562e48e-3723-412a-9ebd-b33d3d3b29dd})|({6e449795-c545-41be-92c0-5d467c147389})|({1e369c7c-6b61-436e-8978-4640687670d6})|({a03d427a-bd2e-42b6-828f-a57f38fac7b5})|({a77fc9b9-6ebb-418d-b0b6-86311c191158})|({a368025b-9828-43a1-8a5c-f6fab61c9be9})|({b1908b02-410d-4778-8856-7e259fbf471d})|({b9425ace-c2e9-4ec4-b564-4062546f4eca})|({b9845b5d-70c9-419c-a9a5-98ea8ee5cc01})|({ba99fee7-9806-4e32-8257-a33ffc3b8539})|({bdf8767d-ae4c-4d45-8f95-0ba29b910600})|({c6c4a718-cf91-4648-aa9b-170d66163cf2})|({ca0f2988-e1a8-4e83-afde-0dca56a17d5f})|({cac5db09-979b-40e3-8c8e-d96397b0eecb})|({d3b5280b-f8d8-4669-bdf6-91f23ae58042})|({d73d2f6a-ea24-4b1b-8c76-563fce9f786d})|({d77fed37-85c0-4b94-89bb-0d2849472b8d})|({d371abec-84bb-481b-acbf-235639451127})|({de47a3b4-dad1-4f4a-bdd6-8666586e29e8})|({ded6afad-2aaa-446b-b6bd-b12a8a61c945})|({e0c3a1ca-8e21-4d1b-b53b-ea115cf59172})|({e6bbf496-6489-4b48-8e5a-799aad4aa742})|({e63b262a-f9b8-4496-9c4b-9d3cbd6aea90})|({e73c1b5d-20f7-4d86-ad16-9de3c27718e2})|({eb01dc49-688f-4a21-aa8d-49bd88a8f319})|({edc9816b-60b4-493c-a090-01125e0b8018})|({effa2f97-0f07-44c8-99cb-32ac760a0621})|({f6e6fd9b-b89f-4e8d-9257-01405bc139a6})|({ff87977a-fefb-4a9d-b703-4b73dce8853d})|({ffea9e62-e516-4238-88a7-d6f9346f4955}))$/">
|
||||
<prefs/>
|
||||
<versionRange minVersion="0" maxVersion="*" severity="3"/>
|
||||
</emItem>
|
||||
</emItems>
|
||||
<pluginItems>
|
||||
<pluginItem blockID="p332">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -48,3 +48,6 @@ support-files =
|
|||
file_favicon_redirect.html
|
||||
file_favicon_redirect.ico
|
||||
file_favicon_redirect.ico^headers^
|
||||
[browser_rooticon.js]
|
||||
support-files =
|
||||
blank.html
|
||||
|
|
|
|||
21
browser/base/content/test/favicons/browser_rooticon.js
Normal file
21
browser/base/content/test/favicons/browser_rooticon.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
add_task(async () => {
|
||||
const testPath = "http://example.com/browser/browser/base/content/test/favicons/blank.html";
|
||||
const expectedIcon = "http://example.com/favicon.ico";
|
||||
|
||||
let tab = BrowserTestUtils.addTab(gBrowser, testPath);
|
||||
gBrowser.selectedTab = tab;
|
||||
let browser = tab.linkedBrowser;
|
||||
|
||||
let faviconPromise = waitForLinkAvailable(browser);
|
||||
await BrowserTestUtils.browserLoaded(browser);
|
||||
let iconURI = await faviconPromise;
|
||||
is(iconURI, expectedIcon, "Got correct initial icon.");
|
||||
|
||||
faviconPromise = waitForLinkAvailable(browser);
|
||||
BrowserTestUtils.loadURI(browser, testPath);
|
||||
await BrowserTestUtils.browserLoaded(browser);
|
||||
iconURI = await faviconPromise;
|
||||
is(iconURI, expectedIcon, "Got correct icon on second load.");
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
// browsing mode don't persist once the private browsing window closes.
|
||||
|
||||
const CB_PREF = "browser.contentblocking.enabled";
|
||||
const CB_UI_PREF = "browser.contentblocking.ui.enabled";
|
||||
const TP_PB_PREF = "privacy.trackingprotection.enabled";
|
||||
const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
|
||||
var TrackingProtection = null;
|
||||
|
|
@ -60,11 +61,13 @@ function testTrackingPage(window) {
|
|||
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
|
||||
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
|
||||
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is not showing add blocking");
|
||||
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is set to blocked");
|
||||
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is not showing add blocking");
|
||||
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is set to blocked");
|
||||
}
|
||||
}
|
||||
|
||||
function testTrackingPageUnblocked() {
|
||||
|
|
@ -85,11 +88,13 @@ function testTrackingPageUnblocked() {
|
|||
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
|
||||
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
|
||||
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is not showing add blocking");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is not set to blocked");
|
||||
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is not showing add blocking");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is not set to blocked");
|
||||
}
|
||||
}
|
||||
|
||||
add_task(async function testExceptionAddition() {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
const CB_PREF = "browser.contentblocking.enabled";
|
||||
const CB_UI_PREF = "browser.contentblocking.ui.enabled";
|
||||
const TP_PREF = "privacy.trackingprotection.enabled";
|
||||
const TP_PB_PREF = "privacy.trackingprotection.enabled";
|
||||
const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html";
|
||||
|
|
@ -66,7 +67,9 @@ function testBenignPage() {
|
|||
ok(!hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-detected"), "blocking detected label is hidden");
|
||||
|
||||
ok(hidden("#identity-popup-content-blocking-category-list"), "category list is hidden");
|
||||
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
|
||||
ok(hidden("#identity-popup-content-blocking-category-list"), "category list is hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function testBenignPageWithException() {
|
||||
|
|
@ -90,7 +93,9 @@ function testBenignPageWithException() {
|
|||
ok(!hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-detected"), "blocking detected label is hidden");
|
||||
|
||||
ok(hidden("#identity-popup-content-blocking-category-list"), "category list is hidden");
|
||||
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
|
||||
ok(hidden("#identity-popup-content-blocking-category-list"), "category list is hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function testTrackingPage(window) {
|
||||
|
|
@ -117,11 +122,13 @@ function testTrackingPage(window) {
|
|||
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
|
||||
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
|
||||
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is not showing add blocking");
|
||||
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is set to blocked");
|
||||
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is not showing add blocking");
|
||||
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is set to blocked");
|
||||
}
|
||||
}
|
||||
|
||||
function testTrackingPageUnblocked() {
|
||||
|
|
@ -142,11 +149,13 @@ function testTrackingPageUnblocked() {
|
|||
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
|
||||
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
|
||||
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is not showing add blocking");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is not set to blocked");
|
||||
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is not showing add blocking");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is not set to blocked");
|
||||
}
|
||||
}
|
||||
|
||||
function testTrackingPageWithCBDisabled() {
|
||||
|
|
@ -166,11 +175,13 @@ function testTrackingPageWithCBDisabled() {
|
|||
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
|
||||
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
|
||||
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is showing add blocking");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is not set to blocked");
|
||||
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
|
||||
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
|
||||
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
|
||||
"TP category item is showing add blocking");
|
||||
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
|
||||
"TP category item is not set to blocked");
|
||||
}
|
||||
}
|
||||
|
||||
async function testContentBlockingEnabled(tab) {
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@
|
|||
enabled="false"
|
||||
oncommand="ContentBlocking.onGlobalToggleCommand();" />
|
||||
</toolbaritem>
|
||||
<toolbarseparator id="appMenu-tp-separator" hidden="true" />
|
||||
<toolbarseparator id="appMenu-tp-separator"/>
|
||||
<toolbarbutton id="appMenu-new-window-button"
|
||||
class="subviewbutton subviewbutton-iconic"
|
||||
label="&newNavigatorCmd.label;"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
add_task(async function testSetup() {
|
||||
Services.prefs.setBoolPref("toolkit.cosmeticAnimations.enabled", false);
|
||||
});
|
||||
|
||||
add_task(async function testPopupSelectPopup() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
background() {
|
||||
|
|
@ -46,6 +50,13 @@ add_task(async function testPopupSelectPopup() {
|
|||
async function testPanel(browser) {
|
||||
let popupPromise = promisePopupShown(selectPopup);
|
||||
|
||||
// Wait the select element in the popup window to be ready before sending a
|
||||
// mouse event to open the select popup.
|
||||
await ContentTask.spawn(browser, null, async () => {
|
||||
await ContentTaskUtils.waitForCondition(() => {
|
||||
return content.document && content.document.querySelector("#select");
|
||||
});
|
||||
});
|
||||
BrowserTestUtils.synthesizeMouseAtCenter("#select", {}, browser);
|
||||
|
||||
await popupPromise;
|
||||
|
|
@ -65,6 +76,11 @@ add_task(async function testPopupSelectPopup() {
|
|||
|
||||
is(Math.floor(boxObject.screenY + elemRect.bottom), popupRect.top,
|
||||
"Select popup has the correct y origin");
|
||||
|
||||
// Close the select popup before proceeding to the next test.
|
||||
const onPopupHidden = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
|
||||
selectPopup.hidePopup();
|
||||
await onPopupHidden;
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -87,3 +103,7 @@ add_task(async function testPopupSelectPopup() {
|
|||
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function testTeardown() {
|
||||
Services.prefs.clearUserPref("toolkit.cosmeticAnimations.enabled");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ payment-dialog > header > .page-error {
|
|||
grid-area: main;
|
||||
position: relative;
|
||||
max-height: 100%;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.page {
|
||||
|
|
@ -86,6 +85,7 @@ payment-dialog > header > .page-error {
|
|||
flex-grow: 1;
|
||||
/* The area above the footer should scroll, if necessary. */
|
||||
overflow: auto;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.page > .page-body > h2:empty {
|
||||
|
|
|
|||
|
|
@ -337,8 +337,10 @@ var PermissionPromptPrototype = {
|
|||
this.permissionKey,
|
||||
promptAction.action,
|
||||
scope);
|
||||
} else if (promptAction.action == SitePermissions.BLOCK) {
|
||||
// Temporarily store BLOCK permissions only.
|
||||
} else if (promptAction.action == SitePermissions.BLOCK ||
|
||||
SitePermissions.permitTemporaryAllow(this.permissionKey)) {
|
||||
// Temporarily store BLOCK permissions only unless permission object
|
||||
// sets permitTemporaryAllow: true
|
||||
// SitePermissions does not consider subframes when storing temporary
|
||||
// permissions on a tab, thus storing ALLOW could be exploited.
|
||||
SitePermissions.set(this.principal.URI,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ var gStringBundle =
|
|||
Services.strings.createBundle("chrome://browser/locale/sitePermissions.properties");
|
||||
|
||||
/**
|
||||
* A helper module to manage temporarily blocked permissions.
|
||||
* A helper module to manage temporary permissions.
|
||||
*
|
||||
* Permissions are keyed by browser, so methods take a Browser
|
||||
* element to identify the corresponding permission set.
|
||||
|
|
@ -20,7 +20,7 @@ var gStringBundle =
|
|||
* automatically cleared once the browser stops existing
|
||||
* (once there are no other references to the browser object);
|
||||
*/
|
||||
const TemporaryBlockedPermissions = {
|
||||
const TemporaryPermissions = {
|
||||
// This is a three level deep map with the following structure:
|
||||
//
|
||||
// Browser => {
|
||||
|
|
@ -38,20 +38,20 @@ const TemporaryBlockedPermissions = {
|
|||
|
||||
// Private helper method that bundles some shared behavior for
|
||||
// get() and getAll(), e.g. deleting permissions when they have expired.
|
||||
_get(entry, prePath, id, timeStamp) {
|
||||
if (timeStamp == null) {
|
||||
_get(entry, prePath, id, permission) {
|
||||
if (permission == null || permission.timeStamp == null) {
|
||||
delete entry[prePath][id];
|
||||
return null;
|
||||
}
|
||||
if (timeStamp + SitePermissions.temporaryPermissionExpireTime < Date.now()) {
|
||||
if (permission.timeStamp + SitePermissions.temporaryPermissionExpireTime < Date.now()) {
|
||||
delete entry[prePath][id];
|
||||
return null;
|
||||
}
|
||||
return {id, state: SitePermissions.BLOCK, scope: SitePermissions.SCOPE_TEMPORARY};
|
||||
return {id, state: permission.state, scope: SitePermissions.SCOPE_TEMPORARY};
|
||||
},
|
||||
|
||||
// Sets a new permission for the specified browser.
|
||||
set(browser, id) {
|
||||
set(browser, id, state) {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ const TemporaryBlockedPermissions = {
|
|||
if (!entry[prePath]) {
|
||||
entry[prePath] = {};
|
||||
}
|
||||
entry[prePath][id] = Date.now();
|
||||
entry[prePath][id] = {timeStamp: Date.now(), state};
|
||||
},
|
||||
|
||||
// Removes a permission with the specified id for the specified browser.
|
||||
|
|
@ -292,7 +292,7 @@ var SitePermissions = {
|
|||
getAllForBrowser(browser) {
|
||||
let permissions = {};
|
||||
|
||||
for (let permission of TemporaryBlockedPermissions.getAll(browser)) {
|
||||
for (let permission of TemporaryPermissions.getAll(browser)) {
|
||||
permission.scope = this.SCOPE_TEMPORARY;
|
||||
permissions[permission.id] = permission;
|
||||
}
|
||||
|
|
@ -417,6 +417,23 @@ var SitePermissions = {
|
|||
return false;
|
||||
},
|
||||
|
||||
/*
|
||||
* Return whether SitePermissions is permitted to store a TEMPORARY ALLOW
|
||||
* state for a particular permission.
|
||||
*
|
||||
* @param {string} permissionID
|
||||
* The ID to get the state for.
|
||||
*
|
||||
* @return boolean Whether storing TEMPORARY ALLOW is permitted.
|
||||
*/
|
||||
permitTemporaryAllow(permissionID) {
|
||||
if (permissionID in gPermissionObject &&
|
||||
gPermissionObject[permissionID].permitTemporaryAllow)
|
||||
return gPermissionObject[permissionID].permitTemporaryAllow;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the state and scope of a particular permission for a given URI.
|
||||
*
|
||||
|
|
@ -461,7 +478,7 @@ var SitePermissions = {
|
|||
if (result.state == defaultState) {
|
||||
// If there's no persistent permission saved, check if we have something
|
||||
// set temporarily.
|
||||
let value = TemporaryBlockedPermissions.get(browser, permissionID);
|
||||
let value = TemporaryPermissions.get(browser, permissionID);
|
||||
|
||||
if (value) {
|
||||
result.state = value.state;
|
||||
|
|
@ -520,9 +537,9 @@ var SitePermissions = {
|
|||
// URI and only consider the current browser URI, to avoid notification spamming.
|
||||
//
|
||||
// If you ever consider removing this line, you likely want to implement
|
||||
// a more fine-grained TemporaryBlockedPermissions that temporarily blocks for the
|
||||
// a more fine-grained TemporaryPermissions that temporarily blocks for the
|
||||
// entire browser, but temporarily allows only for specific frames.
|
||||
if (state != this.BLOCK) {
|
||||
if (state != this.BLOCK && !this.permitTemporaryAllow(permissionID)) {
|
||||
throw "'Block' is the only permission we can save temporarily on a browser";
|
||||
}
|
||||
|
||||
|
|
@ -530,7 +547,7 @@ var SitePermissions = {
|
|||
throw "TEMPORARY scoped permissions require a browser object";
|
||||
}
|
||||
|
||||
TemporaryBlockedPermissions.set(browser, permissionID);
|
||||
TemporaryPermissions.set(browser, permissionID, state);
|
||||
|
||||
browser.dispatchEvent(new browser.ownerGlobal
|
||||
.CustomEvent("PermissionStateChange"));
|
||||
|
|
@ -562,10 +579,10 @@ var SitePermissions = {
|
|||
if (this.isSupportedURI(uri))
|
||||
Services.perms.remove(uri, permissionID);
|
||||
|
||||
// TemporaryBlockedPermissions.get() deletes expired permissions automatically,
|
||||
if (TemporaryBlockedPermissions.get(browser, permissionID)) {
|
||||
// TemporaryPermissions.get() deletes expired permissions automatically,
|
||||
if (TemporaryPermissions.get(browser, permissionID)) {
|
||||
// If it exists but has not expired, remove it explicitly.
|
||||
TemporaryBlockedPermissions.remove(browser, permissionID);
|
||||
TemporaryPermissions.remove(browser, permissionID);
|
||||
// Send a PermissionStateChange event only if the permission hasn't expired.
|
||||
browser.dispatchEvent(new browser.ownerGlobal
|
||||
.CustomEvent("PermissionStateChange"));
|
||||
|
|
@ -579,7 +596,7 @@ var SitePermissions = {
|
|||
* The browser object to clear.
|
||||
*/
|
||||
clearTemporaryPermissions(browser) {
|
||||
TemporaryBlockedPermissions.clear(browser);
|
||||
TemporaryPermissions.clear(browser);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -592,7 +609,7 @@ var SitePermissions = {
|
|||
* The browser object to copy to.
|
||||
*/
|
||||
copyTemporaryPermissions(browser, newBrowser) {
|
||||
TemporaryBlockedPermissions.copy(browser, newBrowser);
|
||||
TemporaryPermissions.copy(browser, newBrowser);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -702,6 +719,7 @@ var gPermissionObject = {
|
|||
"autoplay-media": {
|
||||
exactHostMatch: true,
|
||||
showGloballyBlocked: true,
|
||||
permitTemporaryAllow: true,
|
||||
getDefault() {
|
||||
let state = Services.prefs.getIntPref("media.autoplay.default",
|
||||
Ci.nsIAutoplay.PROMPT);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,32 @@ add_task(async function testTempAllowThrows() {
|
|||
});
|
||||
});
|
||||
|
||||
// Tests that we can set TEMPORARY ALLOW permissions for autoplay-media
|
||||
add_task(async function testTempAutoplayAllowed() {
|
||||
Services.prefs.setIntPref("media.autoplay.default", 2);
|
||||
|
||||
let uri = Services.io.newURI("https://example.com");
|
||||
let permId = "autoplay-media";
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
|
||||
|
||||
SitePermissions.set(uri, permId, SitePermissions.ALLOW,
|
||||
SitePermissions.SCOPE_TEMPORARY, tab.linkedBrowser);
|
||||
|
||||
let permissions = SitePermissions.getAllPermissionDetailsForBrowser(tab.linkedBrowser);
|
||||
|
||||
let autoplay = permissions.find(({id}) => id === "autoplay-media");
|
||||
Assert.deepEqual(autoplay, {
|
||||
id: "autoplay-media",
|
||||
label: "Automatically Play Media with Sound",
|
||||
state: SitePermissions.ALLOW,
|
||||
scope: SitePermissions.SCOPE_TEMPORARY,
|
||||
});
|
||||
|
||||
Services.prefs.clearUserPref("media.autoplay.default");
|
||||
BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
});
|
||||
|
||||
// This tests the SitePermissions.getAllPermissionDetailsForBrowser function.
|
||||
add_task(async function testGetAllPermissionDetailsForBrowser() {
|
||||
let uri = Services.io.newURI("https://example.com");
|
||||
|
|
|
|||
|
|
@ -272,10 +272,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* CONNECTION ICON, EXTENSION ICON */
|
||||
/* CONNECTION ICON, EXTENSION ICON, REMOTE CONTROL ICON */
|
||||
|
||||
#connection-icon,
|
||||
#extension-icon {
|
||||
#extension-icon,
|
||||
#remote-control-icon {
|
||||
visibility: collapse;
|
||||
}
|
||||
#urlbar[pageproxystate="valid"] > #identity-box.verifiedDomain > #connection-icon,
|
||||
|
|
@ -308,8 +309,6 @@
|
|||
visibility: visible;
|
||||
}
|
||||
|
||||
/* REMOTE CONTROL ICON */
|
||||
|
||||
#main-window[remotecontrol] #remote-control-icon {
|
||||
list-style-image: url(chrome://browser/content/static-robot.png);
|
||||
visibility: visible;
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ def bootstrap(topsrcdir, mozilla_dir=None):
|
|||
|
||||
def should_skip_dispatch(context, handler):
|
||||
# The user is performing a maintenance command.
|
||||
if handler.name in ('bootstrap', 'doctor', 'mach-commands', 'mercurial-setup'):
|
||||
if handler.name in ('bootstrap', 'doctor', 'mach-commands', 'vcs-setup'):
|
||||
return True
|
||||
|
||||
# We are running in automation.
|
||||
|
|
|
|||
|
|
@ -1,7 +0,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/.
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += [
|
||||
'test/browser.ini',
|
||||
]
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// Extend from the shared list of defined globals for mochitests.
|
||||
"extends": "../../../.eslintrc.mochitests.js",
|
||||
"globals": {
|
||||
"helpers": true,
|
||||
"assert": true
|
||||
}
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
[DEFAULT]
|
||||
tags = devtools
|
||||
subsuite = devtools
|
||||
support-files =
|
||||
head.js
|
||||
helpers.js
|
||||
mockCommands.js
|
||||
|
||||
# Bug 1447494 - browser.ini file has to have at least one test file
|
||||
# browser.js is empty, we have this just to expose test helper files
|
||||
# for other folder still using it
|
||||
[browser.js]
|
||||
skip-if = true
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1,41 +0,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/. */
|
||||
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
|
||||
/* import-globals-from helpers.js */
|
||||
/* import-globals-from mockCommands.js */
|
||||
|
||||
"use strict";
|
||||
|
||||
const TEST_BASE_HTTP = "http://example.com/browser/devtools/client/commandline/test/";
|
||||
const TEST_BASE_HTTPS = "https://example.com/browser/devtools/client/commandline/test/";
|
||||
|
||||
var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
var flags = require("devtools/shared/flags");
|
||||
var { Task } = require("devtools/shared/task");
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/shared/test/telemetry-test-helpers.js", this);
|
||||
|
||||
// Import the GCLI test helper
|
||||
var testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
|
||||
Services.scriptloader.loadSubScript(testDir + "/helpers.js", this);
|
||||
Services.scriptloader.loadSubScript(testDir + "/mockCommands.js", this, "UTF-8");
|
||||
|
||||
function whenDelayedStartupFinished(aWindow, aCallback) {
|
||||
Services.obs.addObserver(function observer(aSubject, aTopic) {
|
||||
if (aWindow == aSubject) {
|
||||
Services.obs.removeObserver(observer, aTopic);
|
||||
executeSoon(aCallback);
|
||||
}
|
||||
}, "browser-delayed-startup-finished");
|
||||
}
|
||||
|
||||
/**
|
||||
* Force GC on shutdown, because it seems that GCLI can outrun the garbage
|
||||
* collector in some situations, which causes test failures in later tests
|
||||
* Bug 774619 is an example.
|
||||
*/
|
||||
registerCleanupFunction(function tearDown() {
|
||||
window.windowUtils.garbageCollect();
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,794 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012, Mozilla Foundation and contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
|
||||
// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
|
||||
|
||||
var mockCommands;
|
||||
if (typeof exports !== "undefined") {
|
||||
// If we're being loaded via require();
|
||||
mockCommands = exports;
|
||||
}
|
||||
else {
|
||||
// If we're being loaded via loadScript in mochitest
|
||||
mockCommands = {};
|
||||
}
|
||||
|
||||
// We use an alias for exports here because this module is used in Firefox
|
||||
// mochitests where we don't have define/require
|
||||
|
||||
/**
|
||||
* Registration and de-registration.
|
||||
*/
|
||||
mockCommands.setup = function (requisition) {
|
||||
requisition.system.addItems(mockCommands.items);
|
||||
};
|
||||
|
||||
mockCommands.shutdown = function (requisition) {
|
||||
requisition.system.removeItems(mockCommands.items);
|
||||
};
|
||||
|
||||
function createExec(name) {
|
||||
return function (args, context) {
|
||||
var promises = [];
|
||||
|
||||
Object.keys(args).map(argName => {
|
||||
var value = args[argName];
|
||||
var type = this.getParameterByName(argName).type;
|
||||
var promise = Promise.resolve(type.stringify(value, context));
|
||||
promises.push(promise.then(str => {
|
||||
return { name: argName, value: str };
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(data => {
|
||||
var argValues = {};
|
||||
data.forEach(function (entry) { argValues[entry.name] = entry.value; });
|
||||
|
||||
return context.typedData("testCommandOutput", {
|
||||
name: name,
|
||||
args: argValues
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
mockCommands.items = [
|
||||
{
|
||||
item: "converter",
|
||||
from: "testCommandOutput",
|
||||
to: "dom",
|
||||
exec: function (testCommandOutput, context) {
|
||||
var view = context.createView({
|
||||
data: testCommandOutput,
|
||||
html: "" +
|
||||
"<table>" +
|
||||
"<thead>" +
|
||||
"<tr>" +
|
||||
'<th colspan="3">Exec: ${name}</th>' +
|
||||
"</tr>" +
|
||||
"</thead>" +
|
||||
"<tbody>" +
|
||||
'<tr foreach="key in ${args}">' +
|
||||
"<td> ${key}</td>" +
|
||||
"<td>=</td>" +
|
||||
"<td>${args[key]}</td>" +
|
||||
"</tr>" +
|
||||
"</tbody>" +
|
||||
"</table>",
|
||||
options: {
|
||||
allowEval: true
|
||||
}
|
||||
});
|
||||
|
||||
return view.toDom(context.document);
|
||||
}
|
||||
},
|
||||
{
|
||||
item: "converter",
|
||||
from: "testCommandOutput",
|
||||
to: "string",
|
||||
exec: function (testCommandOutput, context) {
|
||||
var argsOut = Object.keys(testCommandOutput.args).map(function (key) {
|
||||
return key + "=" + testCommandOutput.args[key];
|
||||
}).join(" ");
|
||||
return "Exec: " + testCommandOutput.name + " " + argsOut;
|
||||
}
|
||||
},
|
||||
{
|
||||
item: "type",
|
||||
name: "optionType",
|
||||
parent: "selection",
|
||||
lookup: [
|
||||
{
|
||||
name: "option1",
|
||||
value: "string"
|
||||
},
|
||||
{
|
||||
name: "option2",
|
||||
value: "number"
|
||||
},
|
||||
{
|
||||
name: "option3",
|
||||
value: {
|
||||
name: "selection",
|
||||
lookup: [
|
||||
{ name: "one", value: 1 },
|
||||
{ name: "two", value: 2 },
|
||||
{ name: "three", value: 3 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
item: "type",
|
||||
name: "optionValue",
|
||||
parent: "delegate",
|
||||
delegateType: function (executionContext) {
|
||||
if (executionContext != null) {
|
||||
var option = executionContext.getArgsObject().optionType;
|
||||
if (option != null) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
return "blank";
|
||||
}
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsv",
|
||||
params: [
|
||||
{ name: "optionType", type: "optionType" },
|
||||
{ name: "optionValue", type: "optionValue" }
|
||||
],
|
||||
exec: createExec("tsv")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsr",
|
||||
params: [ { name: "text", type: "string" } ],
|
||||
exec: createExec("tsr")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsrsrsr",
|
||||
params: [
|
||||
{ name: "p1", type: "string" },
|
||||
{ name: "p2", type: "string" },
|
||||
{ name: "p3", type: { name: "string", allowBlank: true} },
|
||||
],
|
||||
exec: createExec("tsrsrsr")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tso",
|
||||
params: [ { name: "text", type: "string", defaultValue: null } ],
|
||||
exec: createExec("tso")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tse",
|
||||
params: [
|
||||
{ name: "node", type: "node" },
|
||||
{
|
||||
group: "options",
|
||||
params: [
|
||||
{ name: "nodes", type: { name: "nodelist" } },
|
||||
{ name: "nodes2", type: { name: "nodelist", allowEmpty: true } }
|
||||
]
|
||||
}
|
||||
],
|
||||
exec: createExec("tse")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsj",
|
||||
params: [ { name: "javascript", type: "javascript" } ],
|
||||
exec: createExec("tsj")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsb",
|
||||
params: [ { name: "toggle", type: "boolean" } ],
|
||||
exec: createExec("tsb")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tss",
|
||||
exec: createExec("tss")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsu",
|
||||
params: [
|
||||
{
|
||||
name: "num",
|
||||
type: {
|
||||
name: "number",
|
||||
max: 10,
|
||||
min: -5,
|
||||
step: 3
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: createExec("tsu")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsf",
|
||||
params: [
|
||||
{
|
||||
name: "num",
|
||||
type: {
|
||||
name: "number",
|
||||
allowFloat: true,
|
||||
max: 11.5,
|
||||
min: -6.5,
|
||||
step: 1.5
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: createExec("tsf")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn"
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn dif",
|
||||
params: [ { name: "text", type: "string", description: "tsn dif text" } ],
|
||||
exec: createExec("tsnDif")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn hidden",
|
||||
hidden: true,
|
||||
exec: createExec("tsnHidden")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn ext",
|
||||
params: [ { name: "text", type: "string" } ],
|
||||
exec: createExec("tsnExt")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn exte",
|
||||
params: [ { name: "text", type: "string" } ],
|
||||
exec: createExec("tsnExte")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn exten",
|
||||
params: [ { name: "text", type: "string" } ],
|
||||
exec: createExec("tsnExten")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn extend",
|
||||
params: [ { name: "text", type: "string" } ],
|
||||
exec: createExec("tsnExtend")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn deep"
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn deep down"
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn deep down nested"
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsn deep down nested cmd",
|
||||
exec: createExec("tsnDeepDownNestedCmd")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tshidden",
|
||||
hidden: true,
|
||||
params: [
|
||||
{
|
||||
group: "Options",
|
||||
params: [
|
||||
{
|
||||
name: "visible",
|
||||
type: "string",
|
||||
short: "v",
|
||||
defaultValue: null,
|
||||
description: "visible"
|
||||
},
|
||||
{
|
||||
name: "invisiblestring",
|
||||
type: "string",
|
||||
short: "i",
|
||||
description: "invisiblestring",
|
||||
defaultValue: null,
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
name: "invisibleboolean",
|
||||
short: "b",
|
||||
type: "boolean",
|
||||
description: "invisibleboolean",
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
exec: createExec("tshidden")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tselarr",
|
||||
params: [
|
||||
{ name: "num", type: { name: "selection", data: [ "1", "2", "3" ] } },
|
||||
{ name: "arr", type: { name: "array", subtype: "string" } }
|
||||
],
|
||||
exec: createExec("tselarr")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsm",
|
||||
description: "a 3-param test selection|string|number",
|
||||
params: [
|
||||
{ name: "abc", type: { name: "selection", data: [ "a", "b", "c" ] } },
|
||||
{ name: "txt", type: "string" },
|
||||
{ name: "num", type: { name: "number", max: 42, min: 0 } }
|
||||
],
|
||||
exec: createExec("tsm")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsg",
|
||||
description: "a param group test",
|
||||
params: [
|
||||
{
|
||||
name: "solo",
|
||||
type: { name: "selection", data: [ "aaa", "bbb", "ccc" ] },
|
||||
description: "solo param"
|
||||
},
|
||||
{
|
||||
group: "First",
|
||||
params: [
|
||||
{
|
||||
name: "txt1",
|
||||
type: "string",
|
||||
defaultValue: null,
|
||||
description: "txt1 param"
|
||||
},
|
||||
{
|
||||
name: "bool",
|
||||
type: "boolean",
|
||||
description: "bool param"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "txt2",
|
||||
type: "string",
|
||||
defaultValue: "d",
|
||||
description: "txt2 param",
|
||||
option: "Second"
|
||||
},
|
||||
{
|
||||
name: "num",
|
||||
type: { name: "number", min: 40 },
|
||||
defaultValue: 42,
|
||||
description: "num param",
|
||||
option: "Second"
|
||||
}
|
||||
],
|
||||
exec: createExec("tsg")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tscook",
|
||||
description: "param group test to catch problems with cookie command",
|
||||
params: [
|
||||
{
|
||||
name: "key",
|
||||
type: "string",
|
||||
description: "tscookKeyDesc"
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "string",
|
||||
description: "tscookValueDesc"
|
||||
},
|
||||
{
|
||||
group: "tscookOptionsDesc",
|
||||
params: [
|
||||
{
|
||||
name: "path",
|
||||
type: "string",
|
||||
defaultValue: "/",
|
||||
description: "tscookPathDesc"
|
||||
},
|
||||
{
|
||||
name: "domain",
|
||||
type: "string",
|
||||
defaultValue: null,
|
||||
description: "tscookDomainDesc"
|
||||
},
|
||||
{
|
||||
name: "secure",
|
||||
type: "boolean",
|
||||
description: "tscookSecureDesc"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
exec: createExec("tscook")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tslong",
|
||||
description: "long param tests to catch problems with the jsb command",
|
||||
params: [
|
||||
{
|
||||
name: "msg",
|
||||
type: "string",
|
||||
description: "msg Desc"
|
||||
},
|
||||
{
|
||||
group: "Options Desc",
|
||||
params: [
|
||||
{
|
||||
name: "num",
|
||||
short: "n",
|
||||
type: "number",
|
||||
description: "num Desc",
|
||||
defaultValue: 2
|
||||
},
|
||||
{
|
||||
name: "sel",
|
||||
short: "s",
|
||||
type: {
|
||||
name: "selection",
|
||||
lookup: [
|
||||
{ name: "space", value: " " },
|
||||
{ name: "tab", value: "\t" }
|
||||
]
|
||||
},
|
||||
description: "sel Desc",
|
||||
defaultValue: " "
|
||||
},
|
||||
{
|
||||
name: "bool",
|
||||
short: "b",
|
||||
type: "boolean",
|
||||
description: "bool Desc"
|
||||
},
|
||||
{
|
||||
name: "num2",
|
||||
short: "m",
|
||||
type: "number",
|
||||
description: "num2 Desc",
|
||||
defaultValue: -1
|
||||
},
|
||||
{
|
||||
name: "bool2",
|
||||
short: "c",
|
||||
type: "boolean",
|
||||
description: "bool2 Desc"
|
||||
},
|
||||
{
|
||||
name: "sel2",
|
||||
short: "t",
|
||||
type: {
|
||||
name: "selection",
|
||||
data: [ "collapse", "basic", "with space", "with two spaces" ]
|
||||
},
|
||||
description: "sel2 Desc",
|
||||
defaultValue: "collapse"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
exec: createExec("tslong")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsdate",
|
||||
description: "long param tests to catch problems with the jsb command",
|
||||
params: [
|
||||
{
|
||||
name: "d1",
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "d2",
|
||||
type: {
|
||||
name: "date",
|
||||
min: "1 jan 2000",
|
||||
max: "28 feb 2000",
|
||||
step: 2
|
||||
}
|
||||
},
|
||||
],
|
||||
exec: createExec("tsdate")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsfail",
|
||||
description: "test errors",
|
||||
params: [
|
||||
{
|
||||
name: "method",
|
||||
type: {
|
||||
name: "selection",
|
||||
data: [
|
||||
"reject", "rejecttyped",
|
||||
"throwerror", "throwstring", "throwinpromise",
|
||||
"noerror"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: function (args, context) {
|
||||
if (args.method === "reject") {
|
||||
return new Promise(function (resolve, reject) {
|
||||
context.environment.window.setTimeout(function () {
|
||||
reject("rejected promise");
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
|
||||
if (args.method === "rejecttyped") {
|
||||
return new Promise(function (resolve, reject) {
|
||||
context.environment.window.setTimeout(function () {
|
||||
reject(context.typedData("number", 54));
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
|
||||
if (args.method === "throwinpromise") {
|
||||
return new Promise(function (resolve, reject) {
|
||||
context.environment.window.setTimeout(function () {
|
||||
resolve("should be lost");
|
||||
}, 10);
|
||||
}).then(function () {
|
||||
var t = null;
|
||||
return t.foo;
|
||||
});
|
||||
}
|
||||
|
||||
if (args.method === "throwerror") {
|
||||
throw new Error("thrown error");
|
||||
}
|
||||
|
||||
if (args.method === "throwstring") {
|
||||
throw "thrown string";
|
||||
}
|
||||
|
||||
return "no error";
|
||||
}
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsfile",
|
||||
description: "test file params",
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsfile open",
|
||||
description: "a file param in open mode",
|
||||
params: [
|
||||
{
|
||||
name: "p1",
|
||||
type: {
|
||||
name: "file",
|
||||
filetype: "file",
|
||||
existing: "yes"
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: createExec("tsfile open")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsfile saveas",
|
||||
description: "a file param in saveas mode",
|
||||
params: [
|
||||
{
|
||||
name: "p1",
|
||||
type: {
|
||||
name: "file",
|
||||
filetype: "file",
|
||||
existing: "no"
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: createExec("tsfile saveas")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsfile save",
|
||||
description: "a file param in save mode",
|
||||
params: [
|
||||
{
|
||||
name: "p1",
|
||||
type: {
|
||||
name: "file",
|
||||
filetype: "file",
|
||||
existing: "maybe"
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: createExec("tsfile save")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsfile cd",
|
||||
description: "a file param in cd mode",
|
||||
params: [
|
||||
{
|
||||
name: "p1",
|
||||
type: {
|
||||
name: "file",
|
||||
filetype: "directory",
|
||||
existing: "yes"
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: createExec("tsfile cd")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsfile mkdir",
|
||||
description: "a file param in mkdir mode",
|
||||
params: [
|
||||
{
|
||||
name: "p1",
|
||||
type: {
|
||||
name: "file",
|
||||
filetype: "directory",
|
||||
existing: "no"
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: createExec("tsfile mkdir")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsfile rm",
|
||||
description: "a file param in rm mode",
|
||||
params: [
|
||||
{
|
||||
name: "p1",
|
||||
type: {
|
||||
name: "file",
|
||||
filetype: "any",
|
||||
existing: "yes"
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: createExec("tsfile rm")
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsslow",
|
||||
params: [
|
||||
{
|
||||
name: "hello",
|
||||
type: {
|
||||
name: "selection",
|
||||
data: function (context) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
context.environment.window.setTimeout(function () {
|
||||
resolve([
|
||||
"Shalom", "Namasté", "Hallo", "Dydd-da",
|
||||
"Chào", "Hej", "Saluton", "Sawubona"
|
||||
]);
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
exec: function (args, context) {
|
||||
return "Test completed";
|
||||
}
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "urlc",
|
||||
params: [
|
||||
{
|
||||
name: "url",
|
||||
type: "url"
|
||||
}
|
||||
],
|
||||
returnType: "json",
|
||||
exec: function (args, context) {
|
||||
return args;
|
||||
}
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "unionc1",
|
||||
params: [
|
||||
{
|
||||
name: "first",
|
||||
type: {
|
||||
name: "union",
|
||||
alternatives: [
|
||||
{
|
||||
name: "selection",
|
||||
lookup: [
|
||||
{ name: "one", value: 1 },
|
||||
{ name: "two", value: 2 },
|
||||
]
|
||||
},
|
||||
"number",
|
||||
{ name: "string" }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
returnType: "json",
|
||||
exec: function (args, context) {
|
||||
return args;
|
||||
}
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "unionc2",
|
||||
params: [
|
||||
{
|
||||
name: "first",
|
||||
type: {
|
||||
name: "union",
|
||||
alternatives: [
|
||||
{
|
||||
name: "selection",
|
||||
lookup: [
|
||||
{ name: "one", value: 1 },
|
||||
{ name: "two", value: 2 },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "url"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
returnType: "json",
|
||||
exec: function (args, context) {
|
||||
return args;
|
||||
}
|
||||
},
|
||||
{
|
||||
item: "command",
|
||||
name: "tsres",
|
||||
params: [
|
||||
{
|
||||
name: "resource",
|
||||
type: "resource"
|
||||
}
|
||||
],
|
||||
exec: createExec("tsres"),
|
||||
}
|
||||
];
|
||||
|
|
@ -5,7 +5,6 @@ skip-if = (os == 'linux' && debug && bits == 32)
|
|||
support-files =
|
||||
head.js
|
||||
helpers.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
!/devtools/client/shared/test/telemetry-test-helpers.js
|
||||
## START-SOURCEMAPPED-FIXTURES - Generated by examples/sourcemapped/build.js
|
||||
|
|
|
|||
|
|
@ -128,7 +128,6 @@ support-files =
|
|||
sjs_post-page.sjs
|
||||
sjs_random-javascript.sjs
|
||||
testactors.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
!/devtools/client/shared/test/telemetry-test-helpers.js
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,6 @@ support-files =
|
|||
sjs_post-page.sjs
|
||||
sjs_random-javascript.sjs
|
||||
testactors.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
!/devtools/client/shared/test/telemetry-test-helpers.js
|
||||
|
||||
|
|
|
|||
|
|
@ -66,8 +66,6 @@ registerCleanupFunction(async function() {
|
|||
var testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
|
||||
testDir = testDir.replace(/\/\//g, "/");
|
||||
testDir = testDir.replace("chrome:/mochitest", "chrome://mochitest");
|
||||
var helpersjs = testDir + "/../../../commandline/test/helpers.js";
|
||||
Services.scriptloader.loadSubScript(helpersjs, this);
|
||||
|
||||
function addWindow(aUrl) {
|
||||
info("Adding window: " + aUrl);
|
||||
|
|
@ -669,7 +667,7 @@ AddonDebugger.prototype = {
|
|||
this.client.addListener("consoleAPICall", this._onConsoleAPICall);
|
||||
deferred.resolve();
|
||||
}, e => {
|
||||
deferred.reject(e);
|
||||
deferred.reject(e);
|
||||
});
|
||||
return deferred.promise;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,5 +6,76 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>SW-test</h1>
|
||||
<script>
|
||||
function register() {
|
||||
return Promise.resolve().then(function() {
|
||||
// While ServiceWorkerContainer.register() returns a promise, it's
|
||||
// still wrapped with a .then() because navigator.serviceWorker is not
|
||||
// defined in insecure contexts unless service worker testing is
|
||||
// enabled, so dereferencing it would throw a ReferenceError (which
|
||||
// is then caught in the .catch() clause).
|
||||
return window.navigator.serviceWorker.register("serviceworker.js");
|
||||
}).then(registration => {
|
||||
return {success: true};
|
||||
}).catch(error => {
|
||||
return {success: false};
|
||||
});
|
||||
}
|
||||
|
||||
function unregister() {
|
||||
return Promise.resolve().then(function() {
|
||||
return window.navigator.serviceWorker.getRegistration();
|
||||
}).then(registration => {
|
||||
return registration.unregister().then(result => {
|
||||
return {success: !!result};
|
||||
});
|
||||
}).catch(_ => {
|
||||
return {success: false};
|
||||
});
|
||||
}
|
||||
|
||||
function iframeRegisterAndUnregister() {
|
||||
var frame = window.document.createElement("iframe");
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
frame.addEventListener("load", function() {
|
||||
Promise.resolve().then(_ => {
|
||||
return frame.contentWindow.navigator.serviceWorker.register("serviceworker.js");
|
||||
}).then(swr => {
|
||||
return swr.unregister();
|
||||
}).then(_ => {
|
||||
frame.remove();
|
||||
resolve({success: true});
|
||||
}).catch(error => {
|
||||
resolve({success: false});
|
||||
});
|
||||
}, {once: true});
|
||||
});
|
||||
frame.src = "browser_toolbox_options_enabled_serviceworkers_testing.html";
|
||||
window.document.body.appendChild(frame);
|
||||
return promise;
|
||||
}
|
||||
|
||||
window.addEventListener("message", function(event) {
|
||||
var response;
|
||||
switch (event.data) {
|
||||
case "devtools:sw-test:register": {
|
||||
response = register();
|
||||
break;
|
||||
}
|
||||
case "devtools:sw-test:unregister": {
|
||||
response = unregister();
|
||||
break;
|
||||
}
|
||||
case "devtools:sw-test:iframe:register-and-unregister": {
|
||||
response = iframeRegisterAndUnregister();
|
||||
break;
|
||||
}
|
||||
}
|
||||
response.then(data => {
|
||||
event.ports[0].postMessage(data);
|
||||
event.ports[0].close();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -9,39 +9,23 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
addMessageListener("devtools:sw-test:register", function(msg) {
|
||||
content.navigator.serviceWorker.register("serviceworker.js")
|
||||
.then(swr => {
|
||||
sendAsyncMessage("devtools:sw-test:register", {success: true});
|
||||
}, error => {
|
||||
sendAsyncMessage("devtools:sw-test:register", {success: false});
|
||||
});
|
||||
});
|
||||
|
||||
addMessageListener("devtools:sw-test:unregister", function(msg) {
|
||||
content.navigator.serviceWorker.getRegistration().then(swr => {
|
||||
swr.unregister().then(result => {
|
||||
sendAsyncMessage("devtools:sw-test:unregister",
|
||||
{success: !!result});
|
||||
});
|
||||
function addMessageForwarder(name) {
|
||||
addMessageListener(name, function(_) {
|
||||
var channel = new MessageChannel();
|
||||
content.postMessage(name, "*", [channel.port2]);
|
||||
channel.port1.onmessage = function(msg) {
|
||||
sendAsyncMessage(name, msg.data);
|
||||
channel.port1.close();
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addMessageListener("devtools:sw-test:iframe:register-and-unregister", function(msg) {
|
||||
var frame = content.document.createElement("iframe");
|
||||
frame.addEventListener("load", function() {
|
||||
frame.contentWindow.navigator.serviceWorker.register("serviceworker.js")
|
||||
.then(swr => {
|
||||
return swr.unregister();
|
||||
}).then(_ => {
|
||||
frame.remove();
|
||||
sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
|
||||
{success: true});
|
||||
}).catch(error => {
|
||||
sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
|
||||
{success: false});
|
||||
});
|
||||
}, {once: true});
|
||||
frame.src = "browser_toolbox_options_enabled_serviceworkers_testing.html";
|
||||
content.document.body.appendChild(frame);
|
||||
});
|
||||
const messages = [
|
||||
"devtools:sw-test:register",
|
||||
"devtools:sw-test:unregister",
|
||||
"devtools:sw-test:iframe:register-and-unregister"
|
||||
];
|
||||
|
||||
for (var msg of messages) {
|
||||
addMessageForwarder(msg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ support-files =
|
|||
doc_multiple_property_types.html
|
||||
doc_timing_combination_animation.html
|
||||
head.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/frame-script-utils.js
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ support-files =
|
|||
doc_boxmodel_iframe1.html
|
||||
doc_boxmodel_iframe2.html
|
||||
head.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ support-files =
|
|||
doc_sourcemaps.html
|
||||
doc_sourcemaps.scss
|
||||
head.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ subsuite = devtools
|
|||
support-files =
|
||||
head.js
|
||||
head_devtools_inspector_sidebar.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ support-files =
|
|||
ostrich-black.ttf
|
||||
ostrich-regular.ttf
|
||||
head.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ subsuite = devtools
|
|||
support-files =
|
||||
doc_iframe_reloaded.html
|
||||
head.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ support-files =
|
|||
lib_react_with_addons_15.3.1_min.js
|
||||
lib_react_with_addons_15.4.1.js
|
||||
react_external_listeners.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/debugger/new/test/mochitest/helpers.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ registerCleanupFunction(() => {
|
|||
* @param {String} filePath The file path, relative to the current directory.
|
||||
* Examples:
|
||||
* - "helper_attributes_test_runner.js"
|
||||
* - "../../../commandline/test/helpers.js"
|
||||
*/
|
||||
function loadHelperScript(filePath) {
|
||||
const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
|
||||
|
|
|
|||
|
|
@ -113,23 +113,9 @@ function parseAttributeValues(attr, doc) {
|
|||
return attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate the string and add ellipsis to the middle of the string.
|
||||
*/
|
||||
function truncateString(str, maxLength) {
|
||||
if (!str || str.length <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
|
||||
return str.substring(0, Math.ceil(maxLength / 2)) +
|
||||
"…" +
|
||||
str.substring(str.length - Math.floor(maxLength / 2));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
flashElementOn,
|
||||
flashElementOff,
|
||||
getAutocompleteMaxWidth,
|
||||
parseAttributeValues,
|
||||
truncateString,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ const {
|
|||
flashElementOn,
|
||||
flashElementOff,
|
||||
parseAttributeValues,
|
||||
truncateString,
|
||||
} = require("devtools/client/inspector/markup/utils");
|
||||
const { truncateString } = require("devtools/shared/inspector/utils");
|
||||
const {editableField, InplaceEditor} =
|
||||
require("devtools/client/shared/inplace-editor");
|
||||
const {parseAttribute} =
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ support-files =
|
|||
doc_variables_1.html
|
||||
doc_variables_2.html
|
||||
head.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ support-files =
|
|||
doc_content_stylesheet_xul.css
|
||||
doc_frame_script.js
|
||||
head.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ support-files =
|
|||
head.js
|
||||
img_browser_inspector_highlighter-eyedropper-image.png
|
||||
shared-head.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
!/devtools/client/shared/test/telemetry-test-helpers.js
|
||||
!/devtools/client/shared/test/test-actor.js
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ support-files =
|
|||
simple_json.json^headers^
|
||||
valid_json.json
|
||||
valid_json.json^headers^
|
||||
!/devtools/client/commandline/test/head.js
|
||||
!/devtools/client/framework/test/head.js
|
||||
!/devtools/client/shared/test/frame-script-utils.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ DIRS += [
|
|||
'accessibility',
|
||||
'application',
|
||||
'canvasdebugger',
|
||||
'commandline',
|
||||
'debugger',
|
||||
'dom',
|
||||
'framework',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ support-files =
|
|||
geolocation.html
|
||||
head.js
|
||||
touch.html
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
!/devtools/client/shared/test/shared-head.js
|
||||
!/devtools/client/shared/test/shared-redux-head.js
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
/* eslint no-unused-vars: [2, {"vars": "local"}] */
|
||||
/* import-globals-from ../../../shared/test/shared-head.js */
|
||||
/* import-globals-from ../../../shared/test/shared-redux-head.js */
|
||||
/* import-globals-from ../../../commandline/test/helpers.js */
|
||||
/* import-globals-from ../../../inspector/test/shared-head.js */
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
|
|
@ -16,11 +15,6 @@ Services.scriptloader.loadSubScript(
|
|||
"chrome://mochitests/content/browser/devtools/client/shared/test/shared-redux-head.js",
|
||||
this);
|
||||
|
||||
// Import the GCLI test helper
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
|
||||
this);
|
||||
|
||||
// Import helpers registering the test-actor in remote targets
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
|
||||
|
|
|
|||
|
|
@ -368,7 +368,6 @@ function once(target, eventName, useCapture = false) {
|
|||
* @param {String} filePath The file path, relative to the current directory.
|
||||
* Examples:
|
||||
* - "helper_attributes_test_runner.js"
|
||||
* - "../../../commandline/test/helpers.js"
|
||||
*/
|
||||
function loadHelperScript(filePath) {
|
||||
const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ function teardown(ed, win) {
|
|||
* @param {String} filePath The file path, relative to the current directory.
|
||||
* Examples:
|
||||
* - "helper_attributes_test_runner.js"
|
||||
* - "../../../commandline/test/helpers.js"
|
||||
*/
|
||||
function loadHelperScript(filePath) {
|
||||
const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ support-files =
|
|||
doc_xulpage.xul
|
||||
sync.html
|
||||
utf-16.css
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
!/devtools/client/inspector/shared/test/head.js
|
||||
!/devtools/client/inspector/test/head.js
|
||||
!/devtools/client/inspector/test/shared-head.js
|
||||
|
|
|
|||
|
|
@ -348,6 +348,12 @@ const AccessibleActor = ActorClassWithSpec(accessibleSpec, {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Check if accessible bounds are invalid.
|
||||
const left = x, right = x + w, top = y, bottom = y + h;
|
||||
if (left === right || top === bottom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { x, y, w, h };
|
||||
},
|
||||
|
||||
|
|
@ -386,9 +392,13 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
this.onPick = this.onPick.bind(this);
|
||||
this.onHovered = this.onHovered.bind(this);
|
||||
this.onKey = this.onKey.bind(this);
|
||||
this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
|
||||
|
||||
this.highlighter = CustomHighlighterActor(this, isXUL(this.rootWin) ?
|
||||
"XULWindowAccessibleHighlighter" : "AccessibleHighlighter");
|
||||
|
||||
this.manage(this.highlighter);
|
||||
this.highlighter.on("highlighter-event", this.onHighlighterEvent);
|
||||
},
|
||||
|
||||
setA11yServiceGetter() {
|
||||
|
|
@ -441,7 +451,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
|
||||
this.reset();
|
||||
|
||||
this.highlighter.destroy();
|
||||
this.highlighter.off("highlighter-event", this.onHighlighterEvent);
|
||||
this.highlighter = null;
|
||||
|
||||
this.targetActor = null;
|
||||
|
|
@ -585,6 +595,10 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
{ accessible: parent, children: parent.children() }));
|
||||
},
|
||||
|
||||
onHighlighterEvent: function(data) {
|
||||
this.emit("highlighter-event", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Accessible event observer function.
|
||||
*
|
||||
|
|
@ -700,13 +714,13 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
* True if highlighter shows the accessible object.
|
||||
*/
|
||||
highlightAccessible(accessible, options = {}) {
|
||||
const bounds = accessible.bounds;
|
||||
const { bounds, name, role } = accessible;
|
||||
if (!bounds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.highlighter.show({ rawNode: accessible.rawAccessible.DOMNode },
|
||||
{ ...options, ...bounds });
|
||||
{ ...options, ...bounds, name, role });
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -793,9 +807,13 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
|
|||
}
|
||||
|
||||
if (this._currentAccessible !== accessible) {
|
||||
const { bounds } = accessible;
|
||||
const { bounds, role, name } = accessible;
|
||||
if (bounds) {
|
||||
this.highlighter.show({ rawNode: event.originalTarget || event.target }, bounds);
|
||||
this.highlighter.show({ rawNode: event.originalTarget || event.target }, {
|
||||
...bounds,
|
||||
role,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
events.emit(this, "picker-accessible-hovered", accessible);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
--highlighter-bubble-arrow-size: 8px;
|
||||
--highlighter-font-family: message-box;
|
||||
--highlighter-font-size: 11px;
|
||||
--highlighter-infobar-color: hsl(210, 30%, 85%);
|
||||
--highlighter-marker-color: #000;
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +211,7 @@
|
|||
}
|
||||
|
||||
:-moz-native-anonymous [class$=infobar-dimensions] {
|
||||
color: hsl(210, 30%, 85%);
|
||||
color: var(--highlighter-infobar-color);
|
||||
border-inline-start: 1px solid #5a6169;
|
||||
margin-inline-start: 6px;
|
||||
padding-inline-start: 6px;
|
||||
|
|
@ -243,7 +244,7 @@
|
|||
}
|
||||
|
||||
:-moz-native-anonymous .css-grid-line-infobar-names:not(:empty) {
|
||||
color: hsl(210, 30%, 85%);
|
||||
color: var(--highlighter-infobar-color);
|
||||
border-inline-start: 1px solid #5a6169;
|
||||
margin-inline-start: 6px;
|
||||
padding-inline-start: 6px;
|
||||
|
|
@ -645,3 +646,19 @@
|
|||
opacity: 0.6;
|
||||
fill: #6a5acd;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-name {
|
||||
color:var(--highlighter-infobar-color);
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-name:not(:empty) {
|
||||
color: var(--highlighter-infobar-color);
|
||||
border-inline-start: 1px solid #5a6169;
|
||||
margin-inline-start: 6px;
|
||||
padding-inline-start: 6px;
|
||||
}
|
||||
|
||||
:-moz-native-anonymous .accessible-infobar-role {
|
||||
color: #9CDCFE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@
|
|||
"use strict";
|
||||
|
||||
const { AutoRefreshHighlighter } = require("./auto-refresh");
|
||||
const { getBounds } = require("./utils/accessibility");
|
||||
|
||||
const { getBounds, Infobar } = require("./utils/accessibility");
|
||||
const {
|
||||
CanvasFrameAnonymousContentHelper,
|
||||
createNode,
|
||||
createSVGNode
|
||||
createSVGNode,
|
||||
} = require("./utils/markup");
|
||||
|
||||
const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
|
||||
|
|
@ -36,21 +35,33 @@ const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
|
|||
* height of the the accessible object
|
||||
* - {Number} duration
|
||||
* Duration of time that the highlighter should be shown.
|
||||
* - {String|null} name
|
||||
* name of the the accessible object
|
||||
* - {String} role
|
||||
* role of the the accessible object
|
||||
*
|
||||
* Structure:
|
||||
* <div class="highlighter-container">
|
||||
* <div class="highlighter-container" aria-hidden="true">
|
||||
* <div class="accessible-root">
|
||||
* <svg class="accessible-elements" hidden="true">
|
||||
* <path class="accessible-bounds" points="..." />
|
||||
* </svg>
|
||||
* <div class="accessible-infobar-container">
|
||||
* <div class="accessible-infobar">
|
||||
* <div class="accessible-infobar-text">
|
||||
* <span class="accessible-infobar-role">Accessible Role</span>
|
||||
* <span class="accessible-infobar-name">Accessible Name</span>
|
||||
* </div>
|
||||
* </div>
|
||||
* </div>
|
||||
* </div>
|
||||
* </div>
|
||||
*/
|
||||
class AccessibleHighlighter extends AutoRefreshHighlighter {
|
||||
constructor(highlighterEnv) {
|
||||
super(highlighterEnv);
|
||||
|
||||
this.ID_CLASS_PREFIX = "accessible-";
|
||||
this.accessibleInfobar = new Infobar(this);
|
||||
|
||||
this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
|
||||
this._buildMarkup.bind(this));
|
||||
|
|
@ -73,7 +84,7 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
const container = createNode(this.win, {
|
||||
attributes: {
|
||||
"class": "highlighter-container",
|
||||
"role": "presentation"
|
||||
"aria-hidden": "true"
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -82,7 +93,6 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
attributes: {
|
||||
"id": "root",
|
||||
"class": "root",
|
||||
"role": "presentation"
|
||||
},
|
||||
prefix: this.ID_CLASS_PREFIX
|
||||
});
|
||||
|
|
@ -96,7 +106,6 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
"width": "100%",
|
||||
"height": "100%",
|
||||
"hidden": "true",
|
||||
"role": "presentation"
|
||||
},
|
||||
prefix: this.ID_CLASS_PREFIX
|
||||
});
|
||||
|
|
@ -107,11 +116,13 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
attributes: {
|
||||
"class": "bounds",
|
||||
"id": "bounds",
|
||||
"role": "presentation"
|
||||
},
|
||||
prefix: this.ID_CLASS_PREFIX
|
||||
});
|
||||
|
||||
// Build the accessible's infobar markup.
|
||||
this.accessibleInfobar.buildMarkup(root);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
|
|
@ -127,6 +138,8 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
this.highlighterEnv.off("will-navigate", this.onWillNavigate);
|
||||
this.pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
|
||||
this.pageListenerTarget = null;
|
||||
this.accessibleInfobar.destroy();
|
||||
this.accessibleInfobar = null;
|
||||
|
||||
this.markup.destroy();
|
||||
AutoRefreshHighlighter.prototype.destroy.call(this);
|
||||
|
|
@ -156,11 +169,15 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
|
||||
const { duration } = this.options;
|
||||
const shown = this._update();
|
||||
if (shown && duration) {
|
||||
this._highlightTimer = setTimeout(() => {
|
||||
this.hide();
|
||||
}, duration);
|
||||
if (shown) {
|
||||
this.emit("highlighter-event", { options: this.options, type: "shown"});
|
||||
if (duration) {
|
||||
this._highlightTimer = setTimeout(() => {
|
||||
this.hide();
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
return shown;
|
||||
}
|
||||
|
||||
|
|
@ -175,6 +192,9 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
|
||||
if (this._updateAccessibleBounds()) {
|
||||
this._showAccessibleBounds();
|
||||
|
||||
this.accessibleInfobar.show();
|
||||
|
||||
shown = true;
|
||||
} else {
|
||||
// Nothing to highlight (0px rectangle like a <script> tag for instance)
|
||||
|
|
@ -193,6 +213,7 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
_hide() {
|
||||
setIgnoreLayoutChanges(true);
|
||||
this._hideAccessibleBounds();
|
||||
this.accessibleInfobar.hide();
|
||||
setIgnoreLayoutChanges(false,
|
||||
this.highlighterEnv.window.document.documentElement);
|
||||
}
|
||||
|
|
@ -205,7 +226,7 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Showthe accessible bounds container.
|
||||
* Show the accessible bounds container.
|
||||
*/
|
||||
_showAccessibleBounds() {
|
||||
this.getElement("elements").removeAttribute("hidden");
|
||||
|
|
@ -230,7 +251,7 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
|
|||
_updateAccessibleBounds() {
|
||||
const bounds = this._bounds;
|
||||
if (!bounds) {
|
||||
this._hideAccessibleBounds();
|
||||
this._hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,356 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { getCurrentZoom } = require("devtools/shared/layout/utils");
|
||||
const { getCurrentZoom, getViewportDimensions } = require("devtools/shared/layout/utils");
|
||||
const { moveInfobar, createNode } = require("./markup");
|
||||
const { truncateString } = require("devtools/shared/inspector/utils");
|
||||
|
||||
// Max string length for truncating accessible name values.
|
||||
const MAX_STRING_LENGTH = 50;
|
||||
|
||||
/**
|
||||
* The AccessibleInfobar is a class responsible for creating the markup for the
|
||||
* accessible highlighter. It is also reponsible for updating content within the
|
||||
* infobar such as role and name values.
|
||||
*/
|
||||
class Infobar {
|
||||
constructor(highlighter) {
|
||||
this.highlighter = highlighter;
|
||||
}
|
||||
|
||||
get document() {
|
||||
return this.highlighter.win.document;
|
||||
}
|
||||
|
||||
get bounds() {
|
||||
return this.highlighter._bounds;
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this.highlighter.options;
|
||||
}
|
||||
|
||||
get prefix() {
|
||||
return this.highlighter.ID_CLASS_PREFIX;
|
||||
}
|
||||
|
||||
get win() {
|
||||
return this.highlighter.win;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the Infobar to the right place in the highlighter.
|
||||
*
|
||||
* @param {Element} container
|
||||
* Container of infobar.
|
||||
*/
|
||||
_moveInfobar(container) {
|
||||
// Position the infobar using accessible's bounds
|
||||
const { left: x, top: y, bottom, width } = this.bounds;
|
||||
const infobarBounds = { x, y, bottom, width };
|
||||
|
||||
moveInfobar(container, infobarBounds, this.win);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build markup for infobar.
|
||||
*
|
||||
* @param {Element} root
|
||||
* Root element to build infobar with.
|
||||
*/
|
||||
buildMarkup(root) {
|
||||
const container = createNode(this.win, {
|
||||
parent: root,
|
||||
attributes: {
|
||||
"class": "infobar-container",
|
||||
"id": "infobar-container",
|
||||
"aria-hidden": "true",
|
||||
"hidden": "true"
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
|
||||
const infobar = createNode(this.win, {
|
||||
parent: container,
|
||||
attributes: {
|
||||
"class": "infobar",
|
||||
"id": "infobar",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
|
||||
const infobarText = createNode(this.win, {
|
||||
parent: infobar,
|
||||
attributes: {
|
||||
"class": "infobar-text",
|
||||
"id": "infobar-text",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
|
||||
createNode(this.win, {
|
||||
nodeType: "span",
|
||||
parent: infobarText,
|
||||
attributes: {
|
||||
"class": "infobar-role",
|
||||
"id": "infobar-role",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
|
||||
createNode(this.win, {
|
||||
nodeType: "span",
|
||||
parent: infobarText,
|
||||
attributes: {
|
||||
"class": "infobar-name",
|
||||
"id": "infobar-name",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the Infobar's highlighter.
|
||||
*/
|
||||
destroy() {
|
||||
this.highlighter = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the element with the specified ID.
|
||||
*
|
||||
* @param {String} id
|
||||
* Element ID.
|
||||
* @return {Element} The element with specified ID.
|
||||
*/
|
||||
getElement(id) {
|
||||
return this.highlighter.getElement(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text content of element.
|
||||
*
|
||||
* @param {String} id
|
||||
* Element ID to retrieve text content from.
|
||||
* @return {String} The text content of the element.
|
||||
*/
|
||||
getTextContent(id) {
|
||||
const anonymousContent = this.highlighter.markup.content;
|
||||
return anonymousContent.getTextContentForElement(`${this.prefix}${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the accessible infobar.
|
||||
*/
|
||||
hide() {
|
||||
const container = this.getElement("infobar-container");
|
||||
container.setAttribute("hidden", "true");
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the accessible infobar highlighter.
|
||||
*/
|
||||
show() {
|
||||
const container = this.getElement("infobar-container");
|
||||
|
||||
// Remove accessible's infobar "hidden" attribute. We do this first to get the
|
||||
// computed styles of the infobar container.
|
||||
container.removeAttribute("hidden");
|
||||
|
||||
// Update the infobar's position and content.
|
||||
this.update(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update content of the infobar.
|
||||
*/
|
||||
update(container) {
|
||||
const { name, role } = this.options;
|
||||
|
||||
this.updateRole(role, this.getElement("infobar-role"));
|
||||
this.updateName(name, this.getElement("infobar-name"));
|
||||
|
||||
// Position the infobar.
|
||||
this._moveInfobar(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text content of the specified element.
|
||||
*
|
||||
* @param {Element} el
|
||||
* Element to set text content on.
|
||||
* @param {String} text
|
||||
* Text for content.
|
||||
*/
|
||||
setTextContent(el, text) {
|
||||
el.setTextContent(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the accessible's name message.
|
||||
*
|
||||
* @param {String} name
|
||||
* Accessible's name value.
|
||||
* @param {Element} el
|
||||
* Element to set text content on.
|
||||
*/
|
||||
updateName(name, el) {
|
||||
const nameText = name ? `"${truncateString(name, MAX_STRING_LENGTH)}"` : "";
|
||||
this.setTextContent(el, nameText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the accessible's role.
|
||||
*
|
||||
* @param {String} role
|
||||
* Accessible's role value.
|
||||
* @param {Element} el
|
||||
* Element to set text content on.
|
||||
*/
|
||||
updateRole(role, el) {
|
||||
this.setTextContent(el, role);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The XULAccessibleInfobar handles building the XUL infobar markup where it isn't
|
||||
* possible with the regular accessible highlighter.
|
||||
*/
|
||||
class XULWindowInfobar extends Infobar {
|
||||
/**
|
||||
* A helper function that calculates the positioning of a XUL accessible's infobar.
|
||||
*
|
||||
* @param {Object} container
|
||||
* The infobar container.
|
||||
*/
|
||||
_moveInfobar(container) {
|
||||
const arrow = this.getElement("arrow");
|
||||
|
||||
// Show the container and arrow elements first.
|
||||
container.removeAttribute("hidden");
|
||||
arrow.removeAttribute("hidden");
|
||||
|
||||
// Set the left value of the infobar container in relation to
|
||||
// highlighter's bounds position.
|
||||
const {
|
||||
left: boundsLeft,
|
||||
right: boundsRight,
|
||||
top: boundsTop,
|
||||
bottom: boundsBottom
|
||||
} = this.bounds;
|
||||
const boundsMidPoint = (boundsLeft + boundsRight) / 2;
|
||||
container.style.left = `${boundsMidPoint}px`;
|
||||
|
||||
const zoom = getCurrentZoom(this.win);
|
||||
let {
|
||||
width: viewportWidth,
|
||||
height: viewportHeight,
|
||||
} = getViewportDimensions(this.win);
|
||||
|
||||
const { width, height, left, } = container.getBoundingClientRect();
|
||||
|
||||
const containerHalfWidth = width / 2;
|
||||
const containerHeight = height;
|
||||
const margin = 100 * zoom;
|
||||
|
||||
viewportHeight *= zoom;
|
||||
viewportWidth *= zoom;
|
||||
|
||||
// Determine viewport boundaries for infobar.
|
||||
const topBoundary = margin;
|
||||
const bottomBoundary = viewportHeight - containerHeight;
|
||||
const leftBoundary = containerHalfWidth;
|
||||
const rightBoundary = viewportWidth - containerHalfWidth;
|
||||
|
||||
// Determine if an infobar's position is offscreen.
|
||||
const isOffScreenOnTop = boundsBottom < topBoundary;
|
||||
const isOffScreenOnBottom = boundsBottom > bottomBoundary;
|
||||
const isOffScreenOnLeft = left < leftBoundary;
|
||||
const isOffScreenOnRight = left > rightBoundary;
|
||||
|
||||
// Check if infobar is offscreen on either left/right of viewport and position.
|
||||
if (isOffScreenOnLeft) {
|
||||
container.style.left = `${leftBoundary + boundsLeft}px`;
|
||||
arrow.setAttribute("hidden", "true");
|
||||
} else if (isOffScreenOnRight) {
|
||||
const leftOffset = rightBoundary - boundsRight;
|
||||
container.style.left = `${rightBoundary - leftOffset - containerHalfWidth}px`;
|
||||
arrow.setAttribute("hidden", "true");
|
||||
}
|
||||
|
||||
// Check if infobar is offscreen on either top/bottom of viewport and position.
|
||||
const bubbleArrowSize = "var(--highlighter-bubble-arrow-size)";
|
||||
|
||||
if (isOffScreenOnTop) {
|
||||
if (boundsTop < 0) {
|
||||
container.style.top = bubbleArrowSize;
|
||||
} else {
|
||||
container.style.top = `calc(${boundsBottom}px + ${bubbleArrowSize})`;
|
||||
}
|
||||
arrow.setAttribute("class", "accessible-arrow top");
|
||||
} else if (isOffScreenOnBottom) {
|
||||
container.style.top = `calc(${bottomBoundary}px - ${bubbleArrowSize})`;
|
||||
arrow.setAttribute("hidden", "true");
|
||||
} else {
|
||||
container.style.top = `calc(${boundsTop}px -
|
||||
(${containerHeight}px + ${bubbleArrowSize}))`;
|
||||
arrow.setAttribute("class", "accessible-arrow bottom");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build markup for XUL window infobar.
|
||||
*
|
||||
* @param {Element} root
|
||||
* Root element to build infobar with.
|
||||
*/
|
||||
buildMarkup(root) {
|
||||
super.buildMarkup(root, createNode);
|
||||
|
||||
createNode(this.win, {
|
||||
parent: this.getElement("infobar"),
|
||||
attributes: {
|
||||
"class": "arrow",
|
||||
"id": "arrow",
|
||||
},
|
||||
prefix: this.prefix,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Override of Infobar class's getTextContent method.
|
||||
*
|
||||
* @param {String} id
|
||||
* Element ID to retrieve text content from.
|
||||
* @return {String} Returns the text content of the element.
|
||||
*/
|
||||
getTextContent(id) {
|
||||
return this.getElement(id).textContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override of Infobar class's getElement method.
|
||||
*
|
||||
* @param {String} id
|
||||
* Element ID.
|
||||
* @return {String} Returns the specified element.
|
||||
*/
|
||||
getElement(id) {
|
||||
return this.win.document.getElementById(`${this.prefix}${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override of Infobar class's setTextContent method.
|
||||
*
|
||||
* @param {Element} el
|
||||
* Element to set text content on.
|
||||
* @param {String} text
|
||||
* Text for content.
|
||||
*/
|
||||
setTextContent(el, text) {
|
||||
el.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function that calculate accessible object bounds and positioning to
|
||||
|
|
@ -30,7 +379,10 @@ const { getCurrentZoom } = require("devtools/shared/layout/utils");
|
|||
function getBounds(win, { x, y, w, h, zoom }) {
|
||||
let { mozInnerScreenX, mozInnerScreenY, scrollX, scrollY } = win;
|
||||
let zoomFactor = getCurrentZoom(win);
|
||||
let left = x, right = x + w, top = y, bottom = y + h;
|
||||
let left = x;
|
||||
let right = x + w;
|
||||
let top = y;
|
||||
let bottom = y + h;
|
||||
|
||||
// For a XUL accessible, normalize the top-level window with its current zoom level.
|
||||
// We need to do this because top-level browser content does not allow zooming.
|
||||
|
|
@ -58,4 +410,7 @@ function getBounds(win, { x, y, w, h, zoom }) {
|
|||
return { left, right, top, bottom, width, height };
|
||||
}
|
||||
|
||||
exports.MAX_STRING_LENGTH = MAX_STRING_LENGTH;
|
||||
exports.getBounds = getBounds;
|
||||
exports.Infobar = Infobar;
|
||||
exports.XULWindowInfobar = XULWindowInfobar;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { getBounds } = require("./utils/accessibility");
|
||||
const { getBounds, XULWindowInfobar } = require("./utils/accessibility");
|
||||
const { createNode, isNodeValid } = require("./utils/markup");
|
||||
const { getCurrentZoom, loadSheet } = require("devtools/shared/layout/utils");
|
||||
|
||||
|
|
@ -13,6 +13,12 @@ const { getCurrentZoom, loadSheet } = require("devtools/shared/layout/utils");
|
|||
* is consistent with the styling of an in-content accessible highlighter.
|
||||
*/
|
||||
const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
|
||||
.highlighter-container {
|
||||
--highlighter-bubble-background-color: hsl(214, 13%, 24%);
|
||||
--highlighter-bubble-border-color: rgba(255, 255, 255, 0.2);
|
||||
--highlighter-bubble-arrow-size: 8px;
|
||||
}
|
||||
|
||||
.accessible-bounds {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
|
|
@ -20,6 +26,66 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon
|
|||
display: block;
|
||||
background-color: #6a5acd!important;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.accessible-infobar-container {
|
||||
position: fixed;
|
||||
max-width: 90%;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.accessible-infobar {
|
||||
position: relative;
|
||||
left: -50%;
|
||||
background-color: var(--highlighter-bubble-background-color);
|
||||
min-width: 75px;
|
||||
border: 1px solid var(--highlighter-bubble-border-color);
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.accessible-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: var(--highlighter-bubble-arrow-size) solid transparent;
|
||||
border-right: var(--highlighter-bubble-arrow-size) solid transparent;
|
||||
left: calc(50% - var(--highlighter-bubble-arrow-size));
|
||||
}
|
||||
|
||||
.top {
|
||||
border-bottom: var(--highlighter-bubble-arrow-size) solid
|
||||
var(--highlighter-bubble-background-color);
|
||||
top: calc(-1 * var(--highlighter-bubble-arrow-size));
|
||||
}
|
||||
|
||||
.bottom {
|
||||
border-top: var(--highlighter-bubble-arrow-size) solid
|
||||
var(--highlighter-bubble-background-color);
|
||||
bottom: calc(-1 * var(--highlighter-bubble-arrow-size));
|
||||
}
|
||||
|
||||
.accessible-infobar-text {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.accessible-infobar-name {
|
||||
color: rgb(221, 0, 169);
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.accessible-infobar-name:not(:empty) {
|
||||
color: hsl(210, 30%, 85%);
|
||||
border-inline-start: 1px solid #5a6169;
|
||||
margin-inline-start: 6px;
|
||||
padding-inline-start: 6px;
|
||||
}
|
||||
|
||||
.accessible-infobar-role {
|
||||
color: #9CDCFE;
|
||||
}`);
|
||||
|
||||
/**
|
||||
|
|
@ -37,8 +103,10 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon
|
|||
*/
|
||||
class XULWindowAccessibleHighlighter {
|
||||
constructor(highlighterEnv) {
|
||||
this.ID_CLASS_PREFIX = "accessible-";
|
||||
this.highlighterEnv = highlighterEnv;
|
||||
this.win = highlighterEnv.window;
|
||||
this.accessibleInfobar = new XULWindowInfobar(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -60,7 +128,7 @@ class XULWindowAccessibleHighlighter {
|
|||
parent: doc.body || doc.documentElement,
|
||||
attributes: {
|
||||
"class": "highlighter-container",
|
||||
"role": "presentation"
|
||||
"aria-hidden": "true"
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -68,9 +136,10 @@ class XULWindowAccessibleHighlighter {
|
|||
parent: this.container,
|
||||
attributes: {
|
||||
"class": "accessible-bounds",
|
||||
"role": "presentation"
|
||||
}
|
||||
});
|
||||
|
||||
this.accessibleInfobar.buildMarkup(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -104,6 +173,11 @@ class XULWindowAccessibleHighlighter {
|
|||
* height of the the accessible object
|
||||
* - duration {Number}
|
||||
* Duration of time that the highlighter should be shown.
|
||||
* - {String|null} name
|
||||
* name of the the accessible object
|
||||
* - {String} role
|
||||
* role of the the accessible object
|
||||
*
|
||||
* @return {Boolean} True if accessible is highlighted, false otherwise.
|
||||
*/
|
||||
show(node, options = {}) {
|
||||
|
|
@ -158,6 +232,7 @@ class XULWindowAccessibleHighlighter {
|
|||
}
|
||||
|
||||
let boundsEl = this.bounds;
|
||||
|
||||
if (!boundsEl) {
|
||||
this._buildMarkup();
|
||||
boundsEl = this.bounds;
|
||||
|
|
@ -168,7 +243,9 @@ class XULWindowAccessibleHighlighter {
|
|||
boundsEl.style.left = `${left}px`;
|
||||
boundsEl.style.width = `${width}px`;
|
||||
boundsEl.style.height = `${height}px`;
|
||||
|
||||
this._showAccessibleBounds();
|
||||
this.accessibleInfobar.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -182,6 +259,8 @@ class XULWindowAccessibleHighlighter {
|
|||
}
|
||||
|
||||
this._hideAccessibleBounds();
|
||||
this.accessibleInfobar.hide();
|
||||
|
||||
this.currentNode = null;
|
||||
this.options = null;
|
||||
}
|
||||
|
|
@ -218,6 +297,9 @@ class XULWindowAccessibleHighlighter {
|
|||
this.container.remove();
|
||||
}
|
||||
|
||||
this.accessibleInfobar.destroy();
|
||||
|
||||
this.accessibleInfobar = null;
|
||||
this.win = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ subsuite = devtools
|
|||
support-files =
|
||||
head.js
|
||||
animation.html
|
||||
doc_accessibility_infobar.html
|
||||
doc_accessibility.html
|
||||
doc_allocations.html
|
||||
doc_force_cc.html
|
||||
|
|
@ -30,6 +31,8 @@ support-files =
|
|||
!/devtools/client/shared/test/telemetry-test-helpers.js
|
||||
!/devtools/server/tests/mochitest/hello-actor.js
|
||||
|
||||
[browser_accessibility_highlighter_infobar.js]
|
||||
[browser_accessibility_infobar_show.js]
|
||||
[browser_accessibility_node.js]
|
||||
[browser_accessibility_node_events.js]
|
||||
[browser_accessibility_simple.js]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
/* 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";
|
||||
|
||||
// Test the accessible highlighter's infobar content.
|
||||
|
||||
const { truncateString } = require("devtools/shared/inspector/utils");
|
||||
const { MAX_STRING_LENGTH } = require("devtools/server/actors/highlighters/utils/accessibility");
|
||||
|
||||
add_task(async function() {
|
||||
const { client, walker, accessibility } =
|
||||
await initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility_infobar.html");
|
||||
|
||||
const a11yWalker = await accessibility.getWalker();
|
||||
await accessibility.enable();
|
||||
|
||||
info("Button front checks");
|
||||
await checkNameAndRole(walker, "#button", a11yWalker, "Accessible Button");
|
||||
|
||||
info("Front with long name checks");
|
||||
await checkNameAndRole(walker, "#h1", a11yWalker,
|
||||
"Lorem ipsum dolor sit ame" + "\u2026" + "e et dolore magna aliqua.");
|
||||
|
||||
await accessibility.disable();
|
||||
await waitForA11yShutdown();
|
||||
await client.close();
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
/**
|
||||
* A helper function for testing the accessible's displayed name and roles.
|
||||
*
|
||||
* @param {Object} walker
|
||||
* The DOM walker.
|
||||
* @param {String} querySelector
|
||||
* The selector for the node to retrieve accessible from.
|
||||
* @param {Object} a11yWalker
|
||||
* The accessibility walker.
|
||||
* @param {String} expectedName
|
||||
* Expected string content for displaying the accessible's name.
|
||||
* We are testing this in particular because name can be truncated.
|
||||
*/
|
||||
async function checkNameAndRole(walker, querySelector, a11yWalker, expectedName) {
|
||||
const node = await walker.querySelector(walker.rootNode, querySelector);
|
||||
const accessibleFront = await a11yWalker.getAccessibleFor(node);
|
||||
|
||||
const { name, role } = accessibleFront;
|
||||
const onHighlightEvent = a11yWalker.once("highlighter-event");
|
||||
|
||||
await a11yWalker.highlightAccessible(accessibleFront);
|
||||
const { options } = await onHighlightEvent;
|
||||
is(options.name, name, "Accessible highlight has correct name option");
|
||||
is(options.role, role, "Accessible highlight has correct role option");
|
||||
|
||||
is(`"${truncateString(name, MAX_STRING_LENGTH)}"`, `"${expectedName}"`,
|
||||
"Accessible has correct displayed name.");
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/* 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";
|
||||
|
||||
// Checks for the AccessibleHighlighter's and XULWindowHighlighter's infobar components.
|
||||
|
||||
add_task(async function() {
|
||||
await BrowserTestUtils.withNewTab({
|
||||
gBrowser,
|
||||
url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
|
||||
}, async function(browser) {
|
||||
await ContentTask.spawn(browser, null, async function() {
|
||||
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const { HighlighterEnvironment } = require("devtools/server/actors/highlighters");
|
||||
const { AccessibleHighlighter } = require("devtools/server/actors/highlighters/accessible");
|
||||
const { XULWindowAccessibleHighlighter } = require("devtools/server/actors/highlighters/xul-accessible");
|
||||
|
||||
/**
|
||||
* Get whether or not infobar container is hidden.
|
||||
*
|
||||
* @param {Object} infobar
|
||||
* Accessible highlighter's infobar component.
|
||||
* @return {String|null} If the infobar container is hidden.
|
||||
*/
|
||||
function isContainerHidden(infobar) {
|
||||
return !!infobar.getElement("infobar-container").getAttribute("hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of accessible object.
|
||||
*
|
||||
* @param {Object} infobar
|
||||
* Accessible highlighter's infobar component.
|
||||
* @return {String} The text content of the infobar-name element.
|
||||
*/
|
||||
function getName(infobar) {
|
||||
return infobar.getTextContent("infobar-name");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role of accessible object.
|
||||
*
|
||||
* @param {Object} infobar
|
||||
* Accessible highlighter's infobar component.
|
||||
* @return {String} The text content of the infobar-role element.
|
||||
*/
|
||||
function getRole(infobar) {
|
||||
return infobar.getTextContent("infobar-role");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for updated content for an infobar with valid bounds.
|
||||
*
|
||||
* @param {Object} infobar
|
||||
* Accessible highlighter's infobar component.
|
||||
* @param {Object} options
|
||||
* Options to pass for the highlighter's show method.
|
||||
* Available options:
|
||||
* - {String} role
|
||||
* Role value of the accessible.
|
||||
* - {String} name
|
||||
* Name value of the accessible.
|
||||
* - {Boolean} shouldBeHidden
|
||||
* If the infobar component should be hidden.
|
||||
*/
|
||||
function checkInfobar(infobar, { shouldBeHidden, role, name }) {
|
||||
is(isContainerHidden(infobar), shouldBeHidden,
|
||||
"Infobar's hidden state is correct.");
|
||||
|
||||
if (shouldBeHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
is(getRole(infobar), role, "infobarRole text content is correct");
|
||||
is(getName(infobar), `"${name}"`, "infoBarName text content is correct");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for updated content of an infobar with valid bounds.
|
||||
*
|
||||
* @param {Element} node
|
||||
* Node to check infobar content on.
|
||||
* @param {Object} highlighter
|
||||
* Accessible highlighter.
|
||||
*/
|
||||
function testInfobar(node, highlighter) {
|
||||
const infobar = highlighter.accessibleInfobar;
|
||||
const bounds = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 250,
|
||||
h: 100,
|
||||
};
|
||||
|
||||
info("Check that infobar is shown with valid bounds.");
|
||||
highlighter.show(node, {
|
||||
...bounds,
|
||||
role: "button",
|
||||
name: "Accessible Button"
|
||||
});
|
||||
|
||||
checkInfobar(infobar, {
|
||||
role: "button",
|
||||
name: "Accessible Button",
|
||||
shouldBeHidden: false
|
||||
});
|
||||
highlighter.hide();
|
||||
|
||||
info("Check that infobar is hidden after .hide() is called.");
|
||||
checkInfobar(infobar, { shouldBeHidden: true });
|
||||
|
||||
info("Check to make sure content is updated with new options.");
|
||||
highlighter.show(node, {
|
||||
...bounds,
|
||||
name: "Test link",
|
||||
role: "link"
|
||||
});
|
||||
checkInfobar(infobar, {
|
||||
name: "Test link",
|
||||
role: "link",
|
||||
shouldBeHidden: false
|
||||
});
|
||||
highlighter.hide();
|
||||
}
|
||||
|
||||
// Start testing. First, create highlighter environment and initialize.
|
||||
const env = new HighlighterEnvironment();
|
||||
env.initFromWindow(content.window);
|
||||
|
||||
// Wait for loading highlighter environment content to complete before creating the
|
||||
// highlighter.
|
||||
await new Promise(resolve => {
|
||||
const doc = env.document;
|
||||
|
||||
function onContentLoaded() {
|
||||
if (doc.readyState === "interactive" || doc.readyState === "complete") {
|
||||
resolve();
|
||||
} else {
|
||||
doc.addEventListener("DOMContentLoaded", onContentLoaded, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
onContentLoaded();
|
||||
});
|
||||
|
||||
// Now, we can test the Infobar and XULWindowInfobar components with their
|
||||
// respective highlighters.
|
||||
const node = content.document.createElement("div");
|
||||
content.document.body.append(node);
|
||||
|
||||
info("Checks for Infobar's show method");
|
||||
const highlighter = new AccessibleHighlighter(env);
|
||||
testInfobar(node, highlighter);
|
||||
|
||||
info("Checks for XULWindowInfobar's show method");
|
||||
const xulWindowHighlighter = new XULWindowAccessibleHighlighter(env);
|
||||
testInfobar(node, xulWindowHighlighter);
|
||||
});
|
||||
});
|
||||
});
|
||||
10
devtools/server/tests/browser/doc_accessibility_infobar.html
Normal file
10
devtools/server/tests/browser/doc_accessibility_infobar.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="h1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</h1>
|
||||
<button id="button">Accessible Button</button>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -5,5 +5,6 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'css-logic.js'
|
||||
'css-logic.js',
|
||||
'utils.js'
|
||||
)
|
||||
|
|
|
|||
20
devtools/shared/inspector/utils.js
Normal file
20
devtools/shared/inspector/utils.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Truncate the string and add ellipsis to the middle of the string.
|
||||
*/
|
||||
function truncateString(str, maxLength) {
|
||||
if (!str || str.length <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
|
||||
return str.substring(0, Math.ceil(maxLength / 2)) +
|
||||
"…" +
|
||||
str.substring(str.length - Math.floor(maxLength / 2));
|
||||
}
|
||||
|
||||
exports.truncateString = truncateString;
|
||||
|
|
@ -104,6 +104,10 @@ const accessibleWalkerSpec = generateActorSpec({
|
|||
},
|
||||
"picker-accessible-canceled": {
|
||||
type: "pickerAccessibleCanceled"
|
||||
},
|
||||
"highlighter-event": {
|
||||
type: "highlighter-event",
|
||||
data: Arg(0, "json")
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
#include "nsPrintfCString.h"
|
||||
#include "mozilla/Sprintf.h"
|
||||
#include "nsGlobalWindow.h"
|
||||
#include "nsReadableUtils.h"
|
||||
|
||||
#include "mozilla/dom/ScriptSettings.h"
|
||||
#include "mozilla/dom/CustomElementRegistry.h"
|
||||
|
|
@ -2781,34 +2782,16 @@ NonVoidByteStringToJsval(JSContext *cx, const nsACString &str,
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
template<typename T> static void
|
||||
NormalizeUSVStringInternal(T& aString)
|
||||
{
|
||||
char16_t* start = aString.BeginWriting();
|
||||
// Must use const here because we can't pass char** to UTF16CharEnumerator as
|
||||
// it expects const char**. Unclear why this is illegal...
|
||||
const char16_t* nextChar = start;
|
||||
const char16_t* end = aString.Data() + aString.Length();
|
||||
while (nextChar < end) {
|
||||
uint32_t enumerated = UTF16CharEnumerator::NextChar(&nextChar, end);
|
||||
if (enumerated == UCS2_REPLACEMENT_CHAR) {
|
||||
int32_t lastCharIndex = (nextChar - start) - 1;
|
||||
start[lastCharIndex] = static_cast<char16_t>(enumerated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NormalizeUSVString(nsAString& aString)
|
||||
{
|
||||
NormalizeUSVStringInternal(aString);
|
||||
EnsureUTF16Validity(aString);
|
||||
}
|
||||
|
||||
void
|
||||
NormalizeUSVString(binding_detail::FakeString& aString)
|
||||
{
|
||||
NormalizeUSVStringInternal(aString);
|
||||
EnsureUTF16ValiditySpan(aString);
|
||||
}
|
||||
|
||||
bool
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "nsString.h"
|
||||
#include "nsStringBuffer.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "mozilla/Span.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
|
@ -69,6 +70,8 @@ struct FakeString {
|
|||
|
||||
nsString::char_type* BeginWriting()
|
||||
{
|
||||
MOZ_ASSERT(!(mDataFlags & nsString::DataFlags::REFCOUNTED) ||
|
||||
!nsStringBuffer::FromData(mData)->IsReadonly());
|
||||
return mData;
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +80,16 @@ struct FakeString {
|
|||
return mLength;
|
||||
}
|
||||
|
||||
operator mozilla::Span<const nsString::char_type>() const
|
||||
{
|
||||
return mozilla::MakeSpan(Data(), Length());
|
||||
}
|
||||
|
||||
operator mozilla::Span<nsString::char_type>()
|
||||
{
|
||||
return mozilla::MakeSpan(BeginWriting(), Length());
|
||||
}
|
||||
|
||||
// Reserve space to write aLength chars, not including null-terminator.
|
||||
bool SetLength(nsString::size_type aLength, mozilla::fallible_t const&) {
|
||||
// Use mInlineStorage for small strings.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ UNIFIED_SOURCES += [
|
|||
'MockMediaResource.cpp',
|
||||
'TestAudioBuffers.cpp',
|
||||
'TestAudioCompactor.cpp',
|
||||
'TestAudioDeviceEnumerator.cpp',
|
||||
'TestAudioMixer.cpp',
|
||||
'TestAudioPacketizer.cpp',
|
||||
'TestAudioSegment.cpp',
|
||||
|
|
@ -47,6 +46,11 @@ if CONFIG['MOZ_WEBM_ENCODER']:
|
|||
'TestWebMWriter.cpp',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_WEBRTC']:
|
||||
UNIFIED_SOURCES += [
|
||||
'TestAudioDeviceEnumerator.cpp',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.gtest += [
|
||||
'../test/gizmo-frag.mp4',
|
||||
'../test/gizmo.mp4',
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ public:
|
|||
|
||||
virtual AudioBlockBuffer* AsAudioBlockBuffer() override { return this; };
|
||||
|
||||
float* ChannelData(uint32_t aChannel)
|
||||
uint32_t ChannelsAllocated() const { return mChannelsAllocated; }
|
||||
float* ChannelData(uint32_t aChannel) const
|
||||
{
|
||||
float* base = reinterpret_cast<float*>(((uintptr_t)(this + 1) + 15) & ~0x0F);
|
||||
ASSERT_ALIGNED16(base);
|
||||
|
|
@ -44,7 +45,7 @@ public:
|
|||
}
|
||||
|
||||
void* m = operator new(size.value());
|
||||
RefPtr<AudioBlockBuffer> p = new (m) AudioBlockBuffer();
|
||||
RefPtr<AudioBlockBuffer> p = new (m) AudioBlockBuffer(aChannelCount);
|
||||
NS_ASSERTION((reinterpret_cast<char*>(p.get() + 1) - reinterpret_cast<char*>(p.get())) % 4 == 0,
|
||||
"AudioBlockBuffers should be at least 4-byte aligned");
|
||||
return p.forget();
|
||||
|
|
@ -59,7 +60,7 @@ public:
|
|||
// Whether this is shared by any owners that are not downstream.
|
||||
// Called only from owners with a reference that is not a downstream
|
||||
// reference. Graph thread only.
|
||||
bool HasLastingShares()
|
||||
bool HasLastingShares() const
|
||||
{
|
||||
// mRefCnt is atomic and so reading its value is defined even when
|
||||
// modifications may happen on other threads. mDownstreamRefCount is
|
||||
|
|
@ -86,10 +87,12 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
AudioBlockBuffer() {}
|
||||
explicit AudioBlockBuffer(uint32_t aChannelsAllocated)
|
||||
: mChannelsAllocated(aChannelsAllocated) {}
|
||||
~AudioBlockBuffer() override { MOZ_ASSERT(mDownstreamRefCount == 0); }
|
||||
|
||||
nsAutoRefCnt mDownstreamRefCount;
|
||||
const uint32_t mChannelsAllocated;
|
||||
};
|
||||
|
||||
AudioBlock::~AudioBlock()
|
||||
|
|
@ -143,11 +146,17 @@ AudioBlock::AllocateChannels(uint32_t aChannelCount)
|
|||
if (mBufferIsDownstreamRef) {
|
||||
// This is not our buffer to re-use.
|
||||
ClearDownstreamMark();
|
||||
} else if (mBuffer && ChannelCount() == aChannelCount) {
|
||||
} else if (mBuffer) {
|
||||
AudioBlockBuffer* buffer = mBuffer->AsAudioBlockBuffer();
|
||||
if (buffer && !buffer->HasLastingShares()) {
|
||||
if (buffer && !buffer->HasLastingShares() &&
|
||||
buffer->ChannelsAllocated() >= aChannelCount) {
|
||||
MOZ_ASSERT(mBufferFormat == AUDIO_FORMAT_FLOAT32);
|
||||
// No need to allocate again.
|
||||
uint32_t previousChannelCount = ChannelCount();
|
||||
mChannelData.SetLength(aChannelCount);
|
||||
for (uint32_t i = previousChannelCount; i < aChannelCount; ++i) {
|
||||
mChannelData[i] = buffer->ChannelData(i);
|
||||
}
|
||||
mVolume = 1.0f;
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,13 +30,64 @@ class ConvolverNodeEngine final : public AudioNodeEngine
|
|||
public:
|
||||
ConvolverNodeEngine(AudioNode* aNode, bool aNormalize)
|
||||
: AudioNodeEngine(aNode)
|
||||
, mLeftOverData(INT32_MIN)
|
||||
, mSampleRate(0.0f)
|
||||
, mUseBackgroundThreads(!aNode->Context()->IsOffline())
|
||||
, mNormalize(aNormalize)
|
||||
{
|
||||
}
|
||||
|
||||
// Indicates how the right output channel is generated.
|
||||
enum class RightConvolverMode {
|
||||
// A right convolver is always used when there is more than one impulse
|
||||
// response channel.
|
||||
Always,
|
||||
// With a single response channel, the mode may be either Direct or
|
||||
// Difference. The decision on which to use is made when stereo input is
|
||||
// received. Once the right convolver is in use, convolver state is
|
||||
// suitable only for the selected mode, and so the mode cannot change
|
||||
// until the right convolver contains only silent history.
|
||||
//
|
||||
// With Direct mode, each convolver processes a corresponding channel.
|
||||
// This mode is selected when input is initially stereo or
|
||||
// channelInterpretation is "discrete" at the time or starting the right
|
||||
// convolver when input changes from non-silent mono to stereo.
|
||||
Direct,
|
||||
// Difference mode is selected if channelInterpretation is "speakers" at
|
||||
// the time starting the right convolver when the input changes from mono
|
||||
// to stereo.
|
||||
//
|
||||
// When non-silent input is initially mono, with a single response
|
||||
// channel, the right output channel is not produced until input becomes
|
||||
// stereo. Only a single convolver is used for mono processing. When
|
||||
// stereo input arrives after mono input, output must be as if the mono
|
||||
// signal remaining in the left convolver is up-mixed, but the right
|
||||
// convolver has not been initialized with the history of the mono input.
|
||||
// Copying the state of the left convolver into the right convolver is not
|
||||
// desirable, because there is considerable state to copy, and the
|
||||
// different convolvers are intended to process out of phase, which means
|
||||
// that state from one convolver would not directly map to state in
|
||||
// another convolver.
|
||||
//
|
||||
// Instead the distributive property of convolution is used to generate
|
||||
// the right output channel using information in the left output channel.
|
||||
// Using l and r to denote the left and right channel input signals, g the
|
||||
// impulse response, and * convolution, the convolution of the right
|
||||
// channel can be given by
|
||||
//
|
||||
// r * g = (l + (r - l)) * g
|
||||
// = l * g + (r - l) * g
|
||||
//
|
||||
// The left convolver continues to process the left channel l to produce
|
||||
// l * g. The right convolver processes the difference of input channel
|
||||
// signals r - l to produce (r - l) * g. The outputs of the two
|
||||
// convolvers are added to generate the right channel output r * g.
|
||||
//
|
||||
// The benefit of doing this is that the history of the r - l input for a
|
||||
// "speakers" up-mixed mono signal is zero, and so an empty convolver
|
||||
// already has exactly the right history for mixing the previous mono
|
||||
// signal with the new stereo signal.
|
||||
Difference
|
||||
};
|
||||
|
||||
enum Parameters {
|
||||
SAMPLE_RATE,
|
||||
NORMALIZE
|
||||
|
|
@ -73,81 +124,62 @@ public:
|
|||
// Very large FFTs will have worse phase errors. Given these constraints 32768 is a good compromise.
|
||||
const size_t MaxFFTSize = 32768;
|
||||
|
||||
mLeftOverData = INT32_MIN; // reset
|
||||
// Reset.
|
||||
mRemainingLeftOutput = INT32_MIN;
|
||||
mRemainingRightOutput = 0;
|
||||
mRemainingRightHistory = 0;
|
||||
|
||||
if (aBuffer.IsNull() || !mSampleRate) {
|
||||
mReverb = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
// Assume for now that convolution of channel difference is not required.
|
||||
// Direct may change to Difference during processing.
|
||||
mRightConvolverMode =
|
||||
aBuffer.ChannelCount() == 1 ? RightConvolverMode::Direct
|
||||
: RightConvolverMode::Always;
|
||||
|
||||
mReverb = new WebCore::Reverb(aBuffer, MaxFFTSize, mUseBackgroundThreads,
|
||||
mNormalize, mSampleRate);
|
||||
}
|
||||
|
||||
void AllocateReverbInput(const AudioBlock& aInput,
|
||||
uint32_t aTotalChannelCount)
|
||||
{
|
||||
uint32_t inputChannelCount = aInput.ChannelCount();
|
||||
MOZ_ASSERT(inputChannelCount <= aTotalChannelCount);
|
||||
mReverbInput.AllocateChannels(aTotalChannelCount);
|
||||
// Pre-multiply the input's volume
|
||||
for (uint32_t i = 0; i < inputChannelCount; ++i) {
|
||||
const float* src = static_cast<const float*>(aInput.mChannelData[i]);
|
||||
float* dest = mReverbInput.ChannelFloatsForWrite(i);
|
||||
AudioBlockCopyChannelWithScale(src, aInput.mVolume, dest);
|
||||
}
|
||||
// Fill remaining channels with silence
|
||||
for (uint32_t i = inputChannelCount; i < aTotalChannelCount; ++i) {
|
||||
float* dest = mReverbInput.ChannelFloatsForWrite(i);
|
||||
std::fill_n(dest, WEBAUDIO_BLOCK_SIZE, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessBlock(AudioNodeStream* aStream,
|
||||
GraphTime aFrom,
|
||||
const AudioBlock& aInput,
|
||||
AudioBlock* aOutput,
|
||||
bool* aFinished) override
|
||||
{
|
||||
if (!mReverb) {
|
||||
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
AudioBlock input = aInput;
|
||||
if (aInput.IsNull()) {
|
||||
if (mLeftOverData > 0) {
|
||||
mLeftOverData -= WEBAUDIO_BLOCK_SIZE;
|
||||
input.AllocateChannels(1);
|
||||
WriteZeroesToAudioBlock(&input, 0, WEBAUDIO_BLOCK_SIZE);
|
||||
} else {
|
||||
if (mLeftOverData != INT32_MIN) {
|
||||
mLeftOverData = INT32_MIN;
|
||||
aStream->ScheduleCheckForInactive();
|
||||
RefPtr<PlayingRefChanged> refchanged =
|
||||
new PlayingRefChanged(aStream, PlayingRefChanged::RELEASE);
|
||||
aStream->Graph()->DispatchToMainThreadAfterStreamStateUpdate(
|
||||
refchanged.forget());
|
||||
}
|
||||
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (aInput.mVolume != 1.0f) {
|
||||
// Pre-multiply the input's volume
|
||||
uint32_t numChannels = aInput.ChannelCount();
|
||||
input.AllocateChannels(numChannels);
|
||||
for (uint32_t i = 0; i < numChannels; ++i) {
|
||||
const float* src = static_cast<const float*>(aInput.mChannelData[i]);
|
||||
float* dest = input.ChannelFloatsForWrite(i);
|
||||
AudioBlockCopyChannelWithScale(src, aInput.mVolume, dest);
|
||||
}
|
||||
}
|
||||
|
||||
if (mLeftOverData <= 0) {
|
||||
RefPtr<PlayingRefChanged> refchanged =
|
||||
new PlayingRefChanged(aStream, PlayingRefChanged::ADDREF);
|
||||
aStream->Graph()->DispatchToMainThreadAfterStreamStateUpdate(
|
||||
refchanged.forget());
|
||||
}
|
||||
mLeftOverData = mReverb->impulseResponseLength();
|
||||
MOZ_ASSERT(mLeftOverData > 0);
|
||||
}
|
||||
aOutput->AllocateChannels(2);
|
||||
|
||||
mReverb->process(&input, aOutput);
|
||||
}
|
||||
bool* aFinished) override;
|
||||
|
||||
bool IsActive() const override
|
||||
{
|
||||
return mLeftOverData != INT32_MIN;
|
||||
return mRemainingLeftOutput != INT32_MIN;
|
||||
}
|
||||
|
||||
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override
|
||||
{
|
||||
size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
|
||||
|
||||
amount += mReverbInput.SizeOfExcludingThis(aMallocSizeOf, false);
|
||||
|
||||
if (mReverb) {
|
||||
amount += mReverb->sizeOfIncludingThis(aMallocSizeOf);
|
||||
}
|
||||
|
|
@ -161,13 +193,203 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
// Keeping mReverbInput across process calls avoids unnecessary reallocation.
|
||||
AudioBlock mReverbInput;
|
||||
nsAutoPtr<WebCore::Reverb> mReverb;
|
||||
int32_t mLeftOverData;
|
||||
float mSampleRate;
|
||||
// Tracks samples of the tail remaining to be output. INT32_MIN is a
|
||||
// special value to indicate that the end of any previous tail has been
|
||||
// handled.
|
||||
int32_t mRemainingLeftOutput = INT32_MIN;
|
||||
// mRemainingRightOutput and mRemainingRightHistory are only used when
|
||||
// mRightOutputMode != Always. There is no special handling required at the
|
||||
// end of tail times and so INT32_MIN is not used.
|
||||
// mRemainingRightOutput tracks how much longer this node needs to continue
|
||||
// to produce a right output channel.
|
||||
int32_t mRemainingRightOutput = 0;
|
||||
// mRemainingRightHistory tracks how much silent input would be required to
|
||||
// drain the right convolver, which may sometimes be longer than the period
|
||||
// a right output channel is required.
|
||||
int32_t mRemainingRightHistory = 0;
|
||||
float mSampleRate = 0.0f;
|
||||
RightConvolverMode mRightConvolverMode = RightConvolverMode::Always;
|
||||
bool mUseBackgroundThreads;
|
||||
bool mNormalize;
|
||||
};
|
||||
|
||||
static void
|
||||
AddScaledLeftToRight(AudioBlock* aBlock, float aScale)
|
||||
{
|
||||
const float* left = static_cast<const float*>(aBlock->mChannelData[0]);
|
||||
float* right = aBlock->ChannelFloatsForWrite(1);
|
||||
AudioBlockAddChannelWithScale(left, aScale, right);
|
||||
}
|
||||
|
||||
void
|
||||
ConvolverNodeEngine::ProcessBlock(AudioNodeStream* aStream,
|
||||
GraphTime aFrom,
|
||||
const AudioBlock& aInput,
|
||||
AudioBlock* aOutput,
|
||||
bool* aFinished)
|
||||
{
|
||||
if (!mReverb) {
|
||||
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t inputChannelCount = aInput.ChannelCount();
|
||||
if (aInput.IsNull()) {
|
||||
if (mRemainingLeftOutput > 0) {
|
||||
mRemainingLeftOutput -= WEBAUDIO_BLOCK_SIZE;
|
||||
AllocateReverbInput(aInput, 1); // floats for silence
|
||||
} else {
|
||||
if (mRemainingLeftOutput != INT32_MIN) {
|
||||
mRemainingLeftOutput = INT32_MIN;
|
||||
MOZ_ASSERT(mRemainingRightOutput <= 0);
|
||||
MOZ_ASSERT(mRemainingRightHistory <= 0);
|
||||
aStream->ScheduleCheckForInactive();
|
||||
RefPtr<PlayingRefChanged> refchanged =
|
||||
new PlayingRefChanged(aStream, PlayingRefChanged::RELEASE);
|
||||
aStream->Graph()->
|
||||
DispatchToMainThreadAfterStreamStateUpdate(refchanged.forget());
|
||||
}
|
||||
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (mRemainingLeftOutput <= 0) {
|
||||
RefPtr<PlayingRefChanged> refchanged =
|
||||
new PlayingRefChanged(aStream, PlayingRefChanged::ADDREF);
|
||||
aStream->Graph()->
|
||||
DispatchToMainThreadAfterStreamStateUpdate(refchanged.forget());
|
||||
}
|
||||
|
||||
// Use mVolume as a flag to detect whether AllocateReverbInput() gets
|
||||
// called.
|
||||
mReverbInput.mVolume = 0.0f;
|
||||
|
||||
// Special handling of input channel count changes is used when there is
|
||||
// only a single impulse response channel. See RightConvolverMode.
|
||||
if (mRightConvolverMode != RightConvolverMode::Always) {
|
||||
ChannelInterpretation channelInterpretation =
|
||||
aStream->GetChannelInterpretation();
|
||||
if (inputChannelCount == 2) {
|
||||
if (mRemainingRightHistory <= 0) {
|
||||
// Will start the second convolver. Choose to convolve the right
|
||||
// channel directly if there is no left tail to up-mix or up-mixing
|
||||
// is "discrete".
|
||||
mRightConvolverMode =
|
||||
(mRemainingLeftOutput <= 0 ||
|
||||
channelInterpretation == ChannelInterpretation::Discrete) ?
|
||||
RightConvolverMode::Direct : RightConvolverMode::Difference;
|
||||
}
|
||||
// The extra WEBAUDIO_BLOCK_SIZE is subtracted below.
|
||||
mRemainingRightOutput =
|
||||
mReverb->impulseResponseLength() + WEBAUDIO_BLOCK_SIZE;
|
||||
mRemainingRightHistory = mRemainingRightOutput;
|
||||
if (mRightConvolverMode == RightConvolverMode::Difference) {
|
||||
AllocateReverbInput(aInput, 2);
|
||||
// Subtract left from right.
|
||||
AddScaledLeftToRight(&mReverbInput, -1.0f);
|
||||
}
|
||||
} else if (mRemainingRightHistory > 0) {
|
||||
// There is one channel of input, but a second convolver also
|
||||
// requires input. Up-mix appropriately for the second convolver.
|
||||
if ((mRightConvolverMode == RightConvolverMode::Difference) ^
|
||||
(channelInterpretation == ChannelInterpretation::Discrete)) {
|
||||
MOZ_ASSERT(
|
||||
(mRightConvolverMode == RightConvolverMode::Difference &&
|
||||
channelInterpretation == ChannelInterpretation::Speakers) ||
|
||||
(mRightConvolverMode == RightConvolverMode::Direct &&
|
||||
channelInterpretation == ChannelInterpretation::Discrete));
|
||||
// The state is one of the following combinations:
|
||||
// 1) Difference and speakers.
|
||||
// Up-mixing gives r = l.
|
||||
// The input to the second convolver is r - l.
|
||||
// 2) Direct and discrete.
|
||||
// Up-mixing gives r = 0.
|
||||
// The input to the second convolver is r.
|
||||
//
|
||||
// In each case the input for the second convolver is silence, which
|
||||
// will drain the convolver.
|
||||
AllocateReverbInput(aInput, 2);
|
||||
} else {
|
||||
if (channelInterpretation == ChannelInterpretation::Discrete) {
|
||||
MOZ_ASSERT(mRightConvolverMode == RightConvolverMode::Difference);
|
||||
// channelInterpretation has changed since the second convolver
|
||||
// was added. "discrete" up-mixing of input would produce a
|
||||
// silent right channel r = 0, but the second convolver needs
|
||||
// r - l for RightConvolverMode::Difference.
|
||||
AllocateReverbInput(aInput, 2);
|
||||
AddScaledLeftToRight(&mReverbInput, -1.0f);
|
||||
} else {
|
||||
MOZ_ASSERT(channelInterpretation ==
|
||||
ChannelInterpretation::Speakers);
|
||||
MOZ_ASSERT(mRightConvolverMode == RightConvolverMode::Direct);
|
||||
// The Reverb will essentially up-mix the single input channel by
|
||||
// feeding it into both convolvers.
|
||||
}
|
||||
// The second convolver does not have silent input, and so it will
|
||||
// not drain. It will need to continue processing up-mixed input
|
||||
// because the next input block may be stereo, which would be mixed
|
||||
// with the signal remaining in the convolvers.
|
||||
// The extra WEBAUDIO_BLOCK_SIZE is subtracted below.
|
||||
mRemainingRightHistory =
|
||||
mReverb->impulseResponseLength() + WEBAUDIO_BLOCK_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mReverbInput.mVolume == 0.0f) { // not yet set
|
||||
if (aInput.mVolume != 1.0f) {
|
||||
AllocateReverbInput(aInput, inputChannelCount); // pre-multiply
|
||||
} else {
|
||||
mReverbInput = aInput;
|
||||
}
|
||||
}
|
||||
|
||||
mRemainingLeftOutput = mReverb->impulseResponseLength();
|
||||
MOZ_ASSERT(mRemainingLeftOutput > 0);
|
||||
}
|
||||
|
||||
// "The ConvolverNode produces a mono output only in the single case where
|
||||
// there is a single input channel and a single-channel buffer."
|
||||
uint32_t outputChannelCount = 2;
|
||||
uint32_t reverbOutputChannelCount = 2;
|
||||
if (mRightConvolverMode != RightConvolverMode::Always) {
|
||||
// When the input changes from stereo to mono, the output continues to be
|
||||
// stereo for the length of the tail time, during which the two channels
|
||||
// may differ.
|
||||
if (mRemainingRightOutput > 0) {
|
||||
MOZ_ASSERT(mRemainingRightHistory > 0);
|
||||
mRemainingRightOutput -= WEBAUDIO_BLOCK_SIZE;
|
||||
} else {
|
||||
outputChannelCount = 1;
|
||||
}
|
||||
// The second convolver keeps processing until it drains.
|
||||
if (mRemainingRightHistory > 0) {
|
||||
mRemainingRightHistory -= WEBAUDIO_BLOCK_SIZE;
|
||||
} else {
|
||||
reverbOutputChannelCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are two convolvers, then they each need an output buffer, even
|
||||
// if the second convolver is only processing to keep history of up-mixed
|
||||
// input.
|
||||
aOutput->AllocateChannels(reverbOutputChannelCount);
|
||||
|
||||
mReverb->process(&mReverbInput, aOutput);
|
||||
|
||||
if (mRightConvolverMode == RightConvolverMode::Difference &&
|
||||
outputChannelCount == 2) {
|
||||
// Add left to right.
|
||||
AddScaledLeftToRight(aOutput, 1.0f);
|
||||
} else {
|
||||
// Trim if outputChannelCount < reverbOutputChannelCount
|
||||
aOutput->mChannelData.TruncateLength(outputChannelCount);
|
||||
}
|
||||
}
|
||||
|
||||
ConvolverNode::ConvolverNode(AudioContext* aContext)
|
||||
: AudioNode(aContext,
|
||||
2,
|
||||
|
|
|
|||
|
|
@ -118,11 +118,17 @@ skip-if = (android_version == '18' && debug) # bug 1158417
|
|||
[test_convolverNode.html]
|
||||
[test_convolverNode_mono_mono.html]
|
||||
[test_convolverNodeChannelCount.html]
|
||||
[test_convolverNodeChannelInterpretationChanges.html]
|
||||
[test_convolverNodeDelay.html]
|
||||
[test_convolverNodeFiniteInfluence.html]
|
||||
[test_convolverNodeNormalization.html]
|
||||
[test_convolverNodePassThrough.html]
|
||||
[test_convolverNodeWithGain.html]
|
||||
[test_convolver-upmixing-1-channel-response.html]
|
||||
# This is a copy of
|
||||
# testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-upmixing-1-channel-response.html,
|
||||
# but WPT are not run with ASan or Android builds.
|
||||
skip-if = !asan && toolkit != android
|
||||
[test_currentTime.html]
|
||||
[test_decodeAudioDataOnDetachedBuffer.html]
|
||||
[test_decodeAudioDataPromise.html]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
<!DOCTYPE html>
|
||||
<title>Test that up-mixing signals in ConvolverNode processing is linear</title>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script>
|
||||
const EPSILON = 3.0 * Math.pow(2, -22);
|
||||
// sampleRate is a power of two so that delay times are exact in base-2
|
||||
// floating point arithmetic.
|
||||
const SAMPLE_RATE = 32768;
|
||||
// Length of stereo convolver input in frames (arbitrary):
|
||||
const STEREO_FRAMES = 256;
|
||||
// Length of mono signal in frames. This is more than two blocks to ensure
|
||||
// that at least one block will be mono, even if interpolation in the
|
||||
// DelayNode means that stereo is output one block earlier and later than
|
||||
// if frames are delayed without interpolation.
|
||||
const MONO_FRAMES = 384;
|
||||
// Length of response buffer:
|
||||
const RESPONSE_FRAMES = 256;
|
||||
|
||||
function test_linear_upmixing(channelInterpretation, initial_mono_frames)
|
||||
{
|
||||
let stereo_input_end = initial_mono_frames + STEREO_FRAMES;
|
||||
// Total length:
|
||||
let length = stereo_input_end + RESPONSE_FRAMES + MONO_FRAMES + STEREO_FRAMES;
|
||||
// The first two channels contain signal where some up-mixing occurs
|
||||
// internally to a ConvolverNode when a stereo signal is added and removed.
|
||||
// The last two channels are expected to contain the same signal, but mono
|
||||
// and stereo signals are convolved independently before up-mixing the mono
|
||||
// output to mix with the stereo output.
|
||||
let context = new OfflineAudioContext({numberOfChannels: 4,
|
||||
length: length,
|
||||
sampleRate: SAMPLE_RATE});
|
||||
|
||||
let response = new AudioBuffer({numberOfChannels: 1,
|
||||
length: RESPONSE_FRAMES,
|
||||
sampleRate: context.sampleRate});
|
||||
|
||||
// Two stereo channel splitters will collect test and reference outputs.
|
||||
let destinationMerger = new ChannelMergerNode(context, {numberOfInputs: 4});
|
||||
destinationMerger.connect(context.destination);
|
||||
let testSplitter =
|
||||
new ChannelSplitterNode(context, {numberOfOutputs: 2});
|
||||
let referenceSplitter =
|
||||
new ChannelSplitterNode(context, {numberOfOutputs: 2});
|
||||
testSplitter.connect(destinationMerger, 0, 0);
|
||||
testSplitter.connect(destinationMerger, 1, 1);
|
||||
referenceSplitter.connect(destinationMerger, 0, 2);
|
||||
referenceSplitter.connect(destinationMerger, 1, 3);
|
||||
|
||||
// A GainNode mixes reference stereo and mono signals because up-mixing
|
||||
// cannot be performed at a channel splitter.
|
||||
let referenceGain = new GainNode(context);
|
||||
referenceGain.connect(referenceSplitter);
|
||||
referenceGain.channelInterpretation = channelInterpretation;
|
||||
|
||||
// The impulse response for convolution contains two impulses so as to test
|
||||
// effects in at least two processing blocks.
|
||||
response.getChannelData(0)[0] = 0.5;
|
||||
response.getChannelData(0)[response.length - 1] = 0.5;
|
||||
|
||||
let testConvolver = new ConvolverNode(context, {disableNormalization: true,
|
||||
buffer: response});
|
||||
testConvolver.channelInterpretation = channelInterpretation;
|
||||
let referenceMonoConvolver = new ConvolverNode(context,
|
||||
{disableNormalization: true,
|
||||
buffer: response});
|
||||
let referenceStereoConvolver = new ConvolverNode(context,
|
||||
{disableNormalization: true,
|
||||
buffer: response});
|
||||
// No need to set referenceStereoConvolver.channelInterpretation because
|
||||
// input is either silent or stereo.
|
||||
testConvolver.connect(testSplitter);
|
||||
// Mix reference convolver output.
|
||||
referenceMonoConvolver.connect(referenceGain);
|
||||
referenceStereoConvolver.connect(referenceGain);
|
||||
|
||||
// The DelayNode initially has a single channel of silence, which is used to
|
||||
// switch the stereo signal in and out. The output of the delay node is
|
||||
// first mono silence (if there is a non-zero initial_mono_frames), then
|
||||
// stereo, then mono silence, and finally stereo again. maxDelayTime is
|
||||
// used to generate the middle mono silence period from the initial silence
|
||||
// in the DelayNode and then generate the final period of stereo from its
|
||||
// initial input.
|
||||
let maxDelayTime = (length - STEREO_FRAMES) / context.sampleRate;
|
||||
let delay =
|
||||
new DelayNode(context,
|
||||
{maxDelayTime: maxDelayTime,
|
||||
delayTime: initial_mono_frames / context.sampleRate});
|
||||
// Schedule an increase in the delay to return to mono silence.
|
||||
delay.delayTime.setValueAtTime(maxDelayTime,
|
||||
stereo_input_end / context.sampleRate);
|
||||
delay.connect(testConvolver);
|
||||
delay.connect(referenceStereoConvolver);
|
||||
|
||||
let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
|
||||
stereoMerger.connect(delay);
|
||||
|
||||
// Three independent signals
|
||||
let monoSignal = new OscillatorNode(context, {frequency: 440});
|
||||
let leftSignal = new OscillatorNode(context, {frequency: 450});
|
||||
let rightSignal = new OscillatorNode(context, {frequency: 460});
|
||||
monoSignal.connect(testConvolver);
|
||||
monoSignal.connect(referenceMonoConvolver);
|
||||
leftSignal.connect(stereoMerger, 0, 0);
|
||||
rightSignal.connect(stereoMerger, 0, 1);
|
||||
monoSignal.start();
|
||||
leftSignal.start();
|
||||
rightSignal.start();
|
||||
|
||||
return context.startRendering().
|
||||
then((buffer) => {
|
||||
let maxDiff = -1.0;
|
||||
let frameIndex = 0;
|
||||
let channelIndex = 0;
|
||||
for (let c = 0; c < 2; ++c) {
|
||||
let testOutput = buffer.getChannelData(0 + c);
|
||||
let referenceOutput = buffer.getChannelData(2 + c);
|
||||
for (var i = 0; i < buffer.length; ++i) {
|
||||
var diff = Math.abs(testOutput[i] - referenceOutput[i]);
|
||||
if (diff > maxDiff) {
|
||||
maxDiff = diff;
|
||||
frameIndex = i;
|
||||
channelIndex = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex],
|
||||
buffer.getChannelData(2 + channelIndex)[frameIndex],
|
||||
EPSILON,
|
||||
`output at ${frameIndex} ` +
|
||||
`in channel ${channelIndex}` );
|
||||
});
|
||||
}
|
||||
|
||||
promise_test(() => test_linear_upmixing("speakers", MONO_FRAMES),
|
||||
"speakers, initially mono");
|
||||
promise_test(() => test_linear_upmixing("discrete", MONO_FRAMES),
|
||||
"discrete");
|
||||
// Gecko uses a separate path for "speakers" up-mixing when the convolver's
|
||||
// first input is stereo, so test that separately.
|
||||
promise_test(() => test_linear_upmixing("speakers", 0),
|
||||
"speakers, initially stereo");
|
||||
</script>
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<!DOCTYPE html>
|
||||
<title>Test up-mixing in ConvolverNode after ChannelInterpretation change</title>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script>
|
||||
// This test is not in wpt because it requires that multiple changes to the
|
||||
// nodes in an AudioContext during a single event will be processed by the
|
||||
// audio thread in a single transaction. Gecko provides that, but this is not
|
||||
// currently required by the Web Audio API.
|
||||
|
||||
const EPSILON = Math.pow(2, -23);
|
||||
// sampleRate is a power of two so that delay times are exact in base-2
|
||||
// floating point arithmetic.
|
||||
const SAMPLE_RATE = 32768;
|
||||
// Length of initial mono signal in frames, if the test has an initial mono
|
||||
// signal. This is more than one block to ensure that at least one block
|
||||
// will be mono, even if interpolation in the DelayNode means that stereo is
|
||||
// output one block earlier than if frames are delayed without interpolation.
|
||||
const MONO_FRAMES = 256;
|
||||
// Length of response buffer. This is greater than 1 to ensure that the
|
||||
// convolver has stereo output at least one block after stereo input is
|
||||
// disconnected.
|
||||
const RESPONSE_FRAMES = 2;
|
||||
|
||||
function test_interpretation_change(t, initialInterpretation, initialMonoFrames)
|
||||
{
|
||||
let context = new AudioContext({sampleRate: SAMPLE_RATE});
|
||||
|
||||
// Three independent signals. These are constant so that results are
|
||||
// independent of the timing of the `ended` event.
|
||||
let monoOffset = 0.25
|
||||
let monoSource = new ConstantSourceNode(context, {offset: monoOffset});
|
||||
let leftOffset = 0.125;
|
||||
let rightOffset = 0.5;
|
||||
let leftSource = new ConstantSourceNode(context, {offset: leftOffset});
|
||||
let rightSource = new ConstantSourceNode(context, {offset: rightOffset});
|
||||
monoSource.start();
|
||||
leftSource.start();
|
||||
rightSource.start();
|
||||
|
||||
let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
|
||||
leftSource.connect(stereoMerger, 0, 0);
|
||||
rightSource.connect(stereoMerger, 0, 1);
|
||||
|
||||
// The DelayNode initially has a single channel of silence, and so the
|
||||
// output of the delay node is first mono silence (if there is a non-zero
|
||||
// initialMonoFrames), then stereo. In Gecko, this triggers a convolver
|
||||
// configuration that is different for different channelInterpretations.
|
||||
let delay =
|
||||
new DelayNode(context,
|
||||
{maxDelayTime: MONO_FRAMES / context.sampleRate,
|
||||
delayTime: initialMonoFrames / context.sampleRate});
|
||||
stereoMerger.connect(delay);
|
||||
|
||||
// Two convolvers with the same impulse response. The test convolver will
|
||||
// process a mix of stereo and mono signals. The reference convolver will
|
||||
// always process stereo, including the up-mixed mono signal.
|
||||
let response = new AudioBuffer({numberOfChannels: 1,
|
||||
length: RESPONSE_FRAMES,
|
||||
sampleRate: context.sampleRate});
|
||||
response.getChannelData(0)[response.length - 1] = 1;
|
||||
|
||||
let testConvolver = new ConvolverNode(context,
|
||||
{disableNormalization: true,
|
||||
buffer: response});
|
||||
testConvolver.channelInterpretation = initialInterpretation;
|
||||
let referenceConvolver = new ConvolverNode(context,
|
||||
{disableNormalization: true,
|
||||
buffer: response});
|
||||
// No need to set referenceConvolver.channelInterpretation because
|
||||
// input is always stereo, due to up-mixing at gain node.
|
||||
let referenceMixer = new GainNode(context);
|
||||
referenceMixer.channelCount = 2;
|
||||
referenceMixer.channelCountMode = "explicit";
|
||||
referenceMixer.channelInterpretation = initialInterpretation;
|
||||
referenceMixer.connect(referenceConvolver);
|
||||
|
||||
delay.connect(testConvolver);
|
||||
delay.connect(referenceMixer);
|
||||
|
||||
monoSource.connect(testConvolver);
|
||||
monoSource.connect(referenceMixer);
|
||||
|
||||
// A timer sends 'ended' when the convolvers are known to be processing
|
||||
// stereo.
|
||||
let timer = new ConstantSourceNode(context);
|
||||
timer.start();
|
||||
timer.stop((initialMonoFrames + 1) / context.sampleRate);
|
||||
|
||||
timer.onended = t.step_func(() => {
|
||||
let changedInterpretation =
|
||||
initialInterpretation == "speakers" ? "discrete" : "speakers";
|
||||
|
||||
// Switch channelInterpretation in test and reference paths.
|
||||
testConvolver.channelInterpretation = changedInterpretation;
|
||||
referenceMixer.channelInterpretation = changedInterpretation;
|
||||
|
||||
// Disconnect the stereo input from both test and reference convolvers.
|
||||
// The disconnected convolvers will continue to output stereo for at least
|
||||
// one frame. The test convolver will up-mix its mono input into its two
|
||||
// buffers.
|
||||
delay.disconnect();
|
||||
|
||||
// Capture the outputs in a script processor.
|
||||
//
|
||||
// The first two channels contain signal where some up-mixing occurs
|
||||
// internally to the test convolver.
|
||||
//
|
||||
// The last two channels are expected to contain the same signal, but
|
||||
// up-mixing was performed at a GainNode prior to convolution.
|
||||
//
|
||||
// Two stereo splitters will collect test and reference outputs.
|
||||
let testSplitter =
|
||||
new ChannelSplitterNode(context, {numberOfOutputs: 2});
|
||||
let referenceSplitter =
|
||||
new ChannelSplitterNode(context, {numberOfOutputs: 2});
|
||||
testConvolver.connect(testSplitter);
|
||||
referenceConvolver.connect(referenceSplitter);
|
||||
|
||||
let outputMerger = new ChannelMergerNode(context, {numberOfInputs: 4});
|
||||
testSplitter.connect(outputMerger, 0, 0);
|
||||
testSplitter.connect(outputMerger, 1, 1);
|
||||
referenceSplitter.connect(outputMerger, 0, 2);
|
||||
referenceSplitter.connect(outputMerger, 1, 3);
|
||||
|
||||
let processor = context.createScriptProcessor(256, 4, 0);
|
||||
outputMerger.connect(processor);
|
||||
|
||||
processor.onaudioprocess = t.step_func_done((e) => {
|
||||
e.target.onaudioprocess = null;
|
||||
outputMerger.disconnect();
|
||||
|
||||
// The test convolver output is stereo for the first block.
|
||||
let length = 128;
|
||||
|
||||
let buffer = e.inputBuffer;
|
||||
let maxDiff = -1.0;
|
||||
let frameIndex = 0;
|
||||
let channelIndex = 0;
|
||||
for (let c = 0; c < 2; ++c) {
|
||||
let testOutput = buffer.getChannelData(0 + c);
|
||||
let referenceOutput = buffer.getChannelData(2 + c);
|
||||
for (var i = 0; i < length; ++i) {
|
||||
var diff = Math.abs(testOutput[i] - referenceOutput[i]);
|
||||
if (diff > maxDiff) {
|
||||
maxDiff = diff;
|
||||
frameIndex = i;
|
||||
channelIndex = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex],
|
||||
buffer.getChannelData(2 + channelIndex)[frameIndex],
|
||||
EPSILON,
|
||||
`output at ${frameIndex} ` +
|
||||
`in channel ${channelIndex}` );
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async_test((t) => test_interpretation_change(t, "speakers", MONO_FRAMES),
|
||||
"speakers to discrete, initially mono");
|
||||
async_test((t) => test_interpretation_change(t, "discrete", MONO_FRAMES),
|
||||
"discrete to speakers");
|
||||
// Gecko uses a separate path for "speakers" initial up-mixing when the
|
||||
// convolver's first input is stereo, so test that separately.
|
||||
async_test((t) => test_interpretation_change(t, "speakers", 0),
|
||||
"speakers to discrete, initially stereo");
|
||||
</script>
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
Exposed=(ServiceWorker)]
|
||||
interface FetchEvent : ExtendableEvent {
|
||||
[SameObject] readonly attribute Request request;
|
||||
readonly attribute DOMString? clientId;
|
||||
readonly attribute DOMString clientId;
|
||||
readonly attribute boolean isReload;
|
||||
|
||||
[Throws]
|
||||
|
|
@ -21,6 +21,6 @@ interface FetchEvent : ExtendableEvent {
|
|||
|
||||
dictionary FetchEventInit : EventInit {
|
||||
required Request request;
|
||||
DOMString? clientId = null;
|
||||
DOMString clientId = "";
|
||||
boolean isReload = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -618,6 +618,16 @@ pub unsafe extern "C" fn encoding_mem_is_str_latin1(buffer: *const u8, len: usiz
|
|||
))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn encoding_mem_utf16_valid_up_to(buffer: *const u16, len: usize) -> usize {
|
||||
encoding_rs::mem::utf16_valid_up_to(::std::slice::from_raw_parts(buffer, len))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn encoding_mem_ensure_utf16_validity(buffer: *mut u16, len: usize) {
|
||||
encoding_rs::mem::ensure_utf16_validity(::std::slice::from_raw_parts_mut(buffer, len));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn encoding_mem_convert_utf16_to_latin1_lossy(
|
||||
src: *const u16,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ fuzzy-if(winWidget&&!layersGPUAccelerated,0-142,0-276) == positioned-container-1
|
|||
== relpos-legend-2.html relpos-legend-2-ref.html
|
||||
== relpos-legend-3.html relpos-legend-3-ref.html
|
||||
== relpos-legend-4.html relpos-legend-4-ref.html
|
||||
fuzzy-if(webrender,17-19,1221-2452) == sticky-legend-1.html sticky-legend-1-ref.html
|
||||
fuzzy-if(webrender,17-19,1217-2452) == sticky-legend-1.html sticky-legend-1-ref.html
|
||||
fuzzy-if(skiaContent,0-1,0-40768) == abs-pos-child-sizing.html abs-pos-child-sizing-ref.html
|
||||
== overflow-hidden.html overflow-hidden-ref.html
|
||||
== legend-rtl.html legend-rtl-ref.html
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ fieldset { height: 100px; width: 100px; }
|
|||
.v-rl .a { width: 40%; background: #eee; }
|
||||
.v-rl .b { width: 60%; background: #ddd; }
|
||||
|
||||
.ltr fieldset, .rtl fieldset { margin: 0 2px; padding: 0.35em 0.625em 0.75em; }
|
||||
.v-rl fieldset { margin: 2px 0; padding: 0.625em 0.35em 0.625em 0.75em; }
|
||||
.ltr fieldset, .rtl fieldset { margin: 0 2px; padding: 0.35em 0.75em 0.625em; }
|
||||
.v-rl fieldset { margin: 2px 0; padding: 0.75em 0.35em 0.75em 0.625em; }
|
||||
|
||||
.ltr legend, .rtl legend { padding: 0 2px; }
|
||||
.v-rl legend { padding: 2px 0; }
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ fieldset {
|
|||
margin-inline-start: 2px;
|
||||
margin-inline-end: 2px;
|
||||
padding-block-start: 0.35em;
|
||||
padding-block-end: 0.75em;
|
||||
padding-inline-start: 0.625em;
|
||||
padding-inline-end: 0.625em;
|
||||
padding-block-end: 0.625em;
|
||||
padding-inline-start: 0.75em;
|
||||
padding-inline-end: 0.75em;
|
||||
border: 2px groove ThreeDLightShadow;
|
||||
min-width: -moz-min-content;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -932,6 +932,12 @@ TransceiverImpl::UpdateVideoConduit()
|
|||
return rv;
|
||||
}
|
||||
|
||||
if (configs.values.empty()) {
|
||||
MOZ_MTLOG(ML_INFO, mPCHandle << "[" << mMid << "]: " << __FUNCTION__ <<
|
||||
" No codecs were negotiated (send).");
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
auto error = conduit->ConfigureSendMediaCodec(configs.values[0]);
|
||||
if (error) {
|
||||
MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: " << __FUNCTION__ <<
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mozilla.gecko.util.GeckoBundle;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import static org.mozilla.gecko.toolbar.PageActionLayout.PageAction;
|
||||
import static org.mozilla.gecko.toolbar.PageActionLayout.PageActionLayoutDelegate;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class TestAddonUICache {
|
||||
private AddonUICache mAddonUICache;
|
||||
private @Mock Menu mMockMenu;
|
||||
private @Mock PageActionLayoutDelegate mMockPalDelegate;
|
||||
private @Captor ArgumentCaptor<List<PageAction>> palCaptor;
|
||||
private static final MessageGenerator addonMessage = new AddonMenuMessageGenerator();
|
||||
private static final MessageGenerator pageActionMessage = new PageActionMessageGenerator();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
when(mMockMenu.add(anyInt(), anyInt(), anyInt(), anyString())).thenReturn(mock(MenuItem.class));
|
||||
|
||||
mAddonUICache = AddonUICache.getInstance();
|
||||
mAddonUICache.init();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
mAddonUICache.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMenuMessageDirectForward() {
|
||||
mAddonUICache.onCreateOptionsMenu(mMockMenu);
|
||||
verify(mMockMenu, never()).add(anyInt(), anyInt(), anyInt(), anyString());
|
||||
|
||||
Map<String, GeckoBundle> sentMessages = sendAddonMessages("Menu:Add",
|
||||
addonMessage);
|
||||
|
||||
ArgumentCaptor<String> menuLabel = ArgumentCaptor.forClass(String.class);
|
||||
verify(mMockMenu, times(4)).add(anyInt(), anyInt(), anyInt(), menuLabel.capture());
|
||||
|
||||
List<String> expectedLabels = new ArrayList<>(sentMessages.keySet());
|
||||
Assert.assertEquals(expectedLabels, menuLabel.getAllValues());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMenuMessageStoreForward() {
|
||||
Map<String, GeckoBundle> sentMessages = sendAddonMessages("Menu:Add",
|
||||
addonMessage);
|
||||
|
||||
mAddonUICache.onCreateOptionsMenu(mMockMenu);
|
||||
ArgumentCaptor<String> menuLabel = ArgumentCaptor.forClass(String.class);
|
||||
verify(mMockMenu, times(4)).add(anyInt(), anyInt(), anyInt(), menuLabel.capture());
|
||||
|
||||
List<String> expectedLabels = new ArrayList<>(sentMessages.keySet());
|
||||
Assert.assertEquals(expectedLabels, menuLabel.getAllValues());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMenuMessageStoreRemoveForward() {
|
||||
Map<String, GeckoBundle> sentMessages = sendAddonMessages("Menu:Add",
|
||||
addonMessage);
|
||||
sendAddonMessages("Menu:Remove", new ArrayList<>(sentMessages.values()));
|
||||
|
||||
mAddonUICache.onCreateOptionsMenu(mMockMenu);
|
||||
verify(mMockMenu, never()).add(anyInt(), anyInt(), anyInt(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolvedPageActionListSaving() {
|
||||
final List<PageAction> pageActionList = new ArrayList<>();
|
||||
mAddonUICache.removePageActionLayoutDelegate(pageActionList);
|
||||
|
||||
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
|
||||
verify(mMockPalDelegate).setCachedPageActions(palCaptor.capture());
|
||||
Assert.assertEquals(pageActionList, palCaptor.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageActionMessageDirectForward() {
|
||||
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
|
||||
verify(mMockPalDelegate, never()).addPageAction(any(GeckoBundle.class));
|
||||
verify(mMockPalDelegate, never()).removePageAction(any(GeckoBundle.class));
|
||||
|
||||
Map<String, GeckoBundle> sentMessages = sendAddonMessages("PageActions:Add",
|
||||
pageActionMessage);
|
||||
|
||||
ArgumentCaptor<GeckoBundle> messages = ArgumentCaptor.forClass(GeckoBundle.class);
|
||||
verify(mMockPalDelegate, times(4)).addPageAction(messages.capture());
|
||||
|
||||
List<GeckoBundle> expectedMessages = new ArrayList<>(sentMessages.values());
|
||||
Assert.assertEquals(expectedMessages, messages.getAllValues());
|
||||
|
||||
sendAddonMessages("PageActions:Remove", new ArrayList<>(sentMessages.values()));
|
||||
messages = ArgumentCaptor.forClass(GeckoBundle.class);
|
||||
verify(mMockPalDelegate, times(4)).removePageAction(messages.capture());
|
||||
|
||||
Assert.assertEquals(expectedMessages, messages.getAllValues());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageActionMessageStoreForward() {
|
||||
Map<String, GeckoBundle> sentMessages = sendAddonMessages("PageActions:Add",
|
||||
pageActionMessage);
|
||||
|
||||
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
|
||||
ArgumentCaptor<GeckoBundle> messages = ArgumentCaptor.forClass(GeckoBundle.class);
|
||||
verify(mMockPalDelegate, times(4)).addPageAction(messages.capture());
|
||||
|
||||
List<GeckoBundle> expectedMessages = new ArrayList<>(sentMessages.values());
|
||||
Assert.assertEquals(expectedMessages, messages.getAllValues());
|
||||
|
||||
verify(mMockPalDelegate, never()).removePageAction(any(GeckoBundle.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageActionMessageStoreRemoveForward() {
|
||||
Map<String, GeckoBundle> sentMessages = sendAddonMessages("PageActions:Add",
|
||||
pageActionMessage);
|
||||
sendAddonMessages("PageActions:Remove", new ArrayList<>(sentMessages.values()));
|
||||
|
||||
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
|
||||
verify(mMockPalDelegate, never()).addPageAction(any(GeckoBundle.class));
|
||||
verify(mMockPalDelegate, never()).removePageAction(any(GeckoBundle.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolvedPageActionListSavingRemoval() {
|
||||
final List<PageAction> pageActionList = new ArrayList<>();
|
||||
final GeckoBundle palMessage = pageActionMessage.getMessage("Frob widget");
|
||||
final PageAction pageAction = new PageAction(palMessage.getString("id"),
|
||||
palMessage.getString("title"), null, null, false);
|
||||
pageActionList.add(pageAction);
|
||||
mAddonUICache.removePageActionLayoutDelegate(pageActionList);
|
||||
|
||||
sendAddonMessage("PageActions:Remove", palMessage);
|
||||
|
||||
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
|
||||
verify(mMockPalDelegate).setCachedPageActions(palCaptor.capture());
|
||||
Assert.assertEquals(pageActionList, palCaptor.getValue());
|
||||
Assert.assertTrue(pageActionList.isEmpty());
|
||||
}
|
||||
|
||||
private Map<String, GeckoBundle> sendAddonMessages(String event, MessageGenerator generator) {
|
||||
Map<String, GeckoBundle> sentMessages = new LinkedHashMap<>();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
String label = "Menu " + i;
|
||||
GeckoBundle message = generator.getMessage(label);
|
||||
sendAddonMessage(event, message);
|
||||
sentMessages.put(label, message);
|
||||
}
|
||||
return sentMessages;
|
||||
}
|
||||
|
||||
private void sendAddonMessages(String event, List<GeckoBundle> messages) {
|
||||
for (GeckoBundle message : messages) {
|
||||
sendAddonMessage(event, message);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendAddonMessage(String event, GeckoBundle message) {
|
||||
mAddonUICache.handleMessage(event, message, null);
|
||||
}
|
||||
|
||||
private interface MessageGenerator {
|
||||
GeckoBundle getMessage(String label);
|
||||
}
|
||||
|
||||
private static class AddonMenuMessageGenerator implements MessageGenerator {
|
||||
@Override
|
||||
public GeckoBundle getMessage(String label) {
|
||||
GeckoBundle message = new GeckoBundle(2);
|
||||
message.putString("name", label);
|
||||
message.putString("uuid", "{" + UUID.randomUUID().toString() + "}");
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PageActionMessageGenerator implements MessageGenerator {
|
||||
@Override
|
||||
public GeckoBundle getMessage(String label) {
|
||||
GeckoBundle message = new GeckoBundle(2);
|
||||
message.putString("title", label);
|
||||
message.putString("id", "{" + UUID.randomUUID().toString() + "}");
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
355
mobile/android/base/java/org/mozilla/gecko/AddonUICache.java
Normal file
355
mobile/android/base/java/org/mozilla/gecko/AddonUICache.java
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* vim: ts=4 sw=4 expandtab:
|
||||
* 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/. */
|
||||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
|
||||
import org.mozilla.gecko.util.BundleEventListener;
|
||||
import org.mozilla.gecko.util.EventCallback;
|
||||
import org.mozilla.gecko.util.GeckoBundle;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mozilla.gecko.toolbar.PageActionLayout.PageAction;
|
||||
import static org.mozilla.gecko.toolbar.PageActionLayout.PageActionLayoutDelegate;
|
||||
|
||||
/**
|
||||
* For certain UI items added by add-ons or other JS/Gecko code, Gecko notifies us whenever an item
|
||||
* is added, changed or removed. Since we must not miss any of these notifications and need to re-
|
||||
* member the current list of active UI items even if e.g. we're in background and our activities
|
||||
* have been destroyed, we need a class whose lifetime matches (or even exceeds) that of Gecko.
|
||||
*
|
||||
* This class fulfills this purpose - it will be initialised early during app startup and just like
|
||||
* Gecko, once initialised it will remain alive until the OS kills our app.
|
||||
*
|
||||
* After initialisation, we will start listening for the appropriate EventDispatcher messages from
|
||||
* Gecko and maintain an internal list of UI items dynamically added by Gecko.
|
||||
* In addition, for each class of UI elements an appropriate API will be provided through which the
|
||||
* intended final consumer can make use of that list in order to actually show those elements in the
|
||||
* UI.
|
||||
*/
|
||||
public class AddonUICache implements BundleEventListener {
|
||||
private static final String LOGTAG = "GeckoAddonUICache";
|
||||
|
||||
private static final int GECKO_TOOLS_MENU_ID = -1;
|
||||
// When changing this, make sure to adjust NativeWindow.toolsMenuID in browser.js, too.
|
||||
private static final String GECKO_TOOLS_MENU_UUID = "{115b9308-2023-44f1-a4e9-3e2197669f07}";
|
||||
private static final int ADDON_MENU_OFFSET = 1000;
|
||||
|
||||
private static class MenuItemInfo {
|
||||
public int id;
|
||||
public String uuid;
|
||||
public String label;
|
||||
public boolean checkable;
|
||||
public boolean checked;
|
||||
public boolean enabled = true;
|
||||
public boolean visible = true;
|
||||
public int parent;
|
||||
}
|
||||
|
||||
private static final AddonUICache instance = new AddonUICache();
|
||||
|
||||
private final Map<String, MenuItemInfo> mAddonMenuItemsCache = new LinkedHashMap<>();
|
||||
private int mAddonMenuNextID = ADDON_MENU_OFFSET;
|
||||
private Menu mMenu;
|
||||
|
||||
// A collection of PageAction messages that are still pending transformation into
|
||||
// a full PageAction - most importantly transformation of the image data URI
|
||||
// into a Drawable.
|
||||
private final Map<String, GeckoBundle> mPendingPageActionQueue = new LinkedHashMap<>();
|
||||
// A collection of PageActions ready for immediate usage.
|
||||
private List<PageAction> mResolvedPageActionCache;
|
||||
private PageActionLayoutDelegate mPageActionDelegate;
|
||||
|
||||
private boolean mInitialized;
|
||||
|
||||
public static AddonUICache getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private AddonUICache() { }
|
||||
|
||||
public void init() {
|
||||
if (mInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventDispatcher.getInstance().registerUiThreadListener(this,
|
||||
"Menu:Add",
|
||||
"Menu:Update",
|
||||
"Menu:Remove",
|
||||
"PageActions:Add",
|
||||
"PageActions:Remove",
|
||||
null);
|
||||
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package, intended private */ void reset() {
|
||||
mAddonMenuItemsCache.clear();
|
||||
mAddonMenuNextID = ADDON_MENU_OFFSET;
|
||||
mMenu = null;
|
||||
mPendingPageActionQueue.clear();
|
||||
mResolvedPageActionCache = null;
|
||||
mPageActionDelegate = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
|
||||
switch (event) {
|
||||
case "Menu:Add":
|
||||
final MenuItemInfo info = new MenuItemInfo();
|
||||
info.label = message.getString("name");
|
||||
if (TextUtils.isEmpty(info.label)) {
|
||||
Log.e(LOGTAG, "Invalid menu item name");
|
||||
return;
|
||||
}
|
||||
info.id = mAddonMenuNextID++;
|
||||
info.uuid = message.getString("uuid");
|
||||
info.checked = message.getBoolean("checked", false);
|
||||
info.enabled = message.getBoolean("enabled", true);
|
||||
info.visible = message.getBoolean("visible", true);
|
||||
info.checkable = message.getBoolean("checkable", false);
|
||||
final String parentUUID = message.getString("parent");
|
||||
if (GECKO_TOOLS_MENU_UUID.equals(parentUUID)) {
|
||||
info.parent = GECKO_TOOLS_MENU_ID;
|
||||
} else if (!TextUtils.isEmpty(parentUUID)) {
|
||||
MenuItemInfo parent = mAddonMenuItemsCache.get(parentUUID);
|
||||
if (parent != null) {
|
||||
info.parent = parent.id;
|
||||
}
|
||||
}
|
||||
addAddonMenuItem(info);
|
||||
break;
|
||||
|
||||
case "Menu:Remove":
|
||||
removeAddonMenuItem(message.getString("uuid"));
|
||||
break;
|
||||
|
||||
case "Menu:Update":
|
||||
updateAddonMenuItem(message.getString("uuid"),
|
||||
message.getBundle("options"));
|
||||
break;
|
||||
|
||||
case "PageActions:Add":
|
||||
if (mPageActionDelegate != null) {
|
||||
mPageActionDelegate.addPageAction(message);
|
||||
} else {
|
||||
mPendingPageActionQueue.put(message.getString("id"), message);
|
||||
}
|
||||
break;
|
||||
|
||||
case "PageActions:Remove":
|
||||
if (mPageActionDelegate != null) {
|
||||
mPageActionDelegate.removePageAction(message);
|
||||
} else {
|
||||
final String id = message.getString("id");
|
||||
mPendingPageActionQueue.remove(id);
|
||||
if (mResolvedPageActionCache != null) {
|
||||
PageAction.removeFromList(mResolvedPageActionCache, id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a list of {@link PageAction PageActions} has previously been provided in
|
||||
* {@link AddonUICache#removePageActionLayoutDelegate}, it will be transferred back to the
|
||||
* {@link PageActionLayoutDelegate}.
|
||||
* In addition, any <code>GeckoBundles</code> containing <code>PageAction</code> messages that
|
||||
* arrived while no delegate was available will now be transmitted.
|
||||
* <p>
|
||||
* Following this, any <code>PageAction</code> messages that arrive will be forwarded
|
||||
* immediately to the provided delegate.
|
||||
*/
|
||||
public void setPageActionLayoutDelegate(final @NonNull PageActionLayoutDelegate newDelegate) {
|
||||
newDelegate.setCachedPageActions(mResolvedPageActionCache);
|
||||
mResolvedPageActionCache = null;
|
||||
|
||||
for (GeckoBundle pageActionMessage : mPendingPageActionQueue.values()) {
|
||||
newDelegate.addPageAction(pageActionMessage);
|
||||
}
|
||||
mPendingPageActionQueue.clear();
|
||||
|
||||
mPageActionDelegate = newDelegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current PageActionDelegate and optionally takes a list of PageActions
|
||||
* that will be stored, e.g. if the class that provided the delegate is going away.
|
||||
*
|
||||
* In addition, all PageAction EventDispatcher messages that arrive while no delegate is
|
||||
* available will be stored for later retrieval.
|
||||
*/
|
||||
public void removePageActionLayoutDelegate(final @Nullable List<PageAction> pageActionsToCache) {
|
||||
mPageActionDelegate = null;
|
||||
mResolvedPageActionCache = pageActionsToCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts handling add-on menu items for the given {@link Menu} and also adds any
|
||||
* menu items that have already been cached.
|
||||
*/
|
||||
public void onCreateOptionsMenu(Menu menu) {
|
||||
mMenu = menu;
|
||||
|
||||
// Add add-on menu items, if any exist.
|
||||
if (mMenu != null) {
|
||||
for (MenuItemInfo item : mAddonMenuItemsCache.values()) {
|
||||
addAddonMenuItemToMenu(mMenu, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the reference to the Menu passed in {@link AddonUICache#onCreateOptionsMenu}.
|
||||
* <p>
|
||||
* Note: Any {@link MenuItem MenuItem(s)} previously added by this class are <i>not</i> removed.
|
||||
*/
|
||||
public void onDestroyOptionsMenu() {
|
||||
mMenu = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an addon menu item/webextension browser action to the menu.
|
||||
*/
|
||||
private void addAddonMenuItem(final MenuItemInfo info) {
|
||||
mAddonMenuItemsCache.put(info.uuid, info);
|
||||
|
||||
if (mMenu == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
addAddonMenuItemToMenu(mMenu, info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an addon menu item/webextension browser action from the menu by its UUID.
|
||||
*/
|
||||
private void removeAddonMenuItem(String uuid) {
|
||||
// Remove add-on menu item from cache, if available.
|
||||
final MenuItemInfo item = mAddonMenuItemsCache.remove(uuid);
|
||||
|
||||
if (mMenu == null || item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final MenuItem menuItem = mMenu.findItem(item.id);
|
||||
if (menuItem != null) {
|
||||
mMenu.removeItem(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the addon menu/webextension browser action with the specified UUID.
|
||||
*/
|
||||
private void updateAddonMenuItem(String uuid, final GeckoBundle options) {
|
||||
// Set attribute for the menu item in cache, if available
|
||||
final MenuItemInfo item = mAddonMenuItemsCache.get(uuid);
|
||||
if (item != null) {
|
||||
item.label = options.getString("name", item.label);
|
||||
item.checkable = options.getBoolean("checkable", item.checkable);
|
||||
item.checked = options.getBoolean("checked", item.checked);
|
||||
item.enabled = options.getBoolean("enabled", item.enabled);
|
||||
item.visible = options.getBoolean("visible", item.visible);
|
||||
}
|
||||
|
||||
if (mMenu == null || item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final MenuItem menuItem = mMenu.findItem(item.id);
|
||||
if (menuItem != null) {
|
||||
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
|
||||
menuItem.setCheckable(options.getBoolean("checkable", menuItem.isCheckable()));
|
||||
menuItem.setChecked(options.getBoolean("checked", menuItem.isChecked()));
|
||||
menuItem.setEnabled(options.getBoolean("enabled", menuItem.isEnabled()));
|
||||
menuItem.setVisible(options.getBoolean("visible", menuItem.isVisible()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the provided item to the provided menu, which should be
|
||||
* the root (mMenu).
|
||||
*/
|
||||
private static void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
|
||||
final Menu destination;
|
||||
if (info.parent == 0) {
|
||||
destination = menu;
|
||||
} else if (info.parent == GECKO_TOOLS_MENU_ID) {
|
||||
// The tools menu only exists in our -v11 resources.
|
||||
final MenuItem tools = menu.findItem(R.id.tools);
|
||||
destination = tools != null ? tools.getSubMenu() : menu;
|
||||
} else {
|
||||
final MenuItem parent = menu.findItem(info.parent);
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Menu parentMenu = findParentMenu(menu, parent);
|
||||
|
||||
if (!parent.hasSubMenu()) {
|
||||
parentMenu.removeItem(parent.getItemId());
|
||||
destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
|
||||
if (parent.getIcon() != null) {
|
||||
((SubMenu) destination).getItem().setIcon(parent.getIcon());
|
||||
}
|
||||
} else {
|
||||
destination = parent.getSubMenu();
|
||||
}
|
||||
}
|
||||
|
||||
final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
|
||||
|
||||
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
final GeckoBundle data = new GeckoBundle(1);
|
||||
data.putString("item", info.uuid);
|
||||
EventDispatcher.getInstance().dispatch("Menu:Clicked", data);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
item.setCheckable(info.checkable);
|
||||
item.setChecked(info.checked);
|
||||
item.setEnabled(info.enabled);
|
||||
item.setVisible(info.visible);
|
||||
}
|
||||
|
||||
private static Menu findParentMenu(Menu menu, MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
|
||||
final int count = (menu != null) ? menu.size() : 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
MenuItem menuItem = menu.getItem(i);
|
||||
if (menuItem.getItemId() == itemId) {
|
||||
return menu;
|
||||
}
|
||||
if (menuItem.hasSubMenu()) {
|
||||
Menu parent = findParentMenu(menuItem.getSubMenu(), item);
|
||||
if (parent != null) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -34,8 +34,6 @@ import android.nfc.NfcEvent;
|
|||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.StrictMode;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
|
@ -56,7 +54,6 @@ import android.view.Menu;
|
|||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -161,7 +158,6 @@ import org.mozilla.gecko.util.GeckoBundle;
|
|||
import org.mozilla.gecko.util.HardwareUtils;
|
||||
import org.mozilla.gecko.util.IntentUtils;
|
||||
import org.mozilla.gecko.util.MenuUtils;
|
||||
import org.mozilla.gecko.util.NetworkUtils;
|
||||
import org.mozilla.gecko.util.PrefUtils;
|
||||
import org.mozilla.gecko.util.ShortcutUtils;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
|
@ -179,7 +175,6 @@ import java.io.FileNotFoundException;
|
|||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
|
|
@ -216,8 +211,6 @@ public class BrowserApp extends GeckoApp
|
|||
private static final String SWITCHBOARD_SERVER = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/experiments/records";
|
||||
|
||||
private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
|
||||
private static final String STATE_ADDON_MENU_ITEM_CACHE = "menuitems_cache";
|
||||
private static final String STATE_BROWSER_ACTION_ITEM_CACHE = "browseractions_cache";
|
||||
|
||||
private static final String BROWSER_SEARCH_TAG = "browser_search";
|
||||
|
||||
|
|
@ -261,9 +254,6 @@ public class BrowserApp extends GeckoApp
|
|||
private ActionModeCompat mActionMode;
|
||||
private TabHistoryController tabHistoryController;
|
||||
|
||||
private static final int GECKO_TOOLS_MENU = -1;
|
||||
private static final int ADDON_MENU_OFFSET = 1000;
|
||||
private static final int BROWSER_ACTION_MENU_OFFSET = 10000;
|
||||
public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment";
|
||||
|
||||
// When the static action bar is shown, only the real toolbar chrome should be
|
||||
|
|
@ -276,98 +266,12 @@ public class BrowserApp extends GeckoApp
|
|||
private Intent startingIntentAfterPip;
|
||||
private boolean isInAutomation;
|
||||
|
||||
private static class MenuItemInfo implements Parcelable {
|
||||
public int id;
|
||||
public String label;
|
||||
public boolean checkable;
|
||||
public boolean checked;
|
||||
public boolean enabled = true;
|
||||
public boolean visible = true;
|
||||
public int parent;
|
||||
public boolean added; // So we can re-add after a locale change.
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(id);
|
||||
dest.writeString(label);
|
||||
dest.writeInt(checkable ? 1 : 0);
|
||||
dest.writeInt(checked ? 1 : 0);
|
||||
dest.writeInt(enabled ? 1 : 0);
|
||||
dest.writeInt(visible ? 1 : 0);
|
||||
dest.writeInt(parent);
|
||||
dest.writeInt(added ? 1 : 0);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MenuItemInfo> CREATOR
|
||||
= new Parcelable.Creator<MenuItemInfo>() {
|
||||
@Override
|
||||
public MenuItemInfo createFromParcel(Parcel source) {
|
||||
return new MenuItemInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MenuItemInfo[] newArray(int size) {
|
||||
return new MenuItemInfo[size];
|
||||
}
|
||||
};
|
||||
|
||||
private MenuItemInfo(Parcel source) {
|
||||
id = source.readInt();
|
||||
label = source.readString();
|
||||
checkable = source.readInt() != 0;
|
||||
checked = source.readInt() != 0;
|
||||
enabled = source.readInt() != 0;
|
||||
visible = source.readInt() != 0;
|
||||
parent = source.readInt();
|
||||
added = source.readInt() != 0;
|
||||
}
|
||||
|
||||
public MenuItemInfo() { }
|
||||
}
|
||||
|
||||
private static class BrowserActionItemInfo extends MenuItemInfo {
|
||||
public String uuid;
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeString(uuid);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<BrowserActionItemInfo> CREATOR
|
||||
= new Parcelable.Creator<BrowserActionItemInfo>() {
|
||||
@Override
|
||||
public BrowserActionItemInfo createFromParcel(Parcel source) {
|
||||
return new BrowserActionItemInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserActionItemInfo[] newArray(int size) {
|
||||
return new BrowserActionItemInfo[size];
|
||||
}
|
||||
};
|
||||
|
||||
private BrowserActionItemInfo(Parcel source) {
|
||||
super(source);
|
||||
uuid = source.readString();
|
||||
}
|
||||
|
||||
public BrowserActionItemInfo() { }
|
||||
}
|
||||
|
||||
// The types of guest mode dialogs we show.
|
||||
public static enum GuestModeDialog {
|
||||
ENTERING,
|
||||
LEAVING
|
||||
}
|
||||
|
||||
private ArrayList<MenuItemInfo> mAddonMenuItemsCache;
|
||||
private ArrayList<BrowserActionItemInfo> mBrowserActionItemsCache;
|
||||
private PropertyAnimator mMainLayoutAnimator;
|
||||
|
||||
private static final Interpolator sTabsInterpolator = new Interpolator() {
|
||||
|
|
@ -774,14 +678,6 @@ public class BrowserApp extends GeckoApp
|
|||
}
|
||||
});
|
||||
|
||||
// If the activity is being restored, the add-ons menu item cache only needs restoring if
|
||||
// Gecko is already running. Otherwise, we'll simply catch the corresponding events when
|
||||
// Gecko and the add-ons are starting up.
|
||||
if (savedInstanceState != null && mIsRestoringActivity) {
|
||||
mAddonMenuItemsCache = savedInstanceState.getParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE);
|
||||
mBrowserActionItemsCache = savedInstanceState.getParcelableArrayList(STATE_BROWSER_ACTION_ITEM_CACHE);
|
||||
}
|
||||
|
||||
app.getLightweightTheme().addListener(this);
|
||||
|
||||
mProgressView = (AnimatedProgressBar) findViewById(R.id.page_progress);
|
||||
|
|
@ -883,12 +779,6 @@ public class BrowserApp extends GeckoApp
|
|||
EventDispatcher.getInstance().registerUiThreadListener(this,
|
||||
"GeckoView:AccessibilityEnabled",
|
||||
"Menu:Open",
|
||||
"Menu:Update",
|
||||
"Menu:Add",
|
||||
"Menu:Remove",
|
||||
"Menu:AddBrowserAction",
|
||||
"Menu:RemoveBrowserAction",
|
||||
"Menu:UpdateBrowserAction",
|
||||
"LightweightTheme:Update",
|
||||
"Tab:Added",
|
||||
"Video:Play",
|
||||
|
|
@ -1580,6 +1470,8 @@ public class BrowserApp extends GeckoApp
|
|||
final GeckoApplication app = (GeckoApplication) getApplication();
|
||||
app.getLightweightTheme().removeListener(this);
|
||||
|
||||
AddonUICache.getInstance().onDestroyOptionsMenu();
|
||||
|
||||
if (mBrowserToolbar != null)
|
||||
mBrowserToolbar.onDestroy();
|
||||
|
||||
|
|
@ -1622,12 +1514,6 @@ public class BrowserApp extends GeckoApp
|
|||
EventDispatcher.getInstance().unregisterUiThreadListener(this,
|
||||
"GeckoView:AccessibilityEnabled",
|
||||
"Menu:Open",
|
||||
"Menu:Update",
|
||||
"Menu:Add",
|
||||
"Menu:Remove",
|
||||
"Menu:AddBrowserAction",
|
||||
"Menu:RemoveBrowserAction",
|
||||
"Menu:UpdateBrowserAction",
|
||||
"LightweightTheme:Update",
|
||||
"Tab:Added",
|
||||
"Video:Play",
|
||||
|
|
@ -1886,53 +1772,6 @@ public class BrowserApp extends GeckoApp
|
|||
openOptionsMenu();
|
||||
break;
|
||||
|
||||
case "Menu:Update":
|
||||
updateAddonMenuItem(message.getInt("id") + ADDON_MENU_OFFSET,
|
||||
message.getBundle("options"));
|
||||
break;
|
||||
|
||||
case "Menu:Add":
|
||||
final MenuItemInfo info = new MenuItemInfo();
|
||||
info.label = message.getString("name");
|
||||
if (info.label == null) {
|
||||
Log.e(LOGTAG, "Invalid menu item name");
|
||||
return;
|
||||
}
|
||||
info.id = message.getInt("id") + ADDON_MENU_OFFSET;
|
||||
info.checked = message.getBoolean("checked", false);
|
||||
info.enabled = message.getBoolean("enabled", true);
|
||||
info.visible = message.getBoolean("visible", true);
|
||||
info.checkable = message.getBoolean("checkable", false);
|
||||
final int parent = message.getInt("parent", 0);
|
||||
info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET;
|
||||
addAddonMenuItem(info);
|
||||
break;
|
||||
|
||||
case "Menu:Remove":
|
||||
removeAddonMenuItem(message.getInt("id") + ADDON_MENU_OFFSET);
|
||||
break;
|
||||
|
||||
case "Menu:AddBrowserAction":
|
||||
final BrowserActionItemInfo browserAction = new BrowserActionItemInfo();
|
||||
browserAction.label = message.getString("name");
|
||||
if (TextUtils.isEmpty(browserAction.label)) {
|
||||
Log.e(LOGTAG, "Invalid browser action name");
|
||||
return;
|
||||
}
|
||||
browserAction.id = message.getInt("id") + BROWSER_ACTION_MENU_OFFSET;
|
||||
browserAction.uuid = message.getString("uuid");
|
||||
addBrowserActionMenuItem(browserAction);
|
||||
break;
|
||||
|
||||
case "Menu:RemoveBrowserAction":
|
||||
removeBrowserActionMenuItem(message.getString("uuid"));
|
||||
break;
|
||||
|
||||
case "Menu:UpdateBrowserAction":
|
||||
updateBrowserActionMenuItem(message.getString("uuid"),
|
||||
message.getBundle("options"));
|
||||
break;
|
||||
|
||||
case "LightweightTheme:Update":
|
||||
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
|
||||
break;
|
||||
|
|
@ -2454,15 +2293,6 @@ public class BrowserApp extends GeckoApp
|
|||
super.onSaveInstanceState(outState);
|
||||
mDynamicToolbar.onSaveInstanceState(outState);
|
||||
outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomeScreenContainer.getPaddingTop());
|
||||
|
||||
// The various add-on UI item caches and event listeners should really live somewhere based
|
||||
// on the Application, so that their lifetime more closely matches that of Gecko itself, as
|
||||
// GeckoView-based activities can start Gecko (and therefore add-ons) while BrowserApp isn't
|
||||
// even running.
|
||||
// For now we'll only guard against the case where BrowserApp is destroyed and later re-
|
||||
// created while Gecko keeps running throughout, and leave the full solution to bug 1414084.
|
||||
outState.putParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE, mAddonMenuItemsCache);
|
||||
outState.putParcelableArrayList(STATE_BROWSER_ACTION_ITEM_CACHE, mBrowserActionItemsCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3213,244 +3043,6 @@ public class BrowserApp extends GeckoApp
|
|||
}
|
||||
}
|
||||
|
||||
private static Menu findParentMenu(Menu menu, MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
|
||||
final int count = (menu != null) ? menu.size() : 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
MenuItem menuItem = menu.getItem(i);
|
||||
if (menuItem.getItemId() == itemId) {
|
||||
return menu;
|
||||
}
|
||||
if (menuItem.hasSubMenu()) {
|
||||
Menu parent = findParentMenu(menuItem.getSubMenu(), item);
|
||||
if (parent != null) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the provided item to the provided menu, which should be
|
||||
* the root (mMenu).
|
||||
*/
|
||||
private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
|
||||
info.added = true;
|
||||
|
||||
final Menu destination;
|
||||
if (info.parent == 0) {
|
||||
destination = menu;
|
||||
} else if (info.parent == GECKO_TOOLS_MENU) {
|
||||
// The tools menu only exists in our -v11 resources.
|
||||
final MenuItem tools = menu.findItem(R.id.tools);
|
||||
destination = tools != null ? tools.getSubMenu() : menu;
|
||||
} else {
|
||||
final MenuItem parent = menu.findItem(info.parent);
|
||||
if (parent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Menu parentMenu = findParentMenu(menu, parent);
|
||||
|
||||
if (!parent.hasSubMenu()) {
|
||||
parentMenu.removeItem(parent.getItemId());
|
||||
destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
|
||||
if (parent.getIcon() != null) {
|
||||
((SubMenu) destination).getItem().setIcon(parent.getIcon());
|
||||
}
|
||||
} else {
|
||||
destination = parent.getSubMenu();
|
||||
}
|
||||
}
|
||||
|
||||
final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
|
||||
|
||||
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
final GeckoBundle data = new GeckoBundle(1);
|
||||
data.putInt("item", info.id - ADDON_MENU_OFFSET);
|
||||
EventDispatcher.getInstance().dispatch("Menu:Clicked", data);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
item.setCheckable(info.checkable);
|
||||
item.setChecked(info.checked);
|
||||
item.setEnabled(info.enabled);
|
||||
item.setVisible(info.visible);
|
||||
}
|
||||
|
||||
private void addAddonMenuItem(final MenuItemInfo info) {
|
||||
if (mAddonMenuItemsCache == null) {
|
||||
mAddonMenuItemsCache = new ArrayList<MenuItemInfo>();
|
||||
}
|
||||
|
||||
// Mark it as added if the menu was ready.
|
||||
info.added = (mMenu != null);
|
||||
|
||||
// Always cache so we can rebuild after a locale switch.
|
||||
mAddonMenuItemsCache.add(info);
|
||||
|
||||
if (mMenu == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
addAddonMenuItemToMenu(mMenu, info);
|
||||
}
|
||||
|
||||
private void removeAddonMenuItem(int id) {
|
||||
// Remove add-on menu item from cache, if available.
|
||||
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
|
||||
for (MenuItemInfo item : mAddonMenuItemsCache) {
|
||||
if (item.id == id) {
|
||||
mAddonMenuItemsCache.remove(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mMenu == null)
|
||||
return;
|
||||
|
||||
final MenuItem menuItem = mMenu.findItem(id);
|
||||
if (menuItem != null)
|
||||
mMenu.removeItem(id);
|
||||
}
|
||||
|
||||
private void updateAddonMenuItem(int id, final GeckoBundle options) {
|
||||
// Set attribute for the menu item in cache, if available
|
||||
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
|
||||
for (MenuItemInfo item : mAddonMenuItemsCache) {
|
||||
if (item.id == id) {
|
||||
item.label = options.getString("name", item.label);
|
||||
item.checkable = options.getBoolean("checkable", item.checkable);
|
||||
item.checked = options.getBoolean("checked", item.checked);
|
||||
item.enabled = options.getBoolean("enabled", item.enabled);
|
||||
item.visible = options.getBoolean("visible", item.visible);
|
||||
item.added = (mMenu != null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mMenu == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final MenuItem menuItem = mMenu.findItem(id);
|
||||
if (menuItem != null) {
|
||||
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
|
||||
menuItem.setCheckable(options.getBoolean("checkable", menuItem.isCheckable()));
|
||||
menuItem.setChecked(options.getBoolean("checked", menuItem.isChecked()));
|
||||
menuItem.setEnabled(options.getBoolean("enabled", menuItem.isEnabled()));
|
||||
menuItem.setVisible(options.getBoolean("visible", menuItem.isVisible()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the provided item to the provided menu, which should be
|
||||
* the root (mMenu).
|
||||
*/
|
||||
private void addBrowserActionMenuItemToMenu(final Menu menu, final BrowserActionItemInfo info) {
|
||||
info.added = true;
|
||||
|
||||
final MenuItem item = menu.add(Menu.NONE, info.id, Menu.NONE, info.label);
|
||||
|
||||
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
final GeckoBundle data = new GeckoBundle(1);
|
||||
data.putString("item", info.uuid);
|
||||
EventDispatcher.getInstance().dispatch("Menu:BrowserActionClicked", data);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
item.setCheckable(info.checkable);
|
||||
item.setChecked(info.checked);
|
||||
item.setEnabled(info.enabled);
|
||||
item.setVisible(info.visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a WebExtension browser action to the menu.
|
||||
*/
|
||||
private void addBrowserActionMenuItem(final BrowserActionItemInfo info) {
|
||||
if (mBrowserActionItemsCache == null) {
|
||||
mBrowserActionItemsCache = new ArrayList<BrowserActionItemInfo>();
|
||||
}
|
||||
|
||||
// Mark it as added if the menu was ready.
|
||||
info.added = (mMenu != null);
|
||||
|
||||
// Always cache so we can rebuild after a locale switch.
|
||||
mBrowserActionItemsCache.add(info);
|
||||
|
||||
if (mMenu == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
addBrowserActionMenuItemToMenu(mMenu, info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a WebExtension browser action from the menu by its UUID.
|
||||
*/
|
||||
private void removeBrowserActionMenuItem(String uuid) {
|
||||
int id = -1;
|
||||
|
||||
// Remove browser action menu item from cache, if available.
|
||||
if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
|
||||
for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
|
||||
if (item.uuid.equals(uuid)) {
|
||||
id = item.id;
|
||||
mBrowserActionItemsCache.remove(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mMenu == null || id == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final MenuItem menuItem = mMenu.findItem(id);
|
||||
if (menuItem != null) {
|
||||
mMenu.removeItem(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the WebExtension browser action with the specified UUID.
|
||||
*/
|
||||
private void updateBrowserActionMenuItem(String uuid, final GeckoBundle options) {
|
||||
int id = -1;
|
||||
|
||||
// Set attribute for the menu item in cache, if available
|
||||
if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
|
||||
for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
|
||||
if (item.uuid.equals(uuid)) {
|
||||
id = item.id;
|
||||
item.label = options.getString("name", item.label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mMenu == null || id == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final MenuItem menuItem = mMenu.findItem(id);
|
||||
if (menuItem != null) {
|
||||
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Sets mMenu = menu.
|
||||
|
|
@ -3465,19 +3057,9 @@ public class BrowserApp extends GeckoApp
|
|||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.browser_app_menu, mMenu);
|
||||
|
||||
// Add browser action menu items, if any exist.
|
||||
if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
|
||||
for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
|
||||
addBrowserActionMenuItemToMenu(mMenu, item);
|
||||
}
|
||||
}
|
||||
|
||||
// Add add-on menu items, if any exist.
|
||||
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
|
||||
for (MenuItemInfo item : mAddonMenuItemsCache) {
|
||||
addAddonMenuItemToMenu(mMenu, item);
|
||||
}
|
||||
}
|
||||
// Let the AddonUICache handle adding (and removing again) any add-on/browser action
|
||||
// menu items as required.
|
||||
AddonUICache.getInstance().onCreateOptionsMenu(mMenu);
|
||||
|
||||
// Action providers are available only ICS+.
|
||||
GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);
|
||||
|
|
|
|||
|
|
@ -334,6 +334,7 @@ public class GeckoApplication extends Application
|
|||
FilePicker.init(context);
|
||||
DownloadsIntegration.init();
|
||||
HomePanelsManager.getInstance().init(context);
|
||||
AddonUICache.getInstance().init();
|
||||
|
||||
GlobalPageMetadata.getInstance().init();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.mozilla.gecko.toolbar;
|
||||
|
||||
import org.mozilla.gecko.AddonUICache;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.R;
|
||||
|
|
@ -14,11 +15,7 @@ import org.mozilla.gecko.preferences.GeckoPreferences;
|
|||
import org.mozilla.gecko.pwa.PwaUtils;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
import org.mozilla.gecko.util.ResourceDrawableUtils;
|
||||
import org.mozilla.gecko.util.BundleEventListener;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
import org.mozilla.gecko.util.EventCallback;
|
||||
import org.mozilla.gecko.util.GeckoBundle;
|
||||
import org.mozilla.gecko.util.ResourceDrawableUtils;
|
||||
import org.mozilla.gecko.util.ShortcutUtils;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
import org.mozilla.gecko.widget.GeckoPopupMenu;
|
||||
|
|
@ -40,6 +37,7 @@ import android.widget.ImageButton;
|
|||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
|
@ -47,17 +45,21 @@ import java.util.ArrayList;
|
|||
|
||||
import static org.mozilla.gecko.toolbar.PageActionLayout.PageAction.UUID_PAGE_ACTION_PWA;
|
||||
|
||||
public class PageActionLayout extends ThemedLinearLayout implements BundleEventListener,
|
||||
View.OnClickListener,
|
||||
View.OnLongClickListener {
|
||||
public class PageActionLayout extends ThemedLinearLayout
|
||||
implements View.OnClickListener, View.OnLongClickListener {
|
||||
private static final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY";
|
||||
private static final int DEFAULT_PAGE_ACTIONS_SHOWN = 2;
|
||||
public static final String PREF_PWA_ONBOARDING = GeckoPreferences.NON_PREF_PREFIX + "pref_pwa_onboarding";
|
||||
|
||||
public interface PageActionLayoutDelegate {
|
||||
void addPageAction(GeckoBundle message);
|
||||
void removePageAction(GeckoBundle message);
|
||||
void setCachedPageActions(List<PageAction> cachedPageActions);
|
||||
}
|
||||
|
||||
private final Context mContext;
|
||||
private final LinearLayout mLayout;
|
||||
private final List<PageAction> mPageActionList;
|
||||
private List<PageAction> mPageActionList;
|
||||
|
||||
private GeckoPopupMenu mPageActionsMenu;
|
||||
|
||||
|
|
@ -69,26 +71,51 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
|
|||
super(context, attrs);
|
||||
mContext = context;
|
||||
mLayout = this;
|
||||
|
||||
mPageActionList = new ArrayList<PageAction>();
|
||||
setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
|
||||
refreshPageActionIcons();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
EventDispatcher.getInstance().registerUiThreadListener(this,
|
||||
"PageActions:Add",
|
||||
"PageActions:Remove");
|
||||
// Calling this will cause the AddonUICache to synchronously call addPageAction for all
|
||||
// PageAction messages it received while no PageActionLayout was available.
|
||||
// Processing those messages entails converting a data: URI into a drawable, which can take
|
||||
// a few ms per message and therefore in theory cause some jank.
|
||||
// In practice however, PageAction messages are commonly only generated when tabs (from the
|
||||
// BrowserApp UI) are actually loading, so the AddonUICache's message queueing mechanism
|
||||
// only becomes relevant if the BrowserApp UI has then been backgrounded *and* subsequently
|
||||
// destroyed by the OS while some tabs are still loading. Merely starting up Gecko through a
|
||||
// GeckoView-based activity on the other hand is not enough to queue up PageAction messages.
|
||||
// Therefore, this case should happen rarely enough that we can tolerate it without having
|
||||
// to think about moving the image processing into some asynchronous code path.
|
||||
AddonUICache.getInstance().setPageActionLayoutDelegate(new PageActionLayoutDelegate() {
|
||||
@Override
|
||||
public void addPageAction(final GeckoBundle message) {
|
||||
onAddPageAction(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePageAction(final GeckoBundle message) {
|
||||
onRemovePageAction(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCachedPageActions(final List<PageAction> cachedPageActions) {
|
||||
if (cachedPageActions != null) {
|
||||
mPageActionList = cachedPageActions;
|
||||
} else {
|
||||
mPageActionList = new ArrayList<>();
|
||||
}
|
||||
|
||||
setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
|
||||
refreshPageActionIcons();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
EventDispatcher.getInstance().unregisterUiThreadListener(this,
|
||||
"PageActions:Add",
|
||||
"PageActions:Remove");
|
||||
AddonUICache.getInstance().removePageActionLayoutDelegate(mPageActionList);
|
||||
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
|
@ -99,7 +126,7 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
|
|||
for (int i = 0; i < getChildCount(); i++) {
|
||||
View child = getChildAt(i);
|
||||
if (child instanceof ThemedImageButton) {
|
||||
((ThemedImageButton) child).setPrivateMode(true);
|
||||
((ThemedImageButton) child).setPrivateMode(isPrivate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,52 +143,51 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
|
|||
}
|
||||
}
|
||||
|
||||
@Override // BundleEventListener
|
||||
public void handleMessage(final String event, final GeckoBundle message,
|
||||
final EventCallback callback) {
|
||||
private void onAddPageAction(final GeckoBundle message) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
hidePreviousConfirmPrompt();
|
||||
|
||||
if ("PageActions:Add".equals(event)) {
|
||||
final String id = message.getString("id");
|
||||
final String id = message.getString("id");
|
||||
|
||||
boolean alreadyAdded = isPwaAdded(id);
|
||||
if (alreadyAdded) {
|
||||
return;
|
||||
if (isPageActionAlreadyAdded(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
maybeShowPwaOnboarding(id);
|
||||
|
||||
final String title = message.getString("title");
|
||||
final String imageURL = message.getString("icon");
|
||||
final boolean important = message.getBoolean("important");
|
||||
final boolean useTint = message.getBoolean("useTint");
|
||||
|
||||
addPageAction(id, title, imageURL, useTint, new OnPageActionClickListeners() {
|
||||
@Override
|
||||
public void onClick(final String id) {
|
||||
if (UUID_PAGE_ACTION_PWA.equals(id)) {
|
||||
mPwaConfirm = PwaConfirm.show(getContext());
|
||||
return;
|
||||
}
|
||||
final GeckoBundle data = new GeckoBundle(1);
|
||||
data.putString("id", id);
|
||||
EventDispatcher.getInstance().dispatch("PageActions:Clicked", data);
|
||||
}
|
||||
|
||||
maybeShowPwaOnboarding(id);
|
||||
@Override
|
||||
public boolean onLongClick(String id) {
|
||||
final GeckoBundle data = new GeckoBundle(1);
|
||||
data.putString("id", id);
|
||||
EventDispatcher.getInstance().dispatch("PageActions:LongClicked", data);
|
||||
return true;
|
||||
}
|
||||
}, important);
|
||||
}
|
||||
|
||||
final String title = message.getString("title");
|
||||
final String imageURL = message.getString("icon");
|
||||
final boolean important = message.getBoolean("important");
|
||||
final boolean useTint = message.getBoolean("useTint");
|
||||
private void onRemovePageAction(final GeckoBundle message) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
addPageAction(id, title, imageURL, useTint, new OnPageActionClickListeners() {
|
||||
@Override
|
||||
public void onClick(final String id) {
|
||||
if (UUID_PAGE_ACTION_PWA.equals(id)) {
|
||||
mPwaConfirm = PwaConfirm.show(getContext());
|
||||
return;
|
||||
}
|
||||
final GeckoBundle data = new GeckoBundle(1);
|
||||
data.putString("id", id);
|
||||
EventDispatcher.getInstance().dispatch("PageActions:Clicked", data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(String id) {
|
||||
final GeckoBundle data = new GeckoBundle(1);
|
||||
data.putString("id", id);
|
||||
EventDispatcher.getInstance().dispatch("PageActions:LongClicked", data);
|
||||
return true;
|
||||
}
|
||||
}, important);
|
||||
|
||||
} else if ("PageActions:Remove".equals(event)) {
|
||||
removePageAction(message.getString("id"));
|
||||
}
|
||||
hidePreviousConfirmPrompt();
|
||||
removePageAction(message.getString("id"));
|
||||
}
|
||||
|
||||
private void maybeShowPwaOnboarding(String id) {
|
||||
|
|
@ -180,7 +206,7 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isPwaAdded(String id) {
|
||||
private boolean isPageActionAlreadyAdded(String id) {
|
||||
for (PageAction pageAction : mPageActionList) {
|
||||
if (pageAction.getID() != null && pageAction.getID().equals(id)) {
|
||||
return true;
|
||||
|
|
@ -223,14 +249,8 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
|
|||
private void removePageAction(String id) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
final Iterator<PageAction> iter = mPageActionList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
final PageAction pageAction = iter.next();
|
||||
if (pageAction.getID().equals(id)) {
|
||||
iter.remove();
|
||||
refreshPageActionIcons();
|
||||
return;
|
||||
}
|
||||
if (PageAction.removeFromList(mPageActionList, id)) {
|
||||
refreshPageActionIcons();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -453,6 +473,19 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return True if any PageAction was actually removed, false otherwise.
|
||||
*/
|
||||
public static boolean removeFromList(Collection<PageAction> pageActionCollection, String id) {
|
||||
final Iterator<PageAction> iter = pageActionCollection.iterator();
|
||||
while (iter.hasNext()) {
|
||||
final PageAction action = iter.next();
|
||||
if (action.getID().equals(id)) {
|
||||
iter.remove();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2336,8 +2336,8 @@ var NativeWindow = {
|
|||
|
||||
menu: {
|
||||
_callbacks: [],
|
||||
_menuId: 1,
|
||||
toolsMenuID: -1,
|
||||
// This value must be kept in sync with GECKO_TOOLS_MENU_UUID in AddonUICache.java.
|
||||
toolsMenuID: "{115b9308-2023-44f1-a4e9-3e2197669f07}",
|
||||
add: function() {
|
||||
let options;
|
||||
if (arguments.length == 1) {
|
||||
|
|
@ -2353,25 +2353,24 @@ var NativeWindow = {
|
|||
}
|
||||
|
||||
options.type = "Menu:Add";
|
||||
options.id = this._menuId;
|
||||
options.uuid = uuidgen.generateUUID().toString();
|
||||
|
||||
GlobalEventDispatcher.sendRequest(options);
|
||||
this._callbacks[this._menuId] = options.callback;
|
||||
this._menuId++;
|
||||
return this._menuId - 1;
|
||||
this._callbacks[options.uuid] = options.callback;
|
||||
return options.uuid;
|
||||
},
|
||||
|
||||
remove: function(aId) {
|
||||
GlobalEventDispatcher.sendRequest({ type: "Menu:Remove", id: aId });
|
||||
remove: function(aUuid) {
|
||||
GlobalEventDispatcher.sendRequest({ type: "Menu:Remove", uuid: aUuid });
|
||||
},
|
||||
|
||||
update: function(aId, aOptions) {
|
||||
update: function(aUuid, aOptions) {
|
||||
if (!aOptions)
|
||||
return;
|
||||
|
||||
GlobalEventDispatcher.sendRequest({
|
||||
type: "Menu:Update",
|
||||
id: aId,
|
||||
uuid: aUuid,
|
||||
options: aOptions
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import shutil
|
|||
import subprocess
|
||||
import zipfile
|
||||
|
||||
from zipfile import ZipFile
|
||||
|
||||
import mozpack.path as mozpath
|
||||
|
||||
from mozfile import TemporaryDirectory
|
||||
|
|
@ -432,7 +434,13 @@ class MachCommands(MachCommandBase):
|
|||
self.substs['GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS'] + ["--continue"] + args,
|
||||
verbose=True)
|
||||
|
||||
return ret
|
||||
if ret != 0:
|
||||
return ret
|
||||
|
||||
# The zip archive is passed along in CI to ship geckoview onto a maven repo
|
||||
_craft_maven_zip_archive(self.topobjdir)
|
||||
|
||||
return 0
|
||||
|
||||
@SubCommand('android', 'geckoview-docs',
|
||||
"""Create GeckoView javadoc and optionally upload to Github""")
|
||||
|
|
@ -580,6 +588,26 @@ class MachCommands(MachCommandBase):
|
|||
pass
|
||||
|
||||
|
||||
def _get_maven_archive_abs_and_relative_paths(maven_folder):
|
||||
for subdir, _, files in os.walk(maven_folder):
|
||||
for file in files:
|
||||
full_path = os.path.join(subdir, file)
|
||||
relative_path = os.path.relpath(full_path, maven_folder)
|
||||
|
||||
# maven-metadata is intended to be generated on the real maven server
|
||||
if 'maven-metadata.xml' not in relative_path:
|
||||
yield full_path, relative_path
|
||||
|
||||
|
||||
def _craft_maven_zip_archive(topobjdir):
|
||||
geckoview_folder = os.path.join(topobjdir, 'gradle/build/mobile/android/geckoview')
|
||||
maven_folder = os.path.join(geckoview_folder, 'maven')
|
||||
|
||||
with ZipFile(os.path.join(geckoview_folder, 'target.maven.zip'), 'w') as target_zip:
|
||||
for abs, rel in _get_maven_archive_abs_and_relative_paths(maven_folder):
|
||||
target_zip.write(abs, arcname=rel)
|
||||
|
||||
|
||||
@CommandProvider
|
||||
class AndroidEmulatorCommands(MachCommandBase):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ var BrowserActions = {
|
|||
|
||||
_initialized: false,
|
||||
|
||||
_nextMenuId: 0,
|
||||
|
||||
/**
|
||||
* Registers the listeners only if they have not been initialized
|
||||
* already and there is at least one browser action.
|
||||
|
|
@ -23,7 +21,7 @@ var BrowserActions = {
|
|||
_maybeRegisterListeners() {
|
||||
if (!this._initialized && Object.keys(this._browserActions).length) {
|
||||
this._initialized = true;
|
||||
EventDispatcher.instance.registerListener(this, "Menu:BrowserActionClicked");
|
||||
EventDispatcher.instance.registerListener(this, "Menu:Clicked");
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -34,26 +32,27 @@ var BrowserActions = {
|
|||
_maybeUnregisterListeners() {
|
||||
if (this._initialized && !Object.keys(this._browserActions).length) {
|
||||
this._initialized = false;
|
||||
EventDispatcher.instance.unregisterListener(this, "Menu:BrowserActionClicked");
|
||||
EventDispatcher.instance.unregisterListener(this, "Menu:Clicked");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a browser action is clicked on.
|
||||
* @param {string} event The name of the event, which should always
|
||||
* be "Menu:BrowserActionClicked".
|
||||
* be "Menu:Clicked".
|
||||
* @param {Object} data An object containing information about the
|
||||
* browser action, which in this case should contain an `item`
|
||||
* property which is browser action's ID.
|
||||
* property which is browser action's UUID.
|
||||
*/
|
||||
onEvent(event, data) {
|
||||
if (event !== "Menu:BrowserActionClicked") {
|
||||
throw new Error(`Expected "Menu:BrowserActionClicked" event - received "${event}" instead`);
|
||||
if (event !== "Menu:Clicked") {
|
||||
throw new Error(`Expected "Menu:Clicked" event - received "${event}" instead`);
|
||||
}
|
||||
|
||||
let browserAction = this._browserActions[data.item];
|
||||
if (!browserAction) {
|
||||
throw new Error(`No browser action found with id ${data.item}`);
|
||||
// This was probably meant for the NativeWindow menu handler.
|
||||
return;
|
||||
}
|
||||
browserAction.onClicked();
|
||||
},
|
||||
|
|
@ -64,8 +63,7 @@ var BrowserActions = {
|
|||
*/
|
||||
register(browserAction) {
|
||||
EventDispatcher.instance.sendRequest({
|
||||
type: "Menu:AddBrowserAction",
|
||||
id: this._nextMenuId++,
|
||||
type: "Menu:Add",
|
||||
uuid: browserAction.uuid,
|
||||
name: browserAction.defaults.name,
|
||||
});
|
||||
|
|
@ -84,7 +82,7 @@ var BrowserActions = {
|
|||
update(uuid, options) {
|
||||
if (options.name) {
|
||||
EventDispatcher.instance.sendRequest({
|
||||
type: "Menu:UpdateBrowserAction",
|
||||
type: "Menu:Update",
|
||||
uuid,
|
||||
options,
|
||||
});
|
||||
|
|
@ -125,7 +123,7 @@ var BrowserActions = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Unregisters the browser action with the specified ID.
|
||||
* Unregisters the browser action with the specified UUID.
|
||||
* @param {string} uuid The UUID of the browser action.
|
||||
*/
|
||||
unregister(uuid) {
|
||||
|
|
@ -134,7 +132,7 @@ var BrowserActions = {
|
|||
throw new Error(`No BrowserAction with UUID ${uuid} was found`);
|
||||
}
|
||||
EventDispatcher.instance.sendRequest({
|
||||
type: "Menu:RemoveBrowserAction",
|
||||
type: "Menu:Remove",
|
||||
uuid,
|
||||
});
|
||||
delete this._browserActions[uuid];
|
||||
|
|
|
|||
|
|
@ -99,8 +99,8 @@ Source code can be obtained by running
|
|||
|
||||
hg clone https://hg.mozilla.org/mozilla-unified
|
||||
|
||||
Or, if you prefer Git, you should install git-cinnabar, and follow the
|
||||
instruction here to clone from the Mercurial repository:
|
||||
Or, if you prefer Git, by following the instruction here to clone from the
|
||||
Mercurial repository:
|
||||
|
||||
https://github.com/glandium/git-cinnabar/wiki/Mozilla:-A-git-workflow-for-Gecko-development
|
||||
|
||||
|
|
@ -121,17 +121,26 @@ optimally configured?
|
|||
|
||||
Please enter your reply: '''
|
||||
|
||||
CLONE_MERCURIAL = '''
|
||||
If you would like to clone the mozilla-unified Mercurial repository, please
|
||||
enter the destination path below.
|
||||
CONFIGURE_GIT = '''
|
||||
Mozilla recommends using git-cinnabar to work with mozilla-central.
|
||||
|
||||
(If you prefer to use Git, leave this blank.)
|
||||
Would you like to run a few configuration steps to ensure Git is
|
||||
optimally configured?
|
||||
|
||||
1. Yes
|
||||
2. No
|
||||
|
||||
Please enter your reply: '''
|
||||
|
||||
CLONE_VCS = '''
|
||||
If you would like to clone the {} {} repository, please
|
||||
enter the destination path below.
|
||||
'''
|
||||
|
||||
CLONE_MERCURIAL_PROMPT = '''
|
||||
Destination directory for Mercurial clone (leave empty to not clone): '''.lstrip()
|
||||
CLONE_VCS_PROMPT = '''
|
||||
Destination directory for {} clone (leave empty to not clone): '''.lstrip()
|
||||
|
||||
CLONE_MERCURIAL_NOT_EMPTY = '''
|
||||
CLONE_VCS_NOT_EMPTY = '''
|
||||
ERROR! Destination directory '{}' is not empty.
|
||||
|
||||
Would you like to clone to '{}'?
|
||||
|
|
@ -142,11 +151,11 @@ Would you like to clone to '{}'?
|
|||
|
||||
Please enter your reply: '''.lstrip()
|
||||
|
||||
CLONE_MERCURIAL_NOT_EMPTY_FALLBACK_FAILED = '''
|
||||
CLONE_VCS_NOT_EMPTY_FALLBACK_FAILED = '''
|
||||
ERROR! Destination directory '{}' is not empty.
|
||||
'''
|
||||
|
||||
CLONE_MERCURIAL_NOT_DIR = '''
|
||||
CLONE_VCS_NOT_DIR = '''
|
||||
ERROR! Destination '{}' exists but is not a directory.
|
||||
'''
|
||||
|
||||
|
|
@ -166,6 +175,16 @@ DEBIAN_DISTROS = (
|
|||
'"elementary"'
|
||||
)
|
||||
|
||||
ADD_GIT_TOOLS_PATH = '''
|
||||
To add git-cinnabar to the PATH, edit your shell initialization script, which
|
||||
may be called ~/.bashrc or ~/.bash_profile or ~/.profile, and add the following
|
||||
lines:
|
||||
|
||||
export PATH="{}:$PATH"
|
||||
|
||||
Then restart your shell.
|
||||
'''
|
||||
|
||||
|
||||
class Bootstrapper(object):
|
||||
"""Main class that performs system bootstrap."""
|
||||
|
|
@ -231,11 +250,16 @@ class Bootstrapper(object):
|
|||
|
||||
self.instance = cls(**args)
|
||||
|
||||
def input_clone_dest(self):
|
||||
print(CLONE_MERCURIAL)
|
||||
def input_clone_dest(self, with_hg=True):
|
||||
repo_name = 'mozilla-unified'
|
||||
vcs = 'Mercurial'
|
||||
if not with_hg:
|
||||
repo_name = 'gecko'
|
||||
vcs = 'Git'
|
||||
print(CLONE_VCS.format(repo_name, vcs))
|
||||
|
||||
while True:
|
||||
dest = raw_input(CLONE_MERCURIAL_PROMPT)
|
||||
dest = raw_input(CLONE_VCS_PROMPT.format(vcs))
|
||||
dest = dest.strip()
|
||||
if not dest:
|
||||
return ''
|
||||
|
|
@ -245,18 +269,18 @@ class Bootstrapper(object):
|
|||
return dest
|
||||
|
||||
if not os.path.isdir(dest):
|
||||
print(CLONE_MERCURIAL_NOT_DIR.format(dest))
|
||||
print(CLONE_VCS_NOT_DIR.format(dest))
|
||||
continue
|
||||
|
||||
if os.listdir(dest) == []:
|
||||
return dest
|
||||
|
||||
newdest = os.path.join(dest, 'mozilla-unified')
|
||||
newdest = os.path.join(dest, repo_name)
|
||||
if os.path.exists(newdest):
|
||||
print(CLONE_MERCURIAL_NOT_EMPTY_FALLBACK_FAILED.format(dest))
|
||||
print(CLONE_VCS_NOT_EMPTY_FALLBACK_FAILED.format(dest))
|
||||
continue
|
||||
|
||||
choice = self.instance.prompt_int(prompt=CLONE_MERCURIAL_NOT_EMPTY.format(dest,
|
||||
choice = self.instance.prompt_int(prompt=CLONE_VCS_NOT_EMPTY.format(dest,
|
||||
newdest), low=1, high=3)
|
||||
if choice == 1:
|
||||
return newdest
|
||||
|
|
@ -360,7 +384,6 @@ class Bootstrapper(object):
|
|||
(checkout_type, checkout_root) = r
|
||||
|
||||
# Possibly configure Mercurial, but not if the current checkout is Git.
|
||||
# TODO offer to configure Git.
|
||||
if hg_installed and state_dir_available and checkout_type != 'git':
|
||||
configure_hg = False
|
||||
if not self.instance.no_interactive:
|
||||
|
|
@ -374,6 +397,21 @@ class Bootstrapper(object):
|
|||
if configure_hg:
|
||||
configure_mercurial(self.instance.which('hg'), state_dir)
|
||||
|
||||
# Offer to configure Git, if the current checkout is Git.
|
||||
elif self.instance.which('git') and checkout_type == 'git':
|
||||
should_configure_git = False
|
||||
if not self.instance.no_interactive:
|
||||
choice = self.instance.prompt_int(prompt=CONFIGURE_GIT,
|
||||
low=1, high=2)
|
||||
if choice == 1:
|
||||
should_configure_git = True
|
||||
else:
|
||||
# Assuming default configuration setting applies to all VCS.
|
||||
should_configure_git = self.hg_configure
|
||||
|
||||
if should_configure_git:
|
||||
configure_git(self.instance.which('git'), state_dir)
|
||||
|
||||
# Offer to clone if we're not inside a clone.
|
||||
have_clone = False
|
||||
|
||||
|
|
@ -382,7 +420,14 @@ class Bootstrapper(object):
|
|||
elif hg_installed and not self.instance.no_interactive:
|
||||
dest = self.input_clone_dest()
|
||||
if dest:
|
||||
have_clone = clone_firefox(self.instance.which('hg'), dest)
|
||||
have_clone = hg_clone_firefox(self.instance.which('hg'), dest)
|
||||
checkout_root = dest
|
||||
elif self.instance.which('git') and checkout_type == 'git':
|
||||
dest = self.input_clone_dest(False)
|
||||
if dest:
|
||||
git = self.instance.which('git')
|
||||
watchman = self.instance.which('watchman')
|
||||
have_clone = git_clone_firefox(git, dest, watchman)
|
||||
checkout_root = dest
|
||||
|
||||
if not have_clone:
|
||||
|
|
@ -470,7 +515,7 @@ def update_mercurial_repo(hg, url, dest, revision):
|
|||
print('=' * 80)
|
||||
|
||||
|
||||
def clone_firefox(hg, dest):
|
||||
def hg_clone_firefox(hg, dest):
|
||||
"""Clone the Firefox repository to a specified destination."""
|
||||
print('Cloning Firefox Mercurial repository to %s' % dest)
|
||||
|
||||
|
|
@ -562,3 +607,90 @@ def current_firefox_checkout(check_output, env, hg=None):
|
|||
break
|
||||
|
||||
return (None, None)
|
||||
|
||||
|
||||
def update_git_tools(git, root_state_dir):
|
||||
"""Ensure git-cinnabar is up to date."""
|
||||
cinnabar_dir = os.path.join(root_state_dir, 'git-cinnabar')
|
||||
|
||||
# Ensure the latest revision of git-cinnabar is present.
|
||||
update_git_repo(git, 'https://github.com/glandium/git-cinnabar.git',
|
||||
cinnabar_dir)
|
||||
|
||||
# Perform a download of cinnabar.
|
||||
download_args = [git, 'cinnabar', 'download']
|
||||
|
||||
try:
|
||||
subprocess.check_call(download_args, cwd=cinnabar_dir)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e)
|
||||
return cinnabar_dir
|
||||
|
||||
|
||||
def update_git_repo(git, url, dest):
|
||||
"""Perform a clone/pull + update of a Git repository."""
|
||||
pull_args = [git]
|
||||
|
||||
if os.path.exists(dest):
|
||||
pull_args.extend(['pull'])
|
||||
cwd = dest
|
||||
else:
|
||||
pull_args.extend(['clone', '--no-checkout', url, dest])
|
||||
cwd = '/'
|
||||
|
||||
update_args = [git, 'checkout']
|
||||
|
||||
print('=' * 80)
|
||||
print('Ensuring %s is up to date at %s' % (url, dest))
|
||||
|
||||
try:
|
||||
subprocess.check_call(pull_args, cwd=cwd)
|
||||
subprocess.check_call(update_args, cwd=dest)
|
||||
finally:
|
||||
print('=' * 80)
|
||||
|
||||
|
||||
def configure_git(git, root_state_dir):
|
||||
"""Run the Git configuration steps."""
|
||||
cinnabar_dir = update_git_tools(git, root_state_dir)
|
||||
|
||||
print(ADD_GIT_TOOLS_PATH.format(cinnabar_dir))
|
||||
|
||||
|
||||
def git_clone_firefox(git, dest, watchman=None):
|
||||
"""Clone the Firefox repository to a specified destination."""
|
||||
print('Cloning Firefox repository to %s' % dest)
|
||||
|
||||
try:
|
||||
# Configure git per the git-cinnabar requirements.
|
||||
subprocess.check_call([git, 'clone', '-b', 'bookmarks/central',
|
||||
'hg::https://hg.mozilla.org/mozilla-unified', dest])
|
||||
subprocess.check_call([git, 'remote', 'add', 'inbound',
|
||||
'hg::ssh://hg.mozilla.org/integration/mozilla-inbound'],
|
||||
cwd=dest)
|
||||
subprocess.check_call([git, 'config', 'remote.inbound.skipDefaultUpdate',
|
||||
'true'], cwd=dest)
|
||||
subprocess.check_call([git, 'config', 'remote.inbound.push',
|
||||
'+HEAD:refs/heads/branches/default/tip'], cwd=dest)
|
||||
subprocess.check_call([git, 'config', 'fetch.prune', 'true'], cwd=dest)
|
||||
subprocess.check_call([git, 'config', 'pull.ff', 'only'], cwd=dest)
|
||||
|
||||
watchman_sample = os.path.join(dest, '.git/hooks/fsmonitor-watchman.sample')
|
||||
# Older versions of git didn't include fsmonitor-watchman.sample.
|
||||
if watchman and watchman_sample:
|
||||
print('Configuring watchman')
|
||||
watchman_config = os.path.join(dest, '.git/hooks/query-watchman')
|
||||
if not os.path.exists(watchman_config):
|
||||
print('Copying %s to %s' % (watchman_sample, watchman_config))
|
||||
copy_args = ['cp', '.git/hooks/fsmonitor-watchman.sample',
|
||||
'.git/hooks/query-watchman']
|
||||
subprocess.check_call(copy_args, cwd=dest)
|
||||
|
||||
config_args = [git, 'config', 'core.fsmonitor', '.git/hooks/query-watchman']
|
||||
subprocess.check_call(config_args, cwd=dest)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return False
|
||||
|
||||
print('Firefox source code available at %s' % dest)
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -43,37 +43,53 @@ class VersionControlCommands(object):
|
|||
def __init__(self, context):
|
||||
self._context = context
|
||||
|
||||
@Command('mercurial-setup', category='devenv',
|
||||
description='Help configure Mercurial for optimal development.')
|
||||
@Command('vcs-setup', category='devenv',
|
||||
description='Help configure a VCS for optimal development.')
|
||||
@CommandArgument('-u', '--update-only', action='store_true',
|
||||
help='Only update recommended extensions, don\'t run the wizard.')
|
||||
def mercurial_setup(self, update_only=False):
|
||||
"""Ensure Mercurial is optimally configured.
|
||||
@CommandArgument('-g', '--git', action='store_true',
|
||||
help='Use Git instead of Mercurial.')
|
||||
def vcs_setup(self, update_only=False, git=False):
|
||||
"""Ensure a Version Control System (Mercurial or Git) is optimally
|
||||
configured.
|
||||
|
||||
This command will inspect your Mercurial configuration and
|
||||
This command will inspect your VCS configuration and
|
||||
guide you through an interactive wizard helping you configure
|
||||
Mercurial for optimal use on Mozilla projects.
|
||||
VCS for optimal use on Mozilla projects.
|
||||
|
||||
User choice is respected: no changes are made without explicit
|
||||
confirmation from you.
|
||||
|
||||
If "--update-only" is used, the interactive wizard is disabled
|
||||
and this command only ensures that remote repositories providing
|
||||
Mercurial extensions are up to date.
|
||||
VCS extensions are up to date.
|
||||
|
||||
If "--git" is used, then Git is selected as the VCS instead of Mercurial,
|
||||
which is the default.
|
||||
"""
|
||||
import which
|
||||
import mozboot.bootstrap as bootstrap
|
||||
|
||||
vcs = 'hg'
|
||||
if git:
|
||||
vcs = 'git'
|
||||
|
||||
# "hg" is an executable script with a shebang, which will be found
|
||||
# be which.which. We need to pass a win32 executable to the function
|
||||
# by which.which. We need to pass a win32 executable to the function
|
||||
# because we spawn a process
|
||||
# from it.
|
||||
if sys.platform in ('win32', 'msys'):
|
||||
hg = which.which('hg.exe')
|
||||
vcs = which.which(vcs + '.exe')
|
||||
else:
|
||||
hg = which.which('hg')
|
||||
vcs = which.which(vcs)
|
||||
|
||||
if update_only:
|
||||
bootstrap.update_vct(hg, self._context.state_dir)
|
||||
if git:
|
||||
bootstrap.update_git_tools(vcs, self._context.state_dir)
|
||||
else:
|
||||
bootstrap.update_vct(vcs, self._context.state_dir)
|
||||
else:
|
||||
bootstrap.configure_mercurial(hg, self._context.state_dir)
|
||||
if git:
|
||||
bootstrap.configure_git(vcs, self._context.state_dir)
|
||||
else:
|
||||
bootstrap.configure_mercurial(vcs, self._context.state_dir)
|
||||
|
|
|
|||
|
|
@ -1174,4 +1174,4 @@ static const TransportSecurityPreload kPublicKeyPinningPreloadList[] = {
|
|||
|
||||
static const int32_t kUnknownId = -1;
|
||||
|
||||
static const PRTime kPreloadPKPinsExpirationTime = INT64_C(1542276268102000);
|
||||
static const PRTime kPreloadPKPinsExpirationTime = INT64_C(1542881093590000);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
/*****************************************************************************/
|
||||
|
||||
#include <stdint.h>
|
||||
const PRTime gPreloadListExpirationTime = INT64_C(1544695448460000);
|
||||
const PRTime gPreloadListExpirationTime = INT64_C(1545300209985000);
|
||||
%%
|
||||
0-1.party, 1
|
||||
0.me.uk, 1
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -208,11 +208,12 @@ async function loadDumpFile(bucket, collection) {
|
|||
|
||||
class RemoteSettingsClient {
|
||||
|
||||
constructor(collectionName, { bucketName, signerName, filterFunc = jexlFilterFunc, lastCheckTimePref }) {
|
||||
constructor(collectionName, { bucketName, signerName, filterFunc = jexlFilterFunc, localFields = [], lastCheckTimePref }) {
|
||||
this.collectionName = collectionName;
|
||||
this.bucketName = bucketName;
|
||||
this.signerName = signerName;
|
||||
this.filterFunc = filterFunc;
|
||||
this.localFields = localFields;
|
||||
this._lastCheckTimePref = lastCheckTimePref;
|
||||
|
||||
this._listeners = new Map();
|
||||
|
|
@ -276,17 +277,26 @@ class RemoteSettingsClient {
|
|||
|
||||
/**
|
||||
* Open the underlying Kinto collection, using the appropriate adapter and
|
||||
* options. This acts as a context manager where the connection is closed
|
||||
* once the specified `callback` has finished.
|
||||
*
|
||||
* @param {callback} function the async function to execute with the open SQlite connection.
|
||||
* @param {Object} options additional advanced options.
|
||||
* @param {string} options.hooks hooks to execute on synchronization (see Kinto.js docs)
|
||||
* options.
|
||||
*/
|
||||
async openCollection(options = {}) {
|
||||
async openCollection() {
|
||||
if (!this._kinto) {
|
||||
this._kinto = new Kinto({ bucket: this.bucketName, adapter: Kinto.adapters.IDB });
|
||||
}
|
||||
const options = {
|
||||
localFields: this.localFields,
|
||||
};
|
||||
// If there is a `signerName` and collection signing is enforced, add a
|
||||
// hook for incoming changes that validates the signature.
|
||||
const verifySignature = Services.prefs.getBoolPref(PREF_SETTINGS_VERIFY_SIGNATURE, true);
|
||||
if (this.signerName && verifySignature) {
|
||||
const remote = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
options.hooks = {
|
||||
"incoming-changes": [(payload, collection) => {
|
||||
return this._validateCollectionSignature(remote, payload, collection);
|
||||
}]
|
||||
};
|
||||
}
|
||||
return this._kinto.collection(this.collectionName, options);
|
||||
}
|
||||
|
||||
|
|
@ -335,22 +345,10 @@ class RemoteSettingsClient {
|
|||
async maybeSync(lastModified, serverTime, options = { loadDump: true }) {
|
||||
const {loadDump} = options;
|
||||
const remote = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
|
||||
const verifySignature = Services.prefs.getBoolPref(PREF_SETTINGS_VERIFY_SIGNATURE, true);
|
||||
|
||||
// if there is a signerName and collection signing is enforced, add a
|
||||
// hook for incoming changes that validates the signature
|
||||
const colOptions = {};
|
||||
if (this.signerName && verifySignature) {
|
||||
colOptions.hooks = {
|
||||
"incoming-changes": [(payload, collection) => {
|
||||
return this._validateCollectionSignature(remote, payload, collection);
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
let reportStatus = null;
|
||||
try {
|
||||
const collection = await this.openCollection(colOptions);
|
||||
const collection = await this.openCollection();
|
||||
// Synchronize remote data into a local Sqlite DB.
|
||||
let collectionLastModified = await collection.db.getLastModified();
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,17 @@ add_task(async function test_records_obtained_from_server_are_stored_in_db() {
|
|||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(async function test_records_can_have_local_fields() {
|
||||
const c = RemoteSettings("password-fields", { localFields: ["accepted"] });
|
||||
await c.maybeSync(2000, Date.now());
|
||||
|
||||
const col = await c.openCollection();
|
||||
await col.update({ id: "9d500963-d80e-3a91-6e74-66f3811b99cc", accepted: true });
|
||||
|
||||
await c.maybeSync(2000, Date.now()); // Does not fail.
|
||||
});
|
||||
add_task(clear_state);
|
||||
|
||||
add_task(async function test_current_server_time_is_saved_in_pref() {
|
||||
const serverTime = Date.now();
|
||||
await client.maybeSync(2000, serverTime);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ fn device_size(device: &Device) -> Size2D<Au> {
|
|||
Size2D::new(Au(width), Au(height))
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#width
|
||||
fn eval_width(
|
||||
device: &Device,
|
||||
value: Option<CSSPixelLength>,
|
||||
|
|
@ -63,6 +64,7 @@ fn eval_width(
|
|||
)
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#device-width
|
||||
fn eval_device_width(
|
||||
device: &Device,
|
||||
value: Option<CSSPixelLength>,
|
||||
|
|
@ -75,6 +77,7 @@ fn eval_device_width(
|
|||
)
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#height
|
||||
fn eval_height(
|
||||
device: &Device,
|
||||
value: Option<CSSPixelLength>,
|
||||
|
|
@ -87,6 +90,7 @@ fn eval_height(
|
|||
)
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#device-height
|
||||
fn eval_device_height(
|
||||
device: &Device,
|
||||
value: Option<CSSPixelLength>,
|
||||
|
|
@ -121,6 +125,7 @@ where
|
|||
)
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#aspect-ratio
|
||||
fn eval_aspect_ratio(
|
||||
device: &Device,
|
||||
query_value: Option<AspectRatio>,
|
||||
|
|
@ -129,6 +134,7 @@ fn eval_aspect_ratio(
|
|||
eval_aspect_ratio_for(device, query_value, range_or_operator, viewport_size)
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#device-aspect-ratio
|
||||
fn eval_device_aspect_ratio(
|
||||
device: &Device,
|
||||
query_value: Option<AspectRatio>,
|
||||
|
|
@ -137,6 +143,11 @@ fn eval_device_aspect_ratio(
|
|||
eval_aspect_ratio_for(device, query_value, range_or_operator, device_size)
|
||||
}
|
||||
|
||||
/// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio
|
||||
///
|
||||
/// FIXME(emilio): This should be an alias of `resolution`, according to the
|
||||
/// spec, and also according to the code in Chromium. Unify with
|
||||
/// `eval_resolution`.
|
||||
fn eval_device_pixel_ratio(
|
||||
device: &Device,
|
||||
query_value: Option<f32>,
|
||||
|
|
@ -183,6 +194,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#orientation
|
||||
fn eval_orientation(
|
||||
device: &Device,
|
||||
value: Option<Orientation>,
|
||||
|
|
@ -190,6 +202,7 @@ fn eval_orientation(
|
|||
eval_orientation_for(device, value, viewport_size)
|
||||
}
|
||||
|
||||
/// FIXME: There's no spec for `-moz-device-orientation`.
|
||||
fn eval_device_orientation(
|
||||
device: &Device,
|
||||
value: Option<Orientation>,
|
||||
|
|
@ -208,6 +221,7 @@ pub enum DisplayMode {
|
|||
Fullscreen,
|
||||
}
|
||||
|
||||
/// https://w3c.github.io/manifest/#the-display-mode-media-feature
|
||||
fn eval_display_mode(
|
||||
device: &Device,
|
||||
query_value: Option<DisplayMode>,
|
||||
|
|
@ -225,6 +239,7 @@ fn eval_display_mode(
|
|||
gecko_display_mode as u8 == query_value as u8
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#grid
|
||||
fn eval_grid(_: &Device, query_value: Option<bool>, _: Option<RangeOrOperator>) -> bool {
|
||||
// Gecko doesn't support grid devices (e.g., ttys), so the 'grid' feature
|
||||
// is always 0.
|
||||
|
|
@ -232,6 +247,7 @@ fn eval_grid(_: &Device, query_value: Option<bool>, _: Option<RangeOrOperator>)
|
|||
query_value.map_or(supports_grid, |v| v == supports_grid)
|
||||
}
|
||||
|
||||
/// https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d
|
||||
fn eval_transform_3d(
|
||||
_: &Device,
|
||||
query_value: Option<bool>,
|
||||
|
|
@ -248,12 +264,14 @@ enum Scan {
|
|||
Interlace,
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#scan
|
||||
fn eval_scan(_: &Device, _: Option<Scan>) -> bool {
|
||||
// Since Gecko doesn't support the 'tv' media type, the 'scan' feature never
|
||||
// matches.
|
||||
false
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#color
|
||||
fn eval_color(
|
||||
device: &Device,
|
||||
query_value: Option<u32>,
|
||||
|
|
@ -268,13 +286,13 @@ fn eval_color(
|
|||
)
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#color-index
|
||||
fn eval_color_index(
|
||||
_: &Device,
|
||||
query_value: Option<u32>,
|
||||
range_or_operator: Option<RangeOrOperator>,
|
||||
) -> bool {
|
||||
// We should return zero if the device does not use a color lookup
|
||||
// table.
|
||||
// We should return zero if the device does not use a color lookup table.
|
||||
let index = 0;
|
||||
RangeOrOperator::evaluate(
|
||||
range_or_operator,
|
||||
|
|
@ -283,6 +301,7 @@ fn eval_color_index(
|
|||
)
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#monochrome
|
||||
fn eval_monochrome(
|
||||
_: &Device,
|
||||
query_value: Option<u32>,
|
||||
|
|
@ -298,6 +317,7 @@ fn eval_monochrome(
|
|||
)
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-4/#resolution
|
||||
fn eval_resolution(
|
||||
device: &Device,
|
||||
query_value: Option<Resolution>,
|
||||
|
|
@ -319,6 +339,7 @@ enum PrefersReducedMotion {
|
|||
Reduce,
|
||||
}
|
||||
|
||||
/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-motion
|
||||
fn eval_prefers_reduced_motion(
|
||||
device: &Device,
|
||||
query_value: Option<PrefersReducedMotion>,
|
||||
|
|
|
|||
|
|
@ -421,46 +421,34 @@ impl NoCalcLength {
|
|||
value: CSSFloat,
|
||||
unit: &str,
|
||||
) -> Result<Self, ()> {
|
||||
match_ignore_ascii_case! { unit,
|
||||
"px" => Ok(NoCalcLength::Absolute(AbsoluteLength::Px(value))),
|
||||
"in" => Ok(NoCalcLength::Absolute(AbsoluteLength::In(value))),
|
||||
"cm" => Ok(NoCalcLength::Absolute(AbsoluteLength::Cm(value))),
|
||||
"mm" => Ok(NoCalcLength::Absolute(AbsoluteLength::Mm(value))),
|
||||
"q" => Ok(NoCalcLength::Absolute(AbsoluteLength::Q(value))),
|
||||
"pt" => Ok(NoCalcLength::Absolute(AbsoluteLength::Pt(value))),
|
||||
"pc" => Ok(NoCalcLength::Absolute(AbsoluteLength::Pc(value))),
|
||||
Ok(match_ignore_ascii_case! { unit,
|
||||
"px" => NoCalcLength::Absolute(AbsoluteLength::Px(value)),
|
||||
"in" => NoCalcLength::Absolute(AbsoluteLength::In(value)),
|
||||
"cm" => NoCalcLength::Absolute(AbsoluteLength::Cm(value)),
|
||||
"mm" => NoCalcLength::Absolute(AbsoluteLength::Mm(value)),
|
||||
"q" => NoCalcLength::Absolute(AbsoluteLength::Q(value)),
|
||||
"pt" => NoCalcLength::Absolute(AbsoluteLength::Pt(value)),
|
||||
"pc" => NoCalcLength::Absolute(AbsoluteLength::Pc(value)),
|
||||
// font-relative
|
||||
"em" => Ok(NoCalcLength::FontRelative(FontRelativeLength::Em(value))),
|
||||
"ex" => Ok(NoCalcLength::FontRelative(FontRelativeLength::Ex(value))),
|
||||
"ch" => Ok(NoCalcLength::FontRelative(FontRelativeLength::Ch(value))),
|
||||
"rem" => Ok(NoCalcLength::FontRelative(FontRelativeLength::Rem(value))),
|
||||
"em" => NoCalcLength::FontRelative(FontRelativeLength::Em(value)),
|
||||
"ex" => NoCalcLength::FontRelative(FontRelativeLength::Ex(value)),
|
||||
"ch" => NoCalcLength::FontRelative(FontRelativeLength::Ch(value)),
|
||||
"rem" => NoCalcLength::FontRelative(FontRelativeLength::Rem(value)),
|
||||
// viewport percentages
|
||||
"vw" => {
|
||||
if context.in_page_rule() {
|
||||
return Err(())
|
||||
}
|
||||
Ok(NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vw(value)))
|
||||
},
|
||||
"vh" => {
|
||||
if context.in_page_rule() {
|
||||
return Err(())
|
||||
}
|
||||
Ok(NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vh(value)))
|
||||
},
|
||||
"vmin" => {
|
||||
if context.in_page_rule() {
|
||||
return Err(())
|
||||
}
|
||||
Ok(NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmin(value)))
|
||||
},
|
||||
"vmax" => {
|
||||
if context.in_page_rule() {
|
||||
return Err(())
|
||||
}
|
||||
Ok(NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmax(value)))
|
||||
},
|
||||
_ => Err(())
|
||||
}
|
||||
"vw" if !context.in_page_rule() => {
|
||||
NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vw(value))
|
||||
}
|
||||
"vh" if !context.in_page_rule() => {
|
||||
NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vh(value))
|
||||
}
|
||||
"vmin" if !context.in_page_rule() => {
|
||||
NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmin(value))
|
||||
}
|
||||
"vmax" if !context.in_page_rule() => {
|
||||
NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmax(value))
|
||||
}
|
||||
_ => return Err(())
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
|||
41
taskcluster/ci/beetmover-geckoview/kind.yml
Normal file
41
taskcluster/ci/beetmover-geckoview/kind.yml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# 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/.
|
||||
|
||||
loader: taskgraph.loader.single_dep:loader
|
||||
|
||||
transforms:
|
||||
- taskgraph.transforms.name_sanity:transforms
|
||||
- taskgraph.transforms.beetmover_geckoview:transforms
|
||||
- taskgraph.transforms.task:transforms
|
||||
|
||||
kind-dependencies:
|
||||
- build # geckoview builds aren't signed
|
||||
|
||||
only-for-attributes:
|
||||
- nightly
|
||||
|
||||
not-for-build-platforms:
|
||||
- linux-nightly/opt
|
||||
- linux64-nightly/opt
|
||||
- macosx64-nightly/opt
|
||||
- win32-nightly/opt
|
||||
- win64-nightly/opt
|
||||
- linux-devedition-nightly/opt
|
||||
- linux64-devedition-nightly/opt
|
||||
- macosx64-devedition-nightly/opt
|
||||
- win32-devedition-nightly/opt
|
||||
- win64-devedition-nightly/opt
|
||||
- linux64-asan-reporter-nightly/opt
|
||||
- win64-asan-reporter-nightly/opt
|
||||
|
||||
job-template:
|
||||
# Beetmoving geckoview makes it available to the official maven repo. So we want beetmover to
|
||||
# act only when the release is greenlit.
|
||||
shipping-phase: ship
|
||||
bucket-scope:
|
||||
by-project:
|
||||
mozilla-central: 'project:releng:beetmover:bucket:maven-production'
|
||||
mozilla-beta: 'project:releng:beetmover:bucket:maven-production'
|
||||
mozilla-release: 'project:releng:beetmover:bucket:maven-production'
|
||||
default: 'project:releng:beetmover:bucket:maven-staging'
|
||||
|
|
@ -19,9 +19,13 @@ android-api-16/debug:
|
|||
- name: public/android/R
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
|
||||
type: directory
|
||||
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
|
||||
- name: public/android/maven
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
|
||||
type: directory
|
||||
- name: public/build/target.maven.zip
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
|
||||
type: file
|
||||
- name: public/build/geckoview-androidTest.apk
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
|
||||
type: file
|
||||
|
|
@ -130,9 +134,13 @@ android-x86/opt:
|
|||
- name: public/android/R
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
|
||||
type: directory
|
||||
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
|
||||
- name: public/android/maven
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
|
||||
type: directory
|
||||
- name: public/build/target.maven.zip
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
|
||||
type: file
|
||||
- name: public/build/geckoview-androidTest.apk
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
|
||||
type: file
|
||||
|
|
@ -187,9 +195,13 @@ android-x86-nightly/opt:
|
|||
- name: public/android/R
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
|
||||
type: directory
|
||||
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
|
||||
- name: public/android/maven
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
|
||||
type: directory
|
||||
- name: public/build/target.maven.zip
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
|
||||
type: file
|
||||
- name: public/build/geckoview-androidTest.apk
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
|
||||
type: file
|
||||
|
|
@ -241,9 +253,13 @@ android-api-16/opt:
|
|||
- name: public/android/R
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
|
||||
type: directory
|
||||
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
|
||||
- name: public/android/maven
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
|
||||
type: directory
|
||||
- name: public/build/target.maven.zip
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
|
||||
type: file
|
||||
- name: public/build/geckoview-androidTest.apk
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
|
||||
type: file
|
||||
|
|
@ -293,9 +309,13 @@ android-api-16-without-google-play-services/opt:
|
|||
- name: public/android/R
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
|
||||
type: directory
|
||||
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
|
||||
- name: public/android/maven
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
|
||||
type: directory
|
||||
- name: public/build/target.maven.zip
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
|
||||
type: file
|
||||
- name: public/build/geckoview_example.apk
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
|
||||
type: file
|
||||
|
|
@ -348,9 +368,13 @@ android-api-16-nightly/opt:
|
|||
- name: public/android/R
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
|
||||
type: directory
|
||||
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
|
||||
- name: public/android/maven
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
|
||||
type: directory
|
||||
- name: public/build/target.maven.zip
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
|
||||
type: file
|
||||
- name: public/build/geckoview-androidTest.apk
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
|
||||
type: file
|
||||
|
|
@ -402,9 +426,13 @@ android-aarch64/opt:
|
|||
- name: public/android/R
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
|
||||
type: directory
|
||||
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
|
||||
- name: public/android/maven
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
|
||||
type: directory
|
||||
- name: public/build/target.maven.zip
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
|
||||
type: file
|
||||
- name: public/build/geckoview-androidTest.apk
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
|
||||
type: file
|
||||
|
|
@ -459,9 +487,13 @@ android-aarch64-nightly/opt:
|
|||
- name: public/android/R
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
|
||||
type: directory
|
||||
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
|
||||
- name: public/android/maven
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
|
||||
type: directory
|
||||
- name: public/build/target.maven.zip
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
|
||||
type: file
|
||||
- name: public/build/geckoview-androidTest.apk
|
||||
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
|
||||
type: file
|
||||
|
|
|
|||
|
|
@ -188,9 +188,11 @@ scriptworker:
|
|||
- 'project:releng:beetmover:bucket:release'
|
||||
- 'project:releng:beetmover:bucket:nightly'
|
||||
- 'project:releng:beetmover:bucket:partner'
|
||||
- 'project:releng:beetmover:bucket:maven-production'
|
||||
'scriptworker-prov-v1/beetmoverworker-dev':
|
||||
- 'project:releng:beetmover:bucket:dep'
|
||||
- 'project:releng:beetmover:bucket:dep-partner'
|
||||
- 'project:releng:beetmover:bucket:maven-staging'
|
||||
'scriptworker-prov-v1/balrogworker-v1':
|
||||
- 'project:releng:balrog:server:nightly'
|
||||
- 'project:releng:balrog:server:aurora'
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ job-defaults:
|
|||
command: >
|
||||
cd $GECKO_PATH &&
|
||||
./mach jsshell-bench --perfherder={shell} --binary=$JSSHELL {test}
|
||||
run-on-projects: ['mozilla-central', 'try']
|
||||
run-on-projects:
|
||||
by-shell:
|
||||
sm: ['mozilla-central', 'try', 'integration']
|
||||
default: ['mozilla-central', 'try']
|
||||
fetches:
|
||||
build:
|
||||
- target.jsshell.zip
|
||||
|
|
|
|||
|
|
@ -203,6 +203,11 @@ beetmover-source
|
|||
|
||||
Beetmover-source publishes release source. This is part of release promotion.
|
||||
|
||||
beetmover-geckoview
|
||||
-------------------
|
||||
|
||||
Beetmover-geckoview publishes the Android library called "geckoview".
|
||||
|
||||
checksums-signing
|
||||
-----------------
|
||||
Checksums-signing take as input the checksums file generated by beetmover tasks
|
||||
|
|
|
|||
156
taskcluster/taskgraph/transforms/beetmover_geckoview.py
Normal file
156
taskcluster/taskgraph/transforms/beetmover_geckoview.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# 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/.
|
||||
"""
|
||||
Transform the beetmover task into an actual task description.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
from taskgraph.transforms.base import TransformSequence
|
||||
from taskgraph.transforms.beetmover import \
|
||||
craft_release_properties as beetmover_craft_release_properties
|
||||
from taskgraph.util.attributes import copy_attributes_from_dependent_job
|
||||
from taskgraph.util.schema import validate_schema, Schema, resolve_keyed_by, optionally_keyed_by
|
||||
from taskgraph.util.scriptworker import (get_phase,
|
||||
get_worker_type_for_scope)
|
||||
from taskgraph.transforms.task import task_description_schema
|
||||
from voluptuous import Required, Optional
|
||||
|
||||
|
||||
_ARTIFACT_ID_PER_PLATFORM = {
|
||||
'android-aarch64': 'geckoview{update_channel}-arm64-v8a',
|
||||
'android-api-16': 'geckoview{update_channel}-armeabi-v7a',
|
||||
'android-x86': 'geckoview{update_channel}-x86',
|
||||
}
|
||||
|
||||
_MOZ_UPDATE_CHANNEL_PER_BRANCH = {
|
||||
'mozilla-release': '',
|
||||
'mozilla-beta': '-beta',
|
||||
'mozilla-central': '-nightly',
|
||||
'maple': '-nightly-maple',
|
||||
}
|
||||
|
||||
task_description_schema = {str(k): v for k, v in task_description_schema.schema.iteritems()}
|
||||
|
||||
transforms = TransformSequence()
|
||||
|
||||
beetmover_description_schema = Schema({
|
||||
Required('dependent-task'): object,
|
||||
Required('depname', default='build'): basestring,
|
||||
Optional('label'): basestring,
|
||||
Optional('treeherder'): task_description_schema['treeherder'],
|
||||
|
||||
Optional('bucket-scope'): optionally_keyed_by('project', basestring),
|
||||
Optional('shipping-phase'): task_description_schema['shipping-phase'],
|
||||
Optional('shipping-product'): task_description_schema['shipping-product'],
|
||||
})
|
||||
|
||||
|
||||
@transforms.add
|
||||
def validate(config, jobs):
|
||||
for job in jobs:
|
||||
label = job.get('dependent-task', object).__dict__.get('label', '?no-label?')
|
||||
validate_schema(
|
||||
beetmover_description_schema, job,
|
||||
"In beetmover-geckoview ({!r} kind) task for {!r}:".format(config.kind, label))
|
||||
yield job
|
||||
|
||||
|
||||
@transforms.add
|
||||
def make_task_description(config, jobs):
|
||||
for job in jobs:
|
||||
dep_job = job['dependent-task']
|
||||
attributes = dep_job.attributes
|
||||
|
||||
treeherder = job.get('treeherder', {})
|
||||
treeherder.setdefault('symbol', 'BM-gv')
|
||||
dep_th_platform = dep_job.task.get('extra', {}).get(
|
||||
'treeherder', {}).get('machine', {}).get('platform', '')
|
||||
treeherder.setdefault('platform',
|
||||
'{}/opt'.format(dep_th_platform))
|
||||
treeherder.setdefault('tier', 3)
|
||||
treeherder.setdefault('kind', 'build')
|
||||
label = job['label']
|
||||
description = (
|
||||
"Beetmover submission for geckoview"
|
||||
"{build_platform}/{build_type}'".format(
|
||||
build_platform=attributes.get('build_platform'),
|
||||
build_type=attributes.get('build_type')
|
||||
)
|
||||
)
|
||||
|
||||
dependent_kind = str(dep_job.kind)
|
||||
dependencies = {dependent_kind: dep_job.label}
|
||||
|
||||
attributes = copy_attributes_from_dependent_job(dep_job)
|
||||
|
||||
if job.get('locale'):
|
||||
attributes['locale'] = job['locale']
|
||||
|
||||
resolve_keyed_by(
|
||||
job, 'bucket-scope', item_name=job['label'],
|
||||
project=config.params['project']
|
||||
)
|
||||
|
||||
task = {
|
||||
'label': label,
|
||||
'description': description,
|
||||
'worker-type': get_worker_type_for_scope(config, job['bucket-scope']),
|
||||
'scopes': [job['bucket-scope'], 'project:releng:beetmover:action:push-to-maven'],
|
||||
'dependencies': dependencies,
|
||||
'attributes': attributes,
|
||||
'run-on-projects': ['mozilla-central'],
|
||||
'treeherder': treeherder,
|
||||
'shipping-phase': job.get('shipping-phase', get_phase(config)),
|
||||
}
|
||||
|
||||
yield task
|
||||
|
||||
|
||||
def generate_upstream_artifacts(build_task_ref):
|
||||
return [{
|
||||
'taskId': {'task-reference': build_task_ref},
|
||||
'taskType': 'build',
|
||||
'paths': ['public/build/target.maven.zip'],
|
||||
'zipExtract': True,
|
||||
}]
|
||||
|
||||
|
||||
@transforms.add
|
||||
def make_task_worker(config, jobs):
|
||||
for job in jobs:
|
||||
valid_beetmover_job = len(job['dependencies']) == 1 and 'build' in job['dependencies']
|
||||
if not valid_beetmover_job:
|
||||
raise NotImplementedError(
|
||||
'Beetmover-geckoview must have a single dependency. Got: {}'.format(
|
||||
job['dependencies']
|
||||
)
|
||||
)
|
||||
|
||||
build_task = list(job["dependencies"].keys())[0]
|
||||
build_task_ref = "<" + str(build_task) + ">"
|
||||
|
||||
worker = {
|
||||
'implementation': 'beetmover-maven',
|
||||
'release-properties': craft_release_properties(config, job),
|
||||
'upstream-artifacts': generate_upstream_artifacts(build_task_ref)
|
||||
}
|
||||
|
||||
job["worker"] = worker
|
||||
|
||||
yield job
|
||||
|
||||
|
||||
def craft_release_properties(config, job):
|
||||
props = beetmover_craft_release_properties(config, job)
|
||||
|
||||
platform = props['platform']
|
||||
update_channel = _MOZ_UPDATE_CHANNEL_PER_BRANCH.get(
|
||||
props['branch'], '-UNKNOWN_MOZ_UPDATE_CHANNEL'
|
||||
)
|
||||
artifact_id = _ARTIFACT_ID_PER_PLATFORM[platform].format(update_channel=update_channel)
|
||||
props['artifact-id'] = artifact_id
|
||||
props['app-name'] = 'geckoview' # this beetmover job is not about pushing Fennec
|
||||
|
||||
return props
|
||||
|
|
@ -510,6 +510,26 @@ task_description_schema = Schema({
|
|||
# the maximum time to run, in seconds
|
||||
Required('max-run-time'): int,
|
||||
Required('product'): basestring,
|
||||
}, {
|
||||
Required('implementation'): 'beetmover-maven',
|
||||
|
||||
Required('max-run-time', default=600): int,
|
||||
Required('release-properties'): {
|
||||
'app-name': basestring,
|
||||
'app-version': basestring,
|
||||
'branch': basestring,
|
||||
'build-id': basestring,
|
||||
'artifact-id': basestring,
|
||||
'hash-type': basestring,
|
||||
'platform': basestring,
|
||||
},
|
||||
|
||||
Required('upstream-artifacts'): [{
|
||||
Required('taskId'): taskref_or_string,
|
||||
Required('taskType'): basestring,
|
||||
Required('paths'): [basestring],
|
||||
Required('zipExtract', default=False): bool,
|
||||
}],
|
||||
}, {
|
||||
Required('implementation'): 'balrog',
|
||||
Required('balrog-action'): Any(*BALROG_ACTIONS),
|
||||
|
|
@ -1077,6 +1097,16 @@ def build_beetmover_push_to_release_payload(config, task, task_def):
|
|||
}
|
||||
|
||||
|
||||
@payload_builder('beetmover-maven')
|
||||
def build_beetmover_maven_payload(config, task, task_def):
|
||||
build_beetmover_payload(config, task, task_def)
|
||||
|
||||
task_def['payload']['artifact_id'] = task['worker']['release-properties']['artifact-id']
|
||||
|
||||
del task_def['payload']['releaseProperties']['hashType']
|
||||
del task_def['payload']['releaseProperties']['platform']
|
||||
|
||||
|
||||
@payload_builder('balrog')
|
||||
def build_balrog_payload(config, task, task_def):
|
||||
worker = task['worker']
|
||||
|
|
@ -1313,11 +1343,9 @@ def set_defaults(config, tasks):
|
|||
worker.setdefault('env', {})
|
||||
worker.setdefault('os-groups', [])
|
||||
worker.setdefault('chain-of-trust', False)
|
||||
elif worker['implementation'] == 'scriptworker-signing':
|
||||
worker.setdefault('max-run-time', 600)
|
||||
elif worker['implementation'] == 'beetmover':
|
||||
worker.setdefault('max-run-time', 600)
|
||||
elif worker['implementation'] == 'beetmover-push-to-release':
|
||||
elif worker['implementation'] in (
|
||||
'scriptworker-signing', 'beetmover', 'beetmover-push-to-release', 'beetmover-maven',
|
||||
):
|
||||
worker.setdefault('max-run-time', 600)
|
||||
elif worker['implementation'] == 'push-apk':
|
||||
worker.setdefault('commit', False)
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ Getting the code, running tests
|
|||
just calls code in runtests.py).
|
||||
|
||||
* Configure Mercurial with helpful extensions for Mozilla
|
||||
development by running `./mach mercurial-setup`.
|
||||
development by running `./mach vcs-setup`.
|
||||
|
||||
* It should install extensions like firefox-trees and set
|
||||
you up to be able to use MozReview, our code-review tool.
|
||||
|
|
|
|||
|
|
@ -51,9 +51,6 @@ class Repackage(BaseScript):
|
|||
return self.abs_dirs
|
||||
abs_dirs = super(Repackage, self).query_abs_dirs()
|
||||
config = self.config
|
||||
for directory in abs_dirs:
|
||||
value = abs_dirs[directory]
|
||||
abs_dirs[directory] = value
|
||||
|
||||
dirs = {}
|
||||
dirs['abs_tools_dir'] = os.path.join(abs_dirs['abs_work_dir'], 'tools')
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue