美文网首页
selenium 自定义代码转换格式拼接

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

作者: 任然_c117 | 来源:发表于2021-04-02 12:02 被阅读0次

    上一篇看完,我们已经拥有了将字符串转换成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 自定义代码转换格式拼接

          本文链接:https://www.haomeiwen.com/subject/iemfkltx.html