iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
自我挑戰組

Playwright 練功房:從零開始的 30 天 E2E 測試教學筆記系列 第 15 篇

Day 15 - 除錯產出物:screenshot、video 與 trace

  • 分享至 

  • xImage
  •  

上一篇我們把測試共用的層級釐清後,實作出了組合 C:只共用 Context,但每支測試各自開新的 Page。這樣既能維持只登入一次的優勢,又能保有測試之間的畫面隔離。

不過上一篇結尾有提到當初說共用 Page 的缺點之一是測試失敗時會拿不到影片,那如果改成只共用context,這個問題還會存在嗎?

要搞懂這件事,我們得先釐清 Playwright 內建的三種除錯工具是怎麼運作的。

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,而大家範本常寫「失敗才留」?原因很簡單——開銷差太多了:

  • screenshot 最輕量:測試結束那一秒拍一張圖。
  • trace 資訊最齊全:會紀錄每一步操作、網路請求跟 DOM 快照。檔案雖然大一點,但除錯最好用。
  • video 最吃資源:全程錄影非常耗 CPU 跟磁碟空間。測試不多的時候還沒什麼問題,但如果測試變多了,就要注意。

所以實務上的做法通常是:通過的測試完全不留,只有失敗的測試才保留素材除錯。

容易碰到的設定陷阱

看一下我們目前 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」的分工,就是各種除錯觀念與實作容易混淆的原因。

為什麼共用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 參數才行。

如何把自建Context設定的影片功能掛上 HTML 報告?

我們回頭修改上一篇寫的 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();
    }
  },

這裡有幾個細節要特別注意:

  1. video.saveAs() 一定要在 page.close() 之後呼叫:因為影片是在 Page 關閉時才完成寫檔,saveAs() 會等待寫入完成。如果順序放錯,只會拿到壞掉或不完整的影片。
  2. 必須手動呼叫 testInfo.attach():因為 Context 是我們自己開的,Playwright 不會自動幫我們掛載影片到報告上,只能手動加上去。
  3. 用 testInfo.status !== testInfo.expectedStatus 判斷失敗:這樣才能達到「失敗才留影片、成功就 delete() 刪除」的效果。
  4. 登入用的臨時分頁影片可以直接刪除:因為登入過程不需要單獨留一支影片,關掉分頁後呼叫 delete() 即可。

實際驗證

我們把 shared-context-demo.spec.ts 裡的「觀察 2」斷言故意改錯(例如把選單名稱改成 NotExist)然後執行:

npx playwright test shared-context-demo --project=chromium --workers=1

跑完打開 HTML 報告可以看到:失敗的「觀察 2」確實拿到了只屬於它自己的影片,成功跑完的觀察 1 與觀察 3 則沒有留下影片檔,而且影片內容完全只有觀察 2 的操作步驟,沒有登入流程也沒有其他測試。

今日總結

今天我們把除錯產出物跟層級的關係整理了一下:

  1. 除錯工具成本不同:screenshot 最輕量、trace 資訊最齊、video 最吃資源,實務上慣例是「失敗才留」。要注意 trace: 'on-first-retry' 在本機預設 retries: 0 時會完全抓不到 Trace。
  2. 產出物與狀態的層級關係:影片錄製由 Context 統一設定並在 Page 關閉時定稿產出,登入狀態則存在 Context 中。因為控制與產出層級的分工,共用 Page 時影片才無法依測試拆分。
  3. 手動 Context 都要自己處理:自己 newContext() 出來的環境不會吃 config 設定,錄影檔也不會自動收進報告,必須手動用 attach() 掛上去。
  4. 只共用context 兼顧了登入效能與獨立錄影:雖然能拿到單一測試的影片跟 Page 隔離,但程式碼要自己寫不少 Fixture 處理邏輯。

下一篇是我們的第二個緩衝日,我們會把目前學到的各種共用方式實際套用到後台 CRUD 流程中,看看實際會遇到什麼問題,以及該怎麼解決!


上一篇
Day 14 - 共用的層級:serial mode 與 context / page 的選擇
下一篇
Day 16 - 緩衝日②:效能與隔離性的取捨實驗
系列文
Playwright 練功房:從零開始的 30 天 E2E 測試教學筆記 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言