/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";
requestLongerTimeout(2);

/**
 * Test MEMBER_OF relation caching on HTML radio buttons
 */
addAccessibleTask(
  `
  <input type="radio" id="r1">I have no name<br>
  <input type="radio" id="r2">I also have no name<br>
  <input type="radio" id="r3" name="n">I have a name<br>
  <input type="radio" id="r4" name="a">I have a different name<br>
  <fieldset role="radiogroup">
    <input type="radio" id="r5" name="n">I have an already used name
     and am in a different part of the tree
    <input type="radio" id="r6" name="r">I have a different name but am
     in the same group
  </fieldset>`,
  async function (browser, accDoc) {
    const r1 = findAccessibleChildByID(accDoc, "r1");
    const r2 = findAccessibleChildByID(accDoc, "r2");
    const r3 = findAccessibleChildByID(accDoc, "r3");
    const r4 = findAccessibleChildByID(accDoc, "r4");
    const r5 = findAccessibleChildByID(accDoc, "r5");
    const r6 = findAccessibleChildByID(accDoc, "r6");

    await testCachedRelation(r1, RELATION_MEMBER_OF, null);
    await testCachedRelation(r2, RELATION_MEMBER_OF, null);
    await testCachedRelation(r3, RELATION_MEMBER_OF, [r3, r5]);
    await testCachedRelation(r4, RELATION_MEMBER_OF, r4);
    await testCachedRelation(r5, RELATION_MEMBER_OF, [r3, r5]);
    await testCachedRelation(r6, RELATION_MEMBER_OF, r6);

    await invokeContentTask(browser, [], () => {
      content.document.getElementById("r5").name = "a";
    });

    await testCachedRelation(r3, RELATION_MEMBER_OF, r3);
    await testCachedRelation(r4, RELATION_MEMBER_OF, [r5, r4]);
    await testCachedRelation(r5, RELATION_MEMBER_OF, [r5, r4]);
  },
  { chrome: true, iframe: true, remoteIframe: true }
);

/*
 * Test MEMBER_OF relation caching on aria radio buttons
 */
addAccessibleTask(
  `
  <div role="radio" id="r1">I have no radio group</div><br>
  <fieldset role="radiogroup" id="fs">
    <div role="radio" id="r2">hello</div><br>
    <div role="radio" id="r3">world</div><br>
  </fieldset>`,
  async function (browser, accDoc) {
    const r1 = findAccessibleChildByID(accDoc, "r1");
    const r2 = findAccessibleChildByID(accDoc, "r2");
    let r3 = findAccessibleChildByID(accDoc, "r3");

    await testCachedRelation(r1, RELATION_MEMBER_OF, null);
    await testCachedRelation(r2, RELATION_MEMBER_OF, [r2, r3]);
    await testCachedRelation(r3, RELATION_MEMBER_OF, [r2, r3]);
    const r = waitForEvent(EVENT_INNER_REORDER, "fs");
    await invokeContentTask(browser, [], () => {
      let innerRadio = content.document.getElementById("r3");
      content.document.body.appendChild(innerRadio);
    });
    await r;

    r3 = findAccessibleChildByID(accDoc, "r3");
    await testCachedRelation(r1, RELATION_MEMBER_OF, null);
    await testCachedRelation(r2, RELATION_MEMBER_OF, r2);
    await testCachedRelation(r3, RELATION_MEMBER_OF, null);
  },
  {
    chrome: true,
    iframe: true,
    remoteIframe: true,
  }
);

/*
 * Test mutation of LABEL relations via accessible shutdown.
 */
addAccessibleTask(
  `
  <div id="d"></div>
  <label id="l">
    <select id="s">
  `,
  async function (browser, accDoc) {
    const label = findAccessibleChildByID(accDoc, "l");
    const select = findAccessibleChildByID(accDoc, "s");
    const div = findAccessibleChildByID(accDoc, "d");

    await testCachedRelation(label, RELATION_LABEL_FOR, select);
    await testCachedRelation(select, RELATION_LABELLED_BY, label);
    await testCachedRelation(div, RELATION_LABELLED_BY, null);

    const r = waitForEvent(EVENT_REORDER, "l");
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("s").remove();
    });
    await r;
    await invokeContentTask(browser, [], () => {
      const l = content.document.getElementById("l");
      l.htmlFor = "d";
    });
    await testCachedRelation(label, RELATION_LABEL_FOR, div);
    await testCachedRelation(div, RELATION_LABELLED_BY, label);
  },
  {
    chrome: false,
    iframe: true,
    remoteIframe: true,
    topLevel: true,
  }
);

