tor-browser

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

browser_caching_attributes.js (27207B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /* import-globals-from ../../mochitest/attributes.js */
      8 loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
      9 
     10 /**
     11 * Default textbox accessible attributes.
     12 */
     13 const defaultAttributes = {
     14  "margin-top": "0px",
     15  "margin-right": "0px",
     16  "margin-bottom": "0px",
     17  "margin-left": "0px",
     18  "text-align": "start",
     19  "text-indent": "0px",
     20  id: "textbox",
     21  tag: "input",
     22  display: "inline-block",
     23 };
     24 
     25 /**
     26 * Test data has the format of:
     27 * {
     28 *   desc        {String}         description for better logging
     29 *   expected    {Object}         expected attributes for given accessibles
     30 *   unexpected  {Object}         unexpected attributes for given accessibles
     31 *
     32 *   action      {?AsyncFunction} an optional action that awaits a change in
     33 *                                attributes
     34 *   attrs       {?Array}         an optional list of attributes to update
     35 *   waitFor     {?Number}        an optional event to wait for
     36 * }
     37 */
     38 const attributesTests = [
     39  {
     40    desc: "Initiall accessible attributes",
     41    expected: defaultAttributes,
     42    unexpected: {
     43      "line-number": "1",
     44      "explicit-name": "true",
     45      "container-live": "polite",
     46      live: "polite",
     47    },
     48  },
     49  {
     50    desc: "@line-number attribute is present when textbox is focused",
     51    async action(browser) {
     52      await invokeFocus(browser, "textbox");
     53    },
     54    waitFor: EVENT_FOCUS,
     55    expected: Object.assign({}, defaultAttributes, { "line-number": "1" }),
     56    unexpected: {
     57      "explicit-name": "true",
     58      "container-live": "polite",
     59      live: "polite",
     60    },
     61  },
     62  {
     63    desc: "@aria-live sets container-live and live attributes",
     64    attrs: [
     65      {
     66        attr: "aria-live",
     67        value: "polite",
     68      },
     69    ],
     70    expected: Object.assign({}, defaultAttributes, {
     71      "line-number": "1",
     72      "container-live": "polite",
     73      live: "polite",
     74    }),
     75    unexpected: {
     76      "explicit-name": "true",
     77    },
     78  },
     79  {
     80    desc: "@title attribute sets explicit-name attribute to true",
     81    attrs: [
     82      {
     83        attr: "title",
     84        value: "textbox",
     85      },
     86    ],
     87    expected: Object.assign({}, defaultAttributes, {
     88      "line-number": "1",
     89      "explicit-name": "true",
     90      "container-live": "polite",
     91      live: "polite",
     92    }),
     93    unexpected: {},
     94  },
     95 ];
     96 
     97 /**
     98 * Test caching of accessible object attributes
     99 */
    100 addAccessibleTask(
    101  `
    102  <input id="textbox" value="hello">`,
    103  async function (browser, accDoc) {
    104    let textbox = findAccessibleChildByID(accDoc, "textbox");
    105    for (let {
    106      desc,
    107      action,
    108      attrs,
    109      expected,
    110      waitFor,
    111      unexpected,
    112    } of attributesTests) {
    113      info(desc);
    114      let onUpdate;
    115 
    116      if (waitFor) {
    117        onUpdate = waitForEvent(waitFor, "textbox");
    118      }
    119 
    120      if (action) {
    121        await action(browser);
    122      } else if (attrs) {
    123        for (let { attr, value } of attrs) {
    124          await invokeSetAttribute(browser, "textbox", attr, value);
    125        }
    126      }
    127 
    128      await onUpdate;
    129      testAttrs(textbox, expected);
    130      testAbsentAttrs(textbox, unexpected);
    131    }
    132  },
    133  {
    134    // These tests don't work yet with the parent process cache.
    135    topLevel: false,
    136    iframe: false,
    137    remoteIframe: false,
    138  }
    139 );
    140 
    141 /**
    142 * Test caching of the tag attribute.
    143 */
    144 addAccessibleTask(
    145  `
    146 <p id="p">text</p>
    147 <textarea id="textarea"></textarea>
    148  `,
    149  async function (browser, docAcc) {
    150    testAttrs(docAcc, { tag: "body" }, true);
    151    const p = findAccessibleChildByID(docAcc, "p");
    152    testAttrs(p, { tag: "p" }, true);
    153    const textLeaf = p.firstChild;
    154    testAbsentAttrs(textLeaf, { tag: "" });
    155    const textarea = findAccessibleChildByID(docAcc, "textarea");
    156    testAttrs(textarea, { tag: "textarea" }, true);
    157  },
    158  { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
    159 );
    160 
    161 /**
    162 * Test caching of the text-input-type attribute.
    163 */
    164 addAccessibleTask(
    165  `
    166  <input id="default">
    167  <input id="email" type="email">
    168  <input id="password" type="password">
    169  <input id="text" type="text">
    170  <input id="date" type="date">
    171  <input id="time" type="time">
    172  <input id="checkbox" type="checkbox">
    173  <input id="radio" type="radio">
    174  `,
    175  async function (browser, docAcc) {
    176    function testInputType(id, inputType) {
    177      if (inputType == undefined) {
    178        testAbsentAttrs(findAccessibleChildByID(docAcc, id), {
    179          "text-input-type": "",
    180        });
    181      } else {
    182        testAttrs(
    183          findAccessibleChildByID(docAcc, id),
    184          { "text-input-type": inputType },
    185          true
    186        );
    187      }
    188    }
    189 
    190    testInputType("default");
    191    testInputType("email", "email");
    192    testInputType("password", "password");
    193    testInputType("text", "text");
    194    testInputType("date", "date");
    195    testInputType("time", "time");
    196    testInputType("checkbox");
    197    testInputType("radio");
    198  },
    199  { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
    200 );
    201 
    202 /**
    203 * Test caching of the display attribute.
    204 */
    205 addAccessibleTask(
    206  `
    207 <div id="div">
    208  <ins id="ins">a</ins>
    209  <button id="button">b</button>
    210 </div>
    211 <p>
    212  <span id="presentationalSpan" role="none"
    213      style="display: block; position: absolute; top: 0; left: 0; translate: 1px;">
    214    a
    215  </span>
    216 </p>
    217  `,
    218  async function (browser, docAcc) {
    219    const div = findAccessibleChildByID(docAcc, "div");
    220    testAttrs(div, { display: "block" }, true);
    221    const ins = findAccessibleChildByID(docAcc, "ins");
    222    testAttrs(ins, { display: "inline" }, true);
    223    const textLeaf = ins.firstChild;
    224    testAbsentAttrs(textLeaf, { display: "" });
    225    const button = findAccessibleChildByID(docAcc, "button");
    226    testAttrs(button, { display: "inline-block" }, true);
    227 
    228    await invokeContentTask(browser, [], () => {
    229      content.document.getElementById("ins").style.display = "block";
    230      content.document.body.offsetTop; // Flush layout.
    231    });
    232    await untilCacheIs(
    233      () => ins.attributes.getStringProperty("display"),
    234      "block",
    235      "ins display attribute changed to block"
    236    );
    237 
    238    // This span has role="none", but we force a generic Accessible because it
    239    // has a transform. role="none" might have been used to avoid exposing
    240    // display: block, so ensure we don't expose that.
    241    const presentationalSpan = findAccessibleChildByID(
    242      docAcc,
    243      "presentationalSpan"
    244    );
    245    testAbsentAttrs(presentationalSpan, { display: "" });
    246  },
    247  { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
    248 );
    249 
    250 /**
    251 * Test that there is no display attribute on image map areas.
    252 */
    253 addAccessibleTask(
    254  `
    255 <map name="normalMap">
    256  <area id="normalArea" shape="default">
    257 </map>
    258 <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png" usemap="#normalMap">
    259 <audio>
    260  <map name="unslottedMap">
    261    <area id="unslottedArea" shape="default">
    262  </map>
    263 </audio>
    264 <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png" usemap="#unslottedMap">
    265  `,
    266  async function (browser, docAcc) {
    267    const normalArea = findAccessibleChildByID(docAcc, "normalArea");
    268    testAbsentAttrs(normalArea, { display: "" });
    269    const unslottedArea = findAccessibleChildByID(docAcc, "unslottedArea");
    270    testAbsentAttrs(unslottedArea, { display: "" });
    271  },
    272  { topLevel: true }
    273 );
    274 
    275 /**
    276 * Test caching of the explicit-name attribute.
    277 */
    278 addAccessibleTask(
    279  `
    280 <h1 id="h1">content</h1>
    281 <button id="buttonContent">content</button>
    282 <button id="buttonLabel" aria-label="label">content</button>
    283 <button id="buttonEmpty"></button>
    284 <button id="buttonSummary"><details><summary>test</summary></details></button>
    285 <div id="div"></div>
    286  `,
    287  async function (browser, docAcc) {
    288    const h1 = findAccessibleChildByID(docAcc, "h1");
    289    testAbsentAttrs(h1, { "explicit-name": "" });
    290    const buttonContent = findAccessibleChildByID(docAcc, "buttonContent");
    291    testAbsentAttrs(buttonContent, { "explicit-name": "" });
    292    const buttonLabel = findAccessibleChildByID(docAcc, "buttonLabel");
    293    testAttrs(buttonLabel, { "explicit-name": "true" }, true);
    294    const buttonEmpty = findAccessibleChildByID(docAcc, "buttonEmpty");
    295    testAbsentAttrs(buttonEmpty, { "explicit-name": "" });
    296    const buttonSummary = findAccessibleChildByID(docAcc, "buttonSummary");
    297    testAbsentAttrs(buttonSummary, { "explicit-name": "" });
    298    const div = findAccessibleChildByID(docAcc, "div");
    299    testAbsentAttrs(div, { "explicit-name": "" });
    300 
    301    info("Setting aria-label on h1");
    302    let nameChanged = waitForEvent(EVENT_NAME_CHANGE, h1);
    303    await invokeContentTask(browser, [], () => {
    304      content.document.getElementById("h1").setAttribute("aria-label", "label");
    305    });
    306    await nameChanged;
    307    testAttrs(h1, { "explicit-name": "true" }, true);
    308  },
    309  { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
    310 );
    311 
    312 /**
    313 * Test caching of ARIA attributes that are exposed via object attributes.
    314 */
    315 addAccessibleTask(
    316  `
    317 <div id="currentTrue" aria-current="true">currentTrue</div>
    318 <div id="currentFalse" aria-current="false">currentFalse</div>
    319 <div id="currentPage" aria-current="page">currentPage</div>
    320 <div id="currentBlah" aria-current="blah">currentBlah</div>
    321 <div id="haspopupMenu" aria-haspopup="menu">haspopup</div>
    322 <div id="rowColCountPositive" role="table" aria-rowcount="1000" aria-colcount="1000">
    323  <div role="row">
    324    <div id="rowColIndexPositive" role="cell" aria-rowindex="100" aria-colindex="100">positive</div>
    325  </div>
    326 </div>
    327 <div id="rowColCountNegative" role="table" aria-rowcount="-1" aria-colcount="-1">
    328  <div role="row">
    329    <div id="rowColIndexNegative" role="cell" aria-rowindex="-1" aria-colindex="-1">negative</div>
    330  </div>
    331 </div>
    332 <div id="rowColCountInvalid" role="table" aria-rowcount="z" aria-colcount="z">
    333  <div role="row">
    334    <div id="rowColIndexInvalid" role="cell" aria-rowindex="z" aria-colindex="z">invalid</div>
    335  </div>
    336 </div>
    337 <div id="foo" aria-foo="bar">foo</div>
    338 <div id="mutate" aria-current="true">mutate</div>
    339  `,
    340  async function (browser, docAcc) {
    341    const currentTrue = findAccessibleChildByID(docAcc, "currentTrue");
    342    testAttrs(currentTrue, { current: "true" }, true);
    343    const currentFalse = findAccessibleChildByID(docAcc, "currentFalse");
    344    testAbsentAttrs(currentFalse, { current: "" });
    345    const currentPage = findAccessibleChildByID(docAcc, "currentPage");
    346    testAttrs(currentPage, { current: "page" }, true);
    347    // Test that token normalization works.
    348    const currentBlah = findAccessibleChildByID(docAcc, "currentBlah");
    349    testAttrs(currentBlah, { current: "true" }, true);
    350    const haspopupMenu = findAccessibleChildByID(docAcc, "haspopupMenu");
    351    testAttrs(haspopupMenu, { haspopup: "menu" }, true);
    352 
    353    // Test normalization of integer values.
    354    const rowColCountPositive = findAccessibleChildByID(
    355      docAcc,
    356      "rowColCountPositive"
    357    );
    358    testAttrs(
    359      rowColCountPositive,
    360      { rowcount: "1000", colcount: "1000" },
    361      true
    362    );
    363    const rowColIndexPositive = findAccessibleChildByID(
    364      docAcc,
    365      "rowColIndexPositive"
    366    );
    367    testAttrs(rowColIndexPositive, { rowindex: "100", colindex: "100" }, true);
    368    const rowColCountNegative = findAccessibleChildByID(
    369      docAcc,
    370      "rowColCountNegative"
    371    );
    372    testAttrs(rowColCountNegative, { rowcount: "-1", colcount: "-1" }, true);
    373    const rowColIndexNegative = findAccessibleChildByID(
    374      docAcc,
    375      "rowColIndexNegative"
    376    );
    377    testAbsentAttrs(rowColIndexNegative, { rowindex: "", colindex: "" });
    378    const rowColCountInvalid = findAccessibleChildByID(
    379      docAcc,
    380      "rowColCountInvalid"
    381    );
    382    testAbsentAttrs(rowColCountInvalid, { rowcount: "", colcount: "" });
    383    const rowColIndexInvalid = findAccessibleChildByID(
    384      docAcc,
    385      "rowColIndexInvalid"
    386    );
    387    testAbsentAttrs(rowColIndexInvalid, { rowindex: "", colindex: "" });
    388 
    389    // Test that unknown aria- attributes get exposed.
    390    const foo = findAccessibleChildByID(docAcc, "foo");
    391    testAttrs(foo, { foo: "bar" }, true);
    392 
    393    const mutate = findAccessibleChildByID(docAcc, "mutate");
    394    testAttrs(mutate, { current: "true" }, true);
    395    info("mutate: Removing aria-current");
    396    let changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, mutate);
    397    await invokeContentTask(browser, [], () => {
    398      content.document.getElementById("mutate").removeAttribute("aria-current");
    399    });
    400    await changed;
    401    testAbsentAttrs(mutate, { current: "" });
    402    info("mutate: Adding aria-current");
    403    changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, mutate);
    404    await invokeContentTask(browser, [], () => {
    405      content.document
    406        .getElementById("mutate")
    407        .setAttribute("aria-current", "page");
    408    });
    409    await changed;
    410    testAttrs(mutate, { current: "page" }, true);
    411  },
    412  { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
    413 );
    414 
    415 /**
    416 * Test support for the xml-roles attribute.
    417 */
    418 addAccessibleTask(
    419  `
    420 <div id="knownRole" role="main">knownRole</div>
    421 <div id="emptyRole" role="">emptyRole</div>
    422 <div id="unknownRole" role="foo">unknownRole</div>
    423 <div id="multiRole" role="foo main">multiRole</div>
    424 <main id="landmarkMarkup">landmarkMarkup</main>
    425 <main id="landmarkMarkupWithRole" role="banner">landmarkMarkupWithRole</main>
    426 <main id="landmarkMarkupWithEmptyRole" role="">landmarkMarkupWithEmptyRole</main>
    427 <article id="markup">markup</article>
    428 <article id="markupWithRole" role="banner">markupWithRole</article>
    429 <article id="markupWithEmptyRole" role="">markupWithEmptyRole</article>
    430  `,
    431  async function (browser, docAcc) {
    432    const knownRole = findAccessibleChildByID(docAcc, "knownRole");
    433    testAttrs(knownRole, { "xml-roles": "main" }, true);
    434    const emptyRole = findAccessibleChildByID(docAcc, "emptyRole");
    435    testAbsentAttrs(emptyRole, { "xml-roles": "" });
    436    const unknownRole = findAccessibleChildByID(docAcc, "unknownRole");
    437    testAttrs(unknownRole, { "xml-roles": "foo" }, true);
    438    const multiRole = findAccessibleChildByID(docAcc, "multiRole");
    439    testAttrs(multiRole, { "xml-roles": "foo main" }, true);
    440    const landmarkMarkup = findAccessibleChildByID(docAcc, "landmarkMarkup");
    441    testAttrs(landmarkMarkup, { "xml-roles": "main" }, true);
    442    const landmarkMarkupWithRole = findAccessibleChildByID(
    443      docAcc,
    444      "landmarkMarkupWithRole"
    445    );
    446    testAttrs(landmarkMarkupWithRole, { "xml-roles": "banner" }, true);
    447    const landmarkMarkupWithEmptyRole = findAccessibleChildByID(
    448      docAcc,
    449      "landmarkMarkupWithEmptyRole"
    450    );
    451    testAttrs(landmarkMarkupWithEmptyRole, { "xml-roles": "main" }, true);
    452    const markup = findAccessibleChildByID(docAcc, "markup");
    453    testAttrs(markup, { "xml-roles": "article" }, true);
    454    const markupWithRole = findAccessibleChildByID(docAcc, "markupWithRole");
    455    testAttrs(markupWithRole, { "xml-roles": "banner" }, true);
    456    const markupWithEmptyRole = findAccessibleChildByID(
    457      docAcc,
    458      "markupWithEmptyRole"
    459    );
    460    testAttrs(markupWithEmptyRole, { "xml-roles": "article" }, true);
    461  },
    462  { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
    463 );
    464 
    465 /**
    466 * Test lie region attributes.
    467 */
    468 addAccessibleTask(
    469  `
    470 <div id="noLive"><p>noLive</p></div>
    471 <output id="liveMarkup"><p>liveMarkup</p></output>
    472 <div id="ariaLive" aria-live="polite"><p>ariaLive</p></div>
    473 <div id="liveRole" role="log"><p>liveRole</p></div>
    474 <div id="nonLiveRole" role="group"><p>nonLiveRole</p></div>
    475 <div id="other" aria-atomic="true" aria-busy="true" aria-relevant="additions"><p>other</p></div>
    476  `,
    477  async function (browser, docAcc) {
    478    const noLive = findAccessibleChildByID(docAcc, "noLive");
    479    for (const acc of [noLive, noLive.firstChild]) {
    480      testAbsentAttrs(acc, {
    481        live: "",
    482        "container-live": "",
    483        "container-live-role": "",
    484        atomic: "",
    485        "container-atomic": "",
    486        busy: "",
    487        "container-busy": "",
    488        relevant: "",
    489        "container-relevant": "",
    490      });
    491    }
    492    const liveMarkup = findAccessibleChildByID(docAcc, "liveMarkup");
    493    testAttrs(liveMarkup, { live: "polite" }, true);
    494    testAttrs(liveMarkup.firstChild, { "container-live": "polite" }, true);
    495    const ariaLive = findAccessibleChildByID(docAcc, "ariaLive");
    496    testAttrs(ariaLive, { live: "polite" }, true);
    497    testAttrs(ariaLive.firstChild, { "container-live": "polite" }, true);
    498    const liveRole = findAccessibleChildByID(docAcc, "liveRole");
    499    testAttrs(liveRole, { live: "polite" }, true);
    500    testAttrs(
    501      liveRole.firstChild,
    502      { "container-live": "polite", "container-live-role": "log" },
    503      true
    504    );
    505    const nonLiveRole = findAccessibleChildByID(docAcc, "nonLiveRole");
    506    testAbsentAttrs(nonLiveRole, { live: "" });
    507    testAbsentAttrs(nonLiveRole.firstChild, {
    508      "container-live": "",
    509      "container-live-role": "",
    510    });
    511    const other = findAccessibleChildByID(docAcc, "other");
    512    testAttrs(
    513      other,
    514      { atomic: "true", busy: "true", relevant: "additions" },
    515      true
    516    );
    517    testAttrs(
    518      other.firstChild,
    519      {
    520        "container-atomic": "true",
    521        "container-busy": "true",
    522        "container-relevant": "additions",
    523      },
    524      true
    525    );
    526  },
    527  { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
    528 );
    529 
    530 /**
    531 * Test the id attribute.
    532 */
    533 addAccessibleTask(
    534  `
    535 <p id="withId">withId</p>
    536 <div id="noIdParent"><p>noId</p></div>
    537  `,
    538  async function (browser, docAcc) {
    539    const withId = findAccessibleChildByID(docAcc, "withId");
    540    testAttrs(withId, { id: "withId" }, true);
    541    const noId = findAccessibleChildByID(docAcc, "noIdParent").firstChild;
    542    testAbsentAttrs(noId, { id: "" });
    543  },
    544  { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
    545 );
    546 
    547 /**
    548 * Test the valuetext attribute.
    549 */
    550 addAccessibleTask(
    551  `
    552 <div id="valuenow" role="slider" aria-valuenow="1"></div>
    553 <div id="valuetext" role="slider" aria-valuetext="text"></div>
    554 <div id="noValue" role="button"></div>
    555  `,
    556  async function (browser, docAcc) {
    557    const valuenow = findAccessibleChildByID(docAcc, "valuenow");
    558    testAttrs(valuenow, { valuetext: "1" }, true);
    559    const valuetext = findAccessibleChildByID(docAcc, "valuetext");
    560    testAttrs(valuetext, { valuetext: "text" }, true);
    561    const noValue = findAccessibleChildByID(docAcc, "noValue");
    562    testAbsentAttrs(noValue, { valuetext: "valuetext" });
    563  },
    564  { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
    565 );
    566 
    567 function untilCacheAttrIs(acc, attr, val, msg) {
    568  return untilCacheOk(() => {
    569    try {
    570      return acc.attributes.getStringProperty(attr) == val;
    571    } catch (e) {
    572      return false;
    573    }
    574  }, msg);
    575 }
    576 
    577 function untilCacheAttrAbsent(acc, attr, msg) {
    578  return untilCacheOk(() => {
    579    try {
    580      acc.attributes.getStringProperty(attr);
    581    } catch (e) {
    582      return true;
    583    }
    584    return false;
    585  }, msg);
    586 }
    587 
    588 /**
    589 * Test the class attribute.
    590 */
    591 addAccessibleTask(
    592  `
    593 <div id="oneClass" class="c1">oneClass</div>
    594 <div id="multiClass" class="c1 c2">multiClass</div>
    595 <div id="noClass">noClass</div>
    596 <div id="mutate">mutate</div>
    597  `,
    598  async function (browser, docAcc) {
    599    const oneClass = findAccessibleChildByID(docAcc, "oneClass");
    600    testAttrs(oneClass, { class: "c1" }, true);
    601    const multiClass = findAccessibleChildByID(docAcc, "multiClass");
    602    testAttrs(multiClass, { class: "c1 c2" }, true);
    603    const noClass = findAccessibleChildByID(docAcc, "noClass");
    604    testAbsentAttrs(noClass, { class: "" });
    605 
    606    const mutate = findAccessibleChildByID(docAcc, "mutate");
    607    testAbsentAttrs(mutate, { class: "" });
    608    info("Adding class to mutate");
    609    await invokeContentTask(browser, [], () => {
    610      content.document.getElementById("mutate").className = "c1 c2";
    611    });
    612    await untilCacheAttrIs(mutate, "class", "c1 c2", "mutate class correct");
    613    info("Removing class from mutate");
    614    await invokeContentTask(browser, [], () => {
    615      content.document.getElementById("mutate").removeAttribute("class");
    616    });
    617    await untilCacheAttrAbsent(mutate, "class", "mutate class not present");
    618  },
    619  { chrome: true, topLevel: true }
    620 );
    621 
    622 /**
    623 * Test the src attribute.
    624 */
    625 const kImgUrl = "https://example.com/a11y/accessible/tests/mochitest/moz.png";
    626 addAccessibleTask(
    627  `
    628 <img id="noAlt" src="${kImgUrl}">
    629 <img id="alt" alt="alt" src="${kImgUrl}">
    630 <img id="mutate">
    631  `,
    632  async function (browser, docAcc) {
    633    const noAlt = findAccessibleChildByID(docAcc, "noAlt");
    634    testAttrs(noAlt, { src: kImgUrl }, true);
    635    const alt = findAccessibleChildByID(docAcc, "alt");
    636    testAttrs(alt, { src: kImgUrl }, true);
    637 
    638    const mutate = findAccessibleChildByID(docAcc, "mutate");
    639    testAbsentAttrs(mutate, { src: "" });
    640    info("Adding src to mutate");
    641    await invokeContentTask(browser, [kImgUrl], url => {
    642      content.document.getElementById("mutate").src = url;
    643    });
    644    await untilCacheAttrIs(mutate, "src", kImgUrl, "mutate src correct");
    645    info("Removing src from mutate");
    646    await invokeContentTask(browser, [], () => {
    647      content.document.getElementById("mutate").removeAttribute("src");
    648    });
    649    await untilCacheAttrAbsent(mutate, "src", "mutate src not present");
    650  },
    651  { chrome: true, topLevel: true }
    652 );
    653 
    654 /**
    655 * Test the placeholder attribute.
    656 */
    657 addAccessibleTask(
    658  `
    659 <input id="htmlWithLabel" aria-label="label" placeholder="HTML">
    660 <input id="htmlNoLabel" placeholder="HTML">
    661 <input id="ariaWithLabel" aria-label="label" aria-placeholder="ARIA">
    662 <input id="ariaNoLabel" aria-placeholder="ARIA">
    663 <input id="both" aria-label="label" placeholder="HTML" aria-placeholder="ARIA">
    664 <input id="mutate" placeholder="HTML">
    665  `,
    666  async function (browser, docAcc) {
    667    const htmlWithLabel = findAccessibleChildByID(docAcc, "htmlWithLabel");
    668    testAttrs(htmlWithLabel, { placeholder: "HTML" }, true);
    669    const htmlNoLabel = findAccessibleChildByID(docAcc, "htmlNoLabel");
    670    // placeholder is used as name, so not exposed as attribute.
    671    testAbsentAttrs(htmlNoLabel, { placeholder: "" });
    672    const ariaWithLabel = findAccessibleChildByID(docAcc, "ariaWithLabel");
    673    testAttrs(ariaWithLabel, { placeholder: "ARIA" }, true);
    674    const ariaNoLabel = findAccessibleChildByID(docAcc, "ariaNoLabel");
    675    // No label doesn't impact aria-placeholder.
    676    testAttrs(ariaNoLabel, { placeholder: "ARIA" }, true);
    677    const both = findAccessibleChildByID(docAcc, "both");
    678    testAttrs(both, { placeholder: "HTML" }, true);
    679 
    680    const mutate = findAccessibleChildByID(docAcc, "mutate");
    681    testAbsentAttrs(mutate, { placeholder: "" });
    682    info("Adding label to mutate");
    683    await invokeContentTask(browser, [], () => {
    684      content.document
    685        .getElementById("mutate")
    686        .setAttribute("aria-label", "label");
    687    });
    688    await untilCacheAttrIs(
    689      mutate,
    690      "placeholder",
    691      "HTML",
    692      "mutate placeholder correct"
    693    );
    694    info("Removing mutate placeholder");
    695    await invokeContentTask(browser, [], () => {
    696      content.document.getElementById("mutate").removeAttribute("placeholder");
    697    });
    698    await untilCacheAttrAbsent(
    699      mutate,
    700      "placeholder",
    701      "mutate placeholder not present"
    702    );
    703    info("Setting mutate aria-placeholder");
    704    await invokeContentTask(browser, [], () => {
    705      content.document
    706        .getElementById("mutate")
    707        .setAttribute("aria-placeholder", "ARIA");
    708    });
    709    await untilCacheAttrIs(
    710      mutate,
    711      "placeholder",
    712      "ARIA",
    713      "mutate placeholder correct"
    714    );
    715    info("Setting mutate placeholder");
    716    await invokeContentTask(browser, [], () => {
    717      content.document
    718        .getElementById("mutate")
    719        .setAttribute("placeholder", "HTML");
    720    });
    721    await untilCacheAttrIs(
    722      mutate,
    723      "placeholder",
    724      "HTML",
    725      "mutate placeholder correct"
    726    );
    727  },
    728  { chrome: true, topLevel: true }
    729 );
    730 
    731 /**
    732 * Test the ispopup attribute.
    733 */
    734 addAccessibleTask(
    735  `<div id="popover" popover>popover</div>`,
    736  async function testIspopup(browser) {
    737    info("Showing popover");
    738    let shown = waitForEvent(EVENT_SHOW, "popover");
    739    await invokeContentTask(browser, [], () => {
    740      content.document.getElementById("popover").showPopover();
    741    });
    742    let popover = (await shown).accessible;
    743    testAttrs(popover, { ispopup: "auto" }, true);
    744    info("Setting popover to null");
    745    // Setting popover causes the Accessible to be recreated.
    746    shown = waitForEvent(EVENT_SHOW, "popover");
    747    await invokeContentTask(browser, [], () => {
    748      content.document.getElementById("popover").popover = null;
    749    });
    750    popover = (await shown).accessible;
    751    testAbsentAttrs(popover, { ispopup: "" });
    752    info("Setting popover to manual and showing");
    753    shown = waitForEvent(EVENT_SHOW, "popover");
    754    await invokeContentTask(browser, [], () => {
    755      const popoverDom = content.document.getElementById("popover");
    756      popoverDom.popover = "manual";
    757      popoverDom.showPopover();
    758    });
    759    popover = (await shown).accessible;
    760    testAttrs(popover, { ispopup: "manual" }, true);
    761  },
    762  { chrome: true, topLevel: true }
    763 );
    764 
    765 /**
    766 * Test has-actions attribute.
    767 */
    768 addAccessibleTask(
    769  `<dialog aria-actions="btn" id="dlg" open>
    770      Hello
    771      <button id="btn">Close</button>
    772      <button id="btn-hidden" hidden>Pin</button>
    773    </dialog>`,
    774  async function testHasActionsAttribute(browser, docAcc) {
    775    function getDlgHasActions() {
    776      try {
    777        return dlg.attributes.getStringProperty("has-actions");
    778      } catch (e) {
    779        return null;
    780      }
    781    }
    782 
    783    const dlg = findAccessibleChildByID(docAcc, "dlg");
    784    is(getDlgHasActions(), "true", "dlg has-actions attribute is true");
    785 
    786    // Removing the 'aria-actions' attribute from the element
    787    // should remove the 'has-actions' attribute from the accessible.
    788    let changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, "dlg");
    789    await invokeSetAttribute(browser, "dlg", "aria-actions");
    790    await changed;
    791    await untilCacheIs(
    792      getDlgHasActions,
    793      null,
    794      "dlg has-actions attribute removed"
    795    );
    796 
    797    // Setting the 'aria-actions' attribute to an empty string
    798    // should make the 'has-actions' accessible attribute true.
    799    changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, "dlg");
    800    await invokeSetAttribute(browser, "dlg", "aria-actions", "");
    801    await changed;
    802    await untilCacheIs(
    803      getDlgHasActions,
    804      "true",
    805      "dlg has-actions attribute re-added"
    806    );
    807 
    808    // Remove again to set up for next test
    809    await invokeSetAttribute(browser, "dlg", "aria-actions");
    810    await untilCacheIs(
    811      getDlgHasActions,
    812      null,
    813      "dlg has-actions attribute removed again"
    814    );
    815 
    816    // Setting the 'aria-actions' attribute to a hidden target
    817    // should still make 'has-actions' true
    818    changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, "dlg");
    819    await invokeSetAttribute(browser, "dlg", "aria-actions", "btn-hidden");
    820    await changed;
    821    await untilCacheIs(
    822      getDlgHasActions,
    823      "true",
    824      "dlg has-actions attribute re-added with hidden target"
    825    );
    826  },
    827  { chrome: true, topLevel: true }
    828 );