selenium 自定义代码转换格式拼接

上一篇看完,我们已经拥有了将字符串转换成selenium代码的能力。

现在简单看下,这个转换有哪些限制,里面的具体参数都有哪些作用。

如果要将自己的代码转成selenium的代码需要哪些规则。

{
    "id": "f82b2216-7207-4952-8339-019abfebfde3",
    "version": "2.0",
    "name": "test",
    "url": "https://baidu.com",
    "tests": [{
        "id": "706b803d-b284-4a34-8102-7975b67306eb",
        "name": "baidu",
        "commands": [{
            "id": "a95c059d-6185-4d0e-9088-19f1a9b24dcd",
            "comment": "",
            "command": "open",
            "target": "https://www.baidu.com/",
            "targets": [],
            "value": ""
        }, {
            "id": "c2afbb00-c6f2-4a3d-af92-61f975eb383e",
            "comment": "",
            "command": "setWindowSize",
            "target": "1050x567",
            "targets": [],
            "value": ""
        }, {
            "id": "286f42e7-da80-444c-a308-7c5eaaab0eba",
            "comment": "",
            "command": "click",
            "target": "id=kw",
            "targets": [
                ["id=kw", "id"],
                ["name=wd", "name"],
                ["css=#kw", "css:finder"],
                ["xpath=//input[@id='kw']", "xpath:attributes"],
                ["xpath=//span[@id='s_kw_wrap']/input", "xpath:idRelative"],
                ["xpath=//input", "xpath:position"]
            ],
            "value": ""
        },]
    }],
    "suites": [{
        "id": "656f702c-9973-4ec2-a3c8-def3ba081212",
        "name": "Default Suite",
        "persistSession": false,
        "parallel": false,
        "timeout": 300,
        "tests": []
    }],
    "urls": ["https://element.eleme.io/"],
    "plugins": []
}

上面是我们精简了一些的转换原始数据。

其实我们关注的内容只存在commands 中,像suites 如果使用test转换的话,不传递也没有问题

name 类型 描述
id string 唯一标识
comment string 描述,如果传递则会作为注释
command string 具体的命令。例如click,open等
target string 目标,也就是操作dom的特征
targets string[] dom的特征数组
value string[] input使用的输入值

其中大多数看一下就能知道换算方法,唯一需要注意的也就是id了。

targets 也简单说一下 ,目前转换的代码会以target为主,targets传递了也不会去取基本没用。

targets只又在ide 运行的时候会自动尝试使用,并且成功找到了也不会把正确的切换为target(如果我的理解有问题欢迎在下方指正)。

接下尝试找一下这个id 的生成规则

很明显id是通过录制生成的,所以先看看录制的逻辑

image.png

首先可以看到录制是会触发toggleRecord 函数

import UiState from '../../stores/view/UiState'
  toggleRecord() {
    UiState.toggleRecord()
  }

他的具体实现在UiState

  @action.bound
  async toggleRecord(isInvalid) {
    await (this.isRecording
      ? this.stopRecording()
      : this.startRecording(isInvalid))
  }

  @action.bound
  async startRecording(isInvalid) {
    let startingUrl = this.baseUrl
    if (!startingUrl) {
      startingUrl = await ModalState.selectBaseUrl({
        isInvalid,
        confirmLabel: 'Start recording',
      })
    }
    try {
      await this.recorder.attach(startingUrl)
      this._setRecordingState(true)
      this.lastRecordedCommand = null
      await this.emitRecordingState()
    } catch (err) {
      ModalState.showAlert({
        title: 'Could not start recording',
        description: err ? err.message : undefined,
      })
    }
  }


不难发现具体的逻辑在try中包含

首先使用了attach 附加地址,这里是重点,因为打开页面我们需要一个地址。
而在浏览器中attach通常标识调试附加器。

async attach(startUrl) {
    if (this.attached || this.isAttaching) {
      return
    }
    try {
      this.isAttaching = true
      browser.tabs.onActivated.addListener(this.tabsOnActivatedHandler)
      browser.windows.onFocusChanged.addListener(
        this.windowsOnFocusChangedHandler
      )
      browser.tabs.onRemoved.addListener(this.tabsOnRemovedHandler)
      browser.webNavigation.onCreatedNavigationTarget.addListener(
        this.webNavigationOnCreatedNavigationTargetHandler
      )
      browser.runtime.onMessage.addListener(this.addCommandMessageHandler)

      await this.attachToExistingRecording(startUrl)

      this.attached = true
      this.isAttaching = false
    } catch (err) {
      this.isAttaching = false
      throw err
    }
  }

一些事件监听函数,我们可以先行忽略,让我们看看attachToExistingRecording 中做了什么工作

 // this will attempt to connect to a previous recording
  // else it will create a new window for recording
  async attachToExistingRecording(url) {
    let testCaseId = getSelectedCase().id
    try {
      if (this.windowSession.currentUsedWindowId[testCaseId]) {
        // test was recorded before and has a dedicated window
        await browser.windows.update(
          this.windowSession.currentUsedWindowId[testCaseId],
          {
            focused: true,
          }
        )
      } else if (
        this.windowSession.generalUseLastPlayedTestCaseId === testCaseId
      ) {
        // the last played test was the one the user wishes to record now
        this.windowSession.dedicateGeneralUseSession(testCaseId)
        await browser.windows.update(
          this.windowSession.currentUsedWindowId[testCaseId],
          {
            focused: true,
          }
        )
      } else {
        // the test was never recorded before, nor it was the last test ran
        await this.createNewRecordingWindow(testCaseId, url)
      }
    } catch (e) {
      // window was deleted at some point by the user, creating a new one
      await this.createNewRecordingWindow(testCaseId, url)
    }
  }

  async createNewRecordingWindow(testCaseId, url) {
    const win = await browser.windows.create({
      url,
    })
    const tab = win.tabs[0]
    this.lastAttachedTabId = tab.id
    this.windowSession.setOpenedWindow(tab.windowId)
    this.windowSession.openedTabIds[testCaseId] = {}

    this.windowSession.currentUsedFrameLocation[testCaseId] = 'root'
    this.windowSession.currentUsedTabId[testCaseId] = tab.id
    this.windowSession.currentUsedWindowId[testCaseId] = tab.windowId
    this.windowSession.openedTabIds[testCaseId][tab.id] = 'root'
    this.windowSession.openedTabCount[testCaseId] = 1
  }

