npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

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

v0.1.5

Published

微信小程序E2E视觉测试工具库

Downloads

142

Readme

@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