/*
 * Test mutation of LABEL relations via DOM ID reuse.
 */
addAccessibleTask(
  `
  <div id="label">before</div><input id="input" aria-labelledby="label">
  `,
  async function (browser, accDoc) {
    let label = findAccessibleChildByID(accDoc, "label");
    const input = findAccessibleChildByID(accDoc, "input");

    await testCachedRelation(label, RELATION_LABEL_FOR, input);
    await testCachedRelation(input, RELATION_LABELLED_BY, label);

    const r = waitForEvent(EVENT_REORDER, accDoc);
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("label").remove();
      let l = content.document.createElement("div");
      l.id = "label";
      l.textContent = "after";
      content.document.body.insertBefore(
        l,
        content.document.getElementById("input")
      );
    });
    await r;
    label = findAccessibleChildByID(accDoc, "label");
    await testCachedRelation(label, RELATION_LABEL_FOR, input);
    await testCachedRelation(input, RELATION_LABELLED_BY, label);
  },
  {
    chrome: true,
    iframe: true,
    remoteIframe: true,
  }
);

/*
 * Test LINKS_TO relation caching an anchor with multiple hashes
 */
addAccessibleTask(
  `
  <a id="link" href="#foo#bar">Origin</a><br>
  <a id="anchor" name="foo#bar">Destination`,
  async function (browser, accDoc) {
    const link = findAccessibleChildByID(accDoc, "link");
    const anchor = findAccessibleChildByID(accDoc, "anchor");

    await testCachedRelation(link, RELATION_LINKS_TO, anchor);
  },
  {
    chrome: true,
    // IA2 doesn't have a LINKS_TO relation and Windows non-cached
    // RemoteAccessible uses IA2, so we can't run these tests in this case.
    topLevel: true,
    iframe: true,
    remoteIframe: true,
  }
);

/*
 * Test mutation of LABEL relations via accessible shutdown.
 */
addAccessibleTask(
  `
  <div id="d"></div>
  <label id="l">
    <select id="s">
  `,
  async function (browser, accDoc) {
    const label = findAccessibleChildByID(accDoc, "l");
    const select = findAccessibleChildByID(accDoc, "s");
    const div = findAccessibleChildByID(accDoc, "d");

    await testCachedRelation(label, RELATION_LABEL_FOR, select);
    await testCachedRelation(select, RELATION_LABELLED_BY, label);
    await testCachedRelation(div, RELATION_LABELLED_BY, null);
    await untilCacheOk(() => {
      try {
        // We should get an acc ID back from this, but we don't have a way of
        // verifying its correctness -- it should be the ID of the select.
        return label.cache.getStringProperty("for");
      } catch (e) {
        ok(false, "Exception thrown while trying to read from the cache");
        return false;
      }
    }, "Label for relation exists");

    const r = waitForEvent(EVENT_REORDER, "l");
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("s").remove();
    });
    await r;
    await untilCacheOk(() => {
      try {
        label.cache.getStringProperty("for");
      } catch (e) {
        // This property should no longer exist in the cache, so we should
        // get an exception if we try to fetch it.
        return true;
      }
      return false;
    }, "Label for relation exists");

    await invokeContentTask(browser, [], () => {
      const l = content.document.getElementById("l");
      l.htmlFor = "d";
    });
    await testCachedRelation(label, RELATION_LABEL_FOR, div);
    await testCachedRelation(div, RELATION_LABELLED_BY, label);
  },
  {
    /**
     * This functionality is broken in our LocalAcccessible implementation,
     * so we avoid running this test in chrome or when the cache is off.
     */
    chrome: false,
    iframe: true,
    remoteIframe: true,
    topLevel: true,
  }
);

/**
 * Test label relations on HTML figure/figcaption.
 */
