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.
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.
// 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
});
});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%'.
// 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%'.
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.
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!