browser_UITour.js (22090B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 var gTestTab; 7 var gContentAPI; 8 9 ChromeUtils.defineESModuleGetters(this, { 10 ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", 11 UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", 12 CustomizableUITestUtils: 13 "resource://testing-common/CustomizableUITestUtils.sys.mjs", 14 }); 15 16 let gCUITestUtils = new CustomizableUITestUtils(window); 17 18 function test() { 19 UITourTest(); 20 } 21 22 var tests = [ 23 function test_untrusted_host(done) { 24 loadUITourTestPage(function () { 25 CustomizableUI.addWidgetToArea( 26 "bookmarks-menu-button", 27 CustomizableUI.AREA_NAVBAR, 28 0 29 ); 30 registerCleanupFunction(() => 31 CustomizableUI.removeWidgetFromArea("bookmarks-menu-button") 32 ); 33 let bookmarksMenu = document.getElementById("bookmarks-menu-button"); 34 is(bookmarksMenu.open, false, "Bookmark menu should initially be closed"); 35 36 gContentAPI.showMenu("bookmarks"); 37 is( 38 bookmarksMenu.open, 39 false, 40 "Bookmark menu should not open on a untrusted host" 41 ); 42 43 done(); 44 }, "http://mochi.test:8888/"); 45 }, 46 function test_testing_host(done) { 47 // Add two testing origins intentionally surrounded by whitespace to be ignored. 48 Services.prefs.setCharPref( 49 "browser.uitour.testingOrigins", 50 "https://test1.example.org, https://test2.example.org:443 " 51 ); 52 53 registerCleanupFunction(() => { 54 Services.prefs.clearUserPref("browser.uitour.testingOrigins"); 55 }); 56 function callback(result) { 57 ok(result, "Callback should be called on a testing origin"); 58 done(); 59 } 60 61 loadUITourTestPage(function () { 62 gContentAPI.getConfiguration("appinfo", callback); 63 }, "https://test2.example.org/"); 64 }, 65 function test_unsecure_host(done) { 66 loadUITourTestPage(function () { 67 let bookmarksMenu = document.getElementById("bookmarks-menu-button"); 68 is(bookmarksMenu.open, false, "Bookmark menu should initially be closed"); 69 70 gContentAPI.showMenu("bookmarks"); 71 is( 72 bookmarksMenu.open, 73 false, 74 "Bookmark menu should not open on a unsecure host" 75 ); 76 77 done(); 78 }, "http://example.org/"); 79 }, 80 function test_disabled(done) { 81 Services.prefs.setBoolPref("browser.uitour.enabled", false); 82 83 let bookmarksMenu = document.getElementById("bookmarks-menu-button"); 84 is(bookmarksMenu.open, false, "Bookmark menu should initially be closed"); 85 86 gContentAPI.showMenu("bookmarks").then(() => { 87 is( 88 bookmarksMenu.open, 89 false, 90 "Bookmark menu should not open when feature is disabled" 91 ); 92 93 Services.prefs.setBoolPref("browser.uitour.enabled", true); 94 }); 95 done(); 96 }, 97 function test_highlight(done) { 98 function test_highlight_2() { 99 let highlight = document.getElementById("UITourHighlight"); 100 gContentAPI.hideHighlight(); 101 102 waitForElementToBeHidden( 103 highlight, 104 test_highlight_3, 105 "Highlight should be hidden after hideHighlight()" 106 ); 107 } 108 function test_highlight_3() { 109 is_element_hidden( 110 highlight, 111 "Highlight should be hidden after hideHighlight()" 112 ); 113 114 gContentAPI.showHighlight("urlbar"); 115 waitForElementToBeVisible( 116 highlight, 117 test_highlight_4, 118 "Highlight should be shown after showHighlight()" 119 ); 120 } 121 function test_highlight_4() { 122 let highlight = document.getElementById("UITourHighlight"); 123 gContentAPI.showHighlight("backForward"); 124 waitForElementToBeVisible( 125 highlight, 126 done, 127 "Highlight should be shown after showHighlight()" 128 ); 129 } 130 131 let highlight = document.getElementById("UITourHighlight"); 132 is_element_hidden(highlight, "Highlight should initially be hidden"); 133 134 gContentAPI.showHighlight("urlbar"); 135 waitForElementToBeVisible( 136 highlight, 137 test_highlight_2, 138 "Highlight should be shown after showHighlight()" 139 ); 140 }, 141 function test_highlight_toolbar_button(done) { 142 function check_highlight_size() { 143 let panel = highlight.parentElement; 144 let anchor = panel.anchorNode; 145 let anchorRect = anchor.getBoundingClientRect(); 146 info( 147 "addons target: width: " + 148 anchorRect.width + 149 " height: " + 150 anchorRect.height 151 ); 152 let dimension = anchorRect.width; 153 let highlightRect = highlight.getBoundingClientRect(); 154 info( 155 "highlight: width: " + 156 highlightRect.width + 157 " height: " + 158 highlightRect.height 159 ); 160 is( 161 Math.round(highlightRect.width), 162 dimension, 163 "The width of the highlight should be equal to the width of the target" 164 ); 165 is( 166 Math.round(highlightRect.height), 167 dimension, 168 "The height of the highlight should be equal to the width of the target" 169 ); 170 is( 171 highlight.classList.contains("rounded-highlight"), 172 true, 173 "Highlight should be rounded-rectangle styled" 174 ); 175 CustomizableUI.removeWidgetFromArea("home-button"); 176 done(); 177 } 178 info("Adding home button."); 179 CustomizableUI.addWidgetToArea("home-button", "nav-bar"); 180 // Force the button to get layout so we can show the highlight. 181 document.getElementById("home-button").clientHeight; 182 let highlight = document.getElementById("UITourHighlight"); 183 is_element_hidden(highlight, "Highlight should initially be hidden"); 184 185 gContentAPI.showHighlight("home"); 186 waitForElementToBeVisible( 187 highlight, 188 check_highlight_size, 189 "Highlight should be shown after showHighlight()" 190 ); 191 }, 192 function test_highlight_addons_auto_open_close(done) { 193 let highlight = document.getElementById("UITourHighlight"); 194 gContentAPI.showHighlight("addons"); 195 waitForElementToBeVisible( 196 highlight, 197 function checkPanelIsOpen() { 198 isnot(PanelUI.panel.state, "closed", "Panel should have opened"); 199 isnot( 200 highlight.classList.contains("rounded-highlight"), 201 true, 202 "Highlight should not be round-rectangle styled." 203 ); 204 205 let hiddenPromise = promisePanelElementHidden(window, PanelUI.panel); 206 // Move the highlight outside which should close the app menu. 207 gContentAPI.showHighlight("appMenu"); 208 hiddenPromise.then(() => { 209 waitForElementToBeVisible( 210 highlight, 211 function checkPanelIsClosed() { 212 isnot( 213 PanelUI.panel.state, 214 "open", 215 "Panel should have closed after the highlight moved elsewhere." 216 ); 217 done(); 218 }, 219 "Highlight should move to the appMenu button" 220 ); 221 }); 222 }, 223 "Highlight should be shown after showHighlight() for fixed panel items" 224 ); 225 }, 226 function test_highlight_addons_manual_open_close(done) { 227 let highlight = document.getElementById("UITourHighlight"); 228 // Manually open the app menu then show a highlight there. The menu should remain open. 229 let shownPromise = promisePanelShown(window); 230 gContentAPI.showMenu("appMenu"); 231 shownPromise 232 .then(() => { 233 isnot(PanelUI.panel.state, "closed", "Panel should have opened"); 234 gContentAPI.showHighlight("addons"); 235 236 waitForElementToBeVisible( 237 highlight, 238 function checkPanelIsStillOpen() { 239 isnot(PanelUI.panel.state, "closed", "Panel should still be open"); 240 241 // Move the highlight outside which shouldn't close the app menu since it was manually opened. 242 gContentAPI.showHighlight("appMenu"); 243 waitForElementToBeVisible( 244 highlight, 245 function () { 246 isnot( 247 PanelUI.panel.state, 248 "closed", 249 "Panel should remain open since UITour didn't open it in the first place" 250 ); 251 gContentAPI.hideMenu("appMenu"); 252 done(); 253 }, 254 "Highlight should move to the appMenu button" 255 ); 256 }, 257 "Highlight should be shown after showHighlight() for fixed panel items" 258 ); 259 }) 260 .catch(console.error); 261 }, 262 function test_highlight_effect(done) { 263 function waitForHighlightWithEffect(highlightEl, effect, next, error) { 264 return waitForCondition( 265 () => highlightEl.getAttribute("active") == effect, 266 next, 267 error 268 ); 269 } 270 function checkDefaultEffect() { 271 is( 272 highlight.getAttribute("active"), 273 "none", 274 "The default should be no effect" 275 ); 276 277 gContentAPI.showHighlight("urlbar", "none"); 278 waitForHighlightWithEffect( 279 highlight, 280 "none", 281 checkZoomEffect, 282 "There should be no effect" 283 ); 284 } 285 function checkZoomEffect() { 286 gContentAPI.showHighlight("urlbar", "zoom"); 287 waitForHighlightWithEffect( 288 highlight, 289 "zoom", 290 () => { 291 let style = window.getComputedStyle(highlight); 292 is( 293 style.animationName, 294 "uitour-zoom", 295 "The animation-name should be uitour-zoom" 296 ); 297 checkSameEffectOnDifferentTarget(); 298 }, 299 "There should be a zoom effect" 300 ); 301 } 302 function checkSameEffectOnDifferentTarget() { 303 gContentAPI.showHighlight("appMenu", "wobble"); 304 waitForHighlightWithEffect( 305 highlight, 306 "wobble", 307 () => { 308 highlight.addEventListener( 309 "animationstart", 310 function () { 311 ok( 312 true, 313 "Animation occurred again even though the effect was the same" 314 ); 315 checkRandomEffect(); 316 }, 317 { once: true } 318 ); 319 gContentAPI.showHighlight("backForward", "wobble"); 320 }, 321 "There should be a wobble effect" 322 ); 323 } 324 function checkRandomEffect() { 325 function waitForActiveHighlight(highlightEl, next, error) { 326 return waitForCondition( 327 () => highlightEl.hasAttribute("active"), 328 next, 329 error 330 ); 331 } 332 333 gContentAPI.hideHighlight(); 334 gContentAPI.showHighlight("urlbar", "random"); 335 waitForActiveHighlight( 336 highlight, 337 () => { 338 ok( 339 highlight.hasAttribute("active"), 340 "The highlight should be active" 341 ); 342 isnot( 343 highlight.getAttribute("active"), 344 "none", 345 "A random effect other than none should have been chosen" 346 ); 347 isnot( 348 highlight.getAttribute("active"), 349 "random", 350 "The random effect shouldn't be 'random'" 351 ); 352 isnot( 353 UITour.highlightEffects.indexOf(highlight.getAttribute("active")), 354 -1, 355 "Check that a supported effect was randomly chosen" 356 ); 357 done(); 358 }, 359 "There should be an active highlight with a random effect" 360 ); 361 } 362 363 let highlight = document.getElementById("UITourHighlight"); 364 is_element_hidden(highlight, "Highlight should initially be hidden"); 365 366 gContentAPI.showHighlight("urlbar"); 367 waitForElementToBeVisible( 368 highlight, 369 checkDefaultEffect, 370 "Highlight should be shown after showHighlight()" 371 ); 372 }, 373 function test_highlight_effect_unsupported(done) { 374 function checkUnsupportedEffect() { 375 is( 376 highlight.getAttribute("active"), 377 "none", 378 "No effect should be used when an unsupported effect is requested" 379 ); 380 done(); 381 } 382 383 let highlight = document.getElementById("UITourHighlight"); 384 is_element_hidden(highlight, "Highlight should initially be hidden"); 385 386 gContentAPI.showHighlight("urlbar", "__UNSUPPORTED__"); 387 waitForElementToBeVisible( 388 highlight, 389 checkUnsupportedEffect, 390 "Highlight should be shown after showHighlight()" 391 ); 392 }, 393 function test_info_1(done) { 394 let popup = document.getElementById("UITourTooltip"); 395 let title = document.getElementById("UITourTooltipTitle"); 396 let desc = document.getElementById("UITourTooltipDescription"); 397 let icon = document.getElementById("UITourTooltipIcon"); 398 let buttons = document.getElementById("UITourTooltipButtons"); 399 400 popup.addEventListener( 401 "popupshown", 402 function () { 403 is( 404 popup.anchorNode, 405 document.getElementById("urlbar"), 406 "Popup should be anchored to the urlbar" 407 ); 408 is(title.textContent, "test title", "Popup should have correct title"); 409 is( 410 desc.textContent, 411 "test text", 412 "Popup should have correct description text" 413 ); 414 is(icon.src, "", "Popup should have no icon"); 415 is(buttons.hasChildNodes(), false, "Popup should have no buttons"); 416 417 popup.addEventListener( 418 "popuphidden", 419 function () { 420 popup.addEventListener( 421 "popupshown", 422 function () { 423 done(); 424 }, 425 { once: true } 426 ); 427 428 gContentAPI.showInfo("urlbar", "test title", "test text"); 429 }, 430 { once: true } 431 ); 432 gContentAPI.hideInfo(); 433 }, 434 { once: true } 435 ); 436 437 gContentAPI.showInfo("urlbar", "test title", "test text"); 438 }, 439 taskify(async function test_info_2() { 440 let popup = document.getElementById("UITourTooltip"); 441 let title = document.getElementById("UITourTooltipTitle"); 442 let desc = document.getElementById("UITourTooltipDescription"); 443 let icon = document.getElementById("UITourTooltipIcon"); 444 let buttons = document.getElementById("UITourTooltipButtons"); 445 446 await showInfoPromise("urlbar", "urlbar title", "urlbar text"); 447 448 is( 449 popup.anchorNode, 450 document.getElementById("urlbar"), 451 "Popup should be anchored to the urlbar" 452 ); 453 is(title.textContent, "urlbar title", "Popup should have correct title"); 454 is( 455 desc.textContent, 456 "urlbar text", 457 "Popup should have correct description text" 458 ); 459 is(icon.src, "", "Popup should have no icon"); 460 is(buttons.hasChildNodes(), false, "Popup should have no buttons"); 461 462 // Place the search bar in the navigation toolbar temporarily. 463 await gCUITestUtils.addSearchBar(); 464 465 await showInfoPromise("search", "search title", "search text"); 466 467 is( 468 popup.anchorNode, 469 Services.prefs.getBoolPref("browser.search.widget.new") 470 ? document.getElementById("searchbar-new") 471 : document.getElementById("searchbar"), 472 "Popup should be anchored to the searchbar" 473 ); 474 is(title.textContent, "search title", "Popup should have correct title"); 475 is( 476 desc.textContent, 477 "search text", 478 "Popup should have correct description text" 479 ); 480 481 gCUITestUtils.removeSearchBar(); 482 }), 483 function test_getConfigurationVersion(done) { 484 function callback(result) { 485 Assert.notStrictEqual( 486 typeof result.version, 487 "undefined", 488 "Check version isn't undefined." 489 ); 490 is( 491 result.version, 492 Services.appinfo.version, 493 "Should have the same version property." 494 ); 495 is( 496 result.defaultUpdateChannel, 497 UpdateUtils.getUpdateChannel(false), 498 "Should have the correct update channel." 499 ); 500 done(); 501 } 502 503 gContentAPI.getConfiguration("appinfo", callback); 504 }, 505 function test_getConfigurationDistribution(done) { 506 gContentAPI.getConfiguration("appinfo", result => { 507 Assert.notStrictEqual( 508 typeof result.distribution, 509 "undefined", 510 "Check distribution isn't undefined." 511 ); 512 // distribution id defaults to "default" for most builds, and 513 // "mozilla-MSIX" for MSIX builds. 514 is( 515 result.distribution, 516 AppConstants.platform === "win" && 517 Services.sysinfo.getProperty("hasWinPackageId") 518 ? "mozilla-MSIX" 519 : "default", 520 'Should be "default" without preference set.' 521 ); 522 523 let defaults = Services.prefs.getDefaultBranch("distribution."); 524 let testDistributionID = "TestDistribution"; 525 defaults.setCharPref("id", testDistributionID); 526 gContentAPI.getConfiguration("appinfo", result2 => { 527 Assert.notStrictEqual( 528 typeof result2.distribution, 529 "undefined", 530 "Check distribution isn't undefined." 531 ); 532 is( 533 result2.distribution, 534 testDistributionID, 535 "Should have the distribution as set in preference." 536 ); 537 538 done(); 539 }); 540 }); 541 }, 542 function test_getConfigurationProfileAge(done) { 543 gContentAPI.getConfiguration("appinfo", result => { 544 Assert.strictEqual( 545 typeof result.profileCreatedWeeksAgo, 546 "number", 547 "profileCreatedWeeksAgo should be number." 548 ); 549 Assert.strictEqual( 550 result.profileResetWeeksAgo, 551 null, 552 "profileResetWeeksAgo should be null." 553 ); 554 555 // Set profile reset date to 15 days ago. 556 ProfileAge().then(profileAccessor => { 557 profileAccessor.recordProfileReset( 558 Date.now() - 15 * 24 * 60 * 60 * 1000 559 ); 560 gContentAPI.getConfiguration("appinfo", result2 => { 561 Assert.strictEqual( 562 typeof result2.profileResetWeeksAgo, 563 "number", 564 "profileResetWeeksAgo should be number." 565 ); 566 is( 567 result2.profileResetWeeksAgo, 568 2, 569 "profileResetWeeksAgo should be 2." 570 ); 571 done(); 572 }); 573 }); 574 }); 575 }, 576 function test_addToolbarButton(done) { 577 let placement = CustomizableUI.getPlacementOfWidget("panic-button"); 578 is(placement, null, "default UI has panic button in the palette"); 579 580 gContentAPI.getConfiguration("availableTargets", data => { 581 let available = data.targets.includes("forget"); 582 ok(!available, "Forget button should not be available by default"); 583 584 gContentAPI.addNavBarWidget("forget", () => { 585 info("addNavBarWidget callback successfully called"); 586 587 let updatedPlacement = 588 CustomizableUI.getPlacementOfWidget("panic-button"); 589 is(updatedPlacement.area, CustomizableUI.AREA_NAVBAR); 590 591 gContentAPI.getConfiguration("availableTargets", data2 => { 592 let updatedAvailable = data2.targets.includes("forget"); 593 ok(updatedAvailable, "Forget button should now be available"); 594 595 // Cleanup 596 CustomizableUI.removeWidgetFromArea("panic-button"); 597 done(); 598 }); 599 }); 600 }); 601 }, 602 taskify(async function test_search() { 603 let defaultEngine = await Services.search.getDefault(); 604 let visibleEngines = await Services.search.getVisibleEngines(); 605 let expectedEngines = visibleEngines 606 .filter(engine => engine.isAppProvided) 607 .map(engine => "searchEngine-" + engine.id); 608 609 let data = await new Promise(resolve => 610 gContentAPI.getConfiguration("search", resolve) 611 ); 612 let engines = data.engines; 613 ok(Array.isArray(engines), "data.engines should be an array"); 614 is( 615 engines.sort().toString(), 616 expectedEngines.sort().toString(), 617 "Engines should be as expected" 618 ); 619 620 is( 621 data.searchEngineIdentifier, 622 defaultEngine.id, 623 "the searchEngineIdentifier property should contain the defaultEngine's id" 624 ); 625 626 let someOtherEngineID = data.engines.filter( 627 t => t != "searchEngine-" + defaultEngine.id 628 )[0]; 629 someOtherEngineID = someOtherEngineID.replace(/^searchEngine-/, ""); 630 631 Services.telemetry.clearEvents(); 632 Services.fog.testResetFOG(); 633 634 await new Promise(resolve => { 635 let observe = function (subject, topic, verb) { 636 Services.obs.removeObserver(observe, "browser-search-engine-modified"); 637 info("browser-search-engine-modified: " + verb); 638 if (verb == "engine-default") { 639 is( 640 Services.search.defaultEngine.id, 641 someOtherEngineID, 642 "correct engine was switched to" 643 ); 644 resolve(); 645 } 646 }; 647 Services.obs.addObserver(observe, "browser-search-engine-modified"); 648 registerCleanupFunction(async () => { 649 await Services.search.setDefault( 650 defaultEngine, 651 Ci.nsISearchService.CHANGE_REASON_UNKNOWN 652 ); 653 }); 654 655 gContentAPI.setDefaultSearchEngine(someOtherEngineID); 656 }); 657 658 let engine = Services.search.getEngineById(someOtherEngineID); 659 660 let submissionUrl = engine 661 .getSubmission("dummy") 662 .uri.spec.replace("dummy", ""); 663 664 let snapshot = await Glean.searchEngineDefault.changed.testGetValue(); 665 delete snapshot[0].timestamp; 666 Assert.deepEqual( 667 snapshot[0], 668 { 669 category: "search.engine.default", 670 name: "changed", 671 extra: { 672 change_reason: "uitour", 673 previous_engine_id: defaultEngine.telemetryId, 674 new_engine_id: engine.telemetryId, 675 new_display_name: engine.name, 676 new_load_path: engine.wrappedJSObject._loadPath, 677 // Glean has a limit of 100 characters. 678 new_submission_url: submissionUrl.slice(0, 100), 679 }, 680 }, 681 "Should have received the correct event details" 682 ); 683 }), 684 taskify(async function test_treatment_tag() { 685 await gContentAPI.setTreatmentTag("foobar", "baz"); 686 await gContentAPI.getTreatmentTag("foobar", data => { 687 is(data.value, "baz", "set and retrieved treatmentTag"); 688 }); 689 }), 690 691 // Make sure this test is last in the file so the appMenu gets left open and done will confirm it got tore down. 692 taskify(async function cleanupMenus() { 693 let shownPromise = promisePanelShown(window); 694 gContentAPI.showMenu("appMenu"); 695 await shownPromise; 696 }), 697 ];