tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 });