browser_ext_browserAction_pageAction_icon.js (19691B)
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set sts=2 sw=2 et tw=80: */ 3 "use strict"; 4 5 function testHiDpiImage(button, images1x, images2x, prop) { 6 const image = getRawListStyleImage(button); 7 info(image); 8 info(button.outerHTML); 9 const image1x = images1x[prop]; 10 const image2x = images2x[prop]; 11 const backgroundImage = 12 image1x === image2x && prop === "browserActionImageURL" 13 ? `url("${image1x}")` 14 : `image-set(url("${image1x}") 1dppx, url("${image2x}") 2dppx)`; 15 16 is(image, backgroundImage, prop); 17 } 18 19 // Test that various combinations of icon details specs, for both paths 20 // and ImageData objects, result in the correct image being displayed in 21 // all display resolutions. 22 add_task(async function testDetailsObjects() { 23 function background() { 24 function getImageData(color) { 25 let canvas = document.createElement("canvas"); 26 canvas.width = 2; 27 canvas.height = 2; 28 let canvasContext = canvas.getContext("2d"); 29 30 canvasContext.clearRect(0, 0, canvas.width, canvas.height); 31 canvasContext.fillStyle = color; 32 canvasContext.fillRect(0, 0, 1, 1); 33 34 return { 35 url: canvas.toDataURL("image/png"), 36 imageData: canvasContext.getImageData( 37 0, 38 0, 39 canvas.width, 40 canvas.height 41 ), 42 }; 43 } 44 45 let imageData = { 46 red: getImageData("red"), 47 green: getImageData("green"), 48 }; 49 50 // eslint-disable indent, indent-legacy 51 let iconDetails = [ 52 // Only paths. 53 { 54 details: { path: "a.png" }, 55 resolutions: { 56 1: { 57 browserActionImageURL: browser.runtime.getURL("data/a.png"), 58 pageActionImageURL: browser.runtime.getURL("data/a.png"), 59 }, 60 2: { 61 browserActionImageURL: browser.runtime.getURL("data/a.png"), 62 pageActionImageURL: browser.runtime.getURL("data/a.png"), 63 }, 64 }, 65 }, 66 { 67 details: { path: "/a.png" }, 68 resolutions: { 69 1: { 70 browserActionImageURL: browser.runtime.getURL("a.png"), 71 pageActionImageURL: browser.runtime.getURL("a.png"), 72 }, 73 2: { 74 browserActionImageURL: browser.runtime.getURL("a.png"), 75 pageActionImageURL: browser.runtime.getURL("a.png"), 76 }, 77 }, 78 }, 79 { 80 details: { path: { 19: "a.png" } }, 81 resolutions: { 82 1: { 83 browserActionImageURL: browser.runtime.getURL("data/a.png"), 84 pageActionImageURL: browser.runtime.getURL("data/a.png"), 85 }, 86 2: { 87 browserActionImageURL: browser.runtime.getURL("data/a.png"), 88 pageActionImageURL: browser.runtime.getURL("data/a.png"), 89 }, 90 }, 91 }, 92 { 93 details: { path: { 38: "a.png" } }, 94 resolutions: { 95 1: { 96 browserActionImageURL: browser.runtime.getURL("data/a.png"), 97 pageActionImageURL: browser.runtime.getURL("data/a.png"), 98 }, 99 2: { 100 browserActionImageURL: browser.runtime.getURL("data/a.png"), 101 pageActionImageURL: browser.runtime.getURL("data/a.png"), 102 }, 103 }, 104 }, 105 { 106 details: { path: { 19: "a.png", 38: "a-x2.png" } }, 107 resolutions: { 108 1: { 109 browserActionImageURL: browser.runtime.getURL("data/a.png"), 110 pageActionImageURL: browser.runtime.getURL("data/a.png"), 111 }, 112 2: { 113 browserActionImageURL: browser.runtime.getURL("data/a-x2.png"), 114 pageActionImageURL: browser.runtime.getURL("data/a-x2.png"), 115 }, 116 }, 117 }, 118 { 119 details: { 120 path: { 16: "a-16.png", 32: "a-32.png", 64: "a-64.png" }, 121 }, 122 resolutions: { 123 1: { 124 browserActionImageURL: browser.runtime.getURL("data/a-16.png"), 125 pageActionImageURL: browser.runtime.getURL("data/a-16.png"), 126 }, 127 2: { 128 browserActionImageURL: browser.runtime.getURL("data/a-32.png"), 129 pageActionImageURL: browser.runtime.getURL("data/a-32.png"), 130 }, 131 }, 132 }, 133 134 // Test that CSS strings are escaped properly. 135 { 136 details: { path: 'a.png#" \\' }, 137 resolutions: { 138 1: { 139 browserActionImageURL: browser.runtime.getURL( 140 "data/a.png#%22%20%5C" 141 ), 142 pageActionImageURL: browser.runtime.getURL("data/a.png#%22%20%5C"), 143 }, 144 2: { 145 browserActionImageURL: browser.runtime.getURL( 146 "data/a.png#%22%20%5C" 147 ), 148 pageActionImageURL: browser.runtime.getURL("data/a.png#%22%20%5C"), 149 }, 150 }, 151 }, 152 153 // Only ImageData objects. 154 { 155 details: { imageData: imageData.red.imageData }, 156 resolutions: { 157 1: { 158 browserActionImageURL: imageData.red.url, 159 pageActionImageURL: imageData.red.url, 160 }, 161 2: { 162 browserActionImageURL: imageData.red.url, 163 pageActionImageURL: imageData.red.url, 164 }, 165 }, 166 }, 167 { 168 details: { imageData: { 19: imageData.red.imageData } }, 169 resolutions: { 170 1: { 171 browserActionImageURL: imageData.red.url, 172 pageActionImageURL: imageData.red.url, 173 }, 174 2: { 175 browserActionImageURL: imageData.red.url, 176 pageActionImageURL: imageData.red.url, 177 }, 178 }, 179 }, 180 { 181 details: { imageData: { 38: imageData.red.imageData } }, 182 resolutions: { 183 1: { 184 browserActionImageURL: imageData.red.url, 185 pageActionImageURL: imageData.red.url, 186 }, 187 2: { 188 browserActionImageURL: imageData.red.url, 189 pageActionImageURL: imageData.red.url, 190 }, 191 }, 192 }, 193 { 194 details: { 195 imageData: { 196 19: imageData.red.imageData, 197 38: imageData.green.imageData, 198 }, 199 }, 200 resolutions: { 201 1: { 202 browserActionImageURL: imageData.red.url, 203 pageActionImageURL: imageData.red.url, 204 }, 205 2: { 206 browserActionImageURL: imageData.green.url, 207 pageActionImageURL: imageData.green.url, 208 }, 209 }, 210 }, 211 212 // Mixed path and imageData objects. 213 // 214 // The behavior is currently undefined if both |path| and 215 // |imageData| specify icons of the same size. 216 { 217 details: { 218 path: { 19: "a.png" }, 219 imageData: { 38: imageData.red.imageData }, 220 }, 221 resolutions: { 222 1: { 223 browserActionImageURL: browser.runtime.getURL("data/a.png"), 224 pageActionImageURL: browser.runtime.getURL("data/a.png"), 225 }, 226 2: { 227 browserActionImageURL: imageData.red.url, 228 pageActionImageURL: imageData.red.url, 229 }, 230 }, 231 }, 232 { 233 details: { 234 path: { 38: "a.png" }, 235 imageData: { 19: imageData.red.imageData }, 236 }, 237 resolutions: { 238 1: { 239 browserActionImageURL: imageData.red.url, 240 pageActionImageURL: imageData.red.url, 241 }, 242 2: { 243 browserActionImageURL: browser.runtime.getURL("data/a.png"), 244 pageActionImageURL: browser.runtime.getURL("data/a.png"), 245 }, 246 }, 247 }, 248 249 // A path or ImageData object by itself is treated as a 19px icon. 250 { 251 details: { 252 path: "a.png", 253 imageData: { 38: imageData.red.imageData }, 254 }, 255 resolutions: { 256 1: { 257 browserActionImageURL: browser.runtime.getURL("data/a.png"), 258 pageActionImageURL: browser.runtime.getURL("data/a.png"), 259 }, 260 2: { 261 browserActionImageURL: imageData.red.url, 262 pageActionImageURL: imageData.red.url, 263 }, 264 }, 265 }, 266 { 267 details: { 268 path: { 38: "a.png" }, 269 imageData: imageData.red.imageData, 270 }, 271 resolutions: { 272 1: { 273 browserActionImageURL: imageData.red.url, 274 pageActionImageURL: imageData.red.url, 275 }, 276 2: { 277 browserActionImageURL: browser.runtime.getURL("data/a.png"), 278 pageActionImageURL: browser.runtime.getURL("data/a.png"), 279 }, 280 }, 281 }, 282 283 // Various resolutions 284 { 285 details: { path: { 18: "a.png", 36: "a-x2.png" } }, 286 resolutions: { 287 1: { 288 browserActionImageURL: browser.runtime.getURL("data/a.png"), 289 pageActionImageURL: browser.runtime.getURL("data/a.png"), 290 }, 291 2: { 292 browserActionImageURL: browser.runtime.getURL("data/a-x2.png"), 293 pageActionImageURL: browser.runtime.getURL("data/a-x2.png"), 294 }, 295 }, 296 }, 297 { 298 details: { path: { 16: "a.png", 30: "a-x2.png" } }, 299 resolutions: { 300 1: { 301 browserActionImageURL: browser.runtime.getURL("data/a.png"), 302 pageActionImageURL: browser.runtime.getURL("data/a.png"), 303 }, 304 2: { 305 browserActionImageURL: browser.runtime.getURL("data/a-x2.png"), 306 pageActionImageURL: browser.runtime.getURL("data/a-x2.png"), 307 }, 308 }, 309 }, 310 { 311 details: { path: { 16: "16.png", 100: "100.png" } }, 312 resolutions: { 313 1: { 314 browserActionImageURL: browser.runtime.getURL("data/16.png"), 315 pageActionImageURL: browser.runtime.getURL("data/16.png"), 316 }, 317 2: { 318 browserActionImageURL: browser.runtime.getURL("data/100.png"), 319 pageActionImageURL: browser.runtime.getURL("data/100.png"), 320 }, 321 }, 322 }, 323 { 324 details: { path: { 2: "2.png" } }, 325 resolutions: { 326 1: { 327 browserActionImageURL: browser.runtime.getURL("data/2.png"), 328 pageActionImageURL: browser.runtime.getURL("data/2.png"), 329 }, 330 2: { 331 browserActionImageURL: browser.runtime.getURL("data/2.png"), 332 pageActionImageURL: browser.runtime.getURL("data/2.png"), 333 }, 334 }, 335 }, 336 { 337 details: { 338 path: { 339 16: "16.svg", 340 18: "18.svg", 341 }, 342 }, 343 resolutions: { 344 1: { 345 browserActionImageURL: browser.runtime.getURL("data/16.svg"), 346 pageActionImageURL: browser.runtime.getURL("data/16.svg"), 347 }, 348 2: { 349 browserActionImageURL: browser.runtime.getURL("data/18.svg"), 350 pageActionImageURL: browser.runtime.getURL("data/18.svg"), 351 }, 352 }, 353 }, 354 { 355 details: { 356 path: { 357 6: "6.png", 358 18: "18.png", 359 36: "36.png", 360 48: "48.png", 361 128: "128.png", 362 }, 363 }, 364 resolutions: { 365 1: { 366 browserActionImageURL: browser.runtime.getURL("data/18.png"), 367 pageActionImageURL: browser.runtime.getURL("data/18.png"), 368 }, 369 2: { 370 browserActionImageURL: browser.runtime.getURL("data/36.png"), 371 pageActionImageURL: browser.runtime.getURL("data/36.png"), 372 }, 373 }, 374 menuResolutions: { 375 1: browser.runtime.getURL("data/36.png"), 376 2: browser.runtime.getURL("data/128.png"), 377 }, 378 }, 379 { 380 details: { 381 path: { 382 16: "16.png", 383 18: "18.png", 384 32: "32.png", 385 48: "48.png", 386 64: "64.png", 387 128: "128.png", 388 }, 389 }, 390 resolutions: { 391 1: { 392 browserActionImageURL: browser.runtime.getURL("data/16.png"), 393 pageActionImageURL: browser.runtime.getURL("data/16.png"), 394 }, 395 2: { 396 browserActionImageURL: browser.runtime.getURL("data/32.png"), 397 pageActionImageURL: browser.runtime.getURL("data/32.png"), 398 }, 399 }, 400 menuResolutions: { 401 1: browser.runtime.getURL("data/32.png"), 402 2: browser.runtime.getURL("data/64.png"), 403 }, 404 }, 405 { 406 details: { 407 path: { 408 18: "18.png", 409 32: "32.png", 410 48: "48.png", 411 128: "128.png", 412 }, 413 }, 414 resolutions: { 415 1: { 416 browserActionImageURL: browser.runtime.getURL("data/32.png"), 417 pageActionImageURL: browser.runtime.getURL("data/32.png"), 418 }, 419 2: { 420 browserActionImageURL: browser.runtime.getURL("data/32.png"), 421 pageActionImageURL: browser.runtime.getURL("data/32.png"), 422 }, 423 }, 424 }, 425 ]; 426 /* eslint-enable indent, indent-legacy */ 427 428 // Allow serializing ImageData objects for logging. 429 ImageData.prototype.toJSON = () => "<ImageData>"; 430 431 let tabId; 432 433 browser.test.onMessage.addListener((msg, test) => { 434 if (msg != "setIcon") { 435 browser.test.fail("expecting 'setIcon' message"); 436 } 437 438 let details = iconDetails[test.index]; 439 440 let detailString = JSON.stringify(details); 441 browser.test.log( 442 `Setting browserAction/pageAction to ${detailString} expecting URLs ${JSON.stringify( 443 details.resolutions 444 )}` 445 ); 446 447 Promise.all([ 448 browser.browserAction.setIcon( 449 Object.assign({ tabId }, details.details) 450 ), 451 browser.pageAction.setIcon(Object.assign({ tabId }, details.details)), 452 ]).then(() => { 453 browser.test.sendMessage("iconSet"); 454 }); 455 }); 456 457 // Generate a list of tests and resolutions to send back to the test 458 // context. 459 // 460 // This process is a bit convoluted, because the outer test context needs 461 // to handle checking the button nodes and changing the screen resolution, 462 // but it can't pass us icon definitions with ImageData objects. This 463 // shouldn't be a problem, since structured clones should handle ImageData 464 // objects without issue. Unfortunately, |cloneInto| implements a slightly 465 // different algorithm than we use in web APIs, and does not handle them 466 // correctly. 467 let tests = []; 468 for (let [idx, icon] of iconDetails.entries()) { 469 tests.push({ 470 index: idx, 471 menuResolutions: icon.menuResolutions, 472 resolutions: icon.resolutions, 473 }); 474 } 475 476 // Sort by resolution, so we don't needlessly switch back and forth 477 // between each test. 478 tests.sort(test => test.resolution); 479 480 browser.tabs.query({ active: true, currentWindow: true }, tabs => { 481 tabId = tabs[0].id; 482 browser.pageAction.show(tabId).then(() => { 483 browser.test.sendMessage("ready", tests); 484 }); 485 }); 486 } 487 488 let extension = ExtensionTestUtils.loadExtension({ 489 manifest: { 490 browser_action: { 491 default_area: "navbar", 492 }, 493 page_action: {}, 494 background: { 495 page: "data/background.html", 496 }, 497 }, 498 499 files: { 500 "data/background.html": `<script src="background.js"></script>`, 501 "data/background.js": background, 502 503 "data/16.svg": imageBuffer, 504 "data/18.svg": imageBuffer, 505 506 "data/16.png": imageBuffer, 507 "data/18.png": imageBuffer, 508 "data/32.png": imageBuffer, 509 "data/36.png": imageBuffer, 510 "data/48.png": imageBuffer, 511 "data/64.png": imageBuffer, 512 "data/128.png": imageBuffer, 513 514 "a.png": imageBuffer, 515 "data/2.png": imageBuffer, 516 "data/100.png": imageBuffer, 517 "data/a.png": imageBuffer, 518 "data/a-x2.png": imageBuffer, 519 }, 520 }); 521 522 await extension.startup(); 523 524 let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID( 525 makeWidgetId(extension.id) 526 ); 527 let browserActionWidget = getBrowserActionWidget(extension); 528 529 let tests = await extension.awaitMessage("ready"); 530 await promiseAnimationFrame(); 531 532 // The initial icon should be the default icon since no icon is in the manifest. 533 const DEFAULT_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg"; 534 let browserActionButton = browserActionWidget 535 .forWindow(window) 536 .node.querySelector(".unified-extensions-item-action-button"); 537 let pageActionImage = document.getElementById(pageActionId); 538 is( 539 getListStyleImage(browserActionButton), 540 DEFAULT_ICON, 541 `browser action has the correct default image` 542 ); 543 is( 544 getListStyleImage(pageActionImage), 545 DEFAULT_ICON, 546 `page action has the correct default image` 547 ); 548 549 for (let test of tests) { 550 extension.sendMessage("setIcon", test); 551 await extension.awaitMessage("iconSet"); 552 553 await promiseAnimationFrame(); 554 555 testHiDpiImage( 556 browserActionButton, 557 test.resolutions[1], 558 test.resolutions[2], 559 "browserActionImageURL" 560 ); 561 testHiDpiImage( 562 pageActionImage, 563 test.resolutions[1], 564 test.resolutions[2], 565 "pageActionImageURL" 566 ); 567 568 if (!test.menuResolutions) { 569 continue; 570 } 571 } 572 573 await extension.unload(); 574 }); 575 576 // NOTE: The current goal of this test is ensuring that Bug 1397196 has been fixed, 577 // and so this test extension manifest has a browser action which specify 578 // a theme based icon and a pageAction, both the pageAction and the browserAction 579 // have a common default_icon. 580 // 581 // Once Bug 1398156 will be fixed, this test should be converted into testing that 582 // the browserAction and pageAction themed icons (as well as any other cached icon, 583 // e.g. the sidebar and devtools panel icons) can be specified in the same extension 584 // and do not conflict with each other. 585 // 586 // This test currently fails without the related fix, but only if the browserAction 587 // API has been already loaded before the pageAction, otherwise the icons will be cached 588 // in the opposite order and the test is not able to reproduce the issue anymore. 589 add_task(async function testPageActionIconLoadingOnBrowserActionThemedIcon() { 590 async function background() { 591 const tabs = await browser.tabs.query({ active: true }); 592 await browser.pageAction.show(tabs[0].id); 593 594 browser.test.sendMessage("ready"); 595 } 596 597 const extension = ExtensionTestUtils.loadExtension({ 598 background, 599 manifest: { 600 name: "Foo Extension", 601 602 browser_action: { 603 default_icon: "common_cached_icon.png", 604 default_popup: "default_popup.html", 605 default_title: "BrowserAction title", 606 default_area: "navbar", 607 theme_icons: [ 608 { 609 dark: "1.png", 610 light: "2.png", 611 size: 16, 612 }, 613 ], 614 }, 615 616 page_action: { 617 default_icon: "common_cached_icon.png", 618 default_popup: "default_popup.html", 619 default_title: "PageAction title", 620 }, 621 622 permissions: ["tabs"], 623 }, 624 625 files: { 626 "common_cached_icon.png": imageBuffer, 627 "1.png": imageBuffer, 628 "2.png": imageBuffer, 629 "default_popup.html": "<!DOCTYPE html><html><body>popup</body></html>", 630 }, 631 }); 632 633 await extension.startup(); 634 635 await extension.awaitMessage("ready"); 636 637 await promiseAnimationFrame(); 638 639 let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID( 640 makeWidgetId(extension.id) 641 ); 642 let pageActionImage = document.getElementById(pageActionId); 643 644 const iconURL = new URL(getListStyleImage(pageActionImage)); 645 646 is( 647 iconURL.pathname, 648 "/common_cached_icon.png", 649 "Got the expected pageAction icon url" 650 ); 651 652 await extension.unload(); 653 });