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 就告一段落了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容