summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/semantics/interactive-elements/the-details-element/name-attribute.html
blob: 2685546e9b00cf39f25fbb790c3543028274b598 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
<!DOCTYPE HTML>
<meta charset=UTF-8>
<title>Test for the name attribute creating exclusive accordions from details elements</title>
<link rel="author" title="L. David Baron" href="https://dbaron.org/">
<link rel="author" title="Google" href="http://www.google.com/">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-details-element">
<link rel="help" href="https://open-ui.org/components/accordion.explainer">
<link rel="help" href="https://github.com/openui/open-ui/issues/725">
<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1444057">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<div id="container">
</div>

<script>

function assert_element_states(elements, expectations, description) {
  assert_array_equals(elements.map(e => Number(e.open)), expectations, description);
}

let container = document.getElementById("container");

promise_test(async t => {
  container.innerHTML = `
    <details name="a">
      <summary>1</summary>
      This is the first item.
    </details>

    <details name="a">
      <summary>2</summary>
      This is the second item.
    </details>
  `;
  let first = container.firstElementChild;
  let second = first.nextElementSibling;
  assert_false(first.open);
  assert_false(second.open);
  first.open = true;
  assert_true(first.open);
  assert_false(second.open);
  second.open = true;
  assert_false(first.open);
  assert_true(second.open);
  second.open = true;
  assert_false(first.open);
  assert_true(second.open);
  second.open = false;
  assert_false(first.open);
  assert_false(second.open);
}, "basic handling of mutually exclusive details");

promise_test(async t => {
  container.innerHTML = `
    <details name="a" open>
      <summary>1</summary>
      This is the first item.
    </details>

    <details name="a">
      <summary>2</summary>
      This is the second item.
    </details>

    <details name="a" open>
      <summary>3</summary>
      This is the third item.
    </details>
  `;
  let first = container.firstElementChild;
  let second = first.nextElementSibling;
  let third = second.nextElementSibling;
  function assert_states(expected_first, expected_second, expected_third, description) {
    assert_array_equals([first.open, second.open, third.open], [expected_first, expected_second, expected_third], description);
  }

  assert_states(true, false, false, "initial states from open attribute");
  first.open = true;
  assert_states(true, false, false, "non-mutation doesn't change state");
  second.open = true;
  assert_states(false, true, false, "mutation closes multiple open elements");
  third.setAttribute("open", "");
  assert_states(false, false, true, "setAttribute closes other open element");
}, "more complex handling of mutually exclusive details");

promise_test(async t => {
  let details_elements_string = `
    <details name="a"></details>
    <details name="a" open></details>
    <details name="b"></details>
    <details name="b"></details>
  `;
  container.innerHTML = `
    ${details_elements_string}
    <div id="shadow_host"></div>
  `;
  let shadow_root = document.getElementById("shadow_host").attachShadow({ mode: "open" });
  shadow_root.innerHTML = details_elements_string;
  let elements = Array.from(container.querySelectorAll("details")).concat(Array.from(shadow_root.querySelectorAll("details")));

  assert_element_states(elements, [0, 1, 0, 0, 0, 1, 0, 0], "initial states from open attribute");
  elements[4].open = true;
  assert_element_states(elements, [0, 1, 0, 0, 1, 0, 0, 0], "after mutation in shadow tree");
  for (let i = 0; i < 8; ++i) {
    elements[i].open = true;
  }
  assert_element_states(elements, [0, 1, 0, 1, 0, 1, 0, 1], "after setting all elements open");
  elements[0].open = true;
  assert_element_states(elements, [1, 0, 0, 1, 0, 1, 0, 1], "after final mutation");
}, "mutually exclusive details across multiple names and multiple tree scopes");

