CYBERSOFT
Đăng nhập

Visual regression bằng AI: so sánh ngữ nghĩa thay vì đếm pixel

Chuyên nâng caoTMĐTAI AgentVisualNâng caoKinh nghiệm
🗓 1 tháng trước21 phút đọc·👁 1,121 lượt xem👤 223 người đọc

Vượt qua pixel-diff: dùng AI/embedding phân loại thay đổi có nghĩa với thay đổi mỹ thuật, giảm dương tính giả do anti-aliasing và nội dung động, phát hiện layout-shift, quản lý baseline, và khi nào phán đoán của AI không đáng tin cùng cách đặt cổng kiểm soát.

1. Bối cảnh: vì sao pixel-diff sinh quá nhiều báo động giả

Trên sàn thương mại điện tử của chúng ta, đội QA chụp ảnh màn hình trang chủ, trang sản phẩm và trang thanh toán ở mỗi lần build, rồi so với ảnh baseline để bắt hồi quy giao diện. Công cụ truyền thống so từng pixel: nếu số pixel khác vượt ngưỡng, test fail. Vấn đề là phần lớn pixel khác nhau lại vô nghĩa với con người — anti-aliasing khác nhau giữa hai lần render, một biểu ngữ khuyến mãi xoay vòng, ngày giờ hiển thị động, hay avatar người dùng khác nhau. Kết quả là báo động giả tràn ngập, đội QA mất niềm tin và bắt đầu bỏ qua kết quả visual, đúng lúc đó một hồi quy thật lọt lưới.

Điều bài này hướng tới là chuyển từ câu hỏi 'có pixel nào khác không' sang câu hỏi 'thay đổi này có ý nghĩa với người dùng không'. Đó là một câu hỏi ngữ nghĩa, và đây là chỗ AI cùng embedding phát huy tác dụng: phân loại thay đổi thành có nghĩa (một nút biến mất, giá sai vị trí, chữ chồng lên nhau) và mỹ thuật thuần túy (mờ viền, dịch một pixel, đổi màu vi tế do nén ảnh). Nhưng ngay từ đầu phải nói rõ: oracle vẫn đứng trên hết. AI không thay bạn quyết định cái gì đúng; nó chỉ giúp lọc bớt nhiễu để con người tập trung vào khác biệt thật sự đáng xem.

Pixel-diff vs semantic-diff Pixel-diff đếm pixel khác anti-aliasing = fail banner động = fail nhiều báo động giả Semantic-diff hỏi: có nghĩa không? bỏ qua mỹ thuật bắt thay đổi thật ít nhiễu hơn Oracle nghiệp vụ vẫn quyết định đâu là hồi quy thật
Chuyển từ đếm pixel sang hỏi thay đổi có ý nghĩa với người dùng không.
Visual regression không thay thế kiểm thử chức năng. Nó bắt lỗi trình bày (layout vỡ, chữ chồng, phần tử mất) mà assertion DOM có thể bỏ sót, nhưng không khẳng định nghiệp vụ tính đúng. Hai lớp bổ sung cho nhau, không loại trừ.

2. Ổn định hóa ảnh chụp trước khi so sánh

Trước khi nghĩ tới AI, phần lớn báo động giả có thể loại bỏ bằng kỹ thuật đơn giản: làm cho ảnh chụp trở nên tất định. Nguồn gây nhiễu phổ biến gồm animation đang chạy, phông chữ chưa tải xong, nội dung động như ngày giờ và banner xoay vòng, và con trỏ nhấp nháy. Playwright cho phép tắt animation khi chụp, chờ web-font ổn định, và che (mask) các vùng động để chúng không tham gia so sánh. Chỉ riêng những bước này thường cắt phần lớn dương tính giả, khiến bất kỳ lớp AI nào phía sau cũng nhẹ gánh hơn nhiều.

Che vùng động là một công cụ mạnh nhưng cần dùng có kỷ luật. Nếu bạn mask cả một vùng lớn 'cho chắc', bạn có thể vô tình che luôn chỗ hồi quy thật sẽ xuất hiện. Nguyên tắc là mask hẹp nhất có thể: chỉ đúng chuỗi ngày giờ, chỉ đúng ô avatar, chỉ đúng banner khuyến mãi. Với mỗi vùng mask, hãy tự hỏi: nếu phần này vỡ layout, liệu một lớp kiểm thử khác có bắt được không? Nếu câu trả lời là không, có lẽ bạn không nên mask nó mà nên tìm cách cố định nội dung — ví dụ tiêm một ngày giờ giả tất định qua clock của Playwright.

ts
// visual-stabilize.spec.ts — tất định hóa trước khi so ảnh
import { test, expect } from '@playwright/test';

test('trang sản phẩm ổn định', async ({ page }) => {
  // cố định thời gian -> ngày giờ hiển thị tất định
  await page.clock.setFixedTime(new Date('2026-07-06T09:00:00Z'));
  await page.goto('/product/sku-12345');
  await page.evaluate(() => document.fonts.ready); // chờ web-font

  await expect(page).toHaveScreenshot('product.png', {
    animations: 'disabled',           // tắt animation khi chụp
    mask: [                            // che HẸP các vùng động
      page.getByTestId('promo-banner'),
      page.getByTestId('user-avatar'),
    ],
    maxDiffPixelRatio: 0.01,           // dung sai nhỏ cho anti-aliasing
  });
});
💡 Ưu tiên cố định nội dung động (clock giả, seed dữ liệu tất định) hơn là mask. Mask giấu vùng khỏi so sánh nên cũng giấu luôn hồi quy có thể xảy ra ở đó; cố định nội dung giữ vùng đó vẫn được kiểm.

