@lx-frontend/node-miniapp-visual-test v0.1.5
@lx-frontend/node-miniapp-visual-test
小程序视觉测试工具库
详细用法请查看 example 和 tests 测试用例。
usage
待完成。
StepsExector
简介
StepExector
旨在简化小程序的视觉测试流程。通过StepsExector,开发者可以通过简单的配置来执行自动化操作,而无需深入理解底层的自动化工具。
用法简介
引入
import { StepsExector } from '@lx-frontend/node-miniapp-visual-test'
实例化
// 在函数中执行
const stepsExector = await StepsExector.create(config)
其中,config
可以是配置对象,也可以是json
配置文件的路径。
配置项如下,详情见automator: | 配置 | 释义 | | ---- | ----| | cliPath | 开发者工具的安装路径 | | projectPath | 项目根目录 |
StepsExector不支持new调用,因为在实例化过程中需要调起开发者工具,这段时间比较长,而构造函数不支持异步,所以在new调用完成后,开发者工具可能未调起,此时任何操作都会报错。故使用静态函数实例化,确认开发者工具调起完成后再返回经Promise包裹的实例对象。
示例
import { StepsExector, IBuiltInStepType, IBuiltInStep, defineSteps } from '@lx-frontend/node-miniapp-visual-test'
const stepsExector = await StepsExector.create({cliPath: 'xxx', projectPath: 'xxx'})
// defineSteps仅仅只是为了在编写步骤时有友好的类型提示
const steps: IBuiltInStep[] = defineSteps([
{
type: IBuiltInStepType.LAUNCH,
title: 'reLaunch到/page/index页',
timeout: 10000,
until: '.class',
url: '/page/index'
},
{
type: IBuiltInStepType.CLICK,
title: '点击.class元素',
selector: '.class'
timeout: 1000,
until: 500
},
{
type: IBuiltInStepType.SCROLL,
title: '#screen-shot元素滚动到顶部',
selector: '#screen-shot',
timeout: 1000,
until: 500
},
{
type: IBuiltInStepType.SCREENSHOT,
title: '#screen-shot元素截图',
selector: '#screen-shot',
timetout: 10000
}
])
// 执行步骤
stepsExector.run(steps)
上面的steps定义个如下几个操作: 1. 用reLaunch跳转到/page/index页,直到页面出现.class元素 2. 点击.class元素,等待500ms 3. id为screen-shot的元素滚动到页面顶部,等待500ms 4. id为screen-shot的元素截屏
其中,timeout为该步骤的超时时间,默认10秒;until为该步骤完成的条件,数字代表等待多少毫秒,字符串代表直到页面出现某个元素,还可以使用自定义函数。
每种步骤类型可能有不同的配置项,IBuiltInStep
类型可以给出友好的配置提示。(IBuiltInStep
是内置的步骤类型,后面还可以自定义步骤类型,用法会稍有区别。)
以上示例是定义好步骤后直接运行,不过也可以不运行,而是将各个步骤配置编译成一个个函数,然后交由用户自行调用,比如结合allure
,将每一步的执行结果整合到测试报告中。通过buildStepsIntoFunction
方法可以将steps配置变成执行函数:
const builtSteps = stepsExector.buildStepsIntoFunction(steps)
// in some async function
for (let i = 0; i < builtSteps.length; i++) {
const stepItem = builtSteps[i]
// stepItem下的fn属性便是步骤的执行函数,不接受任何参数
const res = await stepItem.fn()
// do something with res
}
内置操作类型
类型通用配置 (IStepRequired)
type IStepRequired = {
// 操作类型,内置类型见枚举IBuiltInStep。也可以自定义类型,下面会介绍
type: string
// 操作的标题
title: string
// 该操作超时时间
timeout: number
// 该操作是否完成的判断条件,后面会作详细介绍
until?: string | number | ((page: Page, context: StepsExector) => Promise<boolean>)
// 条件执行,只有当if条件满足时才会执行该步骤
if?: string | ((page: Page, context: StepsExector) => Promise<boolean>)
}
until配置的说明 1. 如果until是字符串,代表一个WXSS选择器,表明该操作会等到页面出现该选择器对应的元素为止。有的操作可能会跳转到其他页面,因无法准确判断页面跳转是否成功,所以每一秒会尝试查找一次元素,直到超时,超时则判定该步骤失败。 2. 如果until是数字,代表毫秒数,该操作等待该长度的时间后即可认为成功 3. 如果until是函数,该函数会传入当前的page和StepsExector实例对象,用户可自定义逻辑,返回一个Promise包裹的true表明操作成功,false表明操作失败。
if配置说明 1. 如果if是字符串,代表一个WXSS选择器,在执行该操作之前会判断该选择器指定的元素是否在页面中存在,存在则返回Promise,表面该步骤需要执行,不存在则返回Promise,表明该步骤跳过。 2. 如果if是函数,该函数会传入当前的page和StepsExector实例对象,用户可自定义逻辑,返回一个Promise包裹的true表明该步骤需要执行,false表明跳过。
以下只对各个类型特殊的配置作以说明
1. click (IBuiltInStepType.CLICK)
type IClickStep = IStepRequired & {
type: IBuiltInStepType.CLICK
selector: string | { selector: string; no: number }
}
该操作会点击selector选中的元素。selector为对象,表明页面有多个selector
指定的元素,no
用来指定要点击的元素的编号。
2. scroll (IBuiltInStepType.SCROLL)
type IScrollStep = IStepRequired & {
type: IBuiltInStepType.SCROLL
selector: string | { selector: string; no: number }
}
该操作会滚动selector选中的元素到顶部。selector为对象,表明页面有多个selector
指定的元素,no
用来指定要滚动的元素的编号。
3. screenshot (IBuiltInStepType.SCREENSHOT)
type IScreenShotStep = IStepRequired & {
type: IBuiltInStepType.SCREENSHOT,
selector: string,
}
该操作对selector选中的元素进行截图,需要注意的是,截图前需要保证该元素在视窗内,超出视窗则默认裁剪。
4. launch (IBuiltInStepType.LAUNCH)
type ILaunchStep = IStepRequired & {
type: IBuiltInStepType.LAUNCH
url: string
}
该操作会用reLaunch的方式打开一个新的页面,url为要打开的url。
5. data (IBuiltInStepType.DATA)
type IDataStep = IStepRequired & {
type: IBuiltInStepType.DATA
value: string
}
该操作会调用page.setData(value)设置页面数据。
6. navigate (IBuiltInStepType.NAVIGATE)
type INavigateStep = IStepRequired & {
type: IBuiltInStepType.NAVIGATE
url: string
}
该操作会调用page.navigateTo(url)打开新的页面。
7. custom (IBuiltInStepType.CUSTOM)
type ICustomStep = IStepRequired & {
type: IBuiltInStepType.CUSTOM
fn: (context: StepsExector) => Promise<boolean>
}
该操作会执行用户自定义的函数。函数参数是StepsExector实例,通过该实例可以获取到MiniProgram实例和VisualTest实例。函数返回必须为Promise包裹的true或者false,true表示操作成功,false表示操作失败。
8. input (IBuiltInStepType.INPUT)
type IInputStep = IStepRequired & {
type: IBuiltInStepType.INPUT
selector: string
value: string | number
}
该操作会将value值输入到selector指定的input
或textarea
。
9. drag (IBuiltInStepType.DRAG)
type IDragStep = IStepRequired & {
type: IBuiltInStepType.DRAG
selector: string | {selector: string; no: number}
diffX: number
diffY: number
}
拖拽selector指定的元素,diffX和diffY表示拖拽的偏移量,diffX为横向偏移量,为正数时向右拖,为负数时向左拖,diffY为纵向偏移量,为正数时向下拖,为负数时向上拖。
内置操作类型配置的快捷方式
类似上面定义steps的方式,每一步都需要一个对象来配置还是略有些繁琐的,观察同一种操作类型的步骤配置,可以发现有很多共性,所以可以定义一些通用的快捷方式,方便更快捷直观地定义操作。
之所以保留对象形式的配置,是想尽可能保留每一步配置的灵活性。
defineClick
// 函数定义
type IClickShortcut = `${string}:${number|string}`
const defineClick = (clickShortcuts: IClickShortcut): IClickStep
该方法接受一个点击快捷方式数组,返回一组点击操作配置组成的数组。
快捷方式的定义格式为:selector:until
selector为要点击的元素,until可以是数字(毫秒数)或者字符串(选择器,直到页面出现该元素则认为操作成功)
['.click-a:500', '.click-b:.until-class-show'].map(defineClick)
// 等同于
[
{
// 点击.click-a,等待500毫秒
type: IBuiltInStepType.CLICK,
title: '点击.click-a',
selector: '.click-a',
timeout: 10000, // 默认给10秒超时时间
until: 500
},
{
// 点击.click-b,直到页面出现.until-class-show
type: IBuiltInStepType.CLICK,
title: '点击.click-b',
selector: '.click-b',
timeout: 10000,
until: '.until-class-show'
}
]
如果selector选择器在页面上有多个元素,可以在选择器后面添加=${nodeIndex}
来指定选择哪个元素。
假设页面有个多个类名为.list-item
的元素,我们希望点击第二个元素,那么可以这样定义快捷方式:
defineClick('.list-item=1:500')
// 等同于
{
// 点击.list-item的第二个元素,等待500毫秒
type: IBuiltInStepType.CLICK,
title: '点击.list-item的第二个元素',
selector: {
selector: '.list-item',
no: 1
},
timeout: 10000,
until: 500
}
defineScreenShot
// 函数定义
const defineScreenShot = (selectors: string): IScreenShotStep
示例:
defineScreenShot('#id')
// 等同于
{
type: IBuiltInStepType.SCREENSHOT,
title: '截图#id',
selector: '#id',
timeout: 10000,
until: 10000
}
defineScroll
// string部分为选择器,number为滚动位置距顶部的距离
type IScrollShotCut = `${string}:${number}`
// 函数定义
const defineScroll = (selectors: IScrollShotCut): IScrollStep
实例一:将.class元素滚动到距离顶部100px的位置
defineScroll('.class:100')
// 等价于
{
type: IBuiltInStepType.SCROLL,
title: '滚动.class元素',
selector: '.class',
timeout: 10000,
until: 500,
top: 100
}
实例二:将编号为1的.class元素滚动到距离顶部100px的位置
defineScroll('.class=1:100')
// 等价于
{
type: IBuiltInStepType.SCROLL,
title: '滚动.class元素',
selector: {
// 页面有多个.class元素,本次操作第二个(下标为1)
selector: '.class',
no: 1
},
timeout: 10000,
until: 500,
top: 100
}
除了上面提供的快捷方式,你也可以定义适合自己的快捷方式,实际上就是如何解析快捷方式,将其转化成配置。
自定义操作类型
内置的操作类型还是很有限的,完全无法覆盖所有场景,假如你发现某个操作很常见,你当然可以通过内置的IBuiltInStepType.CUSTOM
类型自己编写逻辑,但是你也可以定义自己的类型,所以StepsExector提供了用户自己定义操作类型的方式。
自定义一个操作类型,你需要定义两个东西:一是该类型的配置方式,二是该类型对应的执行逻辑。
下面通过一个例子说明用法:
假设我们要定义一个操作步骤print
,打印当前页面的路径:
// 你给该操作类型定义的配置方式
type IPrintStep = {
type: 'print'
title: string
times: number // 打印多少次
timeout: number // 超时时间
until: 500 // 等待多少毫秒
}
// 你给该操作类型定义的执行逻辑
const customPrintHandler: ICustomDefinedTypeRunner<IPrintStep> = {
// type必须和配置定义的type类型一致
type: 'print',
// step就是该类型的配置,context为StepsExector的实例
fn: async (step, context) => {
const page = await context.miniProgram.currentPage()
const { times } = step
for (let i = 0; i < times; i++) {
console.log(`当前步骤:${step.title}, 当前页面:${page?.path}`)
}
return true
}
}
ICustomDefinedTypeRunner
是用来定义自定义类型操作的接口,可以在你编写fn函数的时候给出提示。
需要注意如下几点:
1. 逻辑函数fn,必须是一个异步函数,函数返回值可以是任意类型。
2. 配置中的timeout
和until
(如果有的话),不用自己处理,StepsExector会自动帮你处理,你只需要专注于当前操作的核心逻辑。
接下来,在实例化StepsExector的时候,传入自定义的操作类型:
import { click, defineSteps, IStep } from '@lx-frontend/node-miniapp-visual-test'
const stepsExector = await StepsExector.create<IPrintStep>('/path/to/config.json', [customPrintHandler])
const steps = defineSteps<IPrintStep>([
...[
['.click-a', 500],
['.click-b', '.until-class-show']
].map(([selector, until]) => click(selector, until)),
{
type: 'print',
title: '打印当前页面路径',
times: 3,
timeout: 10000,
until: 500
}
])
// 执行
stepsExector.run(steps)
StepsExector.create
和defineSteps
均是泛型函数,可以接受自定义步骤的配置类型,可以对参数和配置进行类型校验。上面只定义了一个操作类型IPrintStep
,你也可以定义更多的操作类型,此时,泛型函数的泛型参数就是你所有自定义配置的联合类型,比如IPrintStep | IOtherStep
。
实用的实例方法
StepsExector
上提供了几个实用的实例方法,可以帮助你更方便的简化某些操作。
下面假设stepsExector
是StepsExector
的实例。
stepsExector.getPage()
获取小程序当前页面,返回一个Page
对象,当Page
不存在时直接报错。这保证了获取Page对象后,可以直接调用上面的方法,而不用检查Page是否存在,避免了ts的类型检查报错。
const page = stepsExector.getPage()
const ele = await page.$('xxx')
// 相当于
const page = await stepsExector.miniProgram.currentPage()
if (!page) throw new Error('')
const ele = await page.$('xxx') // 在函数中,如果没有上面的判断,这里ts会报错
stepsExector.getElement(selector)
Automator支持的选择器有很多限制,比如无法跨自定义组件选中元素,不支持属性选择器等等。而getElement
方法则通过一套自定义的选择器语法来选取元素,减少复杂情况下选中目标元素需要多步操作的麻烦。
const ele = await stepsExector.getElement('.target{view.class[attr=value]} input')
上面这个例子,被选中的是.target
元素下,input
子元素,但是对.target
有额外的约束,即,该元素下,必须有一个类名包含class
且属性attr
的值为value
的标签view
。
更多自定义语法选择器可参考:自定义选择器
stepsExector.getElements(selector)
和getElement
方法几乎一样,区别仅在于,getElements
方法返回的是一个数组,数组内包含所有符合条件的元素。getElement
方法只返回第一个符合条件的元素。
log
StepsExector还导出了几个辅助打印的函数,用于在测试用例中,打印并缓存测试用例执行的日志。(主要是因为在jest测试用例中,用console.log打印信息,jest会同时打印很多多余的信息)
import { logInfo, logError, logWarn, clearLog, getLogedInfo } from '@lx-frontend/node-miniapp-visual-test'
logInfo, logError, logWarn仅接受一个string类型的参数,将信息打印在控制台,不同的信息类型会有不同的颜色。
同时,以上三个log函数在打印log的同时,还会将log信息保存起来,通过getLogedInfo
函数可以获取所有的日志信息。
clearLog
则清除所有暂存的日志。
custom-selector 自定义选择器
基于miniprogram-automator基础选择器,自定义的一套选择器语法。已经用于当前库的各个工具中,比如stepsExector.getElement(selector)
,参数selector应该遵循该语法。
具体实现见src/utils/custom-selector
,该文件导出了一个customSelector
函数,该函数接受选择器字符串,返回被选中的元素列表。
标签/类名/ID 选择器
原生组件标签名称,如view
、text
、image
等。
类名称,如.class
、.class1.class2
等。
ID,如#id
、#id1
等。
标签名称和类名或ID可以组合,如view.class
、view#id
、view.class#id
。
后代选择器
选择器若包含空格,则空格后面的选择器只能在空格之前的元素范围内继续筛选。
比如:view.class1 image
,筛选的目标是类名包含class1
的view标签下,所有的后代image标签。无论image在view标签下还有多少层的嵌套,都会被筛选出来。
筛选器
这是新引入的一个概念,筛选指的是对已经选中的元素进行筛选,只保留符合筛选器条件的元素。
下标筛选器 :nth(n)
在前面筛选出来的元素列表基础上,筛选下标为n的元素。n为数字,从0开始。
举例:view:nth(2)
,筛选的目标是当前页面中,下标为2的元素,即第三个元素。
逆下标筛选器 :nth-r(n)
在前面筛选出来的元素列表基础上,筛选倒数第n个元素。n为负整数,最大值为-1,表示倒数第一个元素。
假设selectedElements
为经由前面筛选器选中的元素列表,那么:nth-r(n)
选中的就是selectedElements[selectedElements.length + n]
。
文本内容筛选器 :text(text)
在前面筛选出来的元素列表基础上,筛选文本内容包含text的元素。特别注意是包含,不是等于。如果子元素包含text,那么所有满足条件的父元素也会被筛选出来。
<view class="outer-class">
<view class="inner-class">文本</view>
</view>
如上标签结构,customerSelector('view:text(文本)')
返回的列表会包含两个元素,即,.outer-class
和.inner-class
都会被选中。
属性筛选器 attr=value
在前面筛选出来的元素列表基础上,筛选属性attr的值为value的元素。
示例:view[class=class1][attr=value]
选择器筛选器 {/selector/}
在前面筛选出来的元素列表基础上,筛选后代元素包含selector的元素。
假设有如下标签结构:
<view>
<view class="outer-class">
<view class="inner-class">文本1</view>
<image />
</view>
<view class="outer-class">
<view class="inner-class">文本2</view>
<image />
</view>
</view>
目标是筛选第一个.outer-class
元素下的image标签,可以用这个选择器:view.outer-class{view:text(文本1)} image
,view.outer-class
会选中两个.outer-class
元素,接着{view:text(文本1)}
会对两个.outer-class
元素进行筛选,因为第一个.outer-class
元素满足条件约束,所以只保留第一个,之后继续在第一个.outer-class
元素下筛选image
元素。
注意两点:1. 括号内部的筛选器是在前面被修饰的元素列表基础上进行筛选的。2. 括号内部的筛选器只做条件判断使用,不会实际被返回。
选择器筛选器可以通过嵌套实现更复杂的选择器。比如,下面这个复杂的选择器格式是合法的:
tag-name[attr1=value1][attr2=value2]{#id-name .classname{view:text(文本}}{tag-name:nth(0):text(文本)}:nth(0) image