summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/dom/aria-element-reflection.html
blob: 8d4d4b8f6a652ac79fae69a03aef50772098b5a5 (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
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Element Reflection for aria-activedescendant and aria-errormessage</title>
    <link rel=help href="https://whatpr.org/html/3917/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:element">
    <link rel="author" title="Meredith Lane" href="meredithl@chromium.org">
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
  </head>

  <div id="activedescendant" aria-activedescendant="x"></div>

  <div id="parentListbox" role="listbox" aria-activedescendant="i1">
    <div role="option" id="i1">Item 1</div>
    <div role="option" id="i2">Item 2</div>
  </div>

  <script>
  test(function(t) {
    assert_equals(activedescendant.ariaActiveDescendantElement, null,
                  "invalid ID for relationship returns null");

    // Element reference should be set if the content attribute was included.
    assert_equals(parentListbox.getAttribute("aria-activedescendant"), "i1", "check content attribute after parsing.");
    assert_equals(parentListbox.ariaActiveDescendantElement, i1, "check idl attribute after parsing.");
    assert_equals(parentListbox.ariaActiveDescendantElement, parentListbox.ariaActiveDescendantElement, "check idl attribute caching after parsing.");

    // If we set the content attribute, the element reference should reflect this.
    parentListbox.setAttribute("aria-activedescendant", "i2");
    assert_equals(parentListbox.ariaActiveDescendantElement, i2, "setting the content attribute updates the element reference.");
    assert_equals(parentListbox.ariaActiveDescendantElement, parentListbox.ariaActiveDescendantElement, "check idl attribute caching after update.");

    // Setting the element reference should set the empty string in the content attribute.
    parentListbox.ariaActiveDescendantElement = i1;
    assert_equals(parentListbox.ariaActiveDescendantElement, i1, "getter should return the right element reference.");
    assert_equals(parentListbox.getAttribute("aria-activedescendant"), "", "content attribute should be empty.");

    // Both content and IDL attribute should be nullable.
    parentListbox.ariaActiveDescendantElement = null;
    assert_equals(parentListbox.ariaActiveDescendantElement, null);
    assert_false(parentListbox.hasAttribute("aria-activedescendant"));
    assert_equals(parentListbox.getAttribute("aria-activedescendant"), null, "nullifying the idl attribute removes the content attribute.");

    // Setting content attribute to non-existent or non compatible element should nullify the IDL attribute.
    // Reset the element to an existant one.
    parentListbox.setAttribute("aria-activedescendant", "i1");
    assert_equals(parentListbox.ariaActiveDescendantElement, i1, "reset attribute.");

    parentListbox.setAttribute("aria-activedescendant", "non-existent-element");
    assert_equals(parentListbox.getAttribute("aria-activedescendant"), "non-existent-element");
    assert_equals(parentListbox.ariaActiveDescendantElement, null,"non-DOM content attribute should null the element reference");
  }, "aria-activedescendant element reflection");
  </script>

  <div id="parentListbox2" role="listbox" aria-activedescendant="option1">
    <div role="option" id="option1">Item 1</div>
    <div role="option" id="option2">Item 2</div>
  </div>

  <script>
  test(function(t) {
    const option1 = document.getElementById("option1");
    const option2 = document.getElementById("option2");
    assert_equals(parentListbox2.ariaActiveDescendantElement, option1);
    option1.removeAttribute("id");
    option2.setAttribute("id", "option1");
    const option2Duplicate = document.getElementById("option1");
    assert_equals(option2, option2Duplicate);

    assert_equals(parentListbox2.ariaActiveDescendantElement, option2);
  }, "If the content attribute is set directly, the IDL attribute getter always returns the first element whose ID matches the content attribute.");
  </script>

  <div id="blankIdParent" role="listbox">
    <div role="option" id="multiple-id"></div>
    <div role="option" id="multiple-id"></div>
  </div>

  <script>
  test(function(t) {
    // Get second child of parent. This violates the setting of a reflected element
    // as it will not be the first child of the parent with that ID, which should
    // result in an empty string for the content attribute.
    blankIdParent.ariaActiveDescendantElement = blankIdParent.children[1];
    assert_true(blankIdParent.hasAttribute("aria-activedescendant"));
    assert_equals(blankIdParent.getAttribute("aria-activedescendant"), "");
    assert_equals(blankIdParent.ariaActiveDescendantElement, blankIdParent.children[1]);
  }, "Setting the IDL attribute to an element which is not the first element in DOM order with its ID causes the content attribute to be an empty string");
  </script>

  <div id="outerContainer">
    <p id="lightParagraph">Hello world!</p>
    <span id="shadowHost">
    </span>
  </div>

  <script>
  test(function(t) {
    const shadow = shadowHost.attachShadow({mode: "open"});
    const link = document.createElement("a");
    shadow.appendChild(link);

    assert_equals(lightParagraph.ariaActiveDescendantElement, null);

    // The given element crosses a shadow dom boundary, so it cannot be
    // set as an element reference.
    lightParagraph.ariaActiveDescendantElement = link;
    assert_equals(lightParagraph.ariaActiveDescendantElement, null);

    // The given element crosses a shadow dom boundary (upwards), so
    // can be used as an element reference, but the content attribute
    // should reflect the empty string.
    link.ariaActiveDescendantElement = lightParagraph;
    assert_equals(link.ariaActiveDescendantElement, lightParagraph);
    assert_equals(link.getAttribute("aria-activedescendant"), "");
  }, "Setting an element reference that crosses into a shadow tree is disallowed, but setting one that is in a shadow inclusive ancestor is allowed.");
  </script>

  <input id="startTime" ></input>
  <span id="errorMessage">Invalid Time</span>

  <script>
  test(function(t) {
    startTime.ariaErrorMessageElements = [errorMessage];
    assert_equals(startTime.getAttribute("aria-errormessage"), "");
    assert_array_equals(startTime.ariaErrorMessageElements, [errorMessage]);

    startTime.ariaErrorMessageElements = [];
    assert_array_equals(startTime.ariaErrorMessageElements, []);
    assert_equals(startTime.getAttribute("aria-errormessage"), "");

    startTime.setAttribute("aria-errormessage", "errorMessage");
    assert_array_equals(startTime.ariaErrorMessageElements, [errorMessage]);

  }, "aria-errormessage");

  test(function (t) {
      assert_false('ariaErrorMessageElement' in startTime);
  }, 'ariaErrorMessageElement is not defined')

  </script>

  <label>
    Password:
    <input id="passwordField" type="password" aria-details="pw">
  </label>

  <ul>
    <li id="listItem1">First description.</li>
    <li id="listItem2">Second description.</li>
  </ul>

  <script>

  test(function(t) {
    assert_array_equals(passwordField.ariaDetailsElements, []);
    passwordField.ariaDetailsElements = [ listItem1 ];
    assert_equals(passwordField.getAttribute("aria-details"), "");
    assert_array_equals(passwordField.ariaDetailsElements, [ listItem1 ]);

    passwordField.ariaDetailsElements = [ listItem2 ];
    assert_equals(passwordField.getAttribute("aria-details"), "");
    assert_array_equals(passwordField.ariaDetailsElements, [ listItem2 ]);
  }, "aria-details");
  </script>

  <div id="deletionParent" role="listbox" aria-activedescendant="contentAttrElement">
    <div role="option" id="contentAttrElement">Item 1</div>
    <div role="option" id="idlAttrElement">Item 2</div>
  </div>

  <script>

  test(function(t) {
    const contentAttrElement = document.getElementById("contentAttrElement");
    const idlAttrElement = document.getElementById("idlAttrElement");

    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "contentAttrElement");
    assert_equals(deletionParent.ariaActiveDescendantElement, contentAttrElement);

    // Deleting an element set via the content attribute.
    deletionParent.removeChild(contentAttrElement);
    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "contentAttrElement");

    // As it was not explitly set, the attr-associated-element is computed from the content attribute,
    // and since descendant1 has been removed from the DOM, it is not valid.
    assert_equals(deletionParent.ariaActiveDescendantElement, null);

    // Deleting an element set via the IDL attribute.
    deletionParent.ariaActiveDescendantElement = idlAttrElement;
    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "");

    deletionParent.removeChild(idlAttrElement);
    assert_equals(deletionParent.ariaActiveDescendantElement, null);

    // The content attribute is still empty.
    assert_equals(deletionParent.getAttribute("aria-activedescendant"), "");
  }, "Deleting a reflected element should return null for the IDL attribute and the content attribute will be empty.");
  </script>

  <div id="parentNode" role="listbox" aria-activedescendant="changingIdElement">
    <div role="option" id="changingIdElement">Item 1</div>
    <div role="option" id="persistantIDElement">Item 2</div>
  </div>

  <script>
  test(function(t) {
    const changingIdElement = document.getElementById("changingIdElement");
    assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement);

    // Modify the id attribute.
    changingIdElement.setAttribute("id", "new-id");

    // The content attribute still reflects the old id, and we expect the
    // Element reference to be null as there is no DOM node with id "original"
    assert_equals(parentNode.getAttribute("aria-activedescendant"), "changingIdElement");
    assert_equals(parentNode.ariaActiveDescendantElement, null, "Element set via content attribute with a changed id will return null on getting");

    parentNode.ariaActiveDescendantElement = changingIdElement;
    assert_equals(parentNode.getAttribute("aria-activedescendant"), "");
    assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement);

    // The explicitly set element takes precendance over the content attribute.
    // This means that we still return the same element reference, but the
    // content attribute is empty.
    changingIdElement.setAttribute("id", "newer-id");
    assert_equals(parentNode.ariaActiveDescendantElement, changingIdElement, "explicitly set element is still present even after the id has been changed");
    assert_equals(parentNode.getAttribute("aria-activedescendant"), "", "content attribute is empty.");
  }, "Changing the ID of an element doesn't lose the reference.");
  </script>

  <!-- TODO(chrishall): change naming scheme to inner/outer -->
  <div id="lightParent" role="listbox">
    <div id="lightElement" role="option">Hello world!</div>
  </div>
  <div id="shadowHostElement"></div>

  <script>
  test(function(t) {
    const lightElement = document.getElementById("lightElement");
    const shadowRoot = shadowHostElement.attachShadow({mode: "open"});

    assert_equals(lightParent.ariaActiveDescendantElement, null, 'null before');
    assert_equals(lightParent.getAttribute('aria-activedescendant'), null, 'null before');

    lightParent.ariaActiveDescendantElement = lightElement;
    assert_equals(lightParent.ariaActiveDescendantElement, lightElement);
    assert_equals(lightParent.getAttribute('aria-activedescendant'), "");

    // Move the referenced element into shadow DOM.
    // This will cause the computed attr-associated element to be null as the
    // referenced element will no longer be in a valid scope.
    // The underlying reference is kept intact, so if the referenced element is
    // later restored to a valid scope the computed attr-associated element will
    // then reflect
    shadowRoot.appendChild(lightElement);
    assert_equals(lightParent.ariaActiveDescendantElement, null, "computed attr-assoc element should be null as referenced element is in an invalid scope");
    assert_equals(lightParent.getAttribute("aria-activedescendant"), "");

    // Move the referenced element back into light DOM.
    // Since the underlying reference was kept intact, after moving the
    // referenced element back to a valid scope should be reflected in the
    // computed attr-associated element.
    lightParent.appendChild(lightElement);
    assert_equals(lightParent.ariaActiveDescendantElement, lightElement, "computed attr-assoc element should be restored as referenced element is back in a valid scope");
    assert_equals(lightParent.getAttribute("aria-activedescendant"), "");
  }, "Reparenting an element into a descendant shadow scope hides the element reference.");
  </script>

  <div id='fruitbowl' role='listbox'>
    <div id='apple' role='option'>I am an apple</div>
    <div id='pear' role='option'>I am a pear</div>
    <div id='banana' role='option'>I am a banana</div>
  </div>
  <div id='shadowFridge'></div>

  <script>
  test(function(t) {
    const shadowRoot = shadowFridge.attachShadow({mode: "open"});
    const banana = document.getElementById("banana");

    fruitbowl.ariaActiveDescendantElement = apple;
    assert_equals(fruitbowl.ariaActiveDescendantElement, apple);
    assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "");

    // Move the referenced element into shadow DOM.
    shadowRoot.appendChild(apple);
    assert_equals(fruitbowl.ariaActiveDescendantElement, null, "computed attr-assoc element should be null as referenced element is in an invalid scope");
    // The content attribute is still empty.
    assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "");

    // let us rename our banana to an apple
    banana.setAttribute("id", "apple");
    const lyingBanana = document.getElementById("apple");
    assert_equals(lyingBanana, banana);

    // our ariaActiveDescendantElement thankfully isn't tricked.
    // this is thanks to the underlying reference being kept intact, it is
    // checked and found to be in an invalid scope.
    assert_equals(fruitbowl.ariaActiveDescendantElement, null);
    // our content attribute is empty.
    assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "");

    // when we remove our IDL attribute, the content attribute is also thankfully cleared.
    fruitbowl.ariaActiveDescendantElement = null;
    assert_equals(fruitbowl.ariaActiveDescendantElement, null);
    assert_equals(fruitbowl.getAttribute("aria-activedescendant"), null);
  }, "Reparenting referenced element cannot cause retargeting of reference.");
  </script>

  <div id='toaster' role='listbox'></div>
  <div id='shadowPantry'></div>

  <script>
  test(function(t) {
    const shadowRoot = shadowPantry.attachShadow({mode: "open"});

    // Our toast starts in the shadowPantry.
    const toast = document.createElement("div");
    toast.setAttribute("id", "toast");
    shadowRoot.appendChild(toast);

    // Prepare my toast for toasting
    toaster.ariaActiveDescendantElement = toast;
    assert_equals(toaster.ariaActiveDescendantElement, null);
    assert_equals(toaster.getAttribute("aria-activedescendant"), "");

    // Time to make some toast
    toaster.appendChild(toast);
    assert_equals(toaster.ariaActiveDescendantElement, toast);
    // Current spec behaviour:
    assert_equals(toaster.getAttribute("aria-activedescendant"), "");
  }, "Element reference set in invalid scope remains intact throughout move to valid scope.");
  </script>

  <div id="billingElementContainer">
      <div id="billingElement">Billing</div>
  </div>
  <div>
      <div id="nameElement">Name</div>
      <input type="text" id="input1" aria-labelledby="billingElement nameElement"/>
  </div>
  <div>
      <div id="addressElement">Address</div>
      <input type="text" id="input2"/>
  </div>

  <script>
  test(function(t) {
    const billingElement = document.getElementById("billingElement")
    assert_array_equals(input1.ariaLabelledByElements, [billingElement, nameElement], "parsed content attribute sets element references.");
    assert_equals(input1.ariaLabelledByElements, input1.ariaLabelledByElements, "check idl attribute caching after parsing");
    assert_equals(input2.ariaLabelledByElements, null, "Testing missing content attribute after parsing.");

    input2.ariaLabelledByElements = [billingElement, addressElement];
    assert_array_equals(input2.ariaLabelledByElements, [billingElement, addressElement], "Testing IDL setter/getter.");
    assert_equals(input1.ariaLabelledByElements, input1.ariaLabelledByElements, "check idl attribute caching after update");
    assert_equals(input2.getAttribute("aria-labelledby"), "");

    // Remove the billingElement from the DOM.
    // As it was explicitly set the underlying association will remain intact,
    // but it will be hidden until the element is moved back into a valid scope.
    billingElement.remove();
    assert_array_equals(input2.ariaLabelledByElements, [addressElement], "Computed ariaLabelledByElements shouldn't include billing when out of scope.");

    // Insert the billingElement back into the DOM and check that it is visible
    // again, as the underlying association should have been kept intact.
    billingElementContainer.appendChild(billingElement);
    assert_array_equals(input2.ariaLabelledByElements, [billingElement, addressElement], "Billing element back in scope.");

    input2.ariaLabelledByElements = [];
    assert_array_equals(input2.ariaLabelledByElements, [], "Testing IDL setter/getter for empty array.");
    assert_equals(input2.getAttribute("aria-labelledby"), "");

    input1.removeAttribute("aria-labelledby");
    assert_equals(input1.ariaLabelledByElements, null);

    input1.setAttribute("aria-labelledby", "nameElement addressElement");
    assert_array_equals(input1.ariaLabelledByElements, [nameElement, addressElement],
      "computed value after setting attribute directly");

    input1.ariaLabelledByElements = null;
    assert_false(input1.hasAttribute("aria-labelledby", "Nullifying the IDL attribute should remove the content attribute."));
  }, "aria-labelledby.");
  </script>

  <ul role="tablist">
    <li role="presentation"><a id="link1" role="tab" aria-controls="panel1">Tab 1</a></li>
    <li role="presentation"><a id="link2" role="tab">Tab 2</a></li>
  </ul>

  <div role="tabpanel" id="panel1"></div>
  <div role="tabpanel" id="panel2"></div>

  <script>
  test(function(t) {
    assert_array_equals(link1.ariaControlsElements, [panel1]);
    assert_equals(link2.ariaControlsElements, null);

    link2.setAttribute("aria-controls", "panel1 panel2");
    assert_array_equals(link2.ariaControlsElements, [panel1, panel2]);

    link1.ariaControlsElements = [];
    assert_equals(link1.getAttribute("aria-controls"), "");

    link2.ariaControlsElements = [panel1, panel2];
    assert_equals(link2.getAttribute("aria-controls"), "");
    assert_array_equals(link2.ariaControlsElements, [panel1, panel2]);

    link2.removeAttribute("aria-controls");
    assert_equals(link2.ariaControlsElements, null);

    link2.ariaControlsElements = [panel1, panel2];
    assert_equals(link2.getAttribute("aria-controls"), "");
    assert_array_equals(link2.ariaControlsElements, [panel1, panel2]);

    link2.ariaControlsElements = null;
    assert_false(link2.hasAttribute("aria-controls", "Nullifying the IDL attribute should remove the content attribute."));
  }, "aria-controls.");
  </script>

  <a id="describedLink" aria-describedby="description1 description2">Fruit</a>
  <div id="description1">Delicious</div>
  <div id="description2">Nutritious</div>

  <script>
  test(function(t) {
    assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]);

    describedLink.ariaDescribedByElements = [description1, description2];
    assert_equals(describedLink.getAttribute("aria-describedby"), "");
    assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]);

    describedLink.ariaDescribedByElements = [];
    assert_equals(describedLink.getAttribute("aria-describedby"), "");

    describedLink.setAttribute("aria-describedby", "description1");
    assert_array_equals(describedLink.ariaDescribedByElements, [description1]);

    describedLink.removeAttribute("aria-describedby");
    assert_equals(describedLink.ariaDescribedByElements, null);

    describedLink.ariaDescribedByElements = [description1, description2];
    assert_equals(describedLink.getAttribute("aria-describedby"), "");
    assert_array_equals(describedLink.ariaDescribedByElements, [description1, description2]);

    describedLink.ariaDescribedByElements = null;
    assert_false(describedLink.hasAttribute("aria-describedby", "Nullifying the IDL attribute should remove the content attribute."));
  }, "aria-describedby.");
  </script>

  <h2 id="titleHeading" aria-flowto="article1 article2">Title</h2>
  <div>Next</div>
  <article id="article2">Content2</article>
  <article id="article1">Content1</article>

  <script>
  test(function(t) {
    const article1 = document.getElementById("article1");
    const article2 = document.getElementById("article2");

    assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]);

    titleHeading.ariaFlowToElements = [article1, article2];
    assert_equals(titleHeading.getAttribute("aria-flowto"), "");
    assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]);

    titleHeading.ariaFlowToElements = [];
    assert_equals(titleHeading.getAttribute("aria-flowto"), "");

    titleHeading.setAttribute("aria-flowto", "article1");
    assert_array_equals(titleHeading.ariaFlowToElements, [article1]);

    titleHeading.removeAttribute("aria-flowto");
    assert_equals(titleHeading.ariaFlowToElements, null);

    titleHeading.ariaFlowToElements = [article1, article2];
    assert_equals(titleHeading.getAttribute("aria-flowto"), "");
    assert_array_equals(titleHeading.ariaFlowToElements, [article1, article2]);

    titleHeading.ariaFlowToElements = null;
    assert_false(titleHeading.hasAttribute("aria-flowto", "Nullifying the IDL attribute should remove the content attribute."));
  }, "aria-flowto.");
  </script>

  <ul>
    <li id="listItemOwner" aria-owns="child1 child2">Parent</li>
  </ul>
  <ul>
    <li id="child1">Child 1</li>
    <li id="child2">Child 2</li>
  </ul>
  <script>
  test(function(t) {
    assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]);

    listItemOwner.removeAttribute("aria-owns");
    assert_equals(listItemOwner.ariaOwnsElements, null);

    listItemOwner.ariaOwnsElements = [child1, child2];
    assert_equals(listItemOwner.getAttribute("aria-owns"), "");
    assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]);

    listItemOwner.ariaOwnsElements = [];
    assert_equals(listItemOwner.getAttribute("aria-owns"), "");

    listItemOwner.setAttribute("aria-owns", "child1");
    assert_array_equals(listItemOwner.ariaOwnsElements, [child1]);

    listItemOwner.ariaOwnsElements = [child1, child2];
    assert_equals(listItemOwner.getAttribute("aria-owns"), "");
    assert_array_equals(listItemOwner.ariaOwnsElements, [child1, child2]);

    listItemOwner.ariaOwnsElements = null;
    assert_false(listItemOwner.hasAttribute("aria-owns", "Nullifying the IDL attribute should remove the content attribute."));
  }, "aria-owns.");
  </script>

  <div id="lightDomContainer">
    <h2 id="lightDomHeading" aria-flowto="shadowChild1 shadowChild2">Light DOM Heading</h2>
    <div id="host"></div>
    <p id="lightDomText1">Light DOM text</p>
    <p id="lightDomText2">Light DOM text</p>
  </div>

  <script>
  test(function(t) {
    const shadowRoot = host.attachShadow({mode: "open"});
    const shadowChild1 = document.createElement("article");
    shadowChild1.setAttribute("id", "shadowChild1");
    shadowRoot.appendChild(shadowChild1);
    const shadowChild2 = document.createElement("article");
    shadowChild2.setAttribute("id", "shadowChild1");
    shadowRoot.appendChild(shadowChild2);

    // The elements in the content attribute are in a "darker" tree - they
    // enter a shadow encapsulation boundary, so not be associated any more.
    assert_array_equals(lightDomHeading.ariaFlowToElements, []);

    // These elements are in a shadow including ancestor, i.e "lighter" tree.
    // Valid for the IDL attribute, but content attribute should be null.
    shadowChild1.ariaFlowToElements = [lightDomText1, lightDomText2];
    assert_equals(shadowChild1.getAttribute("aria-flowto"), "", "empty content attribute for elements that cross shadow boundaries.");

    // These IDs belong to a different scope, so the attr-associated-element
    // cannot be computed.
    shadowChild2.setAttribute("aria-flowto", "lightDomText1 lightDomText2");
    assert_array_equals(shadowChild2.ariaFlowToElements, []);

    // Elements that cross into shadow DOM are dropped, only reflect the valid
    // elements in IDL and in the content attribute.
    lightDomHeading.ariaFlowToElements = [shadowChild1, shadowChild2, lightDomText1, lightDomText2];
    assert_array_equals(lightDomHeading.ariaFlowToElements, [lightDomText1, lightDomText2], "IDL should only include valid elements");
    assert_equals(lightDomHeading.getAttribute("aria-flowto"), "", "empty content attribute if any given elements cross shadow boundaries");

    // Using a mixture of elements in the same scope and in a shadow including
    // ancestor should set the IDL attribute, but should reflect the empty
    // string in the content attribute.
    shadowChild1.removeAttribute("aria-flowto");
    shadowChild1.ariaFlowToElements = [shadowChild1, lightDomText1];
    assert_equals(shadowChild1.getAttribute("aria-flowto"), "", "Setting IDL elements with a mix of scopes should reflect an empty string in the content attribute")

  }, "shadow DOM behaviour for FrozenArray element reflection.");
  </script>

  <div id="describedButtonContainer">
    <div id="buttonDescription1">Delicious</div>
    <div id="buttonDescription2">Nutritious</div>
    <div id="outerShadowHost"></div>
  </div>

  <script>
  test(function(t) {
    const description1 = document.getElementById("buttonDescription1");
    const description2 = document.getElementById("buttonDescription2");
    const outerShadowRoot = outerShadowHost.attachShadow({mode: "open"});
    const innerShadowHost = document.createElement("div");
    outerShadowRoot.appendChild(innerShadowHost);
    const innerShadowRoot = innerShadowHost.attachShadow({mode: "open"});

    // Create an element, add some attr associated light DOM elements and append it to the outer shadow root.
    const describedElement = document.createElement("button");
    describedButtonContainer.appendChild(describedElement);
    describedElement.ariaDescribedByElements = [description1, description2];

    // All elements were in the same scope, so elements are gettable and the content attribute is empty.
    assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "same scope reference");
    assert_equals(describedElement.getAttribute("aria-describedby"), "");

    outerShadowRoot.appendChild(describedElement);

    // Explicitly set attr-associated-elements should still be gettable because we are referencing elements in a lighter scope.
    // The content attr is empty.
    assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "lighter scope reference");
    assert_equals(describedElement.getAttribute("aria-describedby"), "");

    // Move the explicitly set elements into a deeper shadow DOM to test the relationship should not be gettable.
    innerShadowRoot.appendChild(description1);
    innerShadowRoot.appendChild(description2);

    // Explicitly set elements are no longer retrievable, because they are no longer in a valid scope.
    assert_array_equals(describedElement.ariaDescribedByElements, [], "invalid scope reference");
    assert_equals(describedElement.getAttribute("aria-describedby"), "");

    // Move into the same shadow scope as the explicitly set elements to test that the elements are gettable.
    innerShadowRoot.appendChild(describedElement);
    assert_array_equals(describedElement.ariaDescribedByElements, [description1, description2], "restored valid scope reference");
    assert_equals(describedElement.getAttribute("aria-describedby"), "");
  }, "Moving explicitly set elements across shadow DOM boundaries.");
  </script>

  <div id="sameScopeContainer">
    <div id="labeledby" aria-labeledby="headingLabel1 headingLabel2">Misspelling</div>
    <div id="headingLabel1">Wonderful</div>
    <div id="headingLabel2">Fantastic</div>

    <div id="headingShadowHost"></div>
  </div>

    <script>
    test(function(t) {
      const shadowRoot = headingShadowHost.attachShadow({mode: "open"});
      const headingElement = document.createElement("h1");
      const headingLabel1 = document.getElementById("headingLabel1")
      const headingLabel2 = document.getElementById("headingLabel2")
      shadowRoot.appendChild(headingElement);

      assert_array_equals(labeledby.ariaLabelledByElements, [headingLabel1, headingLabel2], "aria-labeled by is supported by IDL getter.");

      // Explicitly set elements are in a lighter shadow DOM, so that's ok.
      headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2];
      assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Lighter elements are gettable when explicitly set.");
      assert_equals(headingElement.getAttribute("aria-labelledby"), "");

      // Move into Light DOM, explicitly set elements should still be gettable.
      // Note that the content attribute is still empty.
      sameScopeContainer.appendChild(headingElement);
      assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Elements are all in same scope, so gettable.");
      assert_equals(headingElement.getAttribute("aria-labelledby"), "", "Content attribute is empty.");

      // Reset the association, the content attribute is sitll empty.
      headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2];
      assert_equals(headingElement.getAttribute("aria-labelledby"), "");

      // Remove the referring element from the DOM, elements are no longer longer exposed,
      // underlying internal reference is still kept intact.
      headingElement.remove();
      assert_array_equals(headingElement.ariaLabelledByElements, [], "Element is no longer in the document, so references should no longer be exposed.");
      assert_equals(headingElement.getAttribute("aria-labelledby"), "");

      // Insert it back in.
      sameScopeContainer.appendChild(headingElement);
      assert_array_equals(headingElement.ariaLabelledByElements, [headingLabel1, headingLabel2], "Element is restored to valid scope, so should be gettable.");
      assert_equals(headingElement.getAttribute("aria-labelledby"), "");

      // Remove everything from the DOM, nothing is exposed again.
      headingLabel1.remove();
      headingLabel2.remove();
      assert_array_equals(headingElement.ariaLabelledByElements, []);
      assert_equals(headingElement.getAttribute("aria-labelledby"), "");
      assert_equals(document.getElementById("headingLabel1"), null);
      assert_equals(document.getElementById("headingLabel2"), null);

      // Reset the association.
      headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2];
      assert_array_equals(headingElement.ariaLabelledByElements, []);
      assert_equals(headingElement.getAttribute("aria-labelledby"), "");
    }, "Moving explicitly set elements around within the same scope, and removing from the DOM.");
    </script>

  <input id="input">
    <optgroup>
      <option id="first">First option</option>
      <option id="second">Second option</option>
    </optgroup>

  <script>
    test(function(t) {
      input.ariaActiveDescendantElement = first;
      first.parentElement.appendChild(first);

      assert_equals(input.ariaActiveDescendantElement, first);
    }, "Reparenting.");
  </script>

  <div id='fromDiv'></div>

  <script>
    test(function(t) {
      const toSpan = document.createElement('span');
      toSpan.setAttribute("id", "toSpan");
      fromDiv.ariaActiveDescendantElement = toSpan;

      assert_equals(fromDiv.ariaActiveDescendantElement, null, "Referenced element not inserted into document, so is in an invalid scope.");
      assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope, so content attribute not set.");

      fromDiv.appendChild(toSpan);
      assert_equals(fromDiv.ariaActiveDescendantElement, toSpan, "Referenced element now inserted into the document.");
      assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Content attribute remains empty, as it is only updated at set time.");

    }, "Attaching element reference before it's inserted into the DOM.");
  </script>

  <div id='originalDocumentDiv'></div>

  <script>
    test(function(t) {
      const newDoc = document.implementation.createHTMLDocument('new document');
      const newDocSpan = newDoc.createElement('span');
      newDoc.body.appendChild(newDocSpan);

      // Create a reference across documents.
      originalDocumentDiv.ariaActiveDescendantElement = newDocSpan;

      assert_equals(originalDocumentDiv.ariaActiveDescendantElement, null, "Cross-document is an invalid scope, so reference will not be visible.");
      assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope when set, so content attribute not set.");

      // "Move" span to first document.
      originalDocumentDiv.appendChild(newDocSpan);

      // Implementation defined: moving object into same document from other document may cause reference to become visible.
      assert_equals(originalDocumentDiv.ariaActiveDescendantElement, newDocSpan, "Implementation defined: moving object back *may* make reference visible.");
      assert_equals(fromDiv.getAttribute("aria-activedescendant"), "", "Invalid scope when set, so content attribute not set.");
    }, "Cross-document references and moves.");
  </script>


  <script>
    test(function(t) {
      const otherDoc = document.implementation.createHTMLDocument('otherDoc');
      const otherDocDiv = otherDoc.createElement('div');
      const otherDocSpan = otherDoc.createElement('span');
      otherDocDiv.appendChild(otherDocSpan);
      otherDoc.body.appendChild(otherDocDiv);

      otherDocDiv.ariaActiveDescendantElement = otherDocSpan;
      assert_equals(otherDocDiv.ariaActiveDescendantElement, otherDocSpan, "Setting reference on a different document.");

      // Adopt element from other oducment.
      document.body.appendChild(document.adoptNode(otherDocDiv));
      assert_equals(otherDocDiv.ariaActiveDescendantElement, otherDocSpan, "Reference should be kept on the new document too.");
    }, "Adopting element keeps references.");
  </script>

  <div id="cachingInvariantMain"></div>
  <div id="cachingInvariantElement1"></div>
  <div id="cachingInvariantElement2"></div>
  <div id="cachingInvariantElement3"></div>
  <div id="cachingInvariantElement4"></div>
  <div id="cachingInvariantElement5"></div>

  <script>
    test(function(t) {
      cachingInvariantMain.ariaControlsElements = [cachingInvariantElement1, cachingInvariantElement2];
      cachingInvariantMain.ariaDescribedByElements = [cachingInvariantElement3, cachingInvariantElement4];
      cachingInvariantMain.ariaDetailsElements = [cachingInvariantElement5];
      cachingInvariantMain.ariaFlowToElements = [cachingInvariantElement1, cachingInvariantElement3];
      cachingInvariantMain.ariaLabelledByElements = [cachingInvariantElement2, cachingInvariantElement4];
      cachingInvariantMain.ariaOwnsElements = [cachingInvariantElement1, cachingInvariantElement2, cachingInvariantElement3];

      let ariaControlsElementsArray = cachingInvariantMain.ariaControlsElements;
      let ariaDescribedByElementsArray = cachingInvariantMain.ariaDescribedByElements;
      let ariaDetailsElementsArray = cachingInvariantMain.ariaDetailsElements;
      let ariaFlowToElementsArray = cachingInvariantMain.ariaFlowToElements;
      let ariaLabelledByElementsArray = cachingInvariantMain.ariaLabelledByElements;
      let ariaOwnsElementsArray = cachingInvariantMain.ariaOwnsElements;

      assert_equals(ariaControlsElementsArray, cachingInvariantMain.ariaControlsElements, "Caching invariant for ariaControlsElements");
      assert_equals(ariaDescribedByElementsArray, cachingInvariantMain.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements");
      assert_equals(ariaDetailsElementsArray, cachingInvariantMain.ariaDetailsElements, "Caching invariant for ariaDetailsElements");
      assert_equals(ariaFlowToElementsArray, cachingInvariantMain.ariaFlowToElements, "Caching invariant for ariaFlowToElements");
      assert_equals(ariaLabelledByElementsArray, cachingInvariantMain.ariaLabelledByElements, "Caching invariant for ariaLabelledByElements");
      assert_equals(ariaOwnsElementsArray, cachingInvariantMain.ariaOwnsElements, "Caching invariant for ariaOwnsElements");
    }, "Caching invariant different attributes.");
  </script>

  <div id="cachingInvariantMain1"></div>
  <div id="cachingInvariantMain2"></div>

  <script>
    test(function(t) {
      cachingInvariantMain1.ariaDescribedByElements = [cachingInvariantElement1, cachingInvariantElement2];
      cachingInvariantMain2.ariaDescribedByElements = [cachingInvariantElement3, cachingInvariantElement4];

      let ariaDescribedByElementsArray1 = cachingInvariantMain1.ariaDescribedByElements;
      let ariaDescribedByElementsArray2 = cachingInvariantMain2.ariaDescribedByElements;

      assert_equals(ariaDescribedByElementsArray1, cachingInvariantMain1.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements in one elemnt");
      assert_equals(ariaDescribedByElementsArray2, cachingInvariantMain2.ariaDescribedByElements, "Caching invariant for ariaDescribedByElements in onother elemnt");
    }, "Caching invariant different elements.");
  </script>

  <!-- TODO(chrishall): add additional GC test covering:
       if an element is in an invalid scope but attached to the document, it's
       not GC'd;
  -->

  <!-- TODO(chrishall): add additional GC test covering:
       if an element is not attached to the document, but is in a tree fragment
       which is not GC'd because there is a script reference to another element
       in the tree fragment, and the relationship is valid because it is between
       two elements in that tree fragment, the relationship is exposed *and* the
       element is not GC'd
  -->

</html>