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
-
npm
npm init playwright@latest
-
yarn
yarn create playwright
-
pnpm
pnpm 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
)
- 使用
Debug
debug
模式我自己目前是還沒用到,還是比較習慣在測試當中塞入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
的測試腳本寫出來,並且附上我修改過的題目。