Playwright 基本操作
tags: JavaScript Playwright Testing End-to-End
category: Front-End
description: Playwright 基本操作
created_at: 2024/05/04 18:01:00

前言
這一篇會從建立專案開始,到執行測試,再到一些基本的操作,這邊會以 JavaScript 來做說明, Playwright 也有提供 TypeScript 的版本,所以如果你比較習慣用 TypeScript 也可以。
這一篇主要是基本操作的介紹,如果要比較實際的案例會在之後的篇章來介紹。
從建立測試專案開始
首先我們需要建立一個專案,這裡我們先用指令來建立一個 Playwright 的專案。
以往我自己會習慣都先以 npm 來做處理,等東西生出來再用自己習慣的套件管理器(如 pnpm) 來安裝套件,不過因為這邊 Playwright 初始化之後就會自動安裝 Playwright 的相關套件,所以這邊可以直接用你習慣的套件管理器來初始化專案。 當然你也可以 npm 弄出來然後把該砍的砍一砍再重裝XD
-
npmnpm init playwright@latest -
yarnyarn create playwright -
pnpmpnpm create playwright
後面基本上都選擇預設值就好,除了語言的部分為了簡化,我們選擇 JavaScript。
等他跑完之後,畢竟也是一個 JavaScript 專案,所以也可以使用我之前寫的 lai-cmd 去幫你建立開發環境,讓你寫起來比較舒服,至少可以讓他幫你安裝 eslint 和 prettier。
基本指令
安裝完成後,他會在 console 跟你說有一些指令可以用,但這邊我們就只列出比較常用的指令。
npx playwright test
Runs the end-to-end tests.
(執行測試)
npx playwright test --ui
Starts the interactive UI mode.
(開啟互動式 UI)
npx playwright test --debug
Runs the tests in debug mode.
(開啟 debug 模式)
npx playwright codegen
Auto generate tests with Codegen.
(開啟 codegen 工具)
其中我自己最最最常用的是第一個,理由如下
UI- 使用
UI模式的話等於他會多吃一些資源,有時候如果電腦性能比較差,影響比較大的時候會導致你的測試超時(timeout)
- 使用
Debugdebug模式我自己目前是還沒用到,還是比較習慣在測試當中塞入console.log來除錯,因為這樣可以直接對他的過程一目瞭然,不過如果你有需要的話,這個指令也是可以用的。
codegen這個指令我個人是覺得有點鳥,因為他會自動幫你生成測試腳本,但是我自己覺得他生成的腳本有點多餘,而且有時候還會有一些問題,所以我自己是不太會用這個指令。這段不是我寫的,這是copilot寫的;他其實只是幫你開一個codegen工具,然後讓你在上面錄製你的操作來生成腳本,但能夠生成的測試就有限。- 我自己還是習慣自己寫測試腳本,因為這樣可以更加了解測試的過程,而且也可以更加精準的控制測試的過程。
生成的專案結構
生成的專案結構如下(主要)
.
├── package.json
├── playwright.config.js
└── tests
└── example.spec.js
└── tests-examples
└── playwright.spec.js
tests 當中會放你的測試腳本,而 tests-examples 當中會放比較完整的範例測試腳本,這樣你可以參考一下(不過他的量有點多,所以這邊先無視他,可以把它刪掉,有興趣的話在自己看)。
另外我們可以先在 package.json 當中加入 scripts 來方便我們執行測試,這邊我們就加入一個 test 的指令。
{
"scripts": {
"test": "playwright test"
}
}
範例測試腳本
接著就可以執行測試了,這邊我們就先來看一下 example.spec.js 的內容。
const { test, expect } = require('@playwright/test')
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/')
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/)
})
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/')
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click()
// Expects page to have a heading with the name of Installation.
await expect(
page.getByRole('heading', { name: 'Installation' })
).toBeVisible()
})
可以看到他的範例非常簡單,這兩個測試分別做了兩件事情
- 檢查網頁的標題是否包含
Playwright - 點擊
Get started連結,並檢查是否有Installation的標題
這邊對於剛開始接觸測試的人(不論是不是 Playwright)或者是其實對於 HTML 不夠熟悉的人來說,可能看到 getByRole 就不知所措了,到底那個 role 應該填什麼,
以這邊 getByRole('heading', { name: 'Installation' }) 來說,他是要找一個 role 為 heading 且 name 為 Installation 的元素,這邊 heading 則是代表 h1 ~ h6 這些標籤,而 name 則是標籤內的文字。
可以參考 W3C
這時可能就會想說,那這樣去找元素是不是很麻煩,所以其實我自己也不是很常用 getByRole 來找元素,而是會用 locator 來找元素,這樣比較直覺,用起來也比較方便。
執行範例測試
這邊可以先打開 playwright.config.js 來看一下,這邊可以設定一些 config,例如 timeout、測試腳本的目錄、平行執行測試相關設定、產出報告的設定、baseURL與裝置的設定等等。
而這邊我們都先以 Chrome 來做測試,所以這邊就先把 projects 留下 chromium,其他的先註解掉。
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// 註解
],
之後就可以執行測試了,這邊我們就直接執行 npm run test。
> [email protected] test
> playwright test
Running 2 tests using 2 workers
2 passed (7.0s)
To open last HTML report run:
npx playwright show-report
可以看到這邊執行了兩個測試,並且都通過了。
用 locator 找元素
其實有很多方式可以抓到你要的元素,如同官方文件說明,但如同前面所提到的,我們可以用 locator 來找元素,這樣比較直覺,用起來也比較方便。
這邊我們可以先來看一下 locator 的用法。
page.locator(selector);
page.locator(selector, options);
其實就是丟進一個 selector 來找元素,而 selector 可以是 CSS 選擇器、XPath 選擇器 之類的東西,而 options 則是一些選項,例如包含其他的元素或文字等等。
所以這邊就可以改寫一下原本的 get started link 範例變成以下:
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/')
const link = await page.locator('text=Get started')
await link.click()
const h1 = await page.locator('h1', { hasText: 'Installation' })
await expect(h1).toBeVisible()
})
也可以不宣告變數,直接操作與斷言(assert)。
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/')
await page.locator('text=Get started').click()
await expect(
await page.locator('h1', { hasText: 'Installation' })
).toBeVisible()
})
不過這個就看個人喜好了,我自己是比較喜歡宣告變數,這樣比較好閱讀,而且也比較好除錯。
檢查元素是否存在
有時候我們會需要檢查一個元素是否存在,這時我們可以用 locator 的 count 來檢查元素的數量。
test('check element exists', async ({ page }) => {
await page.goto('https://playwright.dev/')
const link = await page.locator('text=Get started')
await link.click()
const h1 = await page.locator('h1', { hasText: 'Installation' })
await expect(await h1.count()).toBe(1)
const h2 = await page.locator('h2', { hasText: 'Installation' })
await expect(await h2.count()).toBe(0)
})
但這時執行後會發現他不會通過,會看到下面的訊息:
Error: expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 0
8 |
9 | const h1 = await page.locator('h1', { hasText: 'Installation' })
> 10 | await expect(await h1.count()).toBe(1);
| ^
11 |
12 | const h2 = await page.locator('h2', { hasText: 'Installation' })
13 | await expect(await h2.count()).toBe(0);
這時會想說奇怪,剛才執行的時候不是有看到 Installation 的標題嗎,為什麼 count 會是 0 呢?
這是因為至少在我寫文章的當下,Playwright 是一個單頁應用程式(SPA),所以當你點擊 Get started 連結後,到你執行 count 的時候,其實 Installation 的標題還沒有出現,所以 count 會是 0。
這時 Playwright 提供了一個方法可以應付之後需要測 SPA 的情況,就是 await page.waitForLoadState('networkidle')。
所以你可以把它加入到連結被點擊後,再執行 count 的前面。
test('check element exists', async ({ page }) => {
await page.goto('https://playwright.dev/')
const link = await page.locator('text=Get started')
await link.click()
await page.waitForLoadState('networkidle') // <-- 加入這行
const h1 = await page.locator('h1', { hasText: 'Installation' })
await expect(await h1.count()).toBe(1)
const h2 = await page.locator('h2', { hasText: 'Installation' })
await expect(await h2.count()).toBe(0)
})
這時你再執行測試,如果裝置跑比較慢,可能他又會失敗,因為畢竟要等到 networkidle 有可能會超時,所以可以在後面設定 timeout。
await page.waitForLoadState('networkidle', { timeout: 10000 })
這樣應該就會通過了。
其他檢查元素是否存在的方法
在上一個做法中是取得元素的 count 再去判斷他的數字,但也有其他方法可以做到,例如直接使用 expect 斷言來判斷元素是否出現N次,或是使用 locator 的 first 方法來取得第一個元素再加以判斷。
例如下面這些做法
const h1 = await page.locator('h1', { hasText: 'Installation' })
await expect(h1).toHaveCount(1)
await expect(h1.first()).toHaveText('Installation')
這樣也可以達到相同的效果,但不過需要注意的是對於 SPA 來說,如果只是檢查 count 還是需要加上 await page.waitForLoadState('networkidle') 來等待網頁的載入。否則你的測試有跟沒有一樣。
以上面的例子來說,expect(h1).toHaveCount(1) 一定要等待 networkidle,而 expect(h1.first()).toHaveText('Installation') 則不用,因為他不只抓出 first 以外,還又多判斷了文字是否正確。
所以可以用下面的程式實驗:
const h1 = await page.locator('h1', { hasText: 'Installation' })
await expect(h1).toHaveCount(0)
await expect(h1).toHaveCount(1)
然後執行一下,你會看到居然通過了,這什麼情況,難道他在你斷言他是0之後剛好他 render 出來然後下一行你執行的時候他就是1了嗎?
絕對不是,不然你可以多跑幾次看看
如果真的不信邪,我 code 都幫你準備好了,你就讓他跑個 100 次吧
for (let i = 0; i < 100; i++) {
test(`get started link - ${i}`, async ({ page }) => {
await page.goto('https://playwright.dev/')
const link = await page.locator('text=Get started')
await link.click()
const h1 = await page.locator('h1', { hasText: 'Installation' })
await expect(h1).toHaveCount(0)
await expect(h1).toHaveCount(1)
})
}
所以在測試 SPA 的時候,一定要非常注意,不然你的測試就會變成有跟沒有一樣。
執行 JavaScript
有時候我們會需要執行一些 JavaScript 來操作網頁,這時我們可以使用 page.evaluate 來執行 JavaScript。
不管你是要讓網頁變成你要的狀態,或是要取得一些資料,都可以使用這個方法。
例如下面這個例子,我們可以用 page.evaluate 來取得網頁的 title。
test('get title', async ({ page }) => {
await page.goto('https://playwright.dev/')
const title = await page.evaluate(() => document.title)
await expect(title).toBe(
'Fast and reliable end-to-end testing for modern web apps | Playwright'
)
})
這樣就可以取得網頁的 title 了,雖然取得 title 這個例子有點多餘,因為 Playwright 本身就有提供讓你拿到 title 的方法,但這邊只是為了示範 page.evaluate 的用法。
但這時執行後有機率會失敗,因為至少在寫文章的當下,他的官方網站是 SPA,所以在 goto 的時候也需要讓他等一下,而 goto 有提供 options 讓你直接設定。
await page.goto('https://playwright.dev/', {
waitUntil: 'networkidle',
})
另外 page.evaluate 也可以讓你對網頁做一些操作,例如下面這個例子,我們可以用 page.evaluate 來對網頁做 scroll。
test('scroll to bottom', async ({ page }) => {
await page.goto('https://playwright.dev/', {
waitUntil: 'networkidle',
})
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight)
})
await page.screenshot({ path: 'screenshot.png' })
})
最後一行我加上 screenshot 來截圖,這樣你就可以看到他有沒有真的 scroll 到底部。
當然你也可以動態去對 DOM 做操作,例如我插入一個 <style> 標籤來改變網頁的樣式。
test('change style', async ({ page }) => {
await page.goto('https://playwright.dev/', {
waitUntil: 'networkidle',
})
await page.evaluate(() => {
const style = document.createElement('style')
style.textContent = `
body {
background-color: #f99;
}
`
document.head.appendChild(style)
})
await page.screenshot({ path: 'screenshot.png' })
})
這麼做就有很大的彈性,不過要注意的是,你在 page.evaluate 中的 JavaScript 是在網頁的 context 中執行的,所以你不能直接使用外部的變數,如果你要使用外部的變數,你必須要傳入給他,而且裡面因為是在網頁的 context 執行,所以裡面的 console.log 也不會顯示在你的 terminal 中,在 debug 的時候要注意。
await page.goto('https://playwright.dev/', {
waitUntil: 'networkidle',
})
const text = 'Hello World'
await page.evaluate((text) => {
const div = document.createElement('div')
div.textContent = text
div.style.color = 'red'
div.style.fontSize = '48px'
document.body.appendChild(div)
window.scrollTo(0, document.body.scrollHeight)
}, text)
await page.screenshot({ path: 'screenshot.png' })
這時你應該會看到網頁底部多了一個 Hello World 的紅色文字。
畫面比對
剛才使用到了 screenshot 來截圖,這時就會需要一個畫面比對的功能。
而 Playwright 有提供 Visual comparisons 功能,來做畫面的比對
這邊可以先做一點小的測試,來試試看怎麼使用他的畫面比對功能。
import { test, expect } from '@playwright/test'
test('example test', async ({ page }) => {
await page.goto('https://playwright.dev')
await expect(page).toHaveScreenshot()
})
之後一樣執行 npm run test,這時第一次執行測試會失敗並產生一張 screenshot,到你的 example.spec.js-snapshots 目錄下,而檔名是 example-test-1-chromium-win32.png。
這邊目錄和檔名都是有規律的,例如目錄名稱其實就是你的測試腳本名稱再加上-snapshots,而檔名預設則是 測試標題-{screenshot次數}-裝置-作業系統.png 等。
當他存在的時候,你再次執行測試,這時就會拿這張 screenshot 來做比對,如果不一樣就會失敗,如果一樣就會通過。
如果想要更新 screenshot,可以使用 --update-snapshots 來更新。
npx playwright test --update-snapshots
如果你使用 npm 的話,想要在指令後面加上 --update-snapshots 的話,可以在 package.json 的 scripts 中加上 --update-snapshots。
{
"scripts": {
"test": "playwright test",
"test:update": "playwright test --update-snapshots"
}
}
或是懶惰一些(?),直接在 -- 後面加上參數。
npm run test -- --update-snapshots
如果是 pnpm 的話則可以直接在指令後面加上 --update-snapshots。
pnpm test --update-snapshots
而預設的比對是要完全符合,你有一些方式可以設定一點容錯率:
maxDiffPixels: 最多能有多少像素差異maxDiffPixelRatio: 最多能有多少像素比例差異threshold: 一個介於0~1之間的數字,代表色彩的容錯率,0代表顏色要完全相同,1代表完全不同,基本上你設定成1的話就是不比對了。
前兩個都很直覺,就是兩張圖的像素差異,只是一個是絕對值,一個是比例,而 threshold 可以用下面的來實驗幫助理解。
await page.goto('http://google.com')
await expect(page).toHaveScreenshot()
先刻意用預設值來比對,然後你可以在目錄下找到 test-results,裡面會有這個快照比對的三種圖檔,分別是 diff、expected、actual,對應到 diff 就是兩張圖的差異,expected 就是你的預期圖,actual 就是實際的圖。
點進去 diff 會發現非常的不同,有很多紅色(差異)的地方,這時你在把 threshold 設定成 1,再執行一次,這時你會發現 diff 的圖檔幾乎是灰色的(pass),可以執行下面的程式碼看看。
await expect(page).toHaveScreenshot({ threshold: 1 })
然後最後再將 threshold 設定成 0.85,這時你會發現 diff 只剩下一點點的紅色。
await expect(page).toHaveScreenshot({ threshold: 0.85 })
然後也會看到下面的錯誤訊息
Error: Screenshot comparison failed:
619 pixels (ratio 0.01 of all image pixels) are different.
代表只剩下 619 個像素不同,也就是 0.01% 的比例不同,就可以在與前兩個參數一起使用,來達到你想要的比對。
例如你可以再將 maxDiffPixelRatio 設為 0.02 這樣他就會通過了。
await expect(page).toHaveScreenshot({
threshold: 0.85,
maxDiffPixelRatio: 0.02,
})
而這邊也稍微提一下他是怎麼實作的,他是用 pixelmatch 這個套件來做比對的,所以可以參考一下 pixelmatch 的文件,如果哪天需要單獨使用的話也許有幫助。
他主要實作了兩篇論文,一篇用在 threshold 感知色差,一篇用在比對像素,有興趣的話可以參考一下。
結語
這一篇主要是紀錄了一些 Playwright 的基本用法,還有很多相關的語法可以用,等到發現上面那些語法湊不出要測的功能的時候,就可以再去翻一下官方文件。
其實還有一些也是基本操作,例如一些生命週期的 hook,或是測試群組(describe)等等,但這滿好理解的而且就算不使用也不會有太大的問題,所以這邊就不先做介紹,等之後下一篇之後用到的時候再做介紹。
接著下一篇就會把整個 speed-test 的測試腳本寫出來,並且附上我修改過的題目。