上一篇我們把測試共用的層級釐清後,實作出了組合 C:只共用 Context,但每支測試各自開新的 Page。這樣既能維持只登入一次的優勢,又能保有測試之間的畫面隔離。
不過上一篇結尾有提到當初說共用 Page 的缺點之一是測試失敗時會拿不到影片,那如果改成只共用context,這個問題還會存在嗎?
要搞懂這件事,我們得先釐清 Playwright 內建的三種除錯工具是怎麼運作的。
測試跑失敗時,只看 terminal 裡的報錯訊息常常抓不到原因,最直接的方式還是想看「失敗當下的畫面長怎樣」。Playwright 內建了三種除錯工具,設定都放在 playwright.config.ts 的 use 區塊:
// playwright.config.ts
use: {
screenshot: 'only-on-failure', // 截圖
video: 'retain-on-failure', // 影片
trace: 'on-first-retry', // 追蹤檔
},
這三個選項可以設定的數值如下表:
| 設定 | 可用值 | 預設值 | 我們專案目前的設定 |
|---|---|---|---|
screenshot |
off / on / only-on-failure |
off |
沒設定 (= off) |
video |
off / on / retain-on-failure / on-first-retry |
off |
retain-on-failure |
trace |
off / on / retain-on-failure / on-first-retry / on-all-retries / retain-on-first-failure |
off |
on-first-retry |
這些產出物跑完後都會放在 test-results/ 資料夾下,點開 Playwright 的 HTML 報告就能直接看截圖、錄影跟 Trace 檔。
不過為什麼官方預設都是 off,而大家範本常寫「失敗才留」?原因很簡單——開銷差太多了:
所以實務上的做法通常是:通過的測試完全不留,只有失敗的測試才保留素材除錯。
看一下我們目前 playwright.config.ts 裡的這兩行:
retries: process.env.CI ? 2 : 0, // 本機預設不重試
trace: 'on-first-retry', // 第一次「重試」時才開 trace
on-first-retry 的意思是「測試失敗後,第一次重試才開始錄 Trace」。但在本機跑測試時 retries 設成 0(不重試),所以測試一失敗就直接結束了。這兩個設定湊在一起會導致一個狀況:在本機測試失敗時,永遠不會有失敗測試的 Trace 檔案。
很多人常常是測試跑壞了想開 Trace 檢查,才發現資料夾裡空空如也。如果你希望本機失敗時也能拿到 Trace,可以把 config 改成 trace: 'retain-on-failure',或是跑指令時直接加上 --trace on:
npx playwright test --trace on
昨天我們講過登入狀態是存在 Context 層級,現在我們把這三種除錯產出物綁定的層級整理如下:
| 產出物 | 綁定的層級 | 能不能手動控制 |
|---|---|---|
| screenshot | Page,隨時能直接呼叫 page.screenshot() |
完全自由 |
| video | Context (設定) / Page (產出),由 Context 統一設定開關,每個 Page 關閉時寫入獨立影片 | 無法單獨對 Page 開關,只能在建立 Context 時設定;但 Context 會為每個 Page 產出一支影片 |
| trace | Context,但可以手動呼叫 start() / stop() |
可以手動指定開關範圍 |
把這件事跟上一篇放在一起看:
Context ← 登入狀態存在這(想共用) / 影片控制在這 (recordVideo)
│
└── Page ← 影片實體產出在每個分頁,關閉時寫入定稿(想隔離)
我們想共用的登入狀態,與影片的控制權同樣在 Context 層級;但影片實體檔案卻是以 Page 為單位獨立產出。 當你選擇共用 Page 時,測試之間的畫面隔離與影片切分功能就會被犧牲掉。
補充說明一下:
video的設定(如recordVideo)是傳給newContext(),無法在建立單一Page後才單獨啟用;但 Playwright 背後會為該 Context 下的每一個Page產出獨立的影片檔,並在分頁關閉時定稿寫檔。這種「設定在 Context,實體產出基於 Page」的分工,就是各種除錯觀念與實作容易混淆的原因。
在workerLoggedInPage中我們直接共用了 Page。既然影片檔是以 Page 為單位產出的,那測試報告裡至少要拿到一支紀錄了全部測試過程的長影片吧?
但實際上點開報告卻什麼影片都沒有,這背後其實有兩個原因:
1. 影片無法依照單一測試去切分
影片必須等 Page 關閉之後才會定稿寫檔。但 workerLoggedInPage 的 Page 從 Worker 啟動後就一直活著,直到所有測試跑完才關閉。所以就算有錄影,也會變成一支把所有測試串在一起的超長影片:
登入流程 → 測試 1 操作 → 測試 2 操作 → 測試 3 操作
(全部硬塞在同一支影片裡)
如果測試 2 跑失敗,你得自己拉進度條慢慢找測試 2 是從第幾分第幾秒開始,導致很難快速定位跟失敗測試相關的紀錄。
2. 手動建立的 Context 影片不會被自動收進報告
Playwright 只會自動幫「它自己建立的預設 Context」收集影片並掛到報告上。但在 workerLoggedInPage 裡,Context 是我們自己呼叫 browser.newContext() 手動開的,Playwright Test Runner 根本不知道它的存在,自然也不會幫我們把影片收集進 HTML 報告裡。
因此手動建立的 Context 不會繼承 config 裡的 use 設定。你在 config 寫 video: 'retain-on-failure',對自己 newContext() 出來的環境是完全無效的,必須手動帶入 recordVideo 參數才行。
我們回頭修改上一篇寫的 Fixture:在 sharedContext 加入 recordVideo 設定,並在 sharedPage 測試結束時把影片檔手動掛到測試報告上:
// fixtures/fixtures.ts
sharedContext: [async ({ browser }, use) => {
console.log('>>> 執行登入流程');
// 手動建立的 context 不會繼承 config 的設定,
// baseURL 與錄影設定 (recordVideo) 都得自己給
const context = await browser.newContext({
baseURL: 'http://localhost:8000',
recordVideo: { dir: 'test-results/videos' },
});
const page = await context.newPage();
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('demo', 'demo');
await page.close();
await page.video()?.delete(); // 登入過程的分頁影片沒用到,直接刪掉
await use(context);
await context.close();
}, { scope: 'worker' }],
sharedPage: async ({ sharedContext }, use, testInfo) => {
const page = await sharedContext.newPage();
await use(page);
await page.close(); // 必須先關閉 page,影片才會定稿寫完
const video = page.video();
if (!video) return;
if (testInfo.status !== testInfo.expectedStatus) {
// 測試失敗:把影片移到該測試的輸出資料夾,並手動掛上報告
const videoPath = testInfo.outputPath('video.webm');
await video.saveAs(videoPath);
await testInfo.attach('video', { path: videoPath, contentType: 'video/webm' });
} else {
// 測試成功:刪除影片檔(模仿 retain-on-failure),避免塞爆硬碟
await video.delete();
}
},
這裡有幾個細節要特別注意:
video.saveAs() 一定要在 page.close() 之後呼叫:因為影片是在 Page 關閉時才完成寫檔,saveAs() 會等待寫入完成。如果順序放錯,只會拿到壞掉或不完整的影片。testInfo.attach():因為 Context 是我們自己開的,Playwright 不會自動幫我們掛載影片到報告上,只能手動加上去。testInfo.status !== testInfo.expectedStatus 判斷失敗:這樣才能達到「失敗才留影片、成功就 delete() 刪除」的效果。delete() 即可。我們把 shared-context-demo.spec.ts 裡的「觀察 2」斷言故意改錯(例如把選單名稱改成 NotExist)然後執行:
npx playwright test shared-context-demo --project=chromium --workers=1
跑完打開 HTML 報告可以看到:失敗的「觀察 2」確實拿到了只屬於它自己的影片,成功跑完的觀察 1 與觀察 3 則沒有留下影片檔,而且影片內容完全只有觀察 2 的操作步驟,沒有登入流程也沒有其他測試。
今天我們把除錯產出物跟層級的關係整理了一下:
trace: 'on-first-retry' 在本機預設 retries: 0 時會完全抓不到 Trace。newContext() 出來的環境不會吃 config 設定,錄影檔也不會自動收進報告,必須手動用 attach() 掛上去。context 兼顧了登入效能與獨立錄影:雖然能拿到單一測試的影片跟 Page 隔離,但程式碼要自己寫不少 Fixture 處理邏輯。下一篇是我們的第二個緩衝日,我們會把目前學到的各種共用方式實際套用到後台 CRUD 流程中,看看實際會遇到什麼問題,以及該怎麼解決!