tor-browser

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

name-attribute.html (18074B)


      1 <!DOCTYPE HTML>
      2 <meta charset=UTF-8>
      3 <title>Test for the name attribute creating exclusive accordions from details elements</title>
      4 <link rel="author" title="L. David Baron" href="https://dbaron.org/">
      5 <link rel="author" title="Google" href="http://www.google.com/">
      6 <link rel="help" href="https://html.spec.whatwg.org/multipage/#the-details-element">
      7 <link rel="help" href="https://open-ui.org/components/accordion.explainer">
      8 <link rel="help" href="https://github.com/openui/open-ui/issues/725">
      9 <link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1444057">
     10 <script src="/resources/testharness.js"></script>
     11 <script src="/resources/testharnessreport.js"></script>
     12 
     13 <div id="container">
     14 </div>
     15 
     16 <script>
     17 
     18 function assert_element_states(elements, expectations, description) {
     19  assert_array_equals(elements.map(e => Number(e.open)), expectations, description);
     20 }
     21 
     22 let container = document.getElementById("container");
     23 
     24 promise_test(async t => {
     25  container.innerHTML = `
     26    <details name="a">
     27      <summary>1</summary>
     28      This is the first item.
     29    </details>
     30 
     31    <details name="a">
     32      <summary>2</summary>
     33      This is the second item.
     34    </details>
     35  `;
     36  let first = container.firstElementChild;
     37  let second = first.nextElementSibling;
     38  assert_false(first.open);
     39  assert_false(second.open);
     40  first.open = true;
     41  assert_true(first.open);
     42  assert_false(second.open);
     43  second.open = true;
     44  assert_false(first.open);
     45  assert_true(second.open);
     46  second.open = true;
     47  assert_false(first.open);
     48  assert_true(second.open);
     49  second.open = false;
     50  assert_false(first.open);
     51  assert_false(second.open);
     52 }, "basic handling of mutually exclusive details");
     53 
     54 promise_test(async t => {
     55  container.innerHTML = `
     56    <details name="a" open>
     57      <summary>1</summary>
     58      This is the first item.
     59    </details>
     60 
     61    <details name="a">
     62      <summary>2</summary>
     63      This is the second item.
     64    </details>
     65 
     66    <details name="a" open>
     67      <summary>3</summary>
     68      This is the third item.
     69    </details>
     70  `;
     71  let first = container.firstElementChild;
     72  let second = first.nextElementSibling;
     73  let third = second.nextElementSibling;
     74  function assert_states(expected_first, expected_second, expected_third, description) {
     75    assert_array_equals([first.open, second.open, third.open], [expected_first, expected_second, expected_third], description);
     76  }
     77 
     78  assert_states(true, false, false, "initial states from open attribute");
     79  first.open = true;
     80  assert_states(true, false, false, "non-mutation doesn't change state");
     81  second.open = true;
     82  assert_states(false, true, false, "mutation closes multiple open elements");
     83  third.setAttribute("open", "");
     84  assert_states(false, false, true, "setAttribute closes other open element");
     85 }, "more complex handling of mutually exclusive details");
     86 
     87 promise_test(async t => {
     88  let details_elements_string = `
     89    <details name="a"></details>
     90    <details name="a" open></details>
     91    <details name="b"></details>
     92    <details name="b"></details>
     93  `;
     94  container.innerHTML = `
     95    ${details_elements_string}
     96    <div id="shadow_host"></div>
     97  `;
     98  let shadow_root = document.getElementById("shadow_host").attachShadow({ mode: "open" });
     99  shadow_root.innerHTML = details_elements_string;
    100  let elements = Array.from(container.querySelectorAll("details")).concat(Array.from(shadow_root.querySelectorAll("details")));
    101 
    102  assert_element_states(elements, [0, 1, 0, 0, 0, 1, 0, 0], "initial states from open attribute");
    103  elements[4].open = true;
    104  assert_element_states(elements, [0, 1, 0, 0, 1, 0, 0, 0], "after mutation in shadow tree");
    105  for (let i = 0; i < 8; ++i) {
    106    elements[i].open = true;
    107  }
    108  assert_element_states(elements, [0, 1, 0, 1, 0, 1, 0, 1], "after setting all elements open");
    109  elements[0].open = true;
    110  assert_element_states(elements, [1, 0, 0, 1, 0, 1, 0, 1], "after final mutation");
    111 }, "mutually exclusive details across multiple names and multiple tree scopes");
    112 
    113 promise_test(async t => {
    114  container.innerHTML = `
    115    <details name="a" id="e0" open></details>
    116    <details name="a" id="e1"></details>
    117    <details name="a" id="e3" open></details>
    118  `;
    119  let e2 = document.createElement("details");
    120  e2.id = "e2";
    121  e2.name = "a";
    122  e2.open = true;
    123  let elements = [ document.getElementById("e0"),
    124                   document.getElementById("e1"),
    125                   e2,
    126                   document.getElementById("e3") ];
    127  container.insertBefore(e2, elements[3]);
    128 
    129  let mutation_event_received_ids = [];
    130  let mutation_listener = event => {
    131    assert_equals(event.type, "DOMSubtreeModified");
    132    assert_equals(event.target.nodeType, Node.ELEMENT_NODE);
    133    let element = event.target;
    134    assert_equals(element.localName, "details");
    135    mutation_event_received_ids.push(element.id);
    136  };
    137  let toggle_event_received_ids = [];
    138  let toggle_event_promises = [];
    139  for (let element of elements) {
    140    element.addEventListener("DOMSubtreeModified", mutation_listener);
    141    toggle_event_promises.push(new Promise((resolve, reject) => {
    142      element.addEventListener("toggle", event => {
    143        assert_equals(event.type, "toggle");
    144        assert_equals(event.target, element);
    145        toggle_event_received_ids.push(element.id);
    146        resolve(undefined);
    147      });
    148    }));
    149  }
    150  assert_array_equals(mutation_event_received_ids, []);
    151  assert_element_states(elements, [1, 0, 0, 0], "states before mutation");
    152  elements[1].open = true;
    153  if (mutation_event_received_ids.length == 0) {
    154    // ok if mutation events are not supported
    155  } else {
    156    assert_array_equals(mutation_event_received_ids, ["e1"],
    157                        "mutation events received only for open attribute mutation and not for closing other element");
    158  }
    159  assert_element_states(elements, [0, 1, 0, 0], "states after mutation");
    160  assert_array_equals(toggle_event_received_ids, [], "toggle events received before awaiting promises");
    161  await Promise.all(toggle_event_promises);
    162  assert_array_equals(toggle_event_received_ids, ["e3", "e2", "e1", "e0"], "toggle events received after awaiting promises, including toggle events from parser insertion");
    163 }, "mutation event and toggle event order");
    164 
    165 // This function is used to guard tests that test behavior that is
    166 // relevant only because of Mutation Events.  If mutation events (for
    167 // attribute addition/removal) are removed from the web, the tests using
    168 // this function can be removed.
    169 function mutation_events_for_attribute_removal_supported() {
    170  if (!("MutationEvent" in window)) {
    171    return false;
    172  }
    173  container.innerHTML = `<div id="event-removal-test"></div>`;
    174  let element = container.firstChild;
    175  let event_fired = false;
    176  element.addEventListener("DOMSubtreeModified", event => event_fired = true);
    177  element.removeAttribute("id");
    178  return event_fired;
    179 }
    180 
    181 promise_test(async t => {
    182  if (!mutation_events_for_attribute_removal_supported()) {
    183    return;
    184  }
    185  container.innerHTML = `
    186    <details name="a" id="e0" open></details>
    187    <details name="a" id="e1"></details>
    188    <details name="a" id="e2" open></details>
    189  `;
    190  let elements = [ document.getElementById("e0"),
    191                   document.getElementById("e1"),
    192                   document.getElementById("e2") ];
    193 
    194  let received_ids = [];
    195  let listener = event => {
    196    received_ids.push(event.target.id);
    197  };
    198  for (let element of elements) {
    199    element.addEventListener("DOMSubtreeModified", listener);
    200  }
    201  assert_array_equals(received_ids, []);
    202  assert_element_states(elements, [1, 0, 0], "states before mutation");
    203  elements[1].open = true;
    204  assert_array_equals(received_ids, ["e1"],
    205                      "mutation events received only for open attribute mutation and not for closing other element");
    206  assert_element_states(elements, [0, 1, 0], "states after mutation");
    207 }, "interaction of open attribute changes with mutation events");
    208 
    209 promise_test(async t => {
    210  container.innerHTML = `
    211    <details></details>
    212    <details></details>
    213    <details name></details>
    214    <details name></details>
    215    <details name=""></details>
    216    <details name=""></details>
    217  `;
    218  let elements = Array.from(container.querySelectorAll("details"));
    219 
    220  assert_element_states(elements, [0, 0, 0, 0, 0, 0], "initial states from open attribute");
    221  for (let i = 0; i < 6; ++i) {
    222    elements[i].open = true;
    223  }
    224  assert_element_states(elements, [1, 1, 1, 1, 1, 1], "after setting all elements open");
    225 }, "empty and missing name attributes do not create groups");
    226 
    227 const connected_scenarios = {
    228  "connected": {
    229    "create": data => container,
    230    "cleanup": data => {},
    231  },
    232  "disconnected": {
    233    "create": data => document.createElement("div"),
    234    "cleanup": data => {},
    235  },
    236  "shadow": {
    237    "create": data => {
    238      let e = document.createElement("div");
    239      container.appendChild(e);
    240      data.wrapper = e;
    241      let shadowRoot = e.attachShadow({ mode: "open" });
    242      let d = document.createElement("div");
    243      shadowRoot.appendChild(d);
    244      return d;
    245    },
    246    "cleanup": data => { data.wrapper.remove(); },
    247  },
    248  "shadow-in-disconnected": {
    249    "create": data => {
    250      let e = document.createElement("div");
    251      let shadowRoot = e.attachShadow({ mode: "open" });
    252      let d = document.createElement("div");
    253      shadowRoot.appendChild(d);
    254      return d;
    255    },
    256    "cleanup": data => {},
    257  },
    258  "template-in-disconnected": {
    259    "create": data => {
    260      let e = document.createElement("div");
    261      e.innerHTML = `
    262        <template>
    263          <div></div>
    264        </template>
    265      `;
    266      return e.firstElementChild.content.firstElementChild;
    267    },
    268    "cleanup": data => {},
    269  },
    270  "connected-in-xhr-response": {
    271    "create": data => new Promise((resolve, reject) => {
    272      let xhr = new XMLHttpRequest();
    273      xhr.open("GET", "support/empty-html-document.html");
    274      xhr.responseType = "document";
    275      xhr.send();
    276      xhr.addEventListener("load", event => { resolve(xhr.response.body); });
    277      let reject_with_type =
    278        event => { reject(`${event.type} event received`); }
    279      xhr.addEventListener("error", reject_with_type);
    280      xhr.addEventListener("abort", reject_with_type);
    281    }),
    282    "cleanup": data => {},
    283  },
    284  "connected-in-implementation-create-document": {
    285    "create": data => {
    286      let doc = document.implementation.createHTMLDocument("impl-created");
    287      return doc.body;
    288    },
    289    "cleanup": data => {},
    290  },
    291  "connected-in-template": {
    292    "create": data => {
    293      container.innerHTML = `
    294        <template>
    295          <div></div>
    296        </template>
    297      `;
    298      return container.firstElementChild.content.firstElementChild;
    299    },
    300    "cleanup": data => { container.innerHTML = ""; },
    301  },
    302 };
    303 
    304 for (const [scenario, scenario_callbacks] of Object.entries(connected_scenarios)) {
    305  promise_test(async t => {
    306    let data = {};
    307    let container = await scenario_callbacks.create(data);
    308    t.add_cleanup(async () => await scenario_callbacks.cleanup(data));
    309    assert_true(container instanceof HTMLDivElement ||
    310                  container instanceof HTMLBodyElement,
    311                "error in test setup");
    312 
    313    container.innerHTML = `
    314      <details name="scenariotest" open></details>
    315      <details name="scenariotest"></details>
    316    `;
    317 
    318    let elements = Array.from(container.querySelectorAll("details[name='scenariotest']"));
    319    assert_element_states(elements, [1, 0], "state before toggle");
    320    elements[1].open = true;
    321    assert_element_states(elements, [0, 1], "state after toggle enforces exclusivity");
    322  }, `exclusivity enforcement with attachment scenario ${scenario}`);
    323 }
    324 
    325 promise_test(async t => {
    326  container.innerHTML = `
    327    <details name="a" id="e0" open></details>
    328    <details name="a" id="e1"></details>
    329    <details name="b" id="e2" open></details>
    330  `;
    331  let elements = [ document.getElementById("e0"),
    332                   document.getElementById("e1"),
    333                   document.getElementById("e2") ];
    334 
    335  let mutation_received_ids = [];
    336  let listener = event => {
    337    mutation_received_ids.push(event.target.id);
    338  };
    339  for (let element of elements) {
    340    element.addEventListener("DOMSubtreeModified", listener);
    341  }
    342 
    343  assert_element_states(elements, [1, 0, 1], "states before first mutation");
    344  assert_array_equals(mutation_received_ids, [], "mutation events received before first mutation");
    345  elements[2].name = "a";
    346  assert_element_states(elements, [1, 0, 0], "states after first mutation");
    347  if (mutation_received_ids.length != 0) {
    348    // OK to not support mutation events, or to send DOMSubtreeModified
    349    // only for attribute addition/removal (open) but not for attribute
    350    // change (name)
    351    assert_array_equals(mutation_received_ids, ["e2"], "mutation events received after first mutation");
    352  }
    353  elements[0].name = "c";
    354  elements[2].open = true;
    355  assert_element_states(elements, [1, 0, 1], "states before second mutation");
    356  if (mutation_received_ids.length != 0) { // OK to not support mutation events
    357    if (mutation_received_ids.length == 1) {
    358      // OK to receive DOMSubtreeModified for attribute addition/removal
    359      // (open) but not for attribute change (name)
    360      assert_array_equals(mutation_received_ids, ["e2"], "mutation events received before second mutation");
    361    } else {
    362      assert_array_equals(mutation_received_ids, ["e2", "e0", "e2"], "mutation events received before second mutation");
    363    }
    364  }
    365  elements[0].name = "a";
    366  assert_element_states(elements, [0, 0, 1], "states after second mutation");
    367  if (mutation_received_ids.length != 0) { // OK to not support mutation events
    368    if (mutation_received_ids.length == 1) {
    369      // OK to receive DOMSubtreeModified for attribute addition/removal
    370      // (open) but not for attribute change (name)
    371      assert_array_equals(mutation_received_ids, ["e2"], "mutation events received before second mutation");
    372    } else {
    373      assert_array_equals(mutation_received_ids, ["e2", "e0", "e2", "e0"], "mutation events received after second mutation");
    374    }
    375  }
    376 }, "handling of name attribute changes");
    377 
    378 promise_test(async t => {
    379  container.innerHTML = `
    380    <details name="a" id="e0" open></details>
    381    <details name="a" id="e1" open></details>
    382    <details open name="a" id="e2"></details>
    383  `;
    384  let elements = [ document.getElementById("e0"),
    385                   document.getElementById("e1"),
    386                   document.getElementById("e2") ];
    387 
    388  assert_element_states(elements, [1, 0, 0], "states after insertion by parser");
    389 }, "closing as a result of parsing doesn't depend on attribute order");
    390 
    391 promise_test(async t => {
    392  container.innerHTML = `
    393    <details name="a" id="e0" open></details>
    394    <details name="a" id="e1"></details>
    395  `;
    396  let elements = [ document.getElementById("e0"),
    397                   document.getElementById("e1") ];
    398 
    399  assert_element_states(elements, [1, 0], "states before first mutation");
    400 
    401  let make_details = () => {
    402    let e = document.createElement("details");
    403    e.setAttribute("name", "a");
    404    return e;
    405  };
    406 
    407  let watch_e0 = new EventWatcher(t, elements[0], ['toggle']);
    408  let watch_e1 = new EventWatcher(t, elements[1], ['toggle']);
    409 
    410  let expect_opening = async (watcher) => {
    411    await watcher.wait_for(['toggle'], {record: 'all'}).then((events) => {
    412      assert_equals(events[0].oldState, "closed");
    413      assert_equals(events[0].newState, "open");
    414    });
    415  };
    416 
    417  let expect_closing = async (watcher) => {
    418    await watcher.wait_for(['toggle'], {record: 'all'}).then((events) => {
    419      assert_equals(events[0].oldState, "open");
    420      assert_equals(events[0].newState, "closed");
    421    });
    422  };
    423 
    424  let track_mutations = (element) => {
    425    let result = { count: 0 };
    426    let listener = event => {
    427      ++result.count;
    428    };
    429    element.addEventListener("DOMSubtreeModified", listener);
    430    return result;
    431  }
    432 
    433  await expect_opening(watch_e0);
    434 
    435  // Test appending an open element in the group.
    436  let new1 = make_details();
    437  let mutations1 = track_mutations(new1);
    438  let watch_new1 = new EventWatcher(t, new1, ['toggle']);
    439  new1.open = true;
    440  assert_in_array(mutations1.count, [0, 1], "mutation events count before inserting new1");
    441  await expect_opening(watch_new1);
    442  container.appendChild(new1);
    443  await expect_closing(watch_new1);
    444  assert_in_array(mutations1.count, [0, 1], "mutation events count after inserting new1");
    445 
    446  // Test appending a closed element in the group.
    447  let new2 = make_details();
    448  let mutations2 = track_mutations(new2);
    449  let watch_new2 = new EventWatcher(t, new2, ['toggle']);
    450  container.appendChild(new2);
    451  assert_equals(mutations2.count, 0, "mutation events count after inserting new2");
    452 
    453  // Test inserting an open element at the start of the group.
    454  let new3 = make_details();
    455  let mutations3 = track_mutations(new3);
    456  new3.open = true; // this time do this before creating the EventWatcher
    457  let watch_new3 = new EventWatcher(t, new3, ['toggle']);
    458  assert_in_array(mutations3.count, [0, 1], "mutation events count before inserting new3");
    459  await expect_opening(watch_new3);
    460  container.insertBefore(new3, elements[0]);
    461  await expect_closing(watch_new3);
    462  assert_in_array(mutations3.count, [0, 1], "mutation events count after inserting new3");
    463 }, "handling of insertion of elements into group");
    464 
    465 promise_test(async t => {
    466  container.remove();
    467  container.innerHTML = `
    468    <details name="a">
    469      <summary>1</summary>
    470      This is the first item.
    471    </details>
    472 
    473    <details name="a">
    474      <summary>2</summary>
    475      This is the second item.
    476    </details>
    477  `;
    478  let first = container.firstElementChild;
    479  let second = first.nextElementSibling;
    480  assert_false(first.open);
    481  assert_false(second.open);
    482  first.open = true;
    483  assert_true(first.open);
    484  assert_false(second.open);
    485  second.open = true;
    486  assert_false(first.open);
    487  assert_true(second.open);
    488  second.open = true;
    489  assert_false(first.open);
    490  assert_true(second.open);
    491  second.open = false;
    492  assert_false(first.open);
    493  assert_false(second.open);
    494 }, "basic handling of mutually exclusive details when the element isn't connected");
    495 
    496 </script>