Frame.js

import EventEmitter from "./EventEmitter.js"
import ElementHandle from "./ElementHandle.js"
import {BoundIpc} from "./ipc.js"
import {TimeoutPromise, proxyBindDecorator} from "./util.js"

/**
 * @class Frame Frame类,webview的iframe,主页面是mainFrame
 * @extends EventEmitter
 *
 * @property {WebviewTag} webview
 * @property {boolean} isMainFrame 是否mainFrame
 * @property {Ipc} ipc ipc通信实例,可用于和webview内内容通信
 * @property {ElementHandle} document 当前frame的document操作句柄
 *
 */
class Frame extends EventEmitter {
  /**
   * @typedef FrameInfo frame信息
   * @property {string} FrameInfo.UUID iframe的UUID值
   * @property {string} FrameInfo.name iframe的name值
   * @property {boolean} FrameInfo.isMainFrame 是否为mainFrame
   * @property {FrameInfo} FrameInfo.parent 父的frame的信息
   */

  /**
   *
   * @constructor Frame
   * @param {Page} page 所属page实例
   * @param {WebviewTag} webviewElement webview节点
   * @param {FrameInfo} frameInfo frame的信息
   */
  constructor(page, webviewElement, frameInfo) {
    super()

    this._frameInfo = frameInfo

    this._page = page
    this.webview = webviewElement
    // webview的webContentsId
    this.webContentsId = frameInfo.routingId
    this.isMainFrame = frameInfo.isMainFrame
    this.ipc = new BoundIpc(this.webview, frameInfo.UUID)
    this.document = new ElementHandle(this, "document", [])

    this._childFrames = []
    this._listenChildFramesRegister()
    this._listenChildFramesUnregister()
  }
  // 监听当前frame下子iframe的注册事件
  _listenChildFramesRegister() {
    this.ipc.on("childFrame.register", (frameInfo) => {
      let originInfo = this._childFrames.find(
        (item) => item.UUID === frameInfo.UUID
      )
      if (originInfo) {
        Object.assign(originInfo, frameInfo)
      } else {
        this._childFrames.push(frameInfo)
      }
    })
  }
  // 监听当前frame下子iframe的注销事件
  _listenChildFramesUnregister() {
    this.ipc.on("childFrame.unregister", (frameInfo) => {
      this._childFrames = this._childFrames.filter(
        (item) => item.UUID !== frameInfo.UUID
      )
    })
  }
  /**
   * 注入一个指定url或(content)的script标签到页面
   * @param {Object} options
   * @property {string} [options.url] 要添加的script的src
   * @property {string} [options.content] 要注入页面的js代码
   * @property {boolean} [options.waitLoad] 如果是url,等待onload回调
   * @property {string} [options.type] type属性,如果要注入 ES6 module,值为'module'
   *
   * @return {Promise<ElementHandle>} 返回注入脚本的dom句柄实例
   */
  addScriptTag(options) {
    return this.ipc.send("frame.addScriptTag", options)
  }
  /**
   * 注入一个link(url)或style(content)标签到页面
   * @param {Object} options
   * @property {string} [options.url] 要添加的css link的src
   * @property {string} [options.content] 要注入页面的style代码
   *
   * @return {Promise<ElementHandle>} 返回注入样式的dom句柄实例
   */
  addStyleTag(options) {
    return this.ipc.send("frame.addStyleTag", options)
  }
  /**
   * 获取当前frame
   */
  childFrames() {
    return this._childFrames.map(
      (info) =>
        new Frame(this._page, this.webview, {
          ...info,
          parent: this._frameInfo,
        })
    )
  }
  /**
   * todo
   * 获取frame的内容
   *
   * @return {string}
   */
  content() {
    return this.ipc.send("frame.content")
  }
  /**
   * 在frame运行指定函数
   * @param {Function} pageFunction
   * @param {string[]|number[]} args 传给pageFunction的参数
   * @param {number} [timeout]  等待超时时间
   *
   * @return {Promise<*>} 返回运行结果,若pageFunction返回Promise,会等待Promise的resolve结果
   */
  evaluate(pageFunction, args, timeout) {
    if (!Array.isArray(args)) {
      timeout = args
      args = []
    }

    if (typeof pageFunction == "string") {
      pageFunction = "function() {return " + pageFunction + "}"
    }

    return this.ipc.send(
      "frame.evaluate",
      {pageFunction: pageFunction.toString(), args: args},
      timeout
    )
  }
  /**
   * 控制当前frame跳转到指定url
   * 会在超时时间内追踪重定向,直到跳转到最终页面
   * @param {string} url
   * @param {Object} [options]
   * @property {string} [options.waitUntil] 认定跳转成功的事件类型 load|domcontentloaded,默认为domcontentloaded
   * @property {number} [options.timeout] 超时时间,单位为ms,默认为10000ms
   *
   * @return {Promise<undefined>}
   */
  async goto(url, options) {
    let timeout = (options && options.timeout) || 1e4
    var waitUntil = (options && options.waitUntil) || "domcontentloaded"

    // 递归获取最终的跳转地址
    // timeout计时结束后就停止监听跳转
    var redirectURL = url
    const _getRedirectUrl = () => {
      this.page().webRequest.onBeforeRedirect(
        {
          urls: ["http://*/*", "https://*/*"],
        },
        (details) => {
          if (details.url === redirectURL) {
            redirectURL = details.redirectURL
            // console.log('onBeforeRedirect: ', redirectURL)
          }
        }
      )
    }
    _getRedirectUrl()

    await this.ipc.send("frame.goto", {
      url: url,
    })

    return new TimeoutPromise((resolve) => {
      this.ipc.on("frame.goto." + waitUntil, (payload) => {
        if (payload.url === redirectURL) {
          resolve(payload)
        }
      })
    }, timeout).catch((err) => {
      if (err === "promise.timeout") {
        return Promise.reject("goto.timeout")
      }
      return Promise.reject(err)
    })
  }
  /**
   * todo
   */
  isDetached() {
    return Promise.reject("todo")
  }
  /**
   * frame的名称
   *
   * @return {string}
   */
  name() {
    return this._frameInfo.name
  }
  /**
   * frame所属的page实例
   *
   * @return {Page}
   */
  page() {
    return this._page
  }
  /**
   * frame的parentFrame
   * 如果当前frame为mainFrame,返回null
   *
   * @return {Frame}
   */
  parentFrame() {
    if (this._frameInfo.parent) {
      return new Frame(this._page, this.webview, this._frameInfo.parent)
    }
    return null
  }
  /**
   * todo
   */
  select() {
    return Promise.reject("todo")
  }
  /**
   * todo
   * 设置页面内容
   * @param {string} html
   *
   * @return {Promise<undefined>}
   */
  setContent(html) {
    return this.ipc.send("frame.setContent", html)
  }
  /**
   * frame的标题
   *
   * @return {string}
   */
  title() {
    if (this.isMainFrame) {
      return this.webview.getTitle()
    }

    return this.ipc.send("frame.title")
  }
  /**
   * todo
   * 未实现,请使用press方法
   * 输入指定内容
   * @param {string} selector 要输入的dom的选择,input或textarea
   * @param {string} text 输入的文本
   * @param {Object} [options]
   * @property {number} [options.delay] // 延迟输入, 操作更像用户
   */
  type(selector, text, options) {
    return this.ipc.send("frame.type", {selector, text, options})
  }
  /**
   * 获取url,如果是mainFrame为当前url,如果是iframe,则是src属性
   *
   * @return {string}
   */
  url() {
    if (this.isMainFrame) {
      return this.webview.getURL()
    }

    return this._frameInfo.url
  }
  /**
   * waitForSelector|waitForFunction|setTimeout的结合体
   * @param {string|number|Function} selectorOrFunctionOrTimeout
   * @param {Object} options
   * @param  {...any} args
   */
  waitFor(selectorOrFunctionOrTimeout, options, ...args) {
    if (typeof selectorOrFunctionOrTimeout === "string") {
      return this.waitForSelector(selectorOrFunctionOrTimeout, options)
    } else if (typeof selectorOrFunctionOrTimeout === "number") {
      return new Promise((resolve) => {
        setTimeout(resolve, selectorOrFunctionOrTimeout)
      })
    } else {
      return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args)
    }
  }
  /**
   * 在指定时间内轮询执行方法,直到方法返回true
   * @param {Function} pageFunction
   * @param {Object} [options]
   * @property {number} [options.timeout] 等待时间
   * @param  {...any} args
   *
   * @return {Promise<boolean>} 成功返回resove(true),超时返回reject
   */
  waitForFunction(pageFunction, options, ...args) {
    if (typeof pageFunction == "string") {
      pageFunction = "function() {return " + pageFunction + "}"
    }

    return this.ipc.send("frame.waitForFunction", {
      pageFunction: pageFunction.toString(),
      args: args,
      options,
    })
  }
  /**
   * 等待跳转完成
   * @param {Object} [options]
   * @property {string} [options.waitUntil] 认定跳转成功的事件类型 load|domcontentloaded,默认为domcontentloaded
   * @property {number} [options.timeout] 超时时间,单位为ms,默认为10000ms
   *
   * @return {Promise<Object>} 返回跳转后frame的信息
   */
  waitForNavigation(options) {
    return new TimeoutPromise((resolve) => {
      var waitUntil = (options && options.waitUntil) || "domcontentloaded"
      this.ipc.once("frame.waitForNavigation." + waitUntil, function (param) {
        resolve(param)
      })
    }, (options && options.timeout) || 1e4).catch((err) => {
      if (err === "promise.timeout") {
        return Promise.reject("waitForNavigation.timeout")
      }
      return Promise.reject(err)
    })
  }
  /**
   * 在指定时间内轮询查询dom节点,直到查找到节点
   * @param {string} selector dom节点选择器
   * @param {Object} [options]
   * @property {boolean} [options.visible] 节点是否可见,如果visible为true时必须查到到dom节点且可见才会返回true
   * @property {number} [options.timeout] 超时时间,单位为ms,默认为10000ms
   *
   * @return {Promise<undefined>} 成功则resolve,失败返回reject
   */
  waitForSelector(selector, options) {
    return this.ipc.send(
      "frame.waitForSelector",
      {
        selector: selector,
        options: options,
      },
      options && options.timeout
    )
  }
  /**
   * todo
   */
  waitForXPath(/* xpath, options */) {
    return Promise.reject("todo")
  }
  /**
   * 点击frame内的指定节点
   * @param {string} selector 选择器
   * @param {Object} options 暂不支持
   *
   * @return {Promise<boolean>}
   */
  click(selector, options) {
    return this.document.$(selector).click(options)
  }
  /**
   * 聚焦frame内的指定节点
   * @param {string} selector 选择器
   * @param {Object} options 暂不支持
   *
   * @return {Promise<boolean>}
   */
  focus(selector, options) {
    return this.document.$(selector).focus(options)
  }
  /**
   * 取消聚焦frame内的指定节点
   * @param {string} selector 选择器
   * @param {Object} options 暂不支持
   *
   * @return {Promise<boolean>}
   */
  blur(selector, options) {
    return this.document.$(selector).blur(options)
  }
  /**
   * 鼠标移入frame内的指定节点,对应mouseover事件
   * @param {string} selector 选择器
   * @param {Object} options 暂不支持
   *
   * @return {Promise<boolean>}
   */
  hover(selector, options) {
    return this.document.$(selector).hover(options)
  }
  /**
   * todo
   */
  tap(selector, options) {
    return this.document.$(selector).tap(options)
  }
  /**
   * 获取localStorage的所有key集合
   */
  localStorageKeys() {
    return this.ipc.send("frame.localStorageKeys")
  }
  /**
   * localStorage.getItem
   * @param {string} key
   *
   * @return {Promise<string>}
   */
  localStorageGet(key) {
    return this.ipc.send("frame.localStorageGet", {
      key: key,
    })
  }
  /**
   * localStorage.setItem
   * @param {string} key
   * @param {string} value
   *
   * @return {Promise<undefined>}
   */
  localStorageSet(key, value) {
    return this.ipc.send("frame.localStorageSet", {
      key: key,
      value: value,
    })
  }
  /**
   * localStorage.removeItem
   * @param {string} key
   *
   * @return {Promise<undefined>}
   */
  localStorageRemove(key) {
    return this.ipc.send("frame.localStorageRemove", {
      key: key,
    })
  }
}

export default proxyBindDecorator(
  [
    /**
     * [frame.document.$的简写]{@link ElementHandle#$}
     * @method Frame#$
     */
    "$",
    /**
     * [frame.document.$$的简写]{@link ElementHandle#$$}
     * @method Frame#$$
     */
    "$$",
    /**
     * [frame.document.$eval的简写]{@link ElementHandle#$eval}
     * @method Frame#$eval
     */
    "$eval",
    /**
     * [frame.document.$$eval的简写]{@link ElementHandle#$$eval}
     * @method Frame#$$eval
     */
    "$$eval",
    /**
     * [frame.document.$x的简写]{@link ElementHandle#$x}
     * @method Frame#$x
     */
    "$x",
  ],
  function () {
    return this.document
  }
)(Frame)