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

在我們的前一章中,我們學習了如何使用這三種強大的技術來測試網頁上的基本情境。
- 尋找元素 →
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
) 啟動測試之前或之後執行某些操作,您可以使用 before
和 after
鉤子來執行。您也可以在使用 全域鉤子 開始測試之前或即將結束測試執行器之前執行操作。
.window.maximize()
將我們的瀏覽器視窗最大化為螢幕大小。📎 鍵盤快捷鍵與剪貼簿
我們在之前的文章中學習了如何使用 .sendKeys()
模擬按鍵,因此包括複製和貼上的快捷鍵應該相當簡單。按下 CONTROL
或 COMMAND
或 SHIFT
將會保持該鍵按下狀態,直到按下 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 中的 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')
})
✍ ️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()
})
我們執行用戶端腳本以捲動到頁面底部,因為我們要互動的 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 中使用它時,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 並將測試執行器切換到該視窗🌍 模擬地理位置
當您的網站或網頁應用程式根據其存取位置而變更時,測試您網站的所有這些位置就變得非常重要。隨著 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 並允許該網站存取您的位置。

別擔心,您可以在測試完成後隨時重置位置的權限。
接下來
使用 Page Object Model 編寫可擴展的測試
今天您學習了很多關於如何使用 Nightwatch 對網站和 Web 應用程式進行深入測試,以模擬複雜的使用案例和情境。在我們關於 Web 測試系列的下一章中,我們將探索測試編寫模式並向您介紹 Page Object Model (POM)。我們將瀏覽不同的模式,這些模式可以改善測試程式碼的結構和可維護性,並且您將學習如何實作 POM – 一種在測試腳本中推廣可重用性和模組化的設計模式。
加入我們的社群 💬
如果您有任何問題,請隨時造訪我們的 Discord 伺服器 並打聲招呼。我們的社群隨時提供支援、分享見解並協助您處理任何與測試相關的問題。我們歡迎您的積極參與,並期待在我們的 Discord 社群中與您建立聯繫。您也可以透過 Twitter 與我們聯繫。
祝您測試愉快!🎉