@lx-frontend/node-miniapp-visual-test
TypeScript icon, indicating that this package has built-in type declarations

0.1.5 • Public • Published

@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指定的inputtextarea

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. 配置中的timeoutuntil(如果有的话),不用自己处理,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.createdefineSteps均是泛型函数,可以接受自定义步骤的配置类型,可以对参数和配置进行类型校验。上面只定义了一个操作类型IPrintStep,你也可以定义更多的操作类型,此时,泛型函数的泛型参数就是你所有自定义配置的联合类型,比如IPrintStep | IOtherStep

实用的实例方法

StepsExector上提供了几个实用的实例方法,可以帮助你更方便的简化某些操作。

下面假设stepsExectorStepsExector的实例。

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 选择器

原生组件标签名称,如viewtextimage等。

类名称,如.class.class1.class2等。

ID,如#id#id1等。

标签名称和类名或ID可以组合,如view.classview#idview.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)} imageview.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

Readme

Keywords

none

Package Sidebar

Install

npm i @lx-frontend/node-miniapp-visual-test

Weekly Downloads

64

Version

0.1.5

License

ISC

Unpacked Size

162 kB

Total Files

47

Last publish

Collaborators

  • ruqimobile
  • haiyulu
  • erica.liuyj
  • pok.h
  • chenzian
  • azumia
  • jeely
  • lichao.franklee
  • mind29
  • yuki.liu
  • lixinleon
  • frontbot
  • owen.huang
  • tiny.tu
  • simba.wang