iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
AI 自動化

用 AI Agent 打造你的產品使用手冊產線系列 第 7

[Day 07] 用 Playwright 驅動 Electron 與 Web

  • 分享至 

  • xImage
  •  

昨天把 Demo App 準備好了,也讓大家看到她的樣子了。今天來讓 Playwright 操作它。

本日目標

今天的目標其實很簡單,就是讓 Playwright 幫我們截一張圖片就好了,並且是 Electron 和 Web 各一張。

通常只要維護一種就好了,但這邊為了 Demo 就都展示一下。

把「怎麼啟動待測物」抽成一層介面

Electron 跟 Web 的啟動方式在底層完全不同,但對 manifest(用來描述一本手冊長什麼樣的設定檔)來說不應該需要知道這個差別。換句話說,一份設定檔寫完,換一個模式就能同時對兩種形態產出手冊。

所以,「啟動待測物」這件事需要抽成一層介面,包含四個方法:

interface AppDriver {
  launch(): Promise<Page>
  setStorage(kv: Record<string, string>): Promise<void>
  resize(width: number, height: number): Promise<void>
  close(): Promise<void>
}
  • launch:把應用程式啟動起來並回傳可操作的 Page
  • setStorage:狀態注入的底層機制,目前只要管「怎麼把值送進去」,至於要送什麼 (或是其他還沒想到的),之後再討論
  • resize:統一視窗尺寸,確保截圖尺寸一致
  • close:收尾

上層只認得這四個方法,底下實際是 WebDriver 還是 ElectronDriver,由 createDriver(config) 依設定檔裡的 app.mode 決定。

設定檔這邊順帶一提:config.json 是一人一份、不進版控的,找不到就退回 config.example.json,所以 clone 下來不必先做任何設定就能跑。

Web 路線

Web 這邊相對單純,就是標準的 Playwright 流程:

啟動瀏覽器 -> 開 context(尺寸在這一步就決定)-> 取得 page -> 跳轉到 URL

const browser = await chromium.launch()
const context = await browser.newContext({
  viewport: { width: 1600, height: 900 },
  deviceScaleFactor: 2,
})
const page = await context.newPage()
await page.goto(url, { waitUntil: 'domcontentloaded' })

viewport 決定畫面尺寸,deviceScaleFactor 決定截圖的解析度倍率。片段裡寫死的數字只是為了好讀,實際的值是從 config.viewport 讀出來的,deviceScaleFactor 沒指定就是 2。

這兩個數字怎麼選其實牽涉到列印需求 (截圖印在 A4 上夠不夠清晰),現在先用一個夠用的數值就好,之後要列印時再評估是否要調整。

Electron 路線

Electron 這邊細節就多了,連順序都跟 Web 不一樣:

啟動 App -> 取得 page(視窗開起來時頁面已經載好了,不用跳轉)-> 改尺寸 (得另外走主行程)

const app = await _electron.launch({
  executablePath: require('electron'),
  args: [projectDir, '--force-device-scale-factor=1'],
  cwd: projectDir,
})
const page = await app.firstWindow()

_electron.launch 的完整參數可以參考 Playwright 官方文件,這邊只挑跟截圖一致性有關的幾個講。

executablePath 指的是 electron 執行檔,不是你的 App。 在 Node 裡 require('electron') 回傳的是 binary 的路徑字串(在 Electron 裡面才是 API 物件);真正指向 App 的是 args 的第一個元素,那個目錄裡的 package.jsonmain 才是主行程進入點。如果 App 已經打包成 .exe,就直接指向那個 exe。

--force-device-scale-factor=1 是為了讓截圖尺寸可重現。 不同作業系統、不同螢幕的系統縮放比例不一樣,不釘死的話,同一份程式碼在不同機器上截出來的圖尺寸就會不一致。而且這件事還牽涉到,後續要在截圖上畫框跟標號時,位置都是靠 boundingBox() 算出來的,座標系一旦跟著系統縮放漂移,標註就會整片對不準。

page 只碰得到渲染行程。 Electron 有主行程(Node.js 環境,能碰作業系統層級的 API)跟渲染行程(瀏覽器環境,一般網頁操作的範圍)兩種執行環境,Playwright 的 page 操作的是後者。所以「視窗尺寸」這種屬於視窗、不屬於網頁的東西,只能用 app.evaluate() 跑到主行程那一側去改:

await app.evaluate(async ({ BrowserWindow }, size) => {
  const win = BrowserWindow.getAllWindows()[0]
  win.setContentSize(size.width, size.height)
  win.setAlwaysOnTop(false)
}, { width, height })

setContentSize 而不是 setSize,是因為 App 開了 useContentSize: true,尺寸指的是內容區。

跑起來確認

今天的目標是「讓 Playwright 截一張圖」,所以驗證的方式就照著目標走一遍:把兩個 driver 兜起來,跑 「啟動 -> 注入狀態 -> 統一尺寸 -> 等畫面就緒 -> 截圖 -> 收尾」 這條路,看 Electron 跟 Web 是不是都真的能落地拿到一張圖。

const driver = createDriver(config, mode)

// launch 之前注入 —— 這是首選路徑,App 讀 localStorage 時值已經在了。
await driver.setStorage({ locale: 'en', role: 'admin' })

const page = await driver.launch()
await driver.resize(config.viewport.width, config.viewport.height)

// 骨架屏消失、清單出現才按快門,不要插入固定延遲。
await page.waitForSelector('[data-testid="camera-list"]', { state: 'visible' })

await page.screenshot({ path: shot })
await driver.close()

各位要測試的話可以下這兩個指令:

npx tsx runner/drivers/smoke.ts --mode electron
npx tsx runner/drivers/smoke.ts --mode web

截圖的畫面長這樣:

(其實就跟昨天文章附的圖片差不多)

「等畫面就緒」那一步等的是 [data-testid="camera-list"] 出現,而不是 sleep(2000) 這種寫死的等待時間。至於為什麼元件要靠 data-testid 來認,明天就會說明,今天先跳過XD

兩邊都注入 locale=enrole=admin,截出來的圖是英文介面、右上角掛著 Administrator 徽章,Electron / Web 的模式徽章也各自正確——表示狀態注入跟 preload 都有生效。

小結

回頭看,今天的目標「Electron 跟 Web 各截一張圖」兩條路線都做到了,順利地完成:

「啟動 -> 注入狀態 -> 統一尺寸 -> 等畫面就緒 -> 截圖 -> 收尾」

雖然連一次點擊都還沒有,但至少證明 Playwright 能驅動這兩種形態,接下來後面所有的操作才有基礎可以延伸討論。

順帶一提,這支腳本之所以叫 smoke.ts,就是因為它做的正是 smoke test 這件事,它不驗證功能對不對,只驗證東西有沒有冒煙、這條路通不通。

到目前為止,已經把基本的內容都介紹得差不多了,接下來就會開始補充這條路線的細節,包含狀態注入、截圖時機等。不過,明天會先從最重要的一件事開始:怎麼讓程式穩定地找到畫面上的元件。

大家明天見!


上一篇
[Day 06] Demo 用的範例 Electron App
下一篇
[Day 08] 產線實作 1:用 data-testid 精準定位元件
系列文
用 AI Agent 打造你的產品使用手冊產線17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言