Source: test-utils.js

// @ts-check

const validator = require('html-validator');

/**
 * @typedef SerializedRemoteEvent
 * @property {string} type
 * @property {any} detail
 */


/**
 * @ignore @typedef {import('puppeteer').ElementHandle} ElementHandle
 */

/**
 * @ignore @typedef {import('puppeteer').JSHandle} JStHandle
 */

/**
 * @ignore @typedef {import('puppeteer').Page} Page
 */


/**
 * @private
 * @param {Page} page 
 * @param {string} path 
 * @param {ElementHandle} container 
 */
const find = async (page, path, container) => {
  const result = await page.evaluateHandle((path, container) => {
    // @ts-ignore
    return window.queryDeepSelector(path, container);
  }, path, container);
  return result;
};

/**
 * @class TestUtils
 * @property {ElementHandle} targetComponent
 * @property {Page} page
 */
class TestUtils {

  /**
   * @param {string} path deeq query selector formatted path
   * @param {ElementHandle} container 
   */
  async find (path, container = this.targetComponent) {
    return await find(this.page, path.trim(), container);
  }

  /**
   * Initializes the showroom test-utils with a puppeteer page instance
   * @param {Page} page 
   */
  async initialize (page) {
    this.page = page;
    await page.evaluate(() => {
      // @ts-ignore
      if (!window.queryDeepSelector) {
        /**
         * @param {string} selectorStr 
         * @param {any} container 
         */
        const queryDeepSelector = (selectorStr, container = document) => {
          const selectorArr = selectorStr.replace(new RegExp('//', 'g'), '%split%//%split%').split('%split%');
          for (const index in selectorArr) {
            const selector = selectorArr[index].trim();

            if (!selector) continue;

            if (selector === '//') {
              container = container.shadowRoot;
            }
            else {
              container = container.querySelector(selector);
            }
            if (!container) break;
          }
          return container;
        };
        window['queryDeepSelector'] = queryDeepSelector;
      }
    });
    return page;
  }

  /**
   * clears the collected events in the browser
   */
  async clearEventList () {
    await this.page.evaluate(() => {
      // @ts-ignore
      dashboard.clearEvents();
    });
  }

  /**
   * Selects a component to be tested
   * @param {string} componentName 
   * @returns ElementHandle
   */
  async setTestSubject (componentName) {
    this.testSubjectName = componentName;
    let lastTargetComponent = this.targetComponent;
    while (lastTargetComponent === this.targetComponent) {
      // @ts-ignore
      await this.page.evaluate((async (componentName) => await showroom.setTestSubject(componentName)), componentName);
      this.targetComponent = await this.testSubject();
    }
    return this.targetComponent;
  }

  /**
   * @return ElementHandle
   */
  async testSubject () {
    /**
     * @type ElementHandle
     */
    // @ts-ignore
    const handle = await this.page.evaluateHandle(() => {
      // @ts-ignore
      return dashboard.targetComponent;
    });
    return handle;
  }

  /**
   * @returns Promise<Array<SerializedRemoteEvent>>
   */
  async getEventList () {
    return await (await this.page.evaluate(() => {
      // @ts-ignore
      return window.dashboard.events.map(({type, detail, bubbles}) => {
        return {
          type,
          detail,
          bubbles
        };
      });
    }));
  }

  /**
   * @returns SerializedRemoteEvent
   */
  async getLastEvent () {
    return this.page.evaluate(() => {
      // @ts-ignore
      const { type, detail, bubbles } = window.dashboard.lastEvent;
      return { type, detail, bubbles };
    });
  }

  /**
   * Tests HTML validity of a remote element
   * @param {ElementHandle} target HTML as string
   * @throws {Error} When validation fails
   */
  async validateHTML (target) { 
    /**
     * @type ElementHandle
     */
    const resolvedTarget = target || this.targetComponent;
    const html = await this.getProperty('innerHTML', resolvedTarget);
    await validator({
      format: 'text',
      data: html
    });
  }