3. Dùng embedding để đo khác biệt ngữ nghĩa giữa hai ảnh

Ý tưởng cốt lõi của semantic diffing là biểu diễn mỗi ảnh (hoặc mỗi vùng ảnh) thành một vector embedding — một dãy số nắm bắt nội dung ở mức khái niệm chứ không phải mức pixel. Khi baseline và ảnh mới được nhúng vào cùng không gian vector, khoảng cách giữa hai vector phản ánh mức thay đổi về ý nghĩa. Anti-aliasing hay dịch một pixel gần như không làm vector di chuyển, nên khoảng cách nhỏ. Ngược lại, một nút biến mất hay bố cục vỡ làm nội dung khái niệm thay đổi rõ, nên vector cách xa. Đây là lý do embedding lọc được nhiễu mỹ thuật mà pixel-diff không phân biệt nổi.

Tuy nhiên phải cẩn trọng với việc so sánh embedding ở mức toàn ảnh. Một thay đổi nhỏ nhưng quan trọng — ví dụ nút 'Đặt hàng' bị ẩn ở góc dưới — có thể chỉ làm dịch chuyển nhẹ vector toàn trang và bị bỏ qua. Vì vậy cách làm mạnh hơn là chia trang thành các vùng nghiệp vụ (header, khối giá, khối nút hành động, footer) rồi so embedding theo từng vùng. Thay đổi khu trú sẽ nổi bật ở vùng của nó thay vì bị pha loãng trong toàn ảnh. Việc phân vùng theo ý nghĩa cũng giúp báo cáo dễ đọc: 'khối nút hành động thay đổi đáng kể' rõ ràng hơn 'ảnh khác 2%'.

ts
// semantic-diff.ts — so embedding theo VÙNG nghiệp vụ, không chỉ toàn ảnh
type Vec = number[];
function cosineDistance(a: Vec, b: Vec): number {
  let dot = 0, na = 0, nb = 0;
  for (let i = 0; i < a.length; i++) { dot += a[i]*b[i]; na += a[i]*a[i]; nb += b[i]*b[i]; }
  return 1 - dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-9);
}

// mỗi vùng có embedding riêng -> thay đổi khu trú không bị pha loãng
export function diffRegions(base: Record<string, Vec>, next: Record<string, Vec>) {
  return Object.keys(base).map((region) => ({
    region,
    distance: cosineDistance(base[region], next[region]),
  })).sort((x, y) => y.distance - x.distance); // vùng đổi nhiều nhất lên đầu
}

Vì sao nên so embedding theo vùng thay vì toàn ảnh?

Vì so toàn ảnh làm loãng thay đổi khu trú: một nút quan trọng bị ẩn chỉ dịch nhẹ vector toàn trang và có thể bị bỏ qua. Chia trang thành vùng nghiệp vụ (header, khối giá, khối nút, footer) và so từng vùng khiến thay đổi khu trú nổi bật đúng chỗ, đồng thời báo cáo dễ đọc hơn: 'khối nút thay đổi đáng kể' rõ hơn 'ảnh khác 2%'.

Embedding phản ánh nội dung khái niệm, nên khoảng cách nhỏ với thay đổi mỹ thuật (mờ viền, dịch pixel) và lớn với thay đổi cấu trúc (phần tử mất, bố cục vỡ). Nhưng khoảng cách chỉ là tín hiệu — nó cần ngưỡng và oracle để trở thành quyết định.

4. Phát hiện layout-shift: khi phần tử dịch chuyển sai chỗ

Một lớp hồi quy giao diện đặc biệt quan trọng với thương mại điện tử là layout-shift: phần tử vẫn còn đó, vẫn đúng nội dung, nhưng dịch sang vị trí sai — nút thanh toán bị đẩy xuống dưới màn hình, giá sản phẩm nhảy sang cột khác, hay khối khuyến mãi chồng lên nút Thêm giỏ hàng. Pixel-diff bắt loại lỗi này rất kém vì tổng số pixel khác có thể vẫn nhỏ nếu phần tử chỉ dịch chút ít. Ngược lại, so sánh dựa trên hộp giới hạn (bounding box) của các phần tử nghiệp vụ bắt layout-shift trực tiếp: bạn đối chiếu toạ độ và kích thước của từng phần tử giữa baseline và bản mới.

🔒

Nội dung bị khoá

Bạn đang xem bản xem trước. Đăng nhập để mở khoá toàn bộ bài viết và tất cả tài liệu Tester/QA.

Đã xem 24% nội dung
  • Đọc trọn vẹn mọi bài viết
  • Lưu bài & ghi chú cá nhân
  • Theo dõi tiến độ đã đọc
  • Luyện phỏng vấn, ISTQB, Mock
Đăng nhập để đọc tiếp

Chưa có mã? Lấy mã qua Fanpage / Zalo CyberSoft.

0

💬 Bình luận (0)

Bạn có thể đọc mọi bình luận. Đăng nhập để bình luận

Chưa có bình luận nào. Hãy là người đầu tiên!