addAccessibleTask(
  `
<figure id="figure1">
  before
  <figcaption id="caption1">caption1</figcaption>
  after
</figure>
<figure id="figure2" aria-labelledby="label">
  <figcaption id="caption2">caption2</figure>
</figure>
<div id="label">label</div>
  `,
  async function (browser, docAcc) {
    const figure1 = findAccessibleChildByID(docAcc, "figure1");
    let caption1 = findAccessibleChildByID(docAcc, "caption1");
    await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1);
    await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1);

    info("Hiding caption1");
    let mutated = waitForEvent(EVENT_HIDE, caption1);
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("caption1").hidden = true;
    });
    await mutated;
    await testCachedRelation(figure1, RELATION_LABELLED_BY, null);

    info("Showing caption1");
    mutated = waitForEvent(EVENT_SHOW, "caption1");
    await invokeContentTask(browser, [], () => {
      content.document.getElementById("caption1").hidden = false;
    });
    caption1 = (await mutated).accessible;
    await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1);
    await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1);

    const figure2 = findAccessibleChildByID(docAcc, "figure2");
    const caption2 = findAccessibleChildByID(docAcc, "caption2");
    const label = findAccessibleChildByID(docAcc, "label");
    await testCachedRelation(figure2, RELATION_LABELLED_BY, [label, caption2]);
    await testCachedRelation(caption2, RELATION_LABEL_FOR, figure2);
    await testCachedRelation(label, RELATION_LABEL_FOR, figure2);
  },
  { chrome: true, topLevel: true }
);

/**
 * Test details relations for the popovertarget content attribute.
 */
addAccessibleTask(
  `
<button id="hide" popovertarget="popover" popovertargetaction="hide">hide</button>
<button id="toggle1" popovertarget="popover">toggle1</button>
<button id="toggle2">toggle2</button>
<button id="toggleSibling">toggleSibling</button>
<div id="popover" popover>popover</div>
<div id="details">details</div>
  `,
  async function testPopoverContent(browser, docAcc) {
    // The popover is hidden, so nothing should be referring to it.
    const hide = findAccessibleChildByID(docAcc, "hide");
    await testCachedRelation(hide, RELATION_DETAILS, []);
    const toggle1 = findAccessibleChildByID(docAcc, "toggle1");
    await testCachedRelation(toggle1, RELATION_DETAILS, []);
    const toggle2 = findAccessibleChildByID(docAcc, "toggle2");
    await testCachedRelation(toggle2, RELATION_DETAILS, []);
    const toggleSibling = findAccessibleChildByID(docAcc, "toggleSibling");
    await testCachedRelation(toggleSibling, RELATION_DETAILS, []);

    info("Showing popover");
    let shown = waitForEvent(EVENT_SHOW, "popover");
    toggle1.doAction(0);
    const popover = (await shown).accessible;
    await testCachedRelation(toggle1, RELATION_DETAILS, popover);
    // toggle2 shouldn't have a details relation because it doesn't have a
    // popovertarget.
    await testCachedRelation(toggle2, RELATION_DETAILS, []);
    // hide shouldn't have a details relation because its action is hide.
    await testCachedRelation(hide, RELATION_DETAILS, []);
    // toggleSibling shouldn't have a details relation because it is a sibling
    // of the popover.
    await testCachedRelation(toggleSibling, RELATION_DETAILS, []);
    await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);

    info("Setting toggle2 popovertarget");
    await invokeSetAttribute(browser, "toggle2", "popovertarget", "popover");
    await testCachedRelation(toggle2, RELATION_DETAILS, popover);
    await testCachedRelation(popover, RELATION_DETAILS_FOR, [toggle1, toggle2]);

    info("Removing toggle2 popovertarget");
    await invokeSetAttribute(browser, "toggle2", "popovertarget", null);
    await testCachedRelation(toggle2, RELATION_DETAILS, []);
    await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);

    info("Setting aria-details on toggle1");
    await invokeSetAttribute(browser, "toggle1", "aria-details", "details");
    const details = findAccessibleChildByID(docAcc, "details");
    // aria-details overrides popover.
    await testCachedRelation(toggle1, RELATION_DETAILS, details);
    await testCachedRelation(popover, RELATION_DETAILS_FOR, []);

    info("Removing aria-details from toggle1");
    await invokeSetAttribute(browser, "toggle1", "aria-details", null);
    await testCachedRelation(toggle1, RELATION_DETAILS, popover);
    await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);

    info("Hiding popover");
    let hidden = waitForEvent(EVENT_HIDE, popover);
    toggle1.doAction(0);
    // The relations between toggle1 and popover are removed when popover shuts
    // down. However, this doesn't cause a cache update notification. Therefore,
    // to avoid timing out in testCachedRelation, we must wait for a hide event
    // first.
    await hidden;
    await testCachedRelation(toggle1, RELATION_DETAILS, []);
  },
  { chrome: false, topLevel: true }
);