  /**
   * Returns the value of a property on the remote target
   * @param {string} property 
   * @param {ElementHandle} [target] defaults to current tested component
   * @returns any|JSHandle
   */
  async getProperty (property, target) {
    const resolvedTarget = target || this.targetComponent;
    return await (await this.page.evaluate((target, prop) => {
      return target[prop];
    }, resolvedTarget, property));
  }

  /**
   * Sets a property on a remote target
   * @param {string} property 
   * @param {any} value 
   * @param {ElementHandle} [target] defaults to current tested component
   */
  async setProperty (property, value, target) {
    const resolvedTarget = target || this.targetComponent;
    await this.page.evaluate((target, prop, value) => {
      // @ts-ignore
      showroom.setProperty(prop, value);
    }, resolvedTarget, property, value);
  }

  async setAttribute (name, value, target) {
    const resolvedTarget = target || this.targetComponent;
    await this.page.evaluate((target, name, value) => {
      // @ts-ignore
      showroom.setAttribute(name, value);
    }, resolvedTarget, name, value);
    return resolvedTarget;
  }

  /**
   * Returns value of a remote element's attribute
   * @param {string} name
   * @param {ElementHandle} target 
   * @returns Promise<string>
   */
  async getAttribute (name, target) {
    const resolvedTarget = target || this.targetComponent;
    /**
     * @type string
     */
    const result = await(await this.page.evaluate((target, name) => {
      return target.getAttribute(name);
    }, resolvedTarget, name));
    return result;
  }

  /**
   * @param {string} name 
   * @param {ElementHandle} target 
   * @returns Promise<boolean>
   */
  async hasAttribute (name, target) {
      const resolvedTarget = target || this.targetComponent;
      /**
       * @type boolean
       */
      const result = await(await this.page.evaluate((target, name) => {
          return target.hasAttribute(name);
      }, resolvedTarget, name));
      return result;
  }

  /**
   * @param {string} name 
   * @param {ElementHandle} target 
   */
  async removeAttribute (name, target) {
    const resolvedTarget = target || this.targetComponent;
    await this.page.evaluate((target, name) => {
      target.removeAttribute(name);
    }, resolvedTarget, name);
    return resolvedTarget;
  }

  /**
   * 
   * @param {ElementHandle} target 
   * @returns boolean;
   */
  async isVisible (target) {
      const resolvedTarget = target || this.targetComponent;
    
      /**
       * @type boolean
       */
      const isIntersectingViewport = await resolvedTarget.isIntersectingViewport();
      /**
       * @type boolean
       */
      const isNotHidden = await this.page.evaluate(async (target) => {
          const style = getComputedStyle(target);
          return style && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
      }, resolvedTarget);
      return isIntersectingViewport && isNotHidden;
  }

  /**
   * 
   * @param {ElementHandle} target 
   * @returns Promise<string>
   */
  async getTextContent(target) {
      const resolvedTarget = target || this.targetComponent;
      await this.page.evaluate(async (target) => target, resolvedTarget);

      const textContent = await resolvedTarget.getProperty('textContent');
      /**
       * @type Promise<string>
       */
      const asText = textContent.jsonValue();
      return asText;
  }

  /**
   * Executes predefined function on a showroom descriptor file for the current tested component
   * @param {string} fnName 
   */
  async trigger (fnName) {
    await this.page.evaluate((fnName) => {
      // @ts-ignore
      dashboard.trigger(fnName);
    }, fnName);
  }
}

/**
 * @param {Page} page
 * @returns TestUtils
 */
module.exports = async function createUtils(page) {
  if (!page) {
    throw new Error('Page object is mandatory');
  }
  const testUtils = new TestUtils();
  await testUtils.initialize(page);
  return testUtils;
}

module.exports.TestUtils = TestUtils;