上一篇看完,我们已经拥有了将字符串转换成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
是通过录制生成的,所以先看看录制的逻辑
首先可以看到录制是会触发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
就告一段落了。