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

info-visual-testing-playwright-baseline

Playwright Test มี assertion toHaveScreenshot() สำหรับจับภาพหน้าเว็บหรือองค์ประกอบ แล้วเทียบกับภาพอ้างอิง หากภาพต่างกันเกินเกณฑ์ที่กำหนด การทดสอบจะล้มเหลวและมีภาพสำหรับตรวจสอบความต่าง ควรใช้ควบคู่กับการตรวจข้อความ พฤติกรรม และการเข้าถึง ไม่ใช่ใช้ภาพแทนทุก assertion

#Visual Testing ทำงานอย่างไร

  1. เปิดหน้าเว็บและจัดข้อมูลให้อยู่ในสภาพที่คาดเดาได้
  2. จับภาพหน้าเว็บหรือเฉพาะส่วนที่สนใจ
  3. รันครั้งแรกเพื่อสร้าง baseline แล้วตรวจภาพก่อนบันทึกใน Git
  4. รันครั้งต่อไปเพื่อเปรียบเทียบภาพใหม่กับ baseline
  5. เมื่อพบความต่าง ให้ดูภาพ expected, actual และ diff ว่าเป็นการเปลี่ยนแปลงที่ต้องการหรือเป็นข้อผิดพลาด
  6. อัปเดต 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 ด้านข้อความและพฤติกรรมเสมอ

#เอกสารอ้างอิง