其实注释中就有就说的很明显了

他通过createNewRecordingWindow来创建新窗口,而里面则是由browser.windows.create 来实现具体创建。
这里并没有找到我们需要的东西,但是我们知道录制通常是通过注入实现,注入则需要窗口创建完成后页面加载时进行,而页面加载可以通过事件监听。因此我们可以往前看看。

通过查找我们在onFocusChanged 找到了关键代码

browser.tabs
      .query({
        windowId: windowId,
        active: true,
      })
      .then(tabs => {
        if (tabs.length === 0 || this.isPrivilegedPage(tabs[0].url)) {
          return
        }

        // The activated tab is not the same as the last
        if (tabs[0].id !== this.windowSession.currentUsedTabId[testCaseId]) {
          // If no command has been recorded, ignore selectWindow command
          // until the user has select a starting page to record commands
          if (!hasRecorded()) return

          // Ignore all unknown tabs, the activated tab may not derived from
          // other opened tabs, or it may managed by other SideeX panels
          if (
            this.windowSession.openedTabIds[testCaseId][tabs[0].id] == undefined
          )
            return

          // Tab information has existed, add selectWindow command
          this.windowSession.currentUsedWindowId[testCaseId] = windowId
          this.windowSession.currentUsedTabId[testCaseId] = tabs[0].id
          this.windowSession.currentUsedFrameLocation[testCaseId] = 'root'
          record(  // core here
            'selectWindow',
            [
              [
                `handle=\${${
                  this.windowSession.openedTabIds[testCaseId][tabs[0].id]
                }}`,
              ],
            ],
            ''
          )
        }
      })

这很容易理解,在页面焦点切换的时候监听当前页面内容。
让我看看里面的实现

// for record module
export default function record(
  command,
  targets,
  value,
  insertBeforeLastCommand
) {
  if (UiState.isSelectingTarget) return
  const test = UiState.displayedTest
  if (isEmpty(test.commands) && command === 'open') {
    addInitialCommands(targets[0][0])
  } else if (command !== 'open') {
    let index = getInsertionIndex(test, insertBeforeLastCommand)
    if (preprocessDoubleClick(command, test, index)) {
      // double click removed the 2 clicks from before
      index -= 2
    }
    const newCommand = recordCommand(command, targets[0][0], value, index)
    if (Commands.list.has(command)) {
      const type = Commands.list.get(command).target
      if (type && type.name === ArgTypes.locator.name) {
        newCommand.setTargets(targets)
      }
    }
  }
}

addInitialCommands 中添加最初的命令,也就是open 以及 setWindowSize之类的这里其实就包含我们要找的id

async function addInitialCommands(recordedUrl) {
  const { test } = UiState.selectedTest
  if (WindowSession.openedTabIds[test.id]) {
    const open = test.createCommand(0)
    open.setCommand('open')
    const setSize = test.createCommand(1)
    setSize.setCommand('setWindowSize')

    const tab = await browser.tabs.get(WindowSession.currentUsedTabId[test.id])
    const win = await browser.windows.get(tab.windowId)

    const url = new URL(recordedUrl ? recordedUrl : tab.url)
    if (!UiState.baseUrl) {
      UiState.setUrl(url.origin, true)
      open.setTarget(`${url.pathname}${url.search}`)
    } else if (url.origin === UiState.baseUrl) {
      open.setTarget(`${url.pathname}${url.search}`)
    } else {
      open.setTarget(recordedUrl)
    }
    setSize.setTarget(`${win.width}x${win.height}`)
    await notifyPluginsOfRecordedCommand(open, test)
    await notifyPluginsOfRecordedCommand(setSize, test)
  }
}

我们找到了命令的创建方式test.createCommand
packages\selenium-ide\src\neo\models\TestCase.js 文件中我们能找到具体的实现方案


 @action.bound
  createCommand(index, c, t, v, comment) {
    if (index !== undefined && index.constructor.name !== 'Number') {
      throw new Error(
        `Expected to receive Number instead received ${
          index !== undefined ? index.constructor.name : index
        }`
      )
    } else {
      const command = new Command(undefined, c, t, v)
      command.addListener(
        'window-handle-name-changed',
        this.updateWindowHandleNames
      )
      if (comment) command.setComment(comment)
      index !== undefined
        ? this.commands.splice(index, 0, command)
        : this.commands.push(command)
      return command
    }
  }


export default class Command {

  constructor(id = uuidv4(), command, target, value) {
    this.id = id
    this.command = command || ''
    this.target = target || ''
    this.value = value || ''
    this.export = this.export.bind(this)
    this[EE] = new EventEmitter()
    mergeEventEmitter(this, this[EE])
  }

以上其实就不用多说了, 我们只要实现这个uuidv4即可

import uuidv4 from 'uuid/v4'

这里也是引用的现有库
至此数据格式我们就可以完全模拟了。

相信即使转换器也无法区分是否为自动生成的脚本啦。

以上selenium ide 就告一段落了。

你可能感兴趣的:(selenium 自定义代码转换格式拼接)