/**
 * Test details relations for the popoverTargetElement WebIDL attribute.
 */
addAccessibleTask(
  `
<button id="toggle1">toggle1</button>
<button id="toggle2">toggle2</button>
between
<div id="popover1" popover>popover1</div>
<button id="toggle3">toggle3</button>
<div id="shadowHost"><template shadowrootmode="open">
  <button id="toggle4">toggle4</button>
  between
  <div id="popover2" popover>popover2</div>
  <button id="toggle5">toggle5</button>
</template></div>
  `,
  async function testPopoverIdl(browser, docAcc) {
    // No popover is showing, so there shouldn't be any details relations.
    const toggle1 = findAccessibleChildByID(docAcc, "toggle1");
    await testCachedRelation(toggle1, RELATION_DETAILS, []);
    const toggle2 = findAccessibleChildByID(docAcc, "toggle2");
    await testCachedRelation(toggle2, RELATION_DETAILS, []);
    const toggle3 = findAccessibleChildByID(docAcc, "toggle3");
    await testCachedRelation(toggle3, RELATION_DETAILS, []);
    const toggle4 = findAccessibleChildByID(docAcc, "toggle4");
    await testCachedRelation(toggle4, RELATION_DETAILS, []);
    const toggle5 = findAccessibleChildByID(docAcc, "toggle5");
    await testCachedRelation(toggle5, RELATION_DETAILS, []);

    info("Showing popover1");
    let shown = waitForEvent(EVENT_SHOW, "popover1");
    toggle1.doAction(0);
    const popover1 = (await shown).accessible;
    await testCachedRelation(toggle1, RELATION_DETAILS, popover1);
    // toggle5 is inside the shadow DOM and popover1 is outside, so the target
    // is valid.
    await testCachedRelation(toggle5, RELATION_DETAILS, popover1);
    await testCachedRelation(popover1, RELATION_DETAILS_FOR, [
      toggle1,
      toggle5,
    ]);
    info("Setting toggle2's popover target to popover1");
    await invokeContentTask(browser, [], () => {
      const toggle2Dom = content.document.getElementById("toggle2");
      const popover1Dom = content.document.getElementById("popover1");
      toggle2Dom.popoverTargetElement = popover1Dom;
    });
    await testCachedRelation(toggle2, RELATION_DETAILS, popover1);
    await testCachedRelation(popover1, RELATION_DETAILS_FOR, [
      toggle1,
      toggle2,
      toggle5,
    ]);
    info("Clearing toggle2's popover target");
    await invokeContentTask(browser, [], () => {
      const toggle2Dom = content.document.getElementById("toggle2");
      toggle2Dom.popoverTargetElement = null;
    });
    await testCachedRelation(toggle2, RELATION_DETAILS, []);
    await testCachedRelation(popover1, RELATION_DETAILS_FOR, [
      toggle1,
      toggle5,
    ]);
    info("Hiding popover1");
    let hidden = waitForEvent(EVENT_HIDE, popover1);
    toggle1.doAction(0);
    await hidden;
    await testCachedRelation(toggle1, RELATION_DETAILS, []);
    await testCachedRelation(toggle2, RELATION_DETAILS, []);
    await testCachedRelation(toggle5, RELATION_DETAILS, []);

    info("Showing popover2");
    shown = waitForEvent(EVENT_SHOW, "popover2");
    toggle4.doAction(0);
    const popover2 = (await shown).accessible;
    // toggle4 is in the same shadow DOM as popover2.
    await testCachedRelation(toggle4, RELATION_DETAILS, popover2);
    // toggle3 is outside popover2's shadow DOM, so the target isn't valid.
    await testCachedRelation(toggle3, RELATION_DETAILS, []);
    await testCachedRelation(popover2, RELATION_DETAILS_FOR, [toggle4]);
    info("Hiding popover2");
    hidden = waitForEvent(EVENT_HIDE, popover2);
    toggle4.doAction(0);
    await hidden;
    await testCachedRelation(toggle4, RELATION_DETAILS, []);
  },
  {
    chrome: true,
    topLevel: true,
    contentSetup: async function contentSetup() {
      const toggle1 = content.document.getElementById("toggle1");
      const popover1 = content.document.getElementById("popover1");
      toggle1.popoverTargetElement = popover1;
      const toggle3 = content.document.getElementById("toggle3");
      const shadow = content.document.getElementById("shadowHost").shadowRoot;
      const toggle4 = shadow.getElementById("toggle4");
      const popover2 = shadow.getElementById("popover2");
      toggle3.popoverTargetElement = popover2;
      toggle4.popoverTargetElement = popover2;
      const toggle5 = shadow.getElementById("toggle5");
      toggle5.popoverTargetElement = popover1;
    },
  }
);

