Puppeteer结合Jest对网页进行测试

news2024/12/30 1:09:37

之前我们使用Puppeteer进行网页爬虫(以及自动化操作),这篇文章主要验证一下Puppeteer测试的可实现性。

项目设置

让我们从设置一个基本的React应用程序开始。 我们将安装其他依赖项,如Puppeteer和Faker。

为了这篇文章的目的,我创建了一个简单的应用程序,其中包含一个表单,并在表单提交时呈现成功消息。 将此应用程序克隆到您的系统中。

git clone https://github.com/rajatgeekyants/test.git

现在,让我们安装开发依赖项。

yarn install

我们不需要安装Jest,它已经在React程序包中预装好了。 如果你再次尝试安装它,你的测试将不起作用,因为两个Jest版本将相互冲突。

接下来,我们需要在 package.json 中更新 test 脚本来调用 Jest。 我们还将添加另一个名为 debug 的脚本。 此脚本将我们的 Node 环境变量设置为调试模式并调用 npm test

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "jest",
  "debug": "NODE_ENV=debug npm test",
  "eject": "react-scripts eject",  
}

使用 Puppeteer,我们可以在无头模式或 Chromium 浏览器中运行我们的测试。 这是一个很好的功能,因为它允许我们查看测试正在评估的视图、DevTools 和网络请求。 唯一的缺点是它会使 Continuous Integration (CI) 的速度非常慢。

我们可以使用环境变量来决定是否以无头模式运行我们的测试。 我将以这样的方式设置我的测试,即当我想看到它们被评估时,我可以运行 debug 脚本。 当我不想看到时,我会运行 test 脚本。

转到 src/App 并创建一个名为 App.test.js 的新文件。 在其中编写以下代码。

我们首先告诉我们的应用程序我们 require Puppeteer。 然后,我们 describe 我们的第一个测试,其中我们检查初始页面加载。 在这里,我正在测试 h1 标签是否包含正确的文本。

在我们的测试描述中,我们需要定义 browserpage 变量。 这些是遍历测试所必需的。

launch 方法帮助我们通过浏览器配置选项,并使我们能够在不同的浏览器设置中控制和测试我们的应用程序。 我们甚至可以通过设置仿真选项来更改浏览器页面的设置。

让我们首先设置我们的浏览器。 我在文件顶部创建了一个名为 isDebugging 的函数。 我们将在 launch 方法中调用此函数。 此函数将具有名为 debugging_mode 的对象,其中包含三个属性:

  • headless: false —— 是否以无头模式(true)或在 Chromium 浏览器中(false)运行我们的测试
  • slowMo: 250 —— 通过 250 毫秒减慢 Puppeteer 操作的速度。
  • devtools: true —— 浏览器与应用程序交互时是否应打开 DevTools (true)。

然后,isDebugging 函数将返回一个三元表达式,该表达式基于环境变量。 三元表达式决定应用程序是否应返回 debuggin_mode 对象还是空对象。

回到我们的 package.json 文件中,我们创建了一个 debug 脚本,该脚本将我们的 Node 环境变量设置为调试。 与我们的测试不同,isDebugging 函数将返回我们的自定义浏览器选项,这取决于我们的环境变量 debug

接下来,我们正在为页面设置一些选项。 这是在 page.emulate 方法中完成的。 我们正在设置 viewport 属性的 widthheight,并将 userAgent 设置为空字符串。

page.emulate 非常有用,因为它使我们能够在各种浏览器选项下运行测试。 我们还可以使用 page.emulate 复制不同的页面属性。

使用 Puppeteer 测试 HTML 内容

我们现在准备开始为我们的 React 应用编写测试。 在本节中,我将测试 <h1> 标签和导航,并确保它们正常工作。

打开 App.test.js 文件,在 test 块中 page.emulate 声明的正下方编写以下代码:

基本上,我们正在告诉 Puppeteer 转到 url http://localhost:3000/. 。 Puppeteer 将评估 App-title 类。 这个类出现在我们的 h1 标签上。

$.eval 方法实际上是在它传入的任何框架内运行 document.querySelector

Puppeteer 找到匹配此类的选择器,它将传递该选择器给回调函数 e.innerHTML。 在这里,Puppeteer 将能够提取 <h1> 元素,并检查它是否说 Welcome to React

一旦 Puppeteer 完成测试,browser.close 将关闭浏览器。

打开命令终端并运行 debug 脚本。

yarn debug

如果你的应用程序通过了测试,你应该在控制台中看到类似的内容:

在这里插入图片描述

接下来,转到 src/App/index.js,你会看到这样的 nav 元素:

<nav className='navbar'> 
  <ul>
    <li className="nav-li"><a href="#">Batman</a></li>
    <li className="nav-li"><a href="#">Supermman</a></li>  
    <li className="nav-li"><a href="#">Aquaman</a></li>
    <li className="nav-li"><a href="#">Wonder Woman</a></li>
  </ul>

请注意,所有 <li> 元素都具有相同的类。 返回到 App.test.js 并编写导航测试。

在此之前,让我们重构我们之前编写的代码

let browser
let page
beforeAll(async () => {
  browser = await puppeteer.launch(isDebugging())
  page = await browser.newPage()
  await page.goto('http://localhost:3000/')
  page.setViewport({ width: 500, height: 2400 })  
})

之前,我在userAgent上没有任何内容。 所以,我只是使用setViewport而不是beforeAll。 现在,我可以摆脱 localhostbrowser.close,并使用 afterAll。 如果应用程序处于调试模式,那么我希望删除该浏览器。

afterAll(() => {     
  if (isDebugging()) {         
    browser.close()     
  }  
})

我们现在可以继续编写导航测试了。 在 describe 块中,创建一个新的 test,如下所示。

test('nav loads correctly', async () => {  
  const navbar = await page.$eval('.navbar', el => el ? true : false)  
  const listItems = await page.$$('.nav-li')   
  expect(navbar).toBe(true)  
  expect(listItems.length).toBe(4)
});

这里,我首先使用 .navbar 类上的 $eval 函数获取 navbar。 然后我使用三元运算符返回 truefalse 来查看元素是否存在。

接下来,我需要获取列表项。 就像之前一样,我在 nav-li 类上使用 $eval 函数。 我们将 expect navbartrue,listItems 的长度等于 4。

您可能已经注意到我在 listItems 上使用了 $$。 这是从页面内运行 document.querySelectorAll 的快捷方式。 当没有与美元符号一起使用 eval 时,就不会有回调。

运行调试脚本以查看代码是否可以通过这两个测试。

在这里插入图片描述

模拟用户活动

让我们看看如何通过模拟键盘输入、鼠标单击和触屏事件来测试表单提交。 这将使用 Faker 生成的随机用户信息来完成。

src 文件夹中,我创建了一个登录组件 Login。 这只是一个带有四个输入框和一个提交按钮的表单。

这里是用 Bit 共享的组件,以便您可以使用 NPM 安装它或直接从自己的项目中导入和开发它。

Bit - 登录/src/app - 来自 geekrajat 的 React 组件

登录成功后显示成功消息的登录组件 - 用 react 写的。 依赖项:react。登录表单的作用域…

bitsrc.io

当用户单击“登录”按钮时,应用程序需要显示成功消息。 这是我创建的另一个组件。

我已经向类 App 添加了一个 state,以及一个 handleSubmit 方法,该方法将阻止默认函数并将 complete 的值更改为 true

state = { complete: false }  

handleSubmit = e => {
  e.preventDefault()
  this.setState({ complete: true }) 
}

页面底部还有一个三元语句。 这将决定是显示 Login 还是 SuccessMessage

{ this.state.complete ?
  <SuccessMessage/>
  :
  <Login submit={this.handleSubmit} />  
}

运行 yarn start 确保您的应用程序完美运行。

我现在将使用 Puppeteer 编写端到端测试,以确保此功能正常工作。 转到 App.test.js 文件并导入 faker。 然后我将像这样创建一个 user 对象:

const faker = require('faker')  

const user = {
  email: faker.internet.email(),
  password: 'test',  
  firstName: faker.name.firstName(),  
  lastName: faker.name.lastName()  
}

在测试中,Faker 非常有用,因为每次运行测试时,它都会生成不同的数据。

describe 块中编写一个新的 test 来测试登录表单。 测试将点击我们的属性并向其中输入一些内容。 然后测试将 click 提交按钮并等待成功消息。 我还会为这个 test 添加超时。

test('login form works correctly', async () => {

  // 点击firstName输入框
  await page.click('[data-testid="firstName"]') 
  
  // 在lastName输入框中输入firstName  
  await page.type('[data-testid="lastName"]', user.firstName)
  
  // 其他输入框的操作

  // 点击提交按钮
  await page.click('[data.testid="submit"]') 
  
  // 等待成功信息显示  
  await page.waitForSelector('[data-testid="success"]')  

}, 1600)

运行 debug 脚本并观察 Puppeteer 如何进行测试!

在测试中设置 Cookie

我现在希望每次提交表单时,应用程序都会将 cookie 保存到页面上。 此 cookie 将保存用户的名字。

为了简单起见,我将重构我的 App.test.js 文件以仅打开一个页面。 这个页面将模拟 iPhone 6。

我想在表单提交时保存 cookie,我们将在表单的上下文中添加测试。

为登录表单编写一个新的 describe 块,然后将我们的登录表单测试复制并粘贴到其中。

describe('login form', () => {
  // 在其中插入登录表单
})

我还将测试重命名为 fills out form and submits。 现在创建一个名为 sets firstName cookie 的新测试块。 此测试将检查是否设置了 firstNameCookie

test('sets firstName cookie', async () => {
  const cookies = await Page.cookies()
  const firstNameCookie = cookies.find(c => c.name === 'firstName' && c.value === user.firstName)
  expect(firstNameCookie).not.toBeUndefined()  
})

Page.cookies 将返回表示每个文档 cookie 的对象数组。 我使用数组原型方法 find 来查看 cookie 是否存在。 好的,我继续翻译:

这将确保应用程序正在使用 Faker 生成的 firstName

如果您现在在终端中运行 test 脚本,您会看到测试失败,因为它返回给我们一个未定义的值。 让我们现在解决这个问题。

App.js 文件中,向 state 对象添加一个 firstName 属性。 它将是一个空字符串。

state = {
  complete: false,
  firstName: '',
}

handleSubmit 方法中添加:

document.cookie = `firstName=${this.state.firstname}`

创建一个名为 handleInput 的新方法。 每个输入时都会触发此方法以更新状态。

handleInput = e => {
  this.setState({firstName: e.currentTarget.value}) 
}

将此方法作为 prop 传递给 Login 组件。

<Login submit={this.handleSubmit} input={this.handleInput} />

Login.js 文件中,向 firstName 输入添加 onChange={props.input}。 通过这种方式,每当用户在 firstName 输入中键入内容时,React 就会触发此输入方法。

现在我需要应用程序在用户单击“登录”按钮时将 firstName cookie 保存到页面上。 运行 npm test 以查看您的应用程序是否通过了所有测试。

如果应用程序需要在执行任何操作之前存在某个 cookie,并且此 cookie 是在一系列之前授权的页面上设置的怎么办?

App.js 文件中,按如下方式重构 handleSubmit 方法:

handleSubmit = e => {
  e.preventDefault()
  if (document.cookie.includes('JWT')){
    this.setState({ complete: true })
  }
  document.cookie = `firstName=${this.state.firstName}` 
}

使用这段代码,只有当文档包含 JWT 时,SuccessMessage 组件才会加载。

App.test.js 文件中进入 fills out form and submits 测试块并编写以下内容:

await page.setCookie({ name: 'JWT', value: 'kdkdkddf' })

这将设置一个实际设置 JSON Web 令牌'JWT' 的 cookie,里面是一些随机的测试内容。 如果您现在在终端中运行 test 脚本,您的应用程序将运行所有测试并通过!

使用 Puppeteer 截图

截图可以帮助我们查看测试失败时它正在查看的内容。 让我们看看如何使用 Puppeteer 拍摄截图并分析我们的测试。

在我们的 App.test.js 文件中,执行名为 nav loads correctlytest。 添加一个条件语句,检查 listItems 的长度不等于 3。 如果是这种情况,那么 Puppeteer 应该截取页面的屏幕截图,并更新测试以期望 listItems 的长度为 3 而不是 4。

if (listItems.length !== 3)
  await page.screenshot({path: 'screenshot.png'}); 
expect(listItems.length).toBe(3);

我们的测试明显会失败,因为在我们的应用程序中我们有 4 个 listItems。 在终端中运行 test 脚本并观察测试失败。 同时,您会在应用程序的根目录中找到一个名为 screenshot.png 的新文件。

您还可以配置截图方法:

  • fullPage —— 如果为 true,Puppeteer 将截取整个页面的屏幕截图。
  • quality —— 这的范围是从 0 到 100,并设置图像的质量。
  • clip —— 这需要一个对象,该对象指定要截图的页面的剪裁区域。

您还可以通过执行 page.pdf 而不是 page.screenshot 来创建页面的 PDF。 这具有其自己的唯一配置。

  • scale —— 这是一个数字,指网页渲染。 默认值为 1。
  • format —— 这指的是纸张格式。 如果设置了它,它将优先于传递给它的任何宽度或高度选项。 默认值为 letter
  • margin —— 这是指页边距。

在测试中处理页面请求

让我们看看 Puppeteer 如何在测试中处理页面请求。 在 App.js 文件中,我将编写一个异步的 componentDidMount 方法。 此方法将从 Pokemon API 获取数据。 对此获取请求的响应将以 JSON 文件的形式返回。 我也会将这些数据添加到我的状态中。

async componentDidMount() {
  const data = await fetch('https://pokeapi.co/api/v2/pokedex/1/').then(res => res.json())  
  this.setState({pokemon: data})
}

请确保向状态对象添加 pokemon: {}。 在应用组件中,添加此 <h3> 标签。

<h3 data-testid="pokemon">  
  {this.state.pokemon.next ? 'Received Pokemon data!' : 'Something went wrong'} 
</h3>

如果运行您的应用程序,您将看到应用程序已成功获取数据。

使用 Puppeteer,我可以编写任务来检查 <h3/> 标签的内容在成功请求的情况下,以及拦截请求并强制失败的情况。 通过这种方式,我可以查看应用程序在成功和失败的情况下的运行方式。

我首先要让 Puppeteer 发送一个请求来拦截获取请求。 然后,如果我的 url 包含“pokeapi”这个词,那么 Puppeteer 应该中止拦截的请求。 否则,一切照旧。

打开 App.test.js 文件并在 beforeAll 方法中编写以下代码。

await page.setRequestInterception(true);
page.on('request', interceptedRequest => {  
  if (interceptedRequest.url.includes('pokeapi')) {
    interceptedRequest.abort();
  } else {
    interceptedRequest.continue();
  }
});

setRequestInterception 是一个标志,它允许我访问页面发出的每个请求。 一旦请求被拦截,就可以使用特定的错误代码中止请求。 我可以强制失败,也可以只是拦截请求并在检查某些条件逻辑后继续。

让我们编写一个名为 fails to fetch pokemon 的新 test。 此测试将评估 h3 标签。 然后我将获取内部 HTML 并确保此文本中的内容为 Received Pokemon data!

test('fails to fetch pokemon', async () => {

  // 检查 h3 标签的内容

  expect(h3Content).toContain('Received Pokemon data!');

});

运行 debug 脚本,这样您就可以实际看到 <h3/>。 您会注意到整个时间 Something went wrong 文本保持不变。 我们的所有测试都通过了,这意味着我们成功中止了 Pokemon 请求。

请注意,在拦截请求时,我们可以控制发送的标头是什么,返回什么错误代码以及返回自定义主体响应。

最后

这个就是简单的测试用例,实际中我们可以不需要jtest,我们只需要 “===” 也是可以校验。嘿嘿

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1408764.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ASP.NET Core WebAPI从HTTPS调整为HTTP启动

使用VS2022创建WebAPI项目时&#xff0c;默认勾选“配置HTTPS(H)”&#xff0c;这样启动WebAPI时以https方式启动。   如果要从HTTPS调整为HTTP启动&#xff0c;需要修改项目中以下几处&#xff0c;首先是Program.cs中删除app.UseHttpsRedirection()语句&#xff0c;删除后…

gitlab runner 安装、注册、配置、使用

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

GitLab升级版本(任意用户密码重置漏洞CVE-2023-7028)

目录 前言漏洞分析影响范围查看自己的GitLab版本升级路程 升级过程13.1.1113.8.8 - 14.0.1214.3.614.9.5 - 16.1.6 前言 最近GitLab发了个紧急漏洞需要修复&#xff0c;ok接到命令立刻着手开始修复&#xff0c;在修复之前先大概了解一下这个漏洞是什么东西 漏洞分析 1、组件…

免费的 UI 设计资源网站 Top 8

今日与大家分享8个优秀的免费 UI 设计资源网站。这些网站的资源包括免费设计材料站、设计工具、字体和其他网站&#xff0c;尤其是一些材料站。它们是免费下载的&#xff0c;材料的风格目前很流行&#xff0c;适合不同的项目。非常适合平面设计WEB/UI设计师收藏&#xff0c;接下…

.git 文件夹结构解析

.git 文件夹结构解析 在这篇文章就让我们来看看这个 Git 仓库里的文件分别都是用来干什么的&#xff0c;以及在执行了相关的 Git 命令后这些文件会如何响应。 hooks&#xff08;钩&#xff09;&#xff1a;存放一些shell脚本info&#xff1a;存放仓库的一些信息logs&#xff…

1分钟部署幻兽帕鲁联机服务,PalWorld服务器搭建教程(阿里云)

1分钟部署幻兽帕鲁联机服务&#xff0c;PalWorld服务器搭建教程 最近这游戏挺火&#xff0c;很多人想跟朋友联机&#xff0c;如果有专用服务器&#xff0c;就不需要房主一直开着电脑&#xff0c;稳定性也好得多。 概述 幻兽帕鲁是Pocketpair开发的一款开放世界生存制作游戏&…

Linux系统Shell脚本编程之条件语句

一、条件测试 Shell 环境根据命令执行后的返回状态值 " $? " 来判断是否执行成功&#xff0c;当返回值为0时表示成功&#xff0c;否则表示失败或异常&#xff08;非0值&#xff09;。使用专门的测试工具 test 命令&#xff0c;可以对特定条件进行测试&#xff0c;并…

金融OCR领域实习日志(一)

一、OCR基础 任务要求&#xff1a; 工作原理 OCR&#xff08;Optical Character Recognition&#xff0c;光学字符识别&#xff09;是指电子设备&#xff08;例如扫描仪或数码相&#xff09;检查纸上打印的字符&#xff0c;经过检测暗、亮的模式肯定其形状&#xff0c;而后用…

先进车辆驾驶舱系统的强大网络安全协议

近年来&#xff0c;车辆驾驶舱系统发展迅速&#xff0c;融入了导航、娱乐和车辆性能监控系统等先进技术。随着驾驶舱变得更加互联和依赖软件&#xff0c;它们也变得更容易受到网络安全威胁。实施强大的网络安全协议对于保护驾驶员和乘客以及保持车辆运行的完整性至关重要。 本…

支付宝推出新年“五福节”活动,新增四大AI玩法;大型语言模型综合指南

&#x1f989; AI新闻 &#x1f680; 支付宝推出新年“五福节”活动&#xff0c;新增四大AI玩法 摘要&#xff1a;支付宝宣布今年的“集五福”活动升级为“五福节”&#xff0c;新增了四大AI玩法&#xff1a;飙戏小剧场、时空照相馆、会说话红包和大家来找福。用户可以通过拼…

uniapp canvas做的刮刮乐解决蒙层能自定义图片

最近给湖南中烟做元春活动&#xff0c;一个月要开发4个小活动&#xff0c;这个是其中一个难度一般&#xff0c;最难的是一个类似鲤鱼跃龙门的小游戏&#xff0c;哎&#xff0c;真实为难我这个“拍黄片”的。下面是主要代码。 <canvas :style"{width:widthpx,height:hei…

c语言-枚举和联合体

文章目录 前言一、枚举类型1.1 枚举类型的定义1.2 枚举类型的使用1.3 枚举的优点 二、联合体类型2.1 联合体类型的定义2.2 联合体类型的特点2.3 联合体大小的计算 总结 前言 本篇文章介绍c语言中的枚举类型和联合体类型 一、枚举类型 1.1 枚举类型的定义 枚举类型和结构体类…

【C++干货铺】C++中的四种类型转换

个人主页点击直达&#xff1a;小白不是程序员 C系列专栏&#xff1a;C干货铺 代码仓库&#xff1a;Gitee 目录 C语言中的类型转换 为什么C需要四种类型转化 C强制类型转换 static_cast reinterpret_cast const_cast dynamic_cast RTTI C语言中的类型转换 在C语言中&…

2023 IoTDB Summit:中核武汉核电运行技术股份有限公司主管工程师方华建《IoTDB在核电数字化转型过程的应用实践》...

12 月 3 日&#xff0c;2023 IoTDB 用户大会在北京成功举行&#xff0c;收获强烈反响。本次峰会汇集了超 20 位大咖嘉宾带来工业互联网行业、技术、应用方向的精彩议题&#xff0c;多位学术泰斗、企业代表、开发者&#xff0c;深度分享了工业物联网时序数据库 IoTDB 的技术创新…

ERP读书笔记20240125-1

ERP&#xff08;Enterprise Resource Planning&#xff0c;企业资源计划&#xff09;的发展经历了从 MRP&#xff08;Material Requirements Planning&#xff0c;物料需求计划&#xff09;到 MRP2&#xff08;Manufacturing Resources Planning&#xff0c;制造资源计划&#…

OpenCV-26 拉普拉斯算子

一、拉普拉斯算子概念及其推导 索贝尔算子是模拟一阶导数&#xff0c;一阶导数越大的地方说明变化越剧烈&#xff0c;越有可能是边缘。 但是如果我们对f&#xff08;t&#xff09;求二阶导数呢&#xff1f; 可以发现边缘处的二阶导数 0&#xff0c;因此&#xff0c;我们可以利…

跟着cherno手搓游戏引擎【10】使用glm窗口特性

修改ImGui层架构&#xff1a; 创建&#xff1a; ImGuiBuild.cpp&#xff1a;引入ImGui #include"ytpch.h" #define IMGUI_IMPL_OPENGL_LOADER_GLAD//opengl的头文件需要的定义&#xff0c;说明使用的是gald #include "backends/imgui_impl_opengl3.cpp" …

C++力扣题目56--合并区间 738--单调递增的数字 968--监控二叉树

56. 合并区间 力扣题目链接(opens new window) 给出一个区间的集合&#xff0c;请合并所有重叠的区间。 示例 1: 输入: intervals [[1,3],[2,6],[8,10],[15,18]]输出: [[1,6],[8,10],[15,18]]解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. 示例 2: 输入: interv…

Netty的解码器和编码器

链路图 一个完整的RPC请求中&#xff0c;netty对请求数据和响应数据的处理流程如下图所示 网络线路中传输的都是二进制数据&#xff0c;之后netty将二进制数据解码乘POJO对象&#xff0c;让客户端或者服务端程序处理。 解码的工具称为解码器&#xff0c;是一个入站处理器InBo…

分布式应用程序设计项目管理

1. 项目的定义 项目是一种特定的、新颖的行动&#xff0c;目的是以有条不紊、逐步的方式构建一个尚未存在确切对应物的未来现实。它是对精心制定的需求的回应&#xff0c;旨在满足业主的需要。项目包括一个可能是物理或智力的目标&#xff0c;并且需要使用给定的资源来执行一系…