簡介

先前在我們的網頁測試系列中

撰寫 Nightwatch 網頁測試的基本知識
學習如何使用這 3 種技術測試網頁上的大多數情境:尋找元素、互動以及斷言其屬性。

在我們的前一章中,我們學習了如何使用這三種強大的技術來測試網頁上的基本情境。

  • 尋找元素 →  browser.element.find()
  • 與元素互動 → .click().sendKeys()
  • 斷言元素 → .getText().assert.contains()

本篇文章概述

今天我們將學習網頁測試中的一些進階技巧和使用案例。這也將幫助您了解 Nightwatch 的廣泛功能。我們將向您介紹以下情境和概念。

概念

  • 測試鉤子
  • 鍵盤快捷鍵與剪貼簿
  • 作業系統與瀏覽器資訊
  • 執行用戶端 JS
  • Actions API
  • iFrame
  • Async/Await
  • 多標籤互動
  • 模擬地理位置

對於以下測試,您可以在撰寫完每個測試後單獨執行它們,或在最後將它們全部一起執行。您也可以將這篇文章作為參考,以便在遇到類似情境時回來查閱。

複雜情境與概念

🪝 測試鉤子

測試鉤子是特殊函數,可讓開發人員在測試執行過程的不同階段執行特定操作。在我們的 home.spec.js 測試中,我們總是希望前往我們 Nightwatch 網站的首頁。我們可以將 browser.navigateTo('/') 添加到 beforeEach 鉤子下,而不是在每個測試中都寫入 browser.navigateTo('/'),這將在每個測試執行之前執行。我們也可以在每個測試後使用 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) 將其貼到輸入元素上並檢查輸入的值屬性來檢查剪貼簿的內容。讓我們測試以下情境:

點擊首頁上的複製按鈕並驗證文字是否已複製。

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')
})

✍ ️Actions API

Actions 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 是延遲載入的。也可以使用 Actions API 執行此捲動到底部的動作,如下所示。

browser
  .perform(function() {
    return this.actions().move({
      origin: browser.element.find(iframe_selector),
    })
  })

🚦 (不) 使用 Async/Await

Javascript 是一種圍繞使用者介面的非同步特性建構的語言。這對開發人員來說很痛苦,為了控制程式碼的執行順序,我們開始使用回呼。但是很快,我們就陷入了回呼地獄,我們很討厭它。然後,引入了Promises的美麗之處,並且在 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 並將測試執行器切換到該視窗

🌍 模擬地理位置

當您的網站或網頁應用程式根據其存取位置而變更時,測試您網站的所有這些位置就變得非常重要。隨著 Chrome DevTools Protocol 的引入,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 陣列中的每個位置,並將瀏覽器設定為該地理位置。然後透過檢查 text-muted 類別是否消失來使用 .waitUntil(fn) 等待地址載入。然後我們驗證地址是否具有正確的文字。

💡
.setGeolocation({ latitude, longitude, accuracy }) 設定瀏覽器的地理位置為指定的 latitude(緯度)、longitude(經度)和 accuracy(精確度),同時模擬地理位置。

為了讓測試能正常運作,您應該先造訪 www.where-am-i.co 並允許該網站存取您的位置。

別擔心,您可以在測試完成後隨時重置位置的權限。

此指令僅適用於基於 Chromium 的瀏覽器,例如 Google Chrome 和 Microsoft Edge。

接下來

使用 Page Object Model 編寫可擴展的測試

今天您學習了很多關於如何使用 Nightwatch 對網站和 Web 應用程式進行深入測試,以模擬複雜的使用案例和情境。在我們關於 Web 測試系列的下一章中,我們將探索測試編寫模式並向您介紹 Page Object Model (POM)。我們將瀏覽不同的模式,這些模式可以改善測試程式碼的結構和可維護性,並且您將學習如何實作 POM – 一種在測試腳本中推廣可重用性和模組化的設計模式。

加入我們的社群 💬

如果您有任何問題,請隨時造訪我們的 Discord 伺服器 並打聲招呼。我們的社群隨時提供支援、分享見解並協助您處理任何與測試相關的問題。我們歡迎您的積極參與,並期待在我們的 Discord 社群中與您建立聯繫。您也可以透過 Twitter 與我們聯繫。

祝您測試愉快!🎉