/**
 * Test relation defaults via element internals
 */
addAccessibleTask(
  `
  <div id="dependant1">label</div>
  <custom-checkbox id="host"></custom-checkbox>
  <div id="dependant2">label2</div>

<script>
customElements.define("custom-checkbox",
  class extends HTMLElement {
    constructor() {
      super();
      this.tabIndex = "0";
      this._internals = this.attachInternals();
      this._internals.role = "checkbox";
      this._internals.ariaChecked = "true";
    }
    get internals() {
      return this._internals;
    }
  }
);
</script>`,
  async function (browser, accDoc) {
    let host = findAccessibleChildByID(accDoc, "host");
    let dependant1 = findAccessibleChildByID(accDoc, "dependant1");
    let dependant2 = findAccessibleChildByID(accDoc, "dependant2");

    function invokeSetInternals(reflectionAttrName, targetIds) {
      if (targetIds) {
        Logger.log(
          `Setting internals reflected ${reflectionAttrName} attribute to ${targetIds} for host`
        );
      } else {
        Logger.log(
          `Removing internals reflected ${reflectionAttrName} attribute from node with host`
        );
      }

      return invokeContentTask(
        browser,
        [reflectionAttrName, targetIds],
        (contentAttr, contentTargetIds) => {
          let internals = content.document.getElementById("host").internals;
          if (contentTargetIds) {
            internals[contentAttr] = contentTargetIds.map(targetId =>
              content.document.getElementById(targetId)
            );
          } else {
            internals[contentAttr] = null;
          }
        }
      );
    }

    async function testInternalsRelation(
      attrName,
      reflectionAttrName,
      hostRelation,
      dependantRelation
    ) {
      info(`setting default ${reflectionAttrName}`);
      await invokeSetInternals(reflectionAttrName, ["dependant1"]);
      await testCachedRelation(host, hostRelation, [dependant1]);
      await testCachedRelation(dependant1, dependantRelation, [host]);
      await testCachedRelation(dependant2, dependantRelation, null);

      info(`setting override ${attrName}`);
      await invokeSetAttribute(browser, "host", attrName, "dependant2");
      await testCachedRelation(host, hostRelation, [dependant2]);
      await testCachedRelation(dependant2, dependantRelation, [host]);
      await testCachedRelation(dependant1, dependantRelation, null);

      info(`unsetting default ${reflectionAttrName} and ${attrName} override`);
      await invokeSetInternals(reflectionAttrName, null);
      await invokeSetAttribute(browser, "host", attrName, null);
      await testCachedRelation(host, hostRelation, null);
      await testCachedRelation(dependant2, dependantRelation, null);
      await testCachedRelation(dependant1, dependantRelation, null);
    }

    await testInternalsRelation(
      "aria-labelledby",
      "ariaLabelledByElements",
      RELATION_LABELLED_BY,
      RELATION_LABEL_FOR
    );
    await testInternalsRelation(
      "aria-describedby",
      "ariaDescribedByElements",
      RELATION_DESCRIBED_BY,
      RELATION_DESCRIPTION_FOR
    );
    await testInternalsRelation(
      "aria-controls",
      "ariaControlsElements",
      RELATION_CONTROLLER_FOR,
      RELATION_CONTROLLED_BY
    );
    await testInternalsRelation(
      "aria-flowto",
      "ariaFlowToElements",
      RELATION_FLOWS_TO,
      RELATION_FLOWS_FROM
    );
    await testInternalsRelation(
      "aria-details",
      "ariaDetailsElements",
      RELATION_DETAILS,
      RELATION_DETAILS_FOR
    );
    await testInternalsRelation(
      "aria-errormessage",
      "ariaErrorMessageElements",
      RELATION_ERRORMSG,
      RELATION_ERRORMSG_FOR
    );
  }
);

/**
 * Moving explicitly set elements across shadow DOM boundaries.
 */