promise_test(async t => {
  container.innerHTML = `
    <details name="a" id="e0" open></details>
    <details name="a" id="e1"></details>
    <details name="a" id="e3" open></details>
  `;
  let e2 = document.createElement("details");
  e2.id = "e2";
  e2.name = "a";
  e2.open = true;
  let elements = [ document.getElementById("e0"),
                   document.getElementById("e1"),
                   e2,
                   document.getElementById("e3") ];
  container.insertBefore(e2, elements[3]);

  let mutation_event_received_ids = [];
  let mutation_listener = event => {
    assert_equals(event.type, "DOMSubtreeModified");
    assert_equals(event.target.nodeType, Node.ELEMENT_NODE);
    let element = event.target;
    assert_equals(element.localName, "details");
    mutation_event_received_ids.push(element.id);
  };
  let toggle_event_received_ids = [];
  let toggle_event_promises = [];
  for (let element of elements) {
    element.addEventListener("DOMSubtreeModified", mutation_listener);
    toggle_event_promises.push(new Promise((resolve, reject) => {
      element.addEventListener("toggle", event => {
        assert_equals(event.type, "toggle");
        assert_equals(event.target, element);
        toggle_event_received_ids.push(element.id);
        resolve(undefined);
      });
    }));
  }
  assert_array_equals(mutation_event_received_ids, []);
  assert_element_states(elements, [1, 0, 0, 0], "states before mutation");
  elements[1].open = true;
  if (mutation_event_received_ids.length == 0) {
    // ok if mutation events are not supported
  } else {
    assert_array_equals(mutation_event_received_ids, ["e1"],
                        "mutation events received only for open attribute mutation and not for closing other element");
  }
  assert_element_states(elements, [0, 1, 0, 0], "states after mutation");
  assert_array_equals(toggle_event_received_ids, [], "toggle events received before awaiting promises");
  await Promise.all(toggle_event_promises);
  assert_array_equals(toggle_event_received_ids, ["e3", "e2", "e1", "e0"], "toggle events received after awaiting promises, including toggle events from parser insertion");
}, "mutation event and toggle event order");

// This function is used to guard tests that test behavior that is
// relevant only because of Mutation Events.  If mutation events (for
// attribute addition/removal) are removed from the web, the tests using
// this function can be removed.
function mutation_events_for_attribute_removal_supported() {
  if (!("MutationEvent" in window)) {
    return false;
  }
  container.innerHTML = `<div id="event-removal-test"></div>`;
  let element = container.firstChild;
  let event_fired = false;
  element.addEventListener("DOMSubtreeModified", event => event_fired = true);
  element.removeAttribute("id");
  return event_fired;
}

promise_test(async t => {
  if (!mutation_events_for_attribute_removal_supported()) {
    return;
  }
  container.innerHTML = `
    <details name="a" id="e0" open></details>
    <details name="a" id="e1"></details>
    <details name="a" id="e2" open></details>
  `;
  let elements = [ document.getElementById("e0"),
                   document.getElementById("e1"),
                   document.getElementById("e2") ];

  let received_ids = [];
  let listener = event => {
    received_ids.push(event.target.id);
    let i = 0;
    for (let element of elements) {
      element.setAttribute("name", `b${i++}`);
    }
  };
  for (let element of elements) {
    element.addEventListener("DOMSubtreeModified", listener);
  }
  assert_array_equals(received_ids, []);
  assert_element_states(elements, [1, 0, 0], "states before mutation");
  elements[1].open = true;
  assert_array_equals(received_ids, ["e1"],
                      "mutation events received only for open attribute mutation and not for closing other element");
  assert_element_states(elements, [0, 1, 0], "states after mutation");
}, "interaction of open attribute changes with mutation events");

promise_test(async t => {
  container.innerHTML = `
    <details></details>
    <details></details>
    <details name></details>
    <details name></details>
    <details name=""></details>
    <details name=""></details>
  `;
  let elements = Array.from(container.querySelectorAll("details"));

  assert_element_states(elements, [0, 0, 0, 0, 0, 0], "initial states from open attribute");
  for (let i = 0; i < 6; ++i) {
    elements[i].open = true;
  }
  assert_element_states(elements, [1, 1, 1, 1, 1, 1], "after setting all elements open");
}, "empty and missing name attributes do not create groups");

