head.js (26992B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { NimbusTestUtils } = ChromeUtils.importESModule( 5 "resource://testing-common/NimbusTestUtils.sys.mjs" 6 ); 7 const { PermissionTestUtils } = ChromeUtils.importESModule( 8 "resource://testing-common/PermissionTestUtils.sys.mjs" 9 ); 10 const { PromptTestUtils } = ChromeUtils.importESModule( 11 "resource://testing-common/PromptTestUtils.sys.mjs" 12 ); 13 14 ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => { 15 const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule( 16 "resource://testing-common/QuickSuggestTestUtils.sys.mjs" 17 ); 18 module.init(this); 19 return module; 20 }); 21 22 ChromeUtils.defineESModuleGetters(this, { 23 ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", 24 QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs", 25 }); 26 27 NimbusTestUtils.init(this); 28 29 const kDefaultWait = 2000; 30 31 function is_element_visible(aElement, aMsg) { 32 isnot(aElement, null, "Element should not be null, when checking visibility"); 33 ok(!BrowserTestUtils.isHidden(aElement), aMsg); 34 } 35 36 function is_element_hidden(aElement, aMsg) { 37 isnot(aElement, null, "Element should not be null, when checking visibility"); 38 ok(BrowserTestUtils.isHidden(aElement), aMsg); 39 } 40 41 function open_preferences(aCallback) { 42 gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:preferences"); 43 let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab); 44 newTabBrowser.addEventListener( 45 "Initialized", 46 function () { 47 aCallback(gBrowser.contentWindow); 48 }, 49 { capture: true, once: true } 50 ); 51 } 52 53 function openAndLoadSubDialog( 54 aURL, 55 aFeatures = null, 56 aParams = null, 57 aClosingCallback = null 58 ) { 59 let promise = promiseLoadSubDialog(aURL); 60 content.gSubDialog.open( 61 aURL, 62 { features: aFeatures, closingCallback: aClosingCallback }, 63 aParams 64 ); 65 return promise; 66 } 67 68 function promiseLoadSubDialog(aURL) { 69 return new Promise(resolve => { 70 content.gSubDialog._dialogStack.addEventListener( 71 "dialogopen", 72 function dialogopen(aEvent) { 73 if ( 74 aEvent.detail.dialog._frame.contentWindow.location == "about:blank" 75 ) { 76 return; 77 } 78 content.gSubDialog._dialogStack.removeEventListener( 79 "dialogopen", 80 dialogopen 81 ); 82 83 is( 84 aEvent.detail.dialog._frame.contentWindow.location.toString(), 85 aURL, 86 "Check the proper URL is loaded" 87 ); 88 89 // Check visibility 90 is_element_visible(aEvent.detail.dialog._overlay, "Overlay is visible"); 91 92 // Check that stylesheets were injected 93 let expectedStyleSheetURLs = 94 aEvent.detail.dialog._injectedStyleSheets.slice(0); 95 for (let styleSheet of aEvent.detail.dialog._frame.contentDocument 96 .styleSheets) { 97 let i = expectedStyleSheetURLs.indexOf(styleSheet.href); 98 if (i >= 0) { 99 info("found " + styleSheet.href); 100 expectedStyleSheetURLs.splice(i, 1); 101 } 102 } 103 is( 104 expectedStyleSheetURLs.length, 105 0, 106 "All expectedStyleSheetURLs should have been found" 107 ); 108 109 // Wait for the next event tick to make sure the remaining part of the 110 // testcase runs after the dialog gets ready for input. 111 executeSoon(() => resolve(aEvent.detail.dialog._frame.contentWindow)); 112 } 113 ); 114 }); 115 } 116 117 async function openPreferencesViaOpenPreferencesAPI(aPane, aOptions) { 118 let finalPaneEvent = Services.prefs.getBoolPref("identity.fxaccounts.enabled") 119 ? "sync-pane-loaded" 120 : "privacy-pane-loaded"; 121 let finalPrefPaneLoaded = TestUtils.topicObserved(finalPaneEvent, () => true); 122 gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", { 123 allowInheritPrincipal: true, 124 }); 125 openPreferences(aPane, aOptions); 126 let newTabBrowser = gBrowser.selectedBrowser; 127 128 if (!newTabBrowser.contentWindow) { 129 await BrowserTestUtils.waitForEvent(newTabBrowser, "Initialized", true); 130 if (newTabBrowser.contentDocument.readyState != "complete") { 131 await BrowserTestUtils.waitForEvent(newTabBrowser.contentWindow, "load"); 132 } 133 await finalPrefPaneLoaded; 134 } 135 136 let win = gBrowser.contentWindow; 137 let selectedPane = win.history.state; 138 if (!aOptions || !aOptions.leaveOpen) { 139 gBrowser.removeCurrentTab(); 140 } 141 return { selectedPane }; 142 } 143 144 async function runSearchInput(input) { 145 let searchInput = gBrowser.contentDocument.getElementById("searchInput"); 146 searchInput.focus(); 147 let searchCompletedPromise = BrowserTestUtils.waitForEvent( 148 gBrowser.contentWindow, 149 "PreferencesSearchCompleted", 150 evt => evt.detail == input 151 ); 152 EventUtils.sendString(input); 153 await searchCompletedPromise; 154 } 155 156 async function evaluateSearchResults( 157 keyword, 158 searchResults, 159 includeExperiments = false 160 ) { 161 searchResults = Array.isArray(searchResults) 162 ? searchResults 163 : [searchResults]; 164 searchResults.push("header-searchResults"); 165 166 await runSearchInput(keyword); 167 168 let mainPrefTag = gBrowser.contentDocument.getElementById("mainPrefPane"); 169 for (let i = 0; i < mainPrefTag.childElementCount; i++) { 170 let child = mainPrefTag.children[i]; 171 if (!includeExperiments && child.id?.startsWith("pane-experimental")) { 172 continue; 173 } 174 if (searchResults.includes(child.id)) { 175 is_element_visible(child, `${child.id} should be in search results`); 176 } else if (child.id) { 177 is_element_hidden(child, `${child.id} should not be in search results`); 178 } 179 } 180 } 181 182 function waitForMutation(target, opts, cb) { 183 return new Promise(resolve => { 184 let observer = new MutationObserver(() => { 185 if (!cb || cb(target)) { 186 observer.disconnect(); 187 resolve(); 188 } 189 }); 190 observer.observe(target, opts); 191 }); 192 } 193 194 /** 195 * Creates observer that waits for and then compares all perm-changes with the observances in order. 196 * 197 * @param {Array} observances permission changes to observe (order is important) 198 * @returns {Promise} Promise object that resolves once all permission changes have been observed 199 */ 200 function createObserveAllPromise(observances) { 201 // Create new promise that resolves once all items 202 // in observances array have been observed. 203 return new Promise(resolve => { 204 let permObserver = { 205 observe(aSubject, aTopic, aData) { 206 if (aTopic != "perm-changed") { 207 return; 208 } 209 210 if (!observances.length) { 211 // See bug 1063410 212 return; 213 } 214 215 let permission = aSubject.QueryInterface(Ci.nsIPermission); 216 let expected = observances.shift(); 217 218 info( 219 `observed perm-changed for ${permission.principal.origin} (remaining ${observances.length})` 220 ); 221 222 is(aData, expected.data, "type of message should be the same"); 223 for (let prop of ["type", "capability", "expireType"]) { 224 if (expected[prop]) { 225 is( 226 permission[prop], 227 expected[prop], 228 `property: "${prop}" should be equal (${permission.principal.origin})` 229 ); 230 } 231 } 232 233 if (expected.origin) { 234 is( 235 permission.principal.origin, 236 expected.origin, 237 `property: "origin" should be equal (${permission.principal.origin})` 238 ); 239 } 240 241 if (!observances.length) { 242 Services.obs.removeObserver(permObserver, "perm-changed"); 243 executeSoon(resolve); 244 } 245 }, 246 }; 247 Services.obs.addObserver(permObserver, "perm-changed"); 248 }); 249 } 250 251 /** 252 * Waits for preference to be set and asserts the value. 253 * 254 * @param {string} pref - Preference key. 255 * @param {*} expectedValue - Expected value of the preference. 256 * @param {string} message - Assertion message. 257 */ 258 async function waitForAndAssertPrefState(pref, expectedValue, message) { 259 await TestUtils.waitForPrefChange(pref, value => { 260 if (value != expectedValue) { 261 return false; 262 } 263 is(value, expectedValue, message); 264 return true; 265 }); 266 } 267 268 /** 269 * The Relay promo is not shown for distributions with a custom FxA instance, 270 * since Relay requires an account on our own server. These prefs are set to a 271 * dummy address by the test harness, filling the prefs with a "user value." 272 * This temporarily sets the default value equal to the dummy value, so that 273 * Firefox thinks we've configured the correct FxA server. 274 * 275 * @returns {Promise<MockFxAUtilityFunctions>} { mock, unmock } 276 */ 277 async function mockDefaultFxAInstance() { 278 /** 279 * @typedef {object} MockFxAUtilityFunctions 280 * @property {function():void} mock - Makes the dummy values default, creating 281 * the illusion of a production FxA instance. 282 * @property {function():void} unmock - Restores the true defaults, creating 283 * the illusion of a custom FxA instance. 284 */ 285 286 const defaultPrefs = Services.prefs.getDefaultBranch(""); 287 const userPrefs = Services.prefs.getBranch(""); 288 const realAuth = defaultPrefs.getCharPref("identity.fxaccounts.auth.uri"); 289 const realRoot = defaultPrefs.getCharPref("identity.fxaccounts.remote.root"); 290 const mockAuth = userPrefs.getCharPref("identity.fxaccounts.auth.uri"); 291 const mockRoot = userPrefs.getCharPref("identity.fxaccounts.remote.root"); 292 const mock = () => { 293 defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth); 294 defaultPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot); 295 userPrefs.clearUserPref("identity.fxaccounts.auth.uri"); 296 userPrefs.clearUserPref("identity.fxaccounts.remote.root"); 297 }; 298 const unmock = () => { 299 defaultPrefs.setCharPref("identity.fxaccounts.auth.uri", realAuth); 300 defaultPrefs.setCharPref("identity.fxaccounts.remote.root", realRoot); 301 userPrefs.setCharPref("identity.fxaccounts.auth.uri", mockAuth); 302 userPrefs.setCharPref("identity.fxaccounts.remote.root", mockRoot); 303 }; 304 305 mock(); 306 registerCleanupFunction(unmock); 307 308 return { mock, unmock }; 309 } 310 311 /** 312 * Runs a test that checks the visibility of the Firefox Suggest preferences UI. 313 * An initial Suggest enabled status is set and visibility is checked. Then a 314 * Nimbus experiment is installed that enables or disables Suggest and 315 * visibility is checked again. Finally the page is reopened and visibility is 316 * checked again. 317 * 318 * @param {boolean} initialSuggestEnabled 319 * Whether Suggest should be enabled initially. 320 * @param {object} initialExpected 321 * The expected visibility after setting the initial enabled status. It should 322 * be an object that can be passed to `assertSuggestVisibility()`. 323 * @param {object} nimbusVariables 324 * An object mapping Nimbus variable names to values. 325 * @param {object} newExpected 326 * The expected visibility after installing the Nimbus experiment. It should 327 * be an object that can be passed to `assertSuggestVisibility()`. 328 * @param {string} pane 329 * The pref pane to open. 330 */ 331 async function doSuggestVisibilityTest({ 332 initialSuggestEnabled, 333 initialExpected, 334 nimbusVariables, 335 newExpected = initialExpected, 336 pane = "search", 337 }) { 338 info( 339 "Running Suggest visibility test: " + 340 JSON.stringify( 341 { 342 initialSuggestEnabled, 343 initialExpected, 344 nimbusVariables, 345 newExpected, 346 }, 347 null, 348 2 349 ) 350 ); 351 352 // Set the initial enabled status. 353 await SpecialPowers.pushPrefEnv({ 354 set: [["browser.urlbar.quicksuggest.enabled", initialSuggestEnabled]], 355 }); 356 357 // Open prefs and check the initial visibility. 358 await openPreferencesViaOpenPreferencesAPI(pane, { leaveOpen: true }); 359 await assertSuggestVisibility(initialExpected); 360 361 // Install a Nimbus experiment. 362 await QuickSuggestTestUtils.withExperiment({ 363 valueOverrides: nimbusVariables, 364 callback: async () => { 365 // Check visibility again. 366 await assertSuggestVisibility(newExpected); 367 368 // To make sure visibility is properly updated on load, close the tab, 369 // open the prefs again, and check visibility. 370 gBrowser.removeCurrentTab(); 371 await openPreferencesViaOpenPreferencesAPI(pane, { leaveOpen: true }); 372 await assertSuggestVisibility(newExpected); 373 }, 374 }); 375 376 gBrowser.removeCurrentTab(); 377 await SpecialPowers.popPrefEnv(); 378 } 379 380 /** 381 * Checks the visibility of the Suggest UI. 382 * 383 * @param {object} expectedByElementId 384 * An object that maps IDs of elements in the current tab to objects with the 385 * following properties: 386 * 387 * {bool} isVisible 388 * Whether the element is expected to be visible. 389 * {string} l10nId 390 * The expected l10n ID of the element. Optional. 391 */ 392 async function assertSuggestVisibility(expectedByElementId) { 393 let doc = gBrowser.selectedBrowser.contentDocument; 394 for (let [elementId, { isVisible, l10nId }] of Object.entries( 395 expectedByElementId 396 )) { 397 let element = doc.getElementById(elementId); 398 await TestUtils.waitForCondition( 399 () => BrowserTestUtils.isVisible(element) == isVisible, 400 "Waiting for element visbility: " + 401 JSON.stringify({ elementId, isVisible }) 402 ); 403 Assert.strictEqual( 404 BrowserTestUtils.isVisible(element), 405 isVisible, 406 "Element should have expected visibility: " + elementId 407 ); 408 if (l10nId) { 409 Assert.equal( 410 element.dataset.l10nId, 411 l10nId, 412 "The l10n ID should be correct for element: " + elementId 413 ); 414 } 415 } 416 } 417 418 const DEFAULT_LABS_RECIPES = [ 419 NimbusTestUtils.factories.recipe("nimbus-qa-1", { 420 targeting: "true", 421 isRollout: true, 422 isFirefoxLabsOptIn: true, 423 firefoxLabsTitle: "experimental-features-ime-search", 424 firefoxLabsDescription: "experimental-features-ime-search-description", 425 firefoxLabsDescriptionLinks: null, 426 firefoxLabsGroup: "experimental-features-group-customize-browsing", 427 requiresRestart: false, 428 branches: [ 429 { 430 slug: "control", 431 ratio: 1, 432 features: [ 433 { 434 featureId: "nimbus-qa-1", 435 value: { 436 value: "recipe-value-1", 437 }, 438 }, 439 ], 440 }, 441 ], 442 }), 443 444 NimbusTestUtils.factories.recipe("nimbus-qa-2", { 445 targeting: "true", 446 isRollout: true, 447 isFirefoxLabsOptIn: true, 448 firefoxLabsTitle: "experimental-features-media-jxl", 449 firefoxLabsDescription: "experimental-features-media-jxl-description", 450 firefoxLabsDescriptionLinks: { 451 bugzilla: "https://example.com", 452 }, 453 firefoxLabsGroup: "experimental-features-group-webpage-display", 454 branches: [ 455 { 456 slug: "control", 457 ratio: 1, 458 features: [ 459 { 460 featureId: "nimbus-qa-2", 461 value: { 462 value: "recipe-value-2", 463 }, 464 }, 465 ], 466 }, 467 ], 468 }), 469 470 NimbusTestUtils.factories.recipe("targeting-false", { 471 targeting: "false", 472 isRollout: true, 473 isFirefoxLabsOptIn: true, 474 firefoxLabsTitle: "experimental-features-ime-search", 475 firefoxLabsDescription: "experimental-features-ime-search-description", 476 firefoxLabsDescriptionLinks: null, 477 firefoxLabsGroup: "experimental-features-group-developer-tools", 478 requiresRestart: false, 479 }), 480 481 NimbusTestUtils.factories.recipe("bucketing-false", { 482 bucketConfig: { 483 ...NimbusTestUtils.factories.recipe.bucketConfig, 484 count: 0, 485 }, 486 isRollout: true, 487 targeting: "true", 488 isFirefoxLabsOptIn: true, 489 firefoxLabsTitle: "experimental-features-ime-search", 490 firefoxLabsDescription: "experimental-features-ime-search-description", 491 firefoxLabsDescriptionLinks: null, 492 firefoxLabsGroup: "experimental-features-group-developer-tools", 493 requiresRestart: false, 494 }), 495 ]; 496 497 async function setupLabsTest(recipes) { 498 await SpecialPowers.pushPrefEnv({ 499 set: [ 500 ["app.normandy.run_interval_seconds", 0], 501 ["app.shield.optoutstudies.enabled", true], 502 ["datareporting.healthreport.uploadEnabled", true], 503 ["messaging-system.log", "debug"], 504 ], 505 clear: [ 506 ["browser.preferences.experimental"], 507 ["browser.preferences.experimental.hidden"], 508 ], 509 }); 510 // Initialize Nimbus and wait for the RemoteSettingsExperimentLoader to finish 511 // updating (with no recipes). 512 await ExperimentAPI.ready(); 513 await ExperimentAPI._rsLoader.finishedUpdating(); 514 515 // Inject some recipes into the Remote Settings client and call 516 // updateRecipes() so that we have available opt-ins. 517 await ExperimentAPI._rsLoader.remoteSettingsClients.experiments.db.importChanges( 518 {}, 519 Date.now(), 520 recipes ?? DEFAULT_LABS_RECIPES, 521 { clear: true } 522 ); 523 await ExperimentAPI._rsLoader.remoteSettingsClients.secureExperiments.db.importChanges( 524 {}, 525 Date.now(), 526 [], 527 { clear: true } 528 ); 529 530 await ExperimentAPI._rsLoader.updateRecipes("test"); 531 532 return async function cleanup() { 533 await NimbusTestUtils.removeStore(ExperimentAPI.manager.store); 534 await SpecialPowers.popPrefEnv(); 535 }; 536 } 537 538 function promiseNimbusStoreUpdate(wantedSlug, wantedActive) { 539 const deferred = Promise.withResolvers(); 540 const listener = (_event, { slug, active }) => { 541 info( 542 `promiseNimbusStoreUpdate: received update for ${slug} active=${active}` 543 ); 544 if (slug === wantedSlug && active === wantedActive) { 545 ExperimentAPI._manager.store.off("update", listener); 546 deferred.resolve(); 547 } 548 }; 549 550 ExperimentAPI._manager.store.on("update", listener); 551 return deferred.promise; 552 } 553 554 function enrollByClick(el, wantedActive) { 555 const slug = el.dataset.nimbusSlug; 556 557 info(`Enrolling in ${slug}:${el.dataset.nimbusBranchSlug}...`); 558 559 const promise = promiseNimbusStoreUpdate(slug, wantedActive); 560 EventUtils.synthesizeMouseAtCenter(el.inputEl, {}, gBrowser.contentWindow); 561 return promise; 562 } 563 564 /** 565 * Clicks a checkbox and waits for the associated preference to change to the expected value. 566 * 567 * @param {Document} doc - The content document. 568 * @param {string} checkboxId - The checkbox element id. 569 * @param {string} prefName - The preference name. 570 * @param {boolean} expectedValue - The expected value after click. 571 * @returns {Promise<HTMLInputElement>} 572 */ 573 async function clickCheckboxAndWaitForPrefChange( 574 doc, 575 checkboxId, 576 prefName, 577 expectedValue 578 ) { 579 let checkbox = doc.getElementById(checkboxId); 580 let prefChange = waitForAndAssertPrefState(prefName, expectedValue); 581 582 checkbox.click(); 583 584 await prefChange; 585 is( 586 checkbox.checked, 587 expectedValue, 588 `The checkbox #${checkboxId} should be in the expected state after being clicked.` 589 ); 590 return checkbox; 591 } 592 593 /** 594 * Clicks a checkbox that triggers a confirmation dialog and handles the dialog response. 595 * 596 * @param {Document} doc - The document containing the checkbox. 597 * @param {string} checkboxId - The ID of the checkbox to click. 598 * @param {string} prefName - The name of the preference that should change. 599 * @param {boolean} expectedValue - The expected value after handling the dialog. 600 * @param {number} buttonNumClick - The button to click in the dialog (0 = cancel, 1 = OK). 601 * @returns {Promise<HTMLInputElement>} 602 */ 603 async function clickCheckboxWithConfirmDialog( 604 doc, 605 checkboxId, 606 prefName, 607 expectedValue, 608 buttonNumClick 609 ) { 610 let checkbox = doc.getElementById(checkboxId); 611 612 let promptPromise = PromptTestUtils.handleNextPrompt( 613 gBrowser.selectedBrowser, 614 { modalType: Services.prompt.MODAL_TYPE_CONTENT }, 615 { buttonNumClick } 616 ); 617 618 let prefChangePromise = null; 619 if (buttonNumClick === 1) { 620 // Only wait for the final preference change to the expected value 621 // The baseline checkbox handler sets the checkbox state directly and 622 // the preference binding handles the actual preference change 623 prefChangePromise = waitForAndAssertPrefState(prefName, expectedValue); 624 } 625 626 checkbox.click(); 627 628 await promptPromise; 629 630 if (prefChangePromise) { 631 await prefChangePromise; 632 } 633 634 is( 635 checkbox.checked, 636 expectedValue, 637 `The checkbox #${checkboxId} should be in the expected state after dialog interaction.` 638 ); 639 640 return checkbox; 641 } 642 643 /** 644 * Select the given history mode via dropdown in the privacy pane. 645 * 646 * @param {Window} win - The preferences window which contains the 647 * dropdown. 648 * @param {string} value - The history mode to select. 649 */ 650 async function selectHistoryMode(win, value) { 651 let historyMode = win.document.getElementById("historyMode").inputEl; 652 653 // Find the index of the option with the given value. Do this before the first 654 // click so we can bail out early if the option does not exist. 655 let optionIndexStr = Array.from(historyMode.children) 656 .findIndex(option => option.value == value) 657 ?.toString(); 658 if (optionIndexStr == null) { 659 throw new Error( 660 "Could not find history mode option item for value: " + value 661 ); 662 } 663 664 // Scroll into view for click to succeed. 665 historyMode.scrollIntoView(); 666 667 let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window); 668 669 await EventUtils.synthesizeMouseAtCenter( 670 historyMode, 671 {}, 672 historyMode.ownerGlobal 673 ); 674 675 let popup = await popupShownPromise; 676 let popupItems = Array.from(popup.children); 677 678 let targetItem = popupItems.find(item => item.value == optionIndexStr); 679 680 if (!targetItem) { 681 throw new Error( 682 "Could not find history mode popup item for value: " + value 683 ); 684 } 685 686 let popupHiddenPromise = BrowserTestUtils.waitForPopupEvent(popup, "hidden"); 687 688 EventUtils.synthesizeMouseAtCenter(targetItem, {}, targetItem.ownerGlobal); 689 690 await popupHiddenPromise; 691 } 692 693 /** 694 * Select the given history mode in the redesigned privacy pane. 695 * 696 * @param {Window} win - The preferences window which contains the 697 * dropdown. 698 * @param {string} value - The history mode to select. 699 */ 700 async function selectRedesignedHistoryMode(win, value) { 701 let historyMode = win.document.querySelector( 702 "setting-group[groupid='history2'] #historyMode" 703 ); 704 let updated = waitForSettingControlChange(historyMode); 705 706 let optionItems = Array.from(historyMode.children); 707 let targetItem = optionItems.find(option => option.value == value); 708 if (!targetItem) { 709 throw new Error( 710 "Could not find history mode popup item for value: " + value 711 ); 712 } 713 714 if (historyMode.value == value) { 715 return; 716 } 717 718 targetItem.click(); 719 await updated; 720 } 721 722 async function updateCheckBoxElement(checkbox, value) { 723 ok(checkbox, "the " + checkbox.id + " checkbox should exist"); 724 is_element_visible( 725 checkbox, 726 "the " + checkbox.id + " checkbox should be visible" 727 ); 728 729 // No need to click if we're already in the desired state. 730 if (checkbox.checked === value) { 731 return; 732 } 733 734 // Scroll into view for click to succeed. 735 checkbox.scrollIntoView(); 736 737 // Toggle the state. 738 await EventUtils.synthesizeMouseAtCenter(checkbox, {}, checkbox.ownerGlobal); 739 } 740 741 async function updateCheckBox(win, id, value) { 742 let checkbox = win.document.getElementById(id); 743 ok(checkbox, "the " + id + " checkbox should exist"); 744 is_element_visible(checkbox, "the " + id + " checkbox should be visible"); 745 746 // No need to click if we're already in the desired state. 747 if (checkbox.checked === value) { 748 return; 749 } 750 751 // Scroll into view for click to succeed. 752 checkbox.scrollIntoView(); 753 754 // Toggle the state. 755 await EventUtils.synthesizeMouseAtCenter(checkbox, {}, checkbox.ownerGlobal); 756 } 757 758 function waitForSettingChange(setting) { 759 return new Promise(resolve => { 760 setting.on("change", function handler() { 761 setting.off("change", handler); 762 resolve(); 763 }); 764 }); 765 } 766 767 async function waitForSettingControlChange(control) { 768 await waitForSettingChange(control.setting); 769 await new Promise(resolve => requestAnimationFrame(resolve)); 770 } 771 772 /** 773 * Wait for the current setting pane to change. 774 * 775 * @param {string} paneId 776 */ 777 async function waitForPaneChange(paneId) { 778 let doc = gBrowser.selectedBrowser.contentDocument; 779 let event = await BrowserTestUtils.waitForEvent(doc, "paneshown"); 780 let expectId = paneId.startsWith("pane") 781 ? paneId 782 : `pane${paneId[0].toUpperCase()}${paneId.substring(1)}`; 783 is(event.detail.category, expectId, "Loaded the correct pane"); 784 } 785 786 function getControl(doc, id) { 787 let control = doc.getElementById(id); 788 ok(control, `Control ${id} exists`); 789 return control; 790 } 791 792 function synthesizeClick(el) { 793 let target = el.buttonEl ?? el.inputEl ?? el; 794 target.scrollIntoView({ block: "center" }); 795 EventUtils.synthesizeMouseAtCenter(target, {}, target.ownerGlobal); 796 } 797 798 function getControlWrapper(doc, id) { 799 return getControl(doc, id).closest("setting-control"); 800 } 801 802 async function openEtpPage() { 803 await openPreferencesViaOpenPreferencesAPI("etp", { leaveOpen: true }); 804 let doc = gBrowser.contentDocument; 805 await BrowserTestUtils.waitForCondition( 806 () => doc.getElementById("contentBlockingCategoryRadioGroup"), 807 "Wait for the ETP advanced radio group to render" 808 ); 809 return { 810 win: gBrowser.contentWindow, 811 doc, 812 tab: gBrowser.selectedTab, 813 }; 814 } 815 816 async function openEtpCustomizePage() { 817 await openPreferencesViaOpenPreferencesAPI("etpCustomize", { 818 leaveOpen: true, 819 }); 820 let doc = gBrowser.contentDocument; 821 await BrowserTestUtils.waitForCondition( 822 () => doc.getElementById("etpAllowListBaselineEnabledCustom"), 823 "Wait for the ETP customize controls to render" 824 ); 825 return { 826 win: gBrowser.contentWindow, 827 doc, 828 }; 829 } 830 831 async function changeMozSelectValue(selectEl, value) { 832 let control = selectEl.control; 833 let changePromise = waitForSettingControlChange(control); 834 selectEl.value = value; 835 selectEl.dispatchEvent(new Event("change", { bubbles: true })); 836 await changePromise; 837 } 838 839 async function clickEtpBaselineCheckboxWithConfirm( 840 doc, 841 controlId, 842 prefName, 843 expectedValue, 844 buttonNumClick 845 ) { 846 let checkbox = getControl(doc, controlId); 847 848 let promptPromise = PromptTestUtils.handleNextPrompt( 849 gBrowser.selectedBrowser, 850 { modalType: Services.prompt.MODAL_TYPE_CONTENT }, 851 { buttonNumClick } 852 ); 853 854 let prefChangePromise = null; 855 if (buttonNumClick === 1) { 856 prefChangePromise = waitForAndAssertPrefState( 857 prefName, 858 expectedValue, 859 `${prefName} updated` 860 ); 861 } 862 863 synthesizeClick(checkbox); 864 865 await promptPromise; 866 867 if (prefChangePromise) { 868 await prefChangePromise; 869 } 870 871 is( 872 checkbox.checked, 873 expectedValue, 874 `Checkbox ${controlId} should be ${expectedValue}` 875 ); 876 877 return checkbox; 878 } 879 880 // Ensure each test leaves the sidebar in its initial state when it completes 881 const initialSidebarState = { ...SidebarController.getUIState(), command: "" }; 882 registerCleanupFunction(async function () { 883 const { ObjectUtils } = ChromeUtils.importESModule( 884 "resource://gre/modules/ObjectUtils.sys.mjs" 885 ); 886 if ( 887 !ObjectUtils.deepEqual(SidebarController.getUIState(), initialSidebarState) 888 ) { 889 info("Restoring to initial sidebar state"); 890 await SidebarController.initializeUIState(initialSidebarState); 891 } 892 });