- Visual Testing ทำงานอย่างไร
- เริ่มต้นโครงการ
- กำหนดสภาพแวดล้อมให้คงที่
- จัดการส่วนที่เปลี่ยนทุกครั้ง
- เลือกเกณฑ์ความต่างอย่างมีเหตุผล
- ตรวจความต่างและอัปเดต Baseline
- ทำงานบน CI
- ปัญหาที่พบบ่อย
- สรุป
- เอกสารอ้างอิง
หน้าเว็บอาจยังคลิกได้และส่งข้อมูลได้ตามปกติ แต่ปุ่มถูกบัง ข้อความล้นกรอบ หรือระยะห่างขององค์ประกอบเปลี่ยนไป Visual Testing ช่วยตรวจปัญหาเหล่านี้ด้วยการเปรียบเทียบภาพหน้าจอปัจจุบันกับภาพอ้างอิงที่ทีมตรวจรับแล้ว หรือ baseline / golden snapshot

Playwright Test มี assertion toHaveScreenshot() สำหรับจับภาพหน้าเว็บหรือองค์ประกอบ แล้วเทียบกับภาพอ้างอิง หากภาพต่างกันเกินเกณฑ์ที่กำหนด การทดสอบจะล้มเหลวและมีภาพสำหรับตรวจสอบความต่าง ควรใช้ควบคู่กับการตรวจข้อความ พฤติกรรม และการเข้าถึง ไม่ใช่ใช้ภาพแทนทุก assertion
#Visual Testing ทำงานอย่างไร
- เปิดหน้าเว็บและจัดข้อมูลให้อยู่ในสภาพที่คาดเดาได้
- จับภาพหน้าเว็บหรือเฉพาะส่วนที่สนใจ
- รันครั้งแรกเพื่อสร้าง baseline แล้วตรวจภาพก่อนบันทึกใน Git
- รันครั้งต่อไปเพื่อเปรียบเทียบภาพใหม่กับ baseline
- เมื่อพบความต่าง ให้ดูภาพ expected, actual และ diff ว่าเป็นการเปลี่ยนแปลงที่ต้องการหรือเป็นข้อผิดพลาด
- อัปเดต baseline เฉพาะหลังตรวจรับการเปลี่ยนแปลงของ UI แล้ว
Playwright รอจนภาพที่จับติดต่อกันสองครั้งตรงกันก่อนนำภาพล่าสุดไปเทียบกับ baseline อย่างไรก็ดี ควรจัดการข้อมูลที่เปลี่ยนตลอดเวลาเอง เช่น เวลา โฆษณา และ API ที่ตอบกลับต่างกันในแต่ละครั้ง
#เริ่มต้นโครงการ
ต้องมี Node.js รุ่นที่ Playwright รองรับ จากนั้นสร้างโครงการและติดตั้งเบราว์เซอร์ตามตัวช่วยติดตั้ง:
npm init playwright@latest
เลือก TypeScript และติดตั้ง browser binaries เมื่อระบบถาม หากมีโครงการ Playwright อยู่แล้ว สามารถเพิ่มไฟล์ทดสอบด้านล่างในโฟลเดอร์ tests/ ได้
#ตัวอย่างที่รันได้: ทดสอบ TodoMVC
สร้าง tests/visual.spec.ts:
import { test, expect } from '@playwright/test';
test('รายการงานแสดงผลตามที่ออกแบบ', async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc/');
const newTodo = page.getByPlaceholder('What needs to be done?');
await newTodo.fill('Learn visual testing');
await newTodo.press('Enter');
// ตรวจข้อมูลที่สำคัญด้วย assertion เชิงพฤติกรรมด้วย
await expect(page.locator('.todo-list li')).toHaveText('Learn visual testing');
// ภาพเฉพาะองค์ประกอบ: จุดเปลี่ยนแปลงเล็ก ๆ ดูง่าย
await expect(page.locator('.todo-list')).toHaveScreenshot('todo-list.png');
// ภาพทั้งหน้า: ตรวจเลย์เอาต์โดยรวม
await expect(page).toHaveScreenshot('todo-page.png', {
fullPage: true,
animations: 'disabled',
});
});
ตัวอย่างนี้อาศัยหน้า TodoMVC สาธารณะของ Playwright หากเว็บไซต์ปลายทางเปลี่ยนหรือใช้งานไม่ได้ ให้เปลี่ยน page.goto() เป็นแอปทดสอบภายในโครงการ และสร้าง baseline ใหม่จากสภาพแวดล้อมเดียวกัน
รันเพื่อสร้างภาพอ้างอิงครั้งแรก แล้วตรวจภาพที่สร้างใน tests/visual.spec.ts-snapshots/:
npx playwright test tests/visual.spec.ts --project=chromium
การรันครั้งแรกที่ยังไม่มี baseline อาจรายงานว่าภาพอ้างอิงไม่มี พร้อมเขียนไฟล์ภาพขึ้นมา ตรวจไฟล์ให้ตรงกับหน้าตาที่ต้องการ แล้ว commit ภาพ baseline ไปพร้อมกับเทสต์ จากนั้นรันคำสั่งเดิมอีกครั้งเพื่อเทียบภาพ
npx playwright test tests/visual.spec.ts --project=chromium
npx playwright show-report
หากชื่อ project ในไฟล์
playwright.config.tsไม่ใช่chromiumให้ใช้ชื่อ project ที่ตั้งไว้ หรือไม่ต้องระบุ--projectแต่จะรันทุกรายการที่กำหนดใน config และอาจสร้าง baseline แยกหลายเบราว์เซอร์
#กำหนดสภาพแวดล้อมให้คงที่
ตัวอย่าง config ที่ลดความต่างจาก viewport, locale, timezone และธีม:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
reporter: 'html',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1280, height: 720 },
locale: 'en-US',
timezoneId: 'UTC',
colorScheme: 'light',
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},
projects: [{ name: 'chromium', use: { browserName: 'chromium' } }],
});
ตัวเลือก screenshot: 'only-on-failure' ใช้บันทึกภาพประกอบเมื่อเทสต์ล้มเหลว ส่วน toHaveScreenshot() คือ assertion ที่เทียบภาพกับ baseline ทั้งสองอย่างมีหน้าที่ต่างกัน ควรใช้ระบบปฏิบัติการ ฟอนต์ รุ่นเบราว์เซอร์ และโหมดการรันที่สอดคล้องกับตอนสร้าง baseline โดยเฉพาะเมื่อรันบน CI
#จัดการส่วนที่เปลี่ยนทุกครั้ง
วิธีที่ดีที่สุดคือทำข้อมูลทดสอบให้คงที่ เช่น mock API เพื่อให้ยอดขาย ชื่อผู้ใช้ และเวลาในหน้าไม่เปลี่ยนโดยไม่จำเป็น หากข้อมูลบางส่วนควบคุมไม่ได้ สามารถปิดบังหรือซ่อนเฉพาะบริเวณนั้น
// ตัวอย่างสำหรับแอปที่มี data-testid="live-clock"
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [page.getByTestId('live-clock')],
animations: 'disabled',
maxDiffPixels: 30,
});
mask วาดกล่องทับพื้นที่ locator ในภาพ จึงควรระวังการปิดบังบริเวณที่ต้องตรวจจริง หากตำแหน่งหรือขนาดส่วนดังกล่าวเปลี่ยน ภาพก็ยังอาจต่างกันได้
อีกทางหนึ่งคือใช้ stylesheet เฉพาะตอนจับภาพ:
/* tests/screenshot.css */
.ad-rotation,
.live-clock {
visibility: hidden !important;
}
import path from 'node:path';
import { test, expect } from '@playwright/test';
test('ภาพ dashboard', async ({ page }) => {
await page.goto('http://localhost:3000/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
stylePath: path.join(__dirname, 'screenshot.css'),
});
});
ตัวอย่าง dashboard นี้ต้องมีแอปและ selector ตามที่ระบุ ปรับ URL และ class ให้ตรงกับโครงการจริง stylePath ใช้ CSS ขณะจับภาพ ไม่ได้แก้ CSS ของแอปถาวร
#เลือกเกณฑ์ความต่างอย่างมีเหตุผล
| ตัวเลือก | ความหมาย | ตัวอย่างการใช้ |
|---|---|---|
maxDiffPixels |
จำนวนพิกเซลที่อนุญาตให้ต่าง | 30 เมื่อมี noise เล็กน้อยที่ทราบสาเหตุ |
maxDiffPixelRatio |
สัดส่วนพิกเซลที่ต่างต่อจำนวนทั้งหมด | 0.001 เท่ากับ 0.1% |
threshold |
ระดับความต่างของสีที่ใช้ตัดสินว่าแต่ละพิกเซลต่างหรือไม่ | ปรับเมื่อสีเกิดความคลาดเล็กน้อย |
เริ่มจากเกณฑ์ที่เข้มและแก้สาเหตุของภาพที่ไม่นิ่งก่อน ถ้าเพิ่มค่าความทนทานสูงเกินไป เทสต์อาจปล่อยให้ความผิดพลาดจริงผ่านไปได้ โดยเฉพาะภาพเต็มหน้าที่มีพื้นที่ขนาดใหญ่
#ตรวจความต่างและอัปเดต Baseline
เมื่อเทสต์ล้มเหลว ให้เปิดรายงานหรือ UI Mode เพื่อดูภาพ expected, actual และ diff:
npx playwright show-report
npx playwright test --ui
ถ้า UI เปลี่ยนตามที่ตั้งใจ อัปเดตภาพอ้างอิงเฉพาะไฟล์ทดสอบที่เกี่ยวข้อง แล้ว review ภาพใหม่ก่อน commit:
npx playwright test tests/visual.spec.ts --project=chromium --update-snapshots
git diff -- tests/visual.spec.ts-snapshots
คำสั่ง --update-snapshots แบบไม่ระบุค่าใช้โหมดอัปเดตภาพที่เปลี่ยนไปตาม CLI ปัจจุบัน การอัปเดต baseline เป็นการยอมรับหน้าตาใหม่ จึงไม่ควรใส่ขั้นตอนนี้ไว้ใน CI ที่รันตรวจทุกครั้ง
#ทำงานบน CI
สำหรับโครงการที่มี package-lock.json และ baseline สร้างจาก Ubuntu ให้ใช้ runner ที่สอดคล้องกัน เช่น:
# .github/workflows/visual-test.yml
name: Visual tests
on: [push, pull_request]
jobs:
visual:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test tests/visual.spec.ts --project=chromium
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 14
หาก baseline ถูกสร้างบน macOS แล้ว CI รัน Ubuntu ภาพอาจต่างเพราะฟอนต์และการเรนเดอร์ ควรสร้าง baseline ใหม่ในสภาพแวดล้อมเดียวกับ CI และตรึงการเปลี่ยนรุ่นของ Playwright/เบราว์เซอร์ผ่าน lockfile การเปลี่ยน browser version อาจต้องทบทวนภาพอ้างอิงใหม่
#ปัญหาที่พบบ่อย
| อาการ | สาเหตุที่ควรตรวจ | วิธีแก้ |
|---|---|---|
| ภาพต่างทุกครั้ง | เวลา ข้อมูลสุ่ม animation | ใช้ข้อมูลคงที่, mock API, mask หรือ stylePath เฉพาะจุด |
| ผ่านบนเครื่องแต่ตกบน CI | OS, ฟอนต์, เบราว์เซอร์หรือ viewport ต่างกัน | ทำสภาพแวดล้อมและ baseline ให้สอดคล้อง |
| ภาพหน้าเว็บไม่ครบ | เนื้อหาโหลดช้าหรือ lazy loading | รอเงื่อนไขที่ตรวจได้จริง เช่น element ที่ต้องใช้ toBeVisible() ก่อนจับภาพ |
| เกณฑ์ผ่านทั้งที่ UI ผิด | อนุญาตความต่างมากหรือ mask กว้าง | ลดเกณฑ์และจำกัดพื้นที่ mask |
| Baseline เปลี่ยนจำนวนมาก | ฟอนต์หรือ dependency เปลี่ยน | ตรวจ diff และอัปเดตอย่างตั้งใจใน pull request |
#สรุป
Visual Testing ที่มีประโยชน์เริ่มจาก ข้อมูลคงที่ → จับภาพจุดสำคัญ → เทียบ baseline → ตรวจ diff → ยอมรับการเปลี่ยนแปลง ใช้ภาพเฉพาะองค์ประกอบเมื่ออยากระบุจุดที่ผิดให้ชัด และใช้ภาพเต็มหน้าเมื่อจำเป็นต้องตรวจองค์ประกอบรวมของหน้า ควบคู่กับ assertion ด้านข้อความและพฤติกรรมเสมอ