const connected_scenarios = {
  "connected": {
    "create": data => container,
    "cleanup": data => {},
  },
  "disconnected": {
    "create": data => document.createElement("div"),
    "cleanup": data => {},
  },
  "shadow": {
    "create": data => {
      let e = document.createElement("div");
      container.appendChild(e);
      data.wrapper = e;
      let shadowRoot = e.attachShadow({ mode: "open" });
      let d = document.createElement("div");
      shadowRoot.appendChild(d);
      return d;
    },
    "cleanup": data => { data.wrapper.remove(); },
  },
  "shadow-in-disconnected": {
    "create": data => {
      let e = document.createElement("div");
      let shadowRoot = e.attachShadow({ mode: "open" });
      let d = document.createElement("div");
      shadowRoot.appendChild(d);
      return d;
    },
    "cleanup": data => {},
  },
  "template-in-disconnected": {
    "create": data => {
      let e = document.createElement("div");
      e.innerHTML = `
        <template>
          <div></div>
        </template>
      `;
      return e.firstElementChild.content.firstElementChild;
    },
    "cleanup": data => {},
  },
  "connected-in-xhr-response": {
    "create": data => new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest();
      xhr.open("GET", "support/empty-html-document.html");
      xhr.responseType = "document";
      xhr.send();
      xhr.addEventListener("load", event => { resolve(xhr.response.body); });
      let reject_with_type =
        event => { reject(`${event.type} event received`); }
      xhr.addEventListener("error", reject_with_type);
      xhr.addEventListener("abort", reject_with_type);
    }),
    "cleanup": data => {},
  },
  "connected-in-implementation-create-document": {
    "create": data => {
      let doc = document.implementation.createHTMLDocument("impl-created");
      return doc.body;
    },
    "cleanup": data => {},
  },
  "connected-in-template": {
    "create": data => {
      container.innerHTML = `
        <template>
          <div></div>
        </template>
      `;
      return container.firstElementChild.content.firstElementChild;
    },
    "cleanup": data => { container.innerHTML = ""; },
  },
};

for (const [scenario, scenario_callbacks] of Object.entries(connected_scenarios)) {
  promise_test(async t => {
    let data = {};
    let container = await scenario_callbacks.create(data);
    t.add_cleanup(async () => await scenario_callbacks.cleanup(data));
    assert_true(container instanceof HTMLDivElement ||
                  container instanceof HTMLBodyElement,
                "error in test setup");

    container.innerHTML = `
      <details name="scenariotest" open></details>
      <details name="scenariotest"></details>
    `;

    let elements = Array.from(container.querySelectorAll("details[name='scenariotest']"));
    assert_element_states(elements, [1, 0], "state before toggle");
    elements[1].open = true;
    assert_element_states(elements, [0, 1], "state after toggle enforces exclusivity");
  }, `exclusivity enforcement with attachment scenario ${scenario}`);
}

promise_test(async t => {
  container.innerHTML = `
    <details name="a" id="e0" open></details>
    <details name="a" id="e1"></details>
    <details name="b" id="e2" open></details>
  `;
  let elements = [ document.getElementById("e0"),
                   document.getElementById("e1"),
                   document.getElementById("e2") ];

  let mutation_received_ids = [];
  let listener = event => {
    mutation_received_ids.push(event.target.id);
  };
  for (let element of elements) {
    element.addEventListener("DOMSubtreeModified", listener);
  }

  assert_element_states(elements, [1, 0, 1], "states before first mutation");
  assert_array_equals(mutation_received_ids, [], "mutation events received before first mutation");
  elements[2].name = "a";
  assert_element_states(elements, [1, 0, 0], "states after first mutation");
  if (mutation_received_ids.length != 0) {
    // OK to not support mutation events, or to send DOMSubtreeModified
    // only for attribute addition/removal (open) but not for attribute
    // change (name)
    assert_array_equals(mutation_received_ids, ["e2"], "mutation events received after first mutation");
  }
  elements[0].name = "c";
  elements[2].open = true;
  assert_element_states(elements, [1, 0, 1], "states before second mutation");
  if (mutation_received_ids.length != 0) { // OK to not support mutation events
    if (mutation_received_ids.length == 1) {
      // OK to receive DOMSubtreeModified for attribute addition/removal
      // (open) but not for attribute change (name)
      assert_array_equals(mutation_received_ids, ["e2"], "mutation events received before second mutation");
    } else {
      assert_array_equals(mutation_received_ids, ["e2", "e0", "e2"], "mutation events received before second mutation");
    }
  }
  elements[0].name = "a";
  assert_element_states(elements, [0, 0, 1], "states after second mutation");
  if (mutation_received_ids.length != 0) { // OK to not support mutation events
    if (mutation_received_ids.length == 1) {
      // OK to receive DOMSubtreeModified for attribute addition/removal
      // (open) but not for attribute change (name)
      assert_array_equals(mutation_received_ids, ["e2"], "mutation events received before second mutation");
    } else {
      assert_array_equals(mutation_received_ids, ["e2", "e0", "e2", "e0"], "mutation events received after second mutation");
    }
  }
}, "handling of name attribute changes");

promise_test(async t => {
  container.innerHTML = `
    <details name="a" id="e0" open></details>
    <details name="a" id="e1" open></details>
    <details open name="a" id="e2"></details>
  `;
  let elements = [ document.getElementById("e0"),
                   document.getElementById("e1"),
                   document.getElementById("e2") ];

  assert_element_states(elements, [1, 0, 0], "states after insertion by parser");
}, "closing as a result of parsing doesn't depend on attribute order");

promise_test(async t => {
  container.innerHTML = `
    <details name="a" id="e0" open></details>
    <details name="a" id="e1"></details>
  `;
  let elements = [ document.getElementById("e0"),
                   document.getElementById("e1") ];

  assert_element_states(elements, [1, 0], "states before first mutation");

  let make_details = () => {
    let e = document.createElement("details");
    e.setAttribute("name", "a");
    return e;
  };

  let watch_e0 = new EventWatcher(t, elements[0], ['toggle']);
  let watch_e1 = new EventWatcher(t, elements[1], ['toggle']);

  let expect_opening = async (watcher) => {
    await watcher.wait_for(['toggle'], {record: 'all'}).then((events) => {
      assert_equals(events[0].oldState, "closed");
      assert_equals(events[0].newState, "open");
    });
  };

  let expect_closing = async (watcher) => {
    await watcher.wait_for(['toggle'], {record: 'all'}).then((events) => {
      assert_equals(events[0].oldState, "open");
      assert_equals(events[0].newState, "closed");
    });
  };

  let track_mutations = (element) => {
    let result = { count: 0 };
    let listener = event => {
      ++result.count;
    };
    element.addEventListener("DOMSubtreeModified", listener);
    return result;
  }

  await expect_opening(watch_e0);

  // Test appending an open element in the group.
  let new1 = make_details();
  let mutations1 = track_mutations(new1);
  let watch_new1 = new EventWatcher(t, new1, ['toggle']);
  new1.open = true;
  assert_in_array(mutations1.count, [0, 1], "mutation events count before inserting new1");
  await expect_opening(watch_new1);
  container.appendChild(new1);
  await expect_closing(watch_new1);
  assert_in_array(mutations1.count, [0, 1], "mutation events count after inserting new1");

  // Test appending a closed element in the group.
  let new2 = make_details();
  let mutations2 = track_mutations(new2);
  let watch_new2 = new EventWatcher(t, new2, ['toggle']);
  container.appendChild(new2);
  assert_equals(mutations2.count, 0, "mutation events count after inserting new2");

  // Test inserting an open element at the start of the group.
  let new3 = make_details();
  let mutations3 = track_mutations(new3);
  new3.open = true; // this time do this before creating the EventWatcher
  let watch_new3 = new EventWatcher(t, new3, ['toggle']);
  assert_in_array(mutations3.count, [0, 1], "mutation events count before inserting new3");
  await expect_opening(watch_new3);
  container.insertBefore(new3, elements[0]);
  await expect_closing(watch_new3);
  assert_in_array(mutations3.count, [0, 1], "mutation events count after inserting new3");
}, "handling of insertion of elements into group");

</script>