tor-browser

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

browser_treeupdate_ariaowns.js (21074B)


      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/role.js */
      8 loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
      9 /* import-globals-from ../../mochitest/states.js */
     10 loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
     11 
     12 requestLongerTimeout(2);
     13 
     14 function invokeSetAriaOwns(
     15  browser,
     16  id,
     17  children = null,
     18  elementReflection = false
     19 ) {
     20  if (!elementReflection) {
     21    return invokeSetAttribute(browser, id, "aria-owns", children);
     22  }
     23 
     24  return invokeContentTask(
     25    browser,
     26    [id, children],
     27    (contentId, contentChildrenIds) => {
     28      let elm = content.document.getElementById(contentId);
     29      if (contentChildrenIds) {
     30        elm.ariaOwnsElements = contentChildrenIds
     31          .split(" ")
     32          .map(childId => content.document.getElementById(childId));
     33      } else {
     34        elm.ariaOwnsElements = null;
     35      }
     36    }
     37  );
     38 }
     39 
     40 async function testContainer1(browser, accDoc, elementReflection = false) {
     41  const id = "t1_container";
     42  const docID = getAccessibleDOMNodeID(accDoc);
     43  const acc = findAccessibleChildByID(accDoc, id);
     44 
     45  /* ================= Initial tree test ==================================== */
     46  // children are swapped by ARIA owns
     47  let tree = {
     48    SECTION: [{ CHECKBUTTON: [{ SECTION: [] }] }, { PUSHBUTTON: [] }],
     49  };
     50  testAccessibleTree(acc, tree);
     51 
     52  /* ================ Change ARIA owns ====================================== */
     53  let onReorder = waitForEvent(EVENT_REORDER, id);
     54  await invokeSetAriaOwns(
     55    browser,
     56    id,
     57    "t1_button t1_subdiv",
     58    elementReflection
     59  );
     60  await onReorder;
     61 
     62  // children are swapped again, button and subdiv are appended to
     63  // the children.
     64  tree = {
     65    SECTION: [
     66      { CHECKBUTTON: [] }, // checkbox, native order
     67      { PUSHBUTTON: [] }, // button, rearranged by ARIA own
     68      { SECTION: [] }, // subdiv from the subtree, ARIA owned
     69    ],
     70  };
     71  testAccessibleTree(acc, tree);
     72 
     73  /* ================ Remove ARIA owns ====================================== */
     74  onReorder = waitForEvent(EVENT_REORDER, id);
     75  await invokeSetAriaOwns(browser, id, null, elementReflection);
     76  await onReorder;
     77 
     78  // children follow the DOM order
     79  tree = {
     80    SECTION: [{ PUSHBUTTON: [] }, { CHECKBUTTON: [{ SECTION: [] }] }],
     81  };
     82  testAccessibleTree(acc, tree);
     83 
     84  /* ================ Set ARIA owns ========================================= */
     85  onReorder = waitForEvent(EVENT_REORDER, id);
     86  await invokeSetAriaOwns(
     87    browser,
     88    id,
     89    "t1_button t1_subdiv",
     90    elementReflection
     91  );
     92  await onReorder;
     93 
     94  // children are swapped again, button and subdiv are appended to
     95  // the children.
     96  tree = {
     97    SECTION: [
     98      { CHECKBUTTON: [] }, // checkbox
     99      { PUSHBUTTON: [] }, // button, rearranged by ARIA own
    100      { SECTION: [] }, // subdiv from the subtree, ARIA owned
    101    ],
    102  };
    103  testAccessibleTree(acc, tree);
    104 
    105  /* ================ Add ID to ARIA owns =================================== */
    106  onReorder = waitForEvent(EVENT_REORDER, docID);
    107  await invokeSetAttribute(
    108    browser,
    109    id,
    110    "aria-owns",
    111    "t1_button t1_subdiv t1_group"
    112  );
    113  await onReorder;
    114 
    115  // children are swapped again, button and subdiv are appended to
    116  // the children.
    117  tree = {
    118    SECTION: [
    119      { CHECKBUTTON: [] }, // t1_checkbox
    120      { PUSHBUTTON: [] }, // button, t1_button
    121      { SECTION: [] }, // subdiv from the subtree, t1_subdiv
    122      { GROUPING: [] }, // group from outside, t1_group
    123    ],
    124  };
    125  testAccessibleTree(acc, tree);
    126 
    127  /* ================ Append element ======================================== */
    128  onReorder = waitForEvent(EVENT_REORDER, id);
    129  await invokeContentTask(browser, [id], contentId => {
    130    let div = content.document.createElement("div");
    131    div.setAttribute("id", "t1_child3");
    132    div.setAttribute("role", "radio");
    133    content.document.getElementById(contentId).appendChild(div);
    134  });
    135  await onReorder;
    136 
    137  // children are invalidated, they includes aria-owns swapped kids and
    138  // newly inserted child.
    139  tree = {
    140    SECTION: [
    141      { CHECKBUTTON: [] }, // existing explicit, t1_checkbox
    142      { RADIOBUTTON: [] }, // new explicit, t1_child3
    143      { PUSHBUTTON: [] }, // ARIA owned, t1_button
    144      { SECTION: [] }, // ARIA owned, t1_subdiv
    145      { GROUPING: [] }, // ARIA owned, t1_group
    146    ],
    147  };
    148  testAccessibleTree(acc, tree);
    149 
    150  /* ================ Remove element ======================================== */
    151  onReorder = waitForEvent(EVENT_REORDER, id);
    152  await invokeContentTask(browser, [], () => {
    153    content.document.getElementById("t1_span").remove();
    154  });
    155  await onReorder;
    156 
    157  // subdiv should go away
    158  tree = {
    159    SECTION: [
    160      { CHECKBUTTON: [] }, // explicit, t1_checkbox
    161      { RADIOBUTTON: [] }, // explicit, t1_child3
    162      { PUSHBUTTON: [] }, // ARIA owned, t1_button
    163      { GROUPING: [] }, // ARIA owned, t1_group
    164    ],
    165  };
    166  testAccessibleTree(acc, tree);
    167 
    168  /* ================ Remove ID ============================================= */
    169  onReorder = waitForEvent(EVENT_REORDER, docID);
    170  await invokeSetAttribute(browser, "t1_group", "id");
    171  await onReorder;
    172 
    173  tree = {
    174    SECTION: [
    175      { CHECKBUTTON: [] },
    176      { RADIOBUTTON: [] },
    177      { PUSHBUTTON: [] }, // ARIA owned, t1_button
    178    ],
    179  };
    180  testAccessibleTree(acc, tree);
    181 
    182  /* ================ Set ID ================================================ */
    183  onReorder = waitForEvent(EVENT_REORDER, docID);
    184  await invokeSetAttribute(browser, "t1_grouptmp", "id", "t1_group");
    185  await onReorder;
    186 
    187  tree = {
    188    SECTION: [
    189      { CHECKBUTTON: [] },
    190      { RADIOBUTTON: [] },
    191      { PUSHBUTTON: [] }, // ARIA owned, t1_button
    192      { GROUPING: [] }, // ARIA owned, t1_group, previously t1_grouptmp
    193    ],
    194  };
    195  testAccessibleTree(acc, tree);
    196 }
    197 
    198 async function removeContainer(browser, accDoc) {
    199  const id = "t2_container1";
    200  const acc = findAccessibleChildByID(accDoc, id);
    201 
    202  let tree = {
    203    SECTION: [
    204      { CHECKBUTTON: [] }, // ARIA owned, 't2_owned'
    205    ],
    206  };
    207  testAccessibleTree(acc, tree);
    208 
    209  let onReorder = waitForEvent(EVENT_REORDER, id);
    210  await invokeContentTask(browser, [], () => {
    211    content.document
    212      .getElementById("t2_container2")
    213      .removeChild(content.document.getElementById("t2_container3"));
    214  });
    215  await onReorder;
    216 
    217  tree = {
    218    SECTION: [],
    219  };
    220  testAccessibleTree(acc, tree);
    221 }
    222 
    223 async function stealAndRecacheChildren(browser, accDoc, elementReflection) {
    224  const id1 = "t3_container1";
    225  const id2 = "t3_container2";
    226  const acc1 = findAccessibleChildByID(accDoc, id1);
    227  const acc2 = findAccessibleChildByID(accDoc, id2);
    228 
    229  /* ================ Attempt to steal from other ARIA owns ================= */
    230  let onReorder = waitForEvent(EVENT_REORDER, id2);
    231  await invokeSetAriaOwns(browser, id2, "t3_child", elementReflection);
    232  await invokeContentTask(browser, [id2], id => {
    233    let div = content.document.createElement("div");
    234    div.setAttribute("role", "radio");
    235    content.document.getElementById(id).appendChild(div);
    236  });
    237  await onReorder;
    238 
    239  let tree = {
    240    SECTION: [
    241      { CHECKBUTTON: [] }, // ARIA owned
    242    ],
    243  };
    244  testAccessibleTree(acc1, tree);
    245 
    246  tree = {
    247    SECTION: [{ RADIOBUTTON: [] }],
    248  };
    249  testAccessibleTree(acc2, tree);
    250 }
    251 
    252 async function showHiddenElement(browser, accDoc) {
    253  const id = "t4_container1";
    254  const acc = findAccessibleChildByID(accDoc, id);
    255 
    256  let tree = {
    257    SECTION: [{ RADIOBUTTON: [] }],
    258  };
    259  testAccessibleTree(acc, tree);
    260 
    261  let onReorder = waitForEvent(EVENT_REORDER, id);
    262  await invokeSetStyle(browser, "t4_child1", "display", "block");
    263  await onReorder;
    264 
    265  tree = {
    266    SECTION: [{ CHECKBUTTON: [] }, { RADIOBUTTON: [] }],
    267  };
    268  testAccessibleTree(acc, tree);
    269 }
    270 
    271 async function rearrangeARIAOwns(browser, accDoc, elementReflection) {
    272  const id = "t5_container";
    273  const acc = findAccessibleChildByID(accDoc, id);
    274  const tests = [
    275    {
    276      val: "t5_checkbox t5_radio t5_button",
    277      roleList: ["CHECKBUTTON", "RADIOBUTTON", "PUSHBUTTON"],
    278    },
    279    {
    280      val: "t5_radio t5_button t5_checkbox",
    281      roleList: ["RADIOBUTTON", "PUSHBUTTON", "CHECKBUTTON"],
    282    },
    283  ];
    284 
    285  for (let { val, roleList } of tests) {
    286    let onReorder = waitForEvent(EVENT_REORDER, id);
    287    await invokeSetAriaOwns(browser, id, val, elementReflection);
    288    await onReorder;
    289 
    290    let tree = { SECTION: [] };
    291    for (let role of roleList) {
    292      let ch = {};
    293      ch[role] = [];
    294      tree.SECTION.push(ch);
    295    }
    296    testAccessibleTree(acc, tree);
    297  }
    298 }
    299 
    300 async function removeNotARIAOwnedEl(browser, accDoc) {
    301  const id = "t6_container";
    302  const acc = findAccessibleChildByID(accDoc, id);
    303 
    304  let tree = {
    305    SECTION: [{ TEXT_LEAF: [] }, { GROUPING: [] }],
    306  };
    307  testAccessibleTree(acc, tree);
    308 
    309  let onReorder = waitForEvent(EVENT_REORDER, id);
    310  await invokeContentTask(browser, [id], contentId => {
    311    content.document
    312      .getElementById(contentId)
    313      .removeChild(content.document.getElementById("t6_span"));
    314  });
    315  await onReorder;
    316 
    317  tree = {
    318    SECTION: [{ GROUPING: [] }],
    319  };
    320  testAccessibleTree(acc, tree);
    321 }
    322 
    323 addAccessibleTask(
    324  "e10s/doc_treeupdate_ariaowns.html",
    325  async function (browser, accDoc) {
    326    await testContainer1(browser, accDoc);
    327    await removeContainer(browser, accDoc);
    328    await stealAndRecacheChildren(browser, accDoc);
    329    await showHiddenElement(browser, accDoc);
    330    await rearrangeARIAOwns(browser, accDoc);
    331    await removeNotARIAOwnedEl(browser, accDoc);
    332  },
    333  { iframe: true, remoteIframe: true }
    334 );
    335 
    336 addAccessibleTask(
    337  "e10s/doc_treeupdate_ariaowns.html",
    338  async function (browser, accDoc) {
    339    await testContainer1(browser, accDoc, true);
    340    await removeContainer(browser, accDoc);
    341    await stealAndRecacheChildren(browser, accDoc, true);
    342    await showHiddenElement(browser, accDoc);
    343    await rearrangeARIAOwns(browser, accDoc, true);
    344    await removeNotARIAOwnedEl(browser, accDoc);
    345  },
    346  { iframe: true, remoteIframe: true }
    347 );
    348 
    349 // Test owning an ancestor which isn't created yet with an iframe in the
    350 // subtree.
    351 addAccessibleTask(
    352  `
    353  <span id="a">
    354    <div id="b" aria-owns="c"></div>
    355  </span>
    356  <div id="c">
    357    <iframe></iframe>
    358  </div>
    359  <script>
    360    document.getElementById("c").setAttribute("aria-owns", "a");
    361  </script>
    362  `,
    363  async function (browser, accDoc) {
    364    testAccessibleTree(accDoc, {
    365      DOCUMENT: [
    366        {
    367          // b
    368          SECTION: [
    369            {
    370              // c
    371              SECTION: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }],
    372            },
    373          ],
    374        },
    375      ],
    376    });
    377  }
    378 );
    379 
    380 // Verify that removing the parent of a DOM-sibling aria-owned child keeps the
    381 // formerly-owned child in the tree.
    382 addAccessibleTask(
    383  `<input id='x'></input><div aria-owns='x'></div>`,
    384  async function (browser, accDoc) {
    385    testAccessibleTree(accDoc, {
    386      DOCUMENT: [{ SECTION: [{ ENTRY: [] }] }],
    387    });
    388 
    389    info("Removing the div that aria-owns a DOM sibling");
    390    let onReorder = waitForEvent(EVENT_REORDER, accDoc);
    391    await invokeContentTask(browser, [], () => {
    392      content.document.querySelector("div").remove();
    393    });
    394    await onReorder;
    395 
    396    info("Verifying that the formerly-owned child is still present");
    397    testAccessibleTree(accDoc, {
    398      DOCUMENT: [{ ENTRY: [] }],
    399    });
    400  },
    401  { chrome: true, iframe: true, remoteIframe: true }
    402 );
    403 
    404 // Verify that removing the parent of multiple DOM-sibling aria-owned children
    405 // keeps all formerly-owned children in the tree.
    406 addAccessibleTask(
    407  `<input id='x'></input><input id='y'><div aria-owns='x y'></div>`,
    408  async function (browser, accDoc) {
    409    testAccessibleTree(accDoc, {
    410      DOCUMENT: [
    411        {
    412          SECTION: [{ ENTRY: [] }, { ENTRY: [] }],
    413        },
    414      ],
    415    });
    416 
    417    info("Removing the div that aria-owns DOM siblings");
    418    let onReorder = waitForEvent(EVENT_REORDER, accDoc);
    419    await invokeContentTask(browser, [], () => {
    420      content.document.querySelector("div").remove();
    421    });
    422    await onReorder;
    423 
    424    info("Verifying that the formerly-owned children are still present");
    425    testAccessibleTree(accDoc, {
    426      DOCUMENT: [{ ENTRY: [] }, { ENTRY: [] }],
    427    });
    428  },
    429  { chrome: true, iframe: true, remoteIframe: true }
    430 );
    431 
    432 // Verify that reordering owned elements by changing the aria-owns attribute
    433 // properly reorders owned elements.
    434 addAccessibleTask(
    435  `
    436 <div id="container" aria-owns="b d c a">
    437  <div id="a" role="button"></div>
    438  <div id="b" role="checkbox"></div>
    439 </div>
    440 <div id="c" role="radio"></div>
    441 <div id="d"></div>`,
    442  async function (browser, accDoc) {
    443    testAccessibleTree(accDoc, {
    444      DOCUMENT: [
    445        {
    446          SECTION: [
    447            { CHECKBUTTON: [] }, // b
    448            { SECTION: [] }, // d
    449            { RADIOBUTTON: [] }, // c
    450            { PUSHBUTTON: [] }, // a
    451          ],
    452        },
    453      ],
    454    });
    455 
    456    info("Removing the div that aria-owns other elements");
    457    let onReorder = waitForEvent(EVENT_REORDER, accDoc);
    458    await invokeContentTask(browser, [], () => {
    459      content.document.querySelector("#container").remove();
    460    });
    461    await onReorder;
    462 
    463    info(
    464      "Verify DOM children are removed, order of remaining elements is correct"
    465    );
    466    testAccessibleTree(accDoc, {
    467      DOCUMENT: [
    468        { RADIOBUTTON: [] }, // c
    469        { SECTION: [] }, // d
    470      ],
    471    });
    472  },
    473  { chrome: true, iframe: true, remoteIframe: true }
    474 );
    475 
    476 // Verify that we avoid sending unwanted hide events when doing multiple
    477 // aria-owns relocations in a single tick. Note that we're avoiding testing
    478 // chrome here since parent process locals don't track moves in the same way,
    479 // meaning our mechanism for avoiding duplicate hide events doesn't work.
    480 addAccessibleTask(
    481  `
    482 <div id='b' aria-owns='a'></div>
    483 <div id='d'></div>
    484 <dd id='f'>
    485  <div id='a' aria-owns='d'></div>
    486 </dd>
    487  `,
    488  async function (browser, accDoc) {
    489    const b = findAccessibleChildByID(accDoc, "b");
    490    const waitFor = {
    491      expected: [
    492        [EVENT_HIDE, b],
    493        [EVENT_SHOW, "d"],
    494        [EVENT_REORDER, accDoc],
    495      ],
    496      unexpected: [
    497        [EVENT_HIDE, "d"],
    498        [EVENT_REORDER, "a"],
    499      ],
    500    };
    501    info(
    502      "Verifying that events are fired properly after doing two aria-owns relocations"
    503    );
    504    await contentSpawnMutation(browser, waitFor, function () {
    505      content.document.querySelector("#b").remove();
    506      content.document.querySelector("#f").remove();
    507    });
    508  },
    509  { chrome: false, iframe: true, remoteIframe: true }
    510 );
    511 
    512 /**
    513 * Test relation defaults via element internals
    514 */
    515 addAccessibleTask(
    516  `
    517 
    518  <div role="listbox">
    519    <div role="listitem" id="l1"></div>
    520    <div role="listitem" id="l2"></div>
    521    <div role="listitem" id="l3"></div>
    522  </div>
    523  <custom-listbox id="listbox"></custom-listbox>
    524  <div role="listbox">
    525    <div role="listitem" id="l4"></div>
    526  </div>
    527 
    528 <script>
    529 customElements.define("custom-listbox",
    530  class extends HTMLElement {
    531    constructor() {
    532      super();
    533      this.tabIndex = "0"
    534      this._internals = this.attachInternals();
    535      this._internals.role = "listbox";
    536      this._internals.ariaOwnsElements = Array.from(this.previousElementSibling.children)
    537    }
    538  }
    539 );
    540 </script>`,
    541  async function (browser, accDoc) {
    542    let listbox = findAccessibleChildByID(accDoc, "listbox");
    543    is(listbox.children.length, 3, "got children");
    544    let onReorder = waitForEvent(EVENT_REORDER, "listbox");
    545    invokeSetAriaOwns(browser, "listbox", "l4");
    546    await onReorder;
    547  }
    548 );
    549 
    550 /**
    551 * Test insertion of relocated by ID child after initial load
    552 */
    553 addAccessibleTask(
    554  `<div id='a' aria-owns='b'></div>`,
    555  async function (browser, accDoc) {
    556    const a = findAccessibleChildByID(accDoc, "a");
    557    is(a.children.length, 0, "'a' has no children");
    558    const waitFor = {
    559      expected: [
    560        [EVENT_SHOW, "b"],
    561        [EVENT_INNER_REORDER, a],
    562        [EVENT_REORDER, accDoc],
    563      ],
    564    };
    565    await contentSpawnMutation(browser, waitFor, function () {
    566      const b = content.document.createElement("div");
    567      b.id = "b";
    568      content.document.body.appendChild(b);
    569    });
    570    is(getAccessibleDOMNodeID(a.firstChild), "b", "'a' owns relocated child");
    571  }
    572 );
    573 
    574 /**
    575 * Test insertion of relocated by child element reflection after initial load
    576 */
    577 addAccessibleTask(`<div id='a'></div>`, async function (browser, accDoc) {
    578  const a = findAccessibleChildByID(accDoc, "a");
    579  is(a.children.length, 0, "'a' has no children");
    580 
    581  // Create div and add it to a's ariaOwnsElements.
    582  // The refresh ticks called in contentSpawnMutation
    583  // will cause a relocation to be scheduled and performed.
    584  // Nothing will happen because 'b' is not parented yet.
    585  let waitFor = {
    586    unexpected: [
    587      [EVENT_SHOW, "b"],
    588      [EVENT_INNER_REORDER, a],
    589      [EVENT_REORDER, accDoc],
    590    ],
    591  };
    592  await contentSpawnMutation(browser, waitFor, function () {
    593    content.b = content.document.createElement("div");
    594    content.b.id = "b";
    595    content.document.getElementById("a").ariaOwnsElements = [content.b];
    596  });
    597 
    598  // Parent 'b'. It should relocate into 'a'.
    599  waitFor = {
    600    expected: [
    601      [EVENT_SHOW, "b"],
    602      [EVENT_INNER_REORDER, a],
    603      [EVENT_REORDER, accDoc],
    604    ],
    605  };
    606  await contentSpawnMutation(browser, waitFor, function () {
    607    content.document.body.appendChild(content.b);
    608  });
    609  is(getAccessibleDOMNodeID(a.firstChild), "b", "'a' owns relocated child");
    610 });
    611 
    612 /*
    613 * Test to assure that aria-owned elements are not relocated into an editable subtree.
    614 */
    615 addAccessibleTask(
    616  `
    617  <button id="btn">World</button>
    618  <div contentEditable="true" id="textbox" role="textbox">
    619    <p id="p" aria-owns="btn">Hello</p>
    620  </div>
    621  `,
    622  async function (browser, accDoc) {
    623    const p = findAccessibleChildByID(accDoc, "p");
    624    const textbox = findAccessibleChildByID(accDoc, "textbox");
    625 
    626    testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0);
    627    isnot(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn");
    628    is(textbox.value, "Hello");
    629 
    630    let expectedEvents = Promise.all([
    631      waitForStateChange(textbox, EXT_STATE_EDITABLE, false, true),
    632      waitForEvent(EVENT_INNER_REORDER, p),
    633    ]);
    634    await invokeContentTask(browser, [], () => {
    635      content.document.getElementById("textbox").contentEditable = false;
    636    });
    637    await expectedEvents;
    638    is(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn");
    639    is(textbox.value, "Hello World");
    640 
    641    expectedEvents = Promise.all([
    642      waitForStateChange(textbox, EXT_STATE_EDITABLE, true, true),
    643      waitForEvent(EVENT_INNER_REORDER, p),
    644    ]);
    645    await invokeContentTask(browser, [], () => {
    646      content.document.getElementById("textbox").contentEditable = true;
    647    });
    648    await expectedEvents;
    649    isnot(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn");
    650    is(textbox.value, "Hello");
    651  }
    652 );
    653 
    654 /*
    655 * Test to ensure that aria-owned elements are not relocated out of editable subtree.
    656 */
    657 addAccessibleTask(
    658  `
    659  <div contentEditable="true" id="textbox" role="textbox">
    660    <button id="btn">World</button>
    661  </div>
    662  <p id="p" aria-owns="btn">Hello</p>
    663  <p id="p2" aria-owns="textbox"></p>
    664  `,
    665  async function (browser, accDoc) {
    666    const p = findAccessibleChildByID(accDoc, "p");
    667    const textbox = findAccessibleChildByID(accDoc, "textbox");
    668    testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0);
    669 
    670    is(
    671      getAccessibleDOMNodeID(textbox.parent),
    672      "p2",
    673      "editable root can be relocated"
    674    );
    675    isnot(
    676      getAccessibleDOMNodeID(p.lastChild),
    677      "btn",
    678      "editable element cannot be relocated"
    679    );
    680    is(textbox.value, "World");
    681 
    682    let expectedEvents = Promise.all([
    683      waitForStateChange(textbox, EXT_STATE_EDITABLE, false, true),
    684      waitForEvent(EVENT_REORDER, p),
    685    ]);
    686    await invokeContentTask(browser, [], () => {
    687      content.document.getElementById("textbox").contentEditable = false;
    688    });
    689    await expectedEvents;
    690    is(
    691      getAccessibleDOMNodeID(p.lastChild),
    692      "btn",
    693      "'p' owns readonly relocated btn"
    694    );
    695    is(textbox.value, "");
    696    is(
    697      getAccessibleDOMNodeID(textbox.parent),
    698      "p2",
    699      "textbox is still relocated"
    700    );
    701  }
    702 );
    703 
    704 /**
    705 * Test relocating a child within its parent while also moving the caret. This
    706 * is based on a fuzzing test case.
    707 */
    708 addAccessibleTask(
    709  `
    710 <address id="a" contenteditable="true"></address>
    711 AAAAAAAA
    712 <label>
    713  `,
    714  async function testRelocateChildWithCaretMove(browser, docAcc) {
    715    let moved = waitForEvent(EVENT_TEXT_CARET_MOVED, docAcc);
    716    await invokeContentTask(browser, [], () => {
    717      content.document.body.setAttribute("aria-owns", "a");
    718      content.getSelection().selectAllChildren(content.document.body);
    719      content.document.documentElement.style.display = "none";
    720      content.document.documentElement.getBoundingClientRect();
    721      content.document.documentElement.style.display = "";
    722      content.getSelection().modify("extend", "right", "line");
    723    });
    724    await moved;
    725  }
    726 );