简介

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

Nightwatch 网页测试基础
学习如何使用这 3 种技术测试网页上大多数场景 → 查找元素、交互和断言其属性。

在之前的章节中,我们学习了如何使用这三种强大的技术来测试网页上的基本场景。

  • 查找元素 →  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) 启动测试之前或之后执行某些操作,可以使用 beforeafter 钩子。你也可以在启动测试运行器之前或退出测试运行器之前执行操作,使用 全局钩子.

💡
.window.maximize() 将浏览器窗口最大化到屏幕大小。

📎 键盘快捷键和剪贴板

我们在 之前的文章 中学习了如何使用 .sendKeys() 模拟按键,所以快捷键,包括复制和粘贴,应该相当简单。按下 CONTROLCOMMANDSHIFT 将按住该键,直到按下 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 中使用 executeScriptexecuteAsyncScript 函数来完成。

💡
.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()
})
💡
.setAttribute(<element>, <attribute>, <new-value>) 允许我们为 DOM 属性设置新值。(阅读更多

.ensure.assert 类似,提供了额外的灵活性。(阅读更多

.alerts.[accept/dismiss/getText/setText]() 可用于与浏览器警报框进行交互。(阅读更多

我们执行客户端脚本以滚动到页面底部,因为我们想要交互的 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 中使用它,那么 asyncawait 就变得必不可少。在测试过程中,这种情况非常少见,但如果你确实遇到了这种情况,你可以使用 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 并允许网站访问你的位置。

不用担心。你可以在测试完成后始终重置位置权限。

此命令仅适用于基于 Chromium 的浏览器,例如 Google Chrome 和 Microsoft Edge。

接下来

使用页面对象模型编写可扩展的测试

今天你学习了很多关于如何使用 Nightwatch 对网站和 Web 应用程序进行深入测试,以模拟复杂的用例和场景。在本系列关于网页测试的下一章中,我们将探讨测试编写模式,并向你介绍页面对象模型 (POM)。我们将介绍不同的模式来改善测试代码的结构和可维护性,你将学习如何实现 POM - 这种设计模式在测试脚本中促进了可重用性和模块化。

加入我们的社区 💬

如果您有任何问题,请随时访问我们的 Discord 服务器 并打个招呼。我们的社区始终乐于提供支持、分享见解并帮助您解答任何与测试相关的疑问。我们欢迎您积极参与,并期待在 Discord 社区中与您联系。您也可以通过 Twitter 联系我们。

祝您测试愉快!🎉