addAccessibleTask(
  `
  <div id="describedButtonContainer">
    <div id="buttonDescription1">Delicious</div>
    <div id="buttonDescription2">Nutritious</div>
    <div id="outerShadowHost"></div>
    <button id="describedElement">Button</button>
  </div>

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

    const describedElement = document.getElementById("describedElement");
    // Add some attr associated light DOM elements.
    describedElement.ariaDescribedByElements = [buttonDescription1, buttonDescription2];
</script>`,
  async function (browser, accDoc) {
    const waitAndReturnRecreated = acc => {
      const id = getAccessibleDOMNodeID(acc);
      return waitForEvents([
        [EVENT_HIDE, acc],
        [EVENT_SHOW, id],
      ]).then(evts => evts[1].accessible);
    };

    let describedAcc = findAccessibleChildByID(accDoc, "describedElement");
    let accDescription1 = findAccessibleChildByID(accDoc, "buttonDescription1");
    let accDescription2 = findAccessibleChildByID(accDoc, "buttonDescription2");

    // All elements were in the same scope, so relations are intact.
    await testCachedRelation(describedAcc, RELATION_DESCRIBED_BY, [
      accDescription1,
      accDescription2,
    ]);
    await testCachedRelation(accDescription1, RELATION_DESCRIPTION_FOR, [
      describedAcc,
    ]);
    await testCachedRelation(accDescription2, RELATION_DESCRIPTION_FOR, [
      describedAcc,
    ]);

    let onRecreated = waitAndReturnRecreated(describedAcc);
    await invokeContentTask(browser, [], () => {
      const outerShadowRoot =
        content.document.getElementById("outerShadowHost").shadowRoot;
      const describedElement =
        content.document.getElementById("describedElement");
      outerShadowRoot.appendChild(describedElement);
    });

    info("Waiting for described accessible to be recreated");
    describedAcc = await onRecreated;
    // Relations should still be intact, we are referencing elements in a lighter scope.
    await testCachedRelation(describedAcc, RELATION_DESCRIBED_BY, [
      accDescription1,
      accDescription2,
    ]);
    await testCachedRelation(accDescription1, RELATION_DESCRIPTION_FOR, [
      describedAcc,
    ]);
    await testCachedRelation(accDescription2, RELATION_DESCRIPTION_FOR, [
      describedAcc,
    ]);

    // Move the explicitly set elements into a deeper shadow DOM.
    onRecreated = Promise.all([
      waitAndReturnRecreated(accDescription1),
      waitAndReturnRecreated(accDescription2),
    ]);
    await invokeContentTask(browser, [], () => {
      const buttonDescription1 =
        content.document.getElementById("buttonDescription1");
      const buttonDescription2 =
        content.document.getElementById("buttonDescription2");
      const innerShadowRoot =
        content.document.getElementById("outerShadowHost").shadowRoot
          .firstElementChild.shadowRoot;
      innerShadowRoot.appendChild(buttonDescription1);
      innerShadowRoot.appendChild(buttonDescription2);
    });

    [accDescription1, accDescription2] = await onRecreated;

    // Relation is severed, because relation dependants are no longer in a valid scope.
    await testCachedRelation(describedAcc, RELATION_DESCRIBED_BY, []);
    await testCachedRelation(accDescription1, RELATION_DESCRIPTION_FOR, []);
    await testCachedRelation(accDescription2, RELATION_DESCRIPTION_FOR, []);

    // Move into the same shadow scope as the explicitly set elements.
    onRecreated = waitAndReturnRecreated(describedAcc);
    await invokeContentTask(browser, [], () => {
      const outerShadowRoot =
        content.document.getElementById("outerShadowHost").shadowRoot;
      const describedElement =
        outerShadowRoot.getElementById("describedElement");
      const innerShadowRoot = outerShadowRoot.firstElementChild.shadowRoot;
      innerShadowRoot.appendChild(describedElement);
    });

    describedAcc = await onRecreated;
    // Relation is restored, because target is now in same shadow scope.
    await testCachedRelation(describedAcc, RELATION_DESCRIBED_BY, [
      accDescription1,
      accDescription2,
    ]);
    await testCachedRelation(accDescription1, RELATION_DESCRIPTION_FOR, [
      describedAcc,
    ]);
    await testCachedRelation(accDescription2, RELATION_DESCRIPTION_FOR, [
      describedAcc,
    ]);
  }
);
