text: import getState from './get-state';
  import SelectorNodeFactory from './selector-node-factory';
  import { getElement, getAssert } from './utils';
  
  /**
   * A selector node in a page object hierarchy. SelectorNodes exist in a tree
   * structure with a PageObject at the root. Each SelectorNode describes a set of
   * DOM elements in a way that handles the ambiguity inherent in selectors as to
   * whether they reference a single DOM element or a list of all matching DOM
   * elements, and also captures something that selectors do not, which is
   * indexing into the list of matching DOM elements.
   *
   * Each SelectorNode describes DOM elements in three different ways:
   *
   * 1. A single (possibly null) DOM element that child SelectorNodes use as the
   *    root for their selectors
   * 2. A list (possibly empty) of DOM elements that can be indexed into to allow
   *    SelectorNodes to act as lists
   * 3. A list (possibly empty) of DOM elements that are used from qunit-dom
   *    assertions
   *
   * The PageObject at the root of the tree supply the equivalent of (1) for its
   * child SelectorNodes. Then, each SelectorNode generates (2) from it's parent
   * (1) by doing the equivalent of
   *
   * `parent.element?.querySelectorAll(this.selector) || []`
   *
   * (1) is generated by taking the first element of (2), or if the SelectorNode
   * has an index, the element of (2) at that index.
   *
   * For SelectorNodes that do not have an index, (3) is the same as (2) (we
   * assert against all matched elements), and for those that do, (3) is the same
   * as (1) (we assert against only the element described by the index).
   */
  export default class SelectorNode {
    /**
     * @param {string} selector this SelectorNode's selector (relative to its
     * parent)
     * @param {*} extensions this SelectorNode's optional extensions object,
     * providing functionality to extend the SelectorNode such as child
     * SelectorNodes, helpers for inspecting/interacting with the underlying DOM
     * element, etc.
     * @param {Number|null} index this SelectorNode's index among its matching DOM
     * elements, if any (if not it references all DOM elements matching its
     * selector)
     */
    constructor(parent, selector, extensions, index = null) {
      let obj = createSelectorNode();
      Object.assign(getState(obj), { parent, selector, extensions, index });
      return obj;
    }
  }
  
  // Ensure instanceof works correctly
  Object.setPrototypeOf(SelectorNode.prototype, Array.prototype);
  
  function createSelectorNode() {
    return new Proxy([], {
      get(array, prop, selectorNode) {
        let { parent, selector, extensions } = getState(selectorNode);
  
        if (prop === 'assert') {
          // `page.foo.assert` -- get the assert object for this SelectorNode
          return getAssert(selectorNode);
        } else if (prop === 'element') {
          // `page.foo.element` -- get the DOM element for this SelectorNode
          return getElement(selectorNode);
        } else if (/^\d+$/.test(prop)) {
          // `page.foo[1]` -- return a SelectorNode that has our same data,
          // but with an index added
          return new SelectorNode(parent, selector, extensions, parseInt(prop, 10));
        } else if (extensions && Reflect.has(extensions, prop)) {
          // Defined on the extensions object
          let descriptor;
          let prototype = extensions;
          while ((prototype = Object.getPrototypeOf(prototype))) {
            descriptor = Object.getOwnPropertyDescriptor(prototype, prop);
            if (descriptor) {
              break;
            }
          }
  
          let value;
          if (descriptor?.get) {
            value = descriptor.get.call(selectorNode);
          } else {
            value = extensions[prop];
          }
  
          // let value = extensions[prop];
  
          if (value instanceof SelectorNodeFactory) {
            // SelectorNodeFactory, so use it to instantiate a SelectorNode that
            // is a child of this SelectorNode
            return value.create(selectorNode);
          } else {
            // Some other value -- just return it
            return value;
          }
        } else  {
          // Not one of our intercepted properties, and not a property on the
          // extensions object, so this must be accessing the underlying array,
          // e.g. `page.list.length` or `page.list.slice(1, 2)`. So populate our
          // array with SelectorNodes for each element we match so the result will
          // be correct.
          let parentElement = getElement(parent);
          let selectors;
          if (parentElement) {
            let elements = Array.from(parentElement.querySelectorAll(selector));
            selectors = elements.map((_e, i) => new SelectorNode(parent, selector, extensions, i));
          } else {
            selectors = [];
          }
          array.splice(0, array.length, ...selectors);  
  
          return array[prop];
        }
      }
    });  
  }
  