简介
我们网页测试系列的之前内容

在之前的章节中,我们学习了如何使用这三种强大的技术来测试网页上的基本场景。
- 查找元素 →
browser.element.find()
- 与元素交互 →
.click()
和.sendKeys()
- 断言元素 →
.getText().assert.contains()
本文概述
今天我们将学习一些网页测试中的高级技巧和用例。 这也有助于你了解 Nightwatch 的强大功能。我们将向你介绍以下场景和概念。
概念
- 测试钩子
- 键盘快捷键和剪贴板
- 操作系统和浏览器信息
- 执行客户端 JS
- 操作 API
- iFrame
- Async/Await
- 多标签页交互
- 模拟地理位置
对于以下测试,你可以在编写完每个测试后单独运行它们,或者在最后一起运行它们。你也可以将本文作为参考,在你遇到类似场景时回来查看。
复杂场景和概念
🪝 测试钩子
测试钩子是特殊函数,允许开发者在测试执行过程的不同阶段执行特定操作。在我们 home.spec.js
测试中,我们总是想访问 nightwatch 网站的首页。而不是在每个测试中都编写 browser.navigateTo('/')
,我们可以将其添加到 beforeEach
钩子中,该钩子将在每个测试运行之前执行。我们也可以在每个测试结束后使用 browser.end()
关闭浏览器。
describe('Nighwatch homepage', function() {
beforeEach(browser => browser.window.maximize().navigateTo('/'))
afterEach(browser => browser.end())
...
})
如果你想在从文件 (home.spec.js
) 启动测试之前或之后执行某些操作,可以使用 before
和 after
钩子。你也可以在启动测试运行器之前或退出测试运行器之前执行操作,使用 全局钩子.
.window.maximize()
将浏览器窗口最大化到屏幕大小。📎 键盘快捷键和剪贴板
我们在 之前的文章 中学习了如何使用 .sendKeys()
模拟按键,所以快捷键,包括复制和粘贴,应该相当简单。按下 CONTROL
或 COMMAND
或 SHIFT
将按住该键,直到按下 Keys.NULL
,这将释放所有按住的键。但是剪贴板对我们来说不可直接访问,无法测试。我们通过使用键盘快捷键 (⌘ + v / Ctrl + v) 将剪贴板内容粘贴到输入元素中,并检查输入元素的 value 属性来检查剪贴板的内容。让我们测试以下场景,
点击首页上的复制按钮,验证文本是否已复制。
it('Should copy the installation command on copy button click', function (browser) {
browser.element.findByText('Copy').click()
browser.element.find('#docsearch').click()
const $inputEl = browser.element.find('.DocSearch-Modal .DocSearch-Form input')
$inputEl.sendKeys([browser.Keys.COMMAND, 'v'])
$inputEl.getAttribute('value').assert.contains('npm init nightwatch')
})
如果我们在 Windows 或 Linux 上运行测试,我们需要将 browser.Keys.COMMAND
替换为 browser.Keys.CONTROL
。这将我们带到下一个主题。
🌐 操作系统和浏览器信息
我们可以从 browser.capabilities
对象中找到有关浏览器和测试运行平台的详细信息。以下三个是使用最频繁的 - .platformName
、.browserName
和 .browserVersion
。我们可以将其用于前面的示例,并将我们的测试重写为在多个平台上运行。
const is_mac = browser.capabilities.platformName.toLowerCase().includes('mac')
...
$inputEl.sendKeys([is_mac ? browser.Keys.COMMAND : browser.Keys.CONTROL, 'v'])
🤝 执行客户端 JS
接下来,我们将尝试在浏览器客户端执行一些 JS。这可以在 Nightwatch 中使用 executeScript
或 executeAsyncScript
函数来完成。
.executeScript(<script>, [...<data>], <optional-return-function>)
<script>
参数可以是字符串 "window.location.reload()"
或函数 function (...<args>) {...}
脚本函数的参数
...<args>
将是发送的 [...<data>]
。<optional-return-function>
将以客户端脚本的返回值作为参数。执行客户端脚本以增加徽标的大小,并将“开始”按钮的文本更改为“{客户端执行}”。尝试找到带有新文本的按钮并单击它。
it('Should should change with client script', async function (browser) {
const change_text = "{Client Side Execution}"
browser.executeScript(function (new_text) {
const $hero_cta = document.querySelector('.hero__action-button--get-started')
$hero_cta.innerHTML = new_text
$hero_cta.style.background = '#ff7f2b'
document.querySelector('header .navigation-list').style.display = 'none'
document.querySelector('header .navigation__logo').style.width = '900px'
}, [change_text])
browser.pause(1000) // Pausing just to notice the changes
browser.element.findByText(change_text).click()
browser.assert.titleMatches('Getting Started')
})
✍ ️操作 API
操作 API 提供对指定输入设备可以执行的操作的精细控制。Nightwatch 为 3 种输入源提供接口
- 键盘设备的按键输入
- 鼠标、笔或触摸设备的指针输入
- 滚动轮设备的滚轮输入
这可以在现有的 .perform()
命令中完成。以下是可用操作 - .clear()
、.click([element])
、.contextClick([element])
、.doubleClick([element])
、.dragAndDrop(from, to)
、.insert(device, ...actions)
、.keyDown(key)
、.keyUp(key)
、.keyboard()
、.mouse()
、.move([options])
、.pause(duration, ...devices)
、.press([button])
、.release([button])
、.sendKeys(...keys)
和 .synchronize(...devices)
。
示例
browser
.perform(function () {
const actions = this.actions({ async: true })
return actions
.keyDown(Keys.SHIFT)
.move({ origin: el })
.press()
.release()
.keyUp(Keys.SHIFT)
})
🖼 iFrame
网页中最棘手的方面之一是 iFrame。这些嵌入的 iFrame 呈现一个完全不同的网页,并且拥有自己的浏览上下文和文档,允许在你的网页内部添加一个新的非继承网页。Nightwatch 提供了一种使用 .frame()
方法切换到这些文档的方法。
.frame(<identifier>)
以及
<identifier>
可以是以下任何一种- id: 我们要定位的 iframe 的 id 属性
- number: iframe 在文档中的位置,从
0
开始-
null
: 用于切换回原始浏览器窗口输入电子邮件并在 iframe 内点击订阅。
it('Should allow for substack subscription', function (browser) {
const iframe_selector = '.footer__wrapper-inner-social-subscribe iframe'
browser
.executeScript(function (iframe_selector) {
document.querySelector(iframe_selector).scrollIntoView()
}, [iframe_selector])
browser.element.find(iframe_selector).setAttribute('id', 'test-nightwatch-123')
browser.frame('test-nightwatch-123')
browser.element.find('input[type=email]').sendKeys('test@nightwatchjs.org')
browser.element.find('button[type=submit]').click()
browser.ensure.alertIsPresent()
browser.alerts.accept()
browser.element.findByText('Sign out').assert.present()
})
我们执行客户端脚本以滚动到页面底部,因为我们想要交互的 iframe 是延迟加载的。滚动到页面底部也可以使用 操作 API 完成,如下所示。
browser
.perform(function() {
return this.actions().move({
origin: browser.element.find(iframe_selector),
})
})
🚦 (不) 使用 Async/Await
Javascript 是一种围绕用户界面异步性质构建的语言。这对开发者来说很痛苦,为了控制代码的执行顺序,我们开始使用回调函数。但很快,我们就陷入了 回调地狱,我们对此很讨厌。然后引入了 Promise 的美妙之处,很快在 ES7 中,我们得到了一种名为 async/await 的语法糖。
在使用 Javascript 编写测试时,人们经常会遇到同样的问题,“为什么这段代码没有执行?”或“这行代码不应该在这个时候执行”。我们 Nightwatch 非常重视解决 Javascript 编程中的这个问题。我们在幕后实现了一个异步的 命令队列,因此作为测试人员,你不必担心这个问题。
Nightwatch 具有内部命令队列,因此你不必担心 Javascript 的异步问题
但在极少数情况下,你从 Nightwatch API 中获取值并在普通 Javascript 中使用它,那么 async
和 await
就变得必不可少。在测试过程中,这种情况非常少见,但如果你确实遇到了这种情况,你可以使用 await
获取值,因为每个 Nightwatch API 都返回一个 Promise。
示例
it('Use values from the webpage', async function(browser) {
const href = await browser.element.find('.navigation-list li a').getAttribute('href').value
// You can do anything with the href value here after
console.log(href)
MyAPI.track(href)
})
🗂 多标签页交互
我们在浏览过程中经常会遇到的一种场景是,点击一个在新标签页中打开的链接。Nightwatch 允许你通过提供一个 API 来在不同的打开文档之间切换来测试这种情况。
点击 GitHub 图标,检查新打开的标签页的 URL。
it('Should lead to the GitHub repo on clicking the Github icon', async function (browser) {
browser.element.find('ul.navigation-list.social li:nth-child(2) a').click()
// wait until window handle for the new window is available
browser.waitUntil(async function () {
const windowHandles = await browser.window.getAllHandles()
return windowHandles.length === 2
})
const allWindows = await browser.window.getAllHandles()
browser.window.switchTo(allWindows[1])
browser.assert.urlContains('github.com/nightwatchjs')
})
每个标签页都被浏览器视为一个窗口。毕竟,我们在每个标签页中都有不同的 window
对象。这里,我们等待新标签页可用,切换到新标签页,并检查浏览器 URL 是否与我们的 GitHub 仓库 URL 相匹配。
.waitUntil(<condition>)
等待一个条件评估为“真值”,如果未评估为“真值”,则会失败。<condition>
可以是任何返回值的函数,或者是一个要等待的 Promise。.window.getAllHandles()
返回所有窗口的 id 数组.window.switchTo(<id>)
接受一个窗口 id,并将测试运行器切换到该窗口🌍 模拟地理位置
当你的网站或 Web 应用程序根据访问它的位置而发生变化时,测试你网站在所有这些位置的运行情况就变得很重要。随着 Chrome DevTools 协议 的引入,Nightwatch 支持在测试运行期间仅使用一个命令来模拟浏览器的地理位置。
检查地球上 3 个不同位置的地址。
it('sets and verifies the geolocation to Japan, USA and Denmark', function (browser) {
const location_tests = [
{
location: { latitude: 35.689487, longitude: 139.691706, accuracy: 100 },
// Tokyo Metropolitan Government Office, 都庁通り, Nishi - Shinjuku 2 - chome, Shinjuku, 163 - 8001, Japan
test_text: 'Japan',
},
{
location: { latitude: 40.730610, longitude: -73.935242, accuracy: 100 },
// 38-20 Review Avenue, New York, NY 11101, United States of America
test_text: 'New York',
},
{
location: { latitude: 55.676098, longitude: 12.568337, accuracy: 100 },
// unnamed road, 1550 København V, Denmark
test_text: 'Denmark',
}
]
const waitTillLoad = async function () {
const geo_dom_class = await browser.element.find('#geolocation_address')
.getAttribute('class').value
return !geo_dom_class.includes('text-muted')
}
location_tests.forEach(obj => {
browser.setGeolocation(obj.location).navigateTo('https://www.where-am-i.co/')
browser.waitUntil(waitTillLoad)
browser.element.find('#geolocation_address').getText().assert.contains(obj.test_text)
})
})
解释
我们遍历 location_tests
数组中的每个位置,并将浏览器设置为该地理位置。然后使用 .waitUntil(fn)
等待地址加载完成,方法是检查 text-muted
类是否已消失。然后我们验证地址是否具有正确的文本。
.setGeolocation(
{ latitude, longitude, accuracy }
)
将浏览器的地理位置设置为给定的 纬度、经度 和 精度,同时模拟地理位置。要使测试正常工作,你应该首先访问 www.where-am-i.co 并允许网站访问你的位置。

不用担心。你可以在测试完成后始终重置位置权限。
接下来
使用页面对象模型编写可扩展的测试
今天你学习了很多关于如何使用 Nightwatch 对网站和 Web 应用程序进行深入测试,以模拟复杂的用例和场景。在本系列关于网页测试的下一章中,我们将探讨测试编写模式,并向你介绍页面对象模型 (POM)。我们将介绍不同的模式来改善测试代码的结构和可维护性,你将学习如何实现 POM - 这种设计模式在测试脚本中促进了可重用性和模块化。
加入我们的社区 💬
如果您有任何问题,请随时访问我们的 Discord 服务器 并打个招呼。我们的社区始终乐于提供支持、分享见解并帮助您解答任何与测试相关的疑问。我们欢迎您积极参与,并期待在 Discord 社区中与您联系。您也可以通过 Twitter 联系我们。
祝您测试愉快!🎉