Puppeteer结合Jest对网页进行测试

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

项目设置

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

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

bash 复制代码
git clone https://github.com/rajatgeekyants/test.git

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

复制代码
yarn install

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

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

json 复制代码
"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 标签是否包含正确的文本。

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

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 属性的 width 和 height,并将 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 脚本。

lua 复制代码
yarn debug

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

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

xml 复制代码
<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 并编写导航测试。

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

csharp 复制代码
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。 现在,我可以摆脱 localhost 和 browser.close,并使用 afterAll。 如果应用程序处于调试模式,那么我希望删除该浏览器。

scss 复制代码
afterAll(() => {     
  if (isDebugging()) {         
    browser.close()     
  }  
})

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

dart 复制代码
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。 然后我使用三元运算符返回 true 或 false 来查看元素是否存在。

接下来,我需要获取列表项。 就像之前一样,我在 nav-li 类上使用 $eval 函数。 我们将 expect navbar 为 true,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。

ini 复制代码
state = { complete: false }  

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

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

javascript 复制代码
{ this.state.complete ?
  <SuccessMessage/>
  :
  <Login submit={this.handleSubmit} />  
}

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

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

scss 复制代码
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 添加超时。

dart 复制代码
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 块,然后将我们的登录表单测试复制并粘贴到其中。

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

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

dart 复制代码
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 属性。 它将是一个空字符串。

yaml 复制代码
state = {
  complete: false,
  firstName: '',
}

在 handleSubmit 方法中添加:

javascript 复制代码
document.cookie = `firstName=${this.state.firstname}`

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

ini 复制代码
handleInput = e => {
  this.setState({firstName: e.currentTarget.value}) 
}

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

kotlin 复制代码
<Login submit={this.handleSubmit} input={this.handleInput} />

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

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

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

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

javascript 复制代码
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 测试块并编写以下内容:

php 复制代码
await page.setCookie({ name: 'JWT', value: 'kdkdkddf' })

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

使用 Puppeteer 截图

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

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

scss 复制代码
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 文件的形式返回。 我也会将这些数据添加到我的状态中。

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

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

kotlin 复制代码
<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 方法中编写以下代码。

dart 复制代码
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!。

scss 复制代码
test('fails to fetch pokemon', async () => {

  // 检查 h3 标签的内容

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

});

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

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

最后

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

相关推荐
子兮曰4 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万4 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝4 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋4 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁4 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95274 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大4 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师4 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学4 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端