Mundarija (33)
- Bu darsda
- 1. Nega bu kerak?
- 2. Qanday ishlaydi
- 2.1 Oltin rasm va solishtirish
- 2.2 Solishtirish — o'quv modeli
- 3. Playwright: toHaveScreenshot
- 3.1 vazifalar dagi test
- 3.2 Alohida loyiha
- 3.3 Oltin rasmlarni yaratish va yangilash
- 4. Topilma: «Qayta urinish» tugmasi
- 4.1 Rasm nima ko'rsatdi
- 4.2 Sabab: hidden va display
- 4.3 Tuzatish: [hidden] qoidasi !important bilan
- 4.4 E2e testi tasdiqlaydi
- 5. Beqaror piksellar: sabablar va davolar
- 6. Windows va Linux: oltin rasm kimniki?
- 6.1 Muammo
- 6.2 Yo'l: Playwright Docker obrazi (hujjat bo'yicha)
- 7. Boshqa vositalar
- 7.1 Vitest: toMatchScreenshot
- 7.2 Bulut xizmatlari
- 8. Vazifalar qadami: visual regression (holat 13)
- 8.1 Nima o'zgardi
- 8.2 Natija va commit
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): maxDiffPixels va o'lcham
- 3-mashq (qiyin): niqob (mask)
- 4-mashq: Vazifalar qadami
- 11. Real ishda
- Xulosa
- Manbalar
Visual regression testlar: Playwright toHaveScreenshot, oltin rasmlar va piksel farqi
Qisqacha: Visual regression test sahifaning suratini oladi va uni oldin saqlangan oltin rasm (baseline) bilan piksel bo'yicha solishtiradi. Farq chegaradan oshsa — test yiqiladi va farq rasmini beradi. Playwright'da bu bitta qator:
await expect(page).toHaveScreenshot("royxat.png"). Birinchi ishga tushirishda rasm yoziladi, ataylab o'zgarishdan keyin--update-snapshotsbilan yangilanadi. Bunday test kod, atribut va matnni emas, ko'zga ko'rinadigan natijani tekshiradi.vazifalarda u 20-qismdan beri yashirin turgan xatoni topdi: «Qayta urinish» tugmasi internet bor paytda ham ko'rinib turgan.
Bu darsda
- Visual regression qanday ishlashini va boshqa testlar nimani ko'rmasligini tushuntira olasiz.
toHaveScreenshotbilan test yozasiz, oltin rasmlarni yaratasiz va yangilaysiz.threshold,maxDiffPixels,maxDiffPixelRatio,mask,animationssozlamalarini tanlaysiz.- Beqaror piksel farqlarining sabablarini (shrift, animatsiya, kursor, sana, platforma) bilasiz.
- Vitest
toMatchScreenshot, Chromatic, Percy, Argos, Lost Pixel o'rnini taqqoslaysiz.
Oldin bilishingiz kerak: Storybook II, Playwright II, Kaskadni boshqarish: @layer, Ikonkalar.
1. Nega bu kerak?
vazifalar da ro'yxat ustida aloqa qatori bor. Internet yo'q yoki server javob bermasa — "Serverga ulanib bo'lmadi…" matni va «Qayta urinish» tugmasi chiqadi. Internet bor paytda tugma yashirin bo'lishi kerak. Kodda hammasi to'g'ri:
<Tugma
id="qayta-tugma"
hidden={tarmoq !== "xato"}
onClick={onQaytaUrin}
>
Qayta urinish
</Tugma>Ilovada esa tugma Ikonkalar darsidan (20-qism) beri doim ko'rinib turgan. Internet bor, ro'yxat yuklangan — tepada esa ma'nosiz «Qayta urinish». Shu vaqt ichida 348 ta Vitest testi, 10 ta Playwright e2e testi va kursning 159 ta brauzer tekshiruvi har holatda yashil bo'lgan. Hech biri xatoni ko'rmagan.
Nega? Ular atributni tekshirgan: "hidden bormi?" — bor edi. Foydalanuvchi esa atributni emas, ekranni ko'radi. Xatoni ekranning suratiga qaraydigan test topdi — bu darsning mavzusi.
Hayotiy o'xshatish: kitob bosmaxonasida har yangi nashrdan oldin bitta sahifani oldingi nashrning sahifasi ustiga qo'yib, yorug'likka tutib qarashadi. Harf siljigan, rasm xira chiqqan bo'lsa — darhol ko'rinadi. Matnni o'qib chiqish bunday xatoni ko'rmaydi.
2. Qanday ishlaydi
2.1 Oltin rasm va solishtirish
Visual regression testi (visual regression test — ko'rinish regressiyasi testi) — sahifa yoki komponent suratini oldingi tasdiqlangan surat bilan solishtirish. Regressiya — avval ishlagan narsaning buzilishi (regressiya testi). Tasdiqlangan surat — oltin rasm (golden image, baseline, reference — uchala nom ham uchraydi).
sequenceDiagram
participant T as Test
participant B as Brauzer
participant D as Disk (oltin rasm)
T->>B: sahifani och, tayyor bo'lishini kut
B-->>T: surat (PNG)
T->>D: oltin rasm bormi?
alt yo'q
T->>D: yoz (birinchi marta)
else bor
T->>T: piksel bo'yicha solishtir
T-->>T: farq > chegara → yiqil + farq rasmi
endRasmda: birinchi ishga tushirishda oltin rasm yoziladi, keyingilarida yangi surat u bilan solishtiriladi. Yiqilganda uchta rasm qoladi: kutilgan, haqiqiy va farq rasmi (diff) — farqli piksellar qizil bilan.
2.2 Solishtirish — o'quv modeli
Ikki rasmni qanday solishtirish mumkin? Rasm — piksellar massivi, har piksel — rang ([r, g, b]). Kichik model: 4 pikselli "rasm", ikki chegara bilan.
// Har piksel — [r, g, b] (0–255). 4 pikselli "rasm"
const golden = [
[255, 255, 255], [255, 255, 255], [20, 23, 38], [255, 255, 255],
];
// Shrift chetidagi yumshatish: 1 piksel ozgina boshqa rang
const antialias = [
[255, 255, 255], [250, 250, 250], [20, 23, 38], [255, 255, 255],
];
// Xato: 2 pikselda tugma paydo bo'ldi
const bug = [
[255, 255, 255], [139, 130, 245], [139, 130, 245], [255, 255, 255],
];
// Ikki rang qanchalik farq qiladi: 0 (bir xil) … 1 (oq va qora)
function colorDistance(a, b) {
const diffs = a.map((v, i) => Math.abs(v - b[i]));
return Math.max(...diffs) / 255;
}
function compare(expected, actual, options = {}) {
const { threshold = 0.2, maxDiffPixelRatio = 0 } = options;
let diff = 0;
for (let i = 0; i < expected.length; i++) {
if (colorDistance(expected[i], actual[i]) > threshold) diff++;
}
const ratio = diff / expected.length;
return { diff, ratio, pass: ratio <= maxDiffPixelRatio };
}
console.log(compare(golden, antialias));
console.log(compare(golden, bug));
console.log(compare(golden, bug, { maxDiffPixelRatio: 0.5 }));Konsolda:
{ diff: 0, ratio: 0, pass: true }
{ diff: 2, ratio: 0.5, pass: false }
{ diff: 2, ratio: 0.5, pass: true }Ikki xil chegara bor va ular har xil savolga javob beradi:
threshold— bitta piksel uchun: "bu ikki rang bir xilmi?"250va255— 0.02 farq, 0.2 dan kichik: bir xil deb hisoblandi. Shrift chetlaridagi mayda farqlar shunday o'tib ketadi.maxDiffPixelRatio— butun rasm uchun: "nechta piksel farq qilsa ham mayli?" 0 — bitta ham. 0.5 — yarmi (bu juda yumshoq: xato ham o'tib ketdi, uchinchi qatorga qarang).
Playwright ham shunday ishlaydi, faqat rangni aqlliroq solishtiradi: pixelmatch kutubxonasi YIQ rang fazosida (ko'z sezadigan farq) hisoblaydi va yumshatilgan (anti-aliased) piksellarni alohida taniydi. Playwright'da threshold sukut bo'yicha 0.2, piksellar soni chegarasi esa sukut bo'yicha yo'q — bitta farqli piksel ham testni yiqitadi.
Tekshirib ko'ring:
threshold: 0vamaxDiffPixelRatio: 0.01— bu qanday test?threshold: 0.9vamaxDiffPixelRatio: 0chi?
Javob
Birinchisi: har qanday rang farqini "farq" deb sanaydi, lekin 1 % gacha shunday pikselga ruxsat beradi — mayda shovqinni o'tkazadi, katta o'zgarishni ushlaydi. Ikkinchisi: deyarli har qanday rang o'zgarishini "bir xil" deb hisoblaydi (oq va kulrang ham), keyin bitta ham farqga ruxsat bermaydi — qattiq ko'rinadi, aslida juda yumshoq: rangi o'zgargan tugmani sezmaydi.
3. Playwright: toHaveScreenshot
3.1 vazifalar dagi test
import { expect, test } from "@playwright/test";
import { soxtaApi } from "./soxta-api.ts";
// tayyor — sahifa ma'lumot bilan chizilganini bildiradigan sarlavha
const SAHIFALAR = [
{ nomi: "royxat", yol: "/", tayyor: "Non olish #bozor" },
{ nomi: "sozlamalar", yol: "/sozlamalar", tayyor: "Sozlamalar" },
{ nomi: "vazifa", yol: "/vazifa/1", tayyor: "Non olish #bozor" },
] as const;
for (const tema of ["light", "dark"] as const) {
test.describe(`${tema} tema`, () => {
// Ilova "tizim" temasida — brauzerning prefers-color-scheme i
test.use({ colorScheme: tema });
for (const { nomi, yol, tayyor } of SAHIFALAR) {
test(`${nomi} sahifasi`, async ({ page }) => {
await soxtaApi(page);
await page.goto(yol);
// Server javobi kelib, sahifa ma'lumot bilan
// chizilguncha kutamiz
await expect(
page.getByText(tayyor, { exact: true }).first(),
).toBeVisible();
await expect(page).toHaveScreenshot(`${nomi}-${tema}.png`, {
fullPage: true,
// Motion (JS) animatsiyalari ham — "disabled" CSS'ni
// to'xtatadi, qolganini sahifa tinchlanguncha
// Playwright o'zi kutadi
animations: "disabled",
// Kursor (caret) miltillashi — har suratda boshqacha
caret: "hide",
});
});
}
});
}Kanon izohlari telefonga sig'ishi uchun bo'lingan. Nima bo'layapti:
- Ikki sikl: 2 tema × 3 sahifa = 6 test, har biri bitta surat.
colorScheme— brauzerprefers-color-schemeni shunday deb aytadi; ilova "tizim" temasida, shuning uchun qorong'i rejim ham suratga tushadi. soxtaApi(page)— Playwright I dagipage.routesoxta serveri: ma'lumot har safar bir xil. Haqiqiy server bilan surat har kuni boshqacha bo'lardi.- Tayyorlikni kutish — eng muhim qator. Surat sahifa ma'lumot bilan chizilgandan keyin olinsin. Birinchi variantda kutish faqat
#aloqaelementiga qarardi, u esa faqat ro'yxat sahifasida bor — 6 testdan 4 tasi "element topilmadi" bilan yiqildi. Har sahifaga o'z "tayyor" matni berildi. fullPage: true— faqat ekran (1280×720) emas, butun uzun sahifa.
toHaveScreenshot suratni darhol olmaydi: u ketma-ket ikki surat bir xil chiqguncha qayta suratga oladi, keyin oxirgisini oltin rasm bilan solishtiradi. Yarim chizilgan sahifa shunday o'tkazib yuboriladi.
3.2 Alohida loyiha
playwright.config.ts da suratli testlar alohida loyiha — vizual:
expect: {
// 22/#13: farqli piksellar ulushi — 1 % gacha. Bir mashinada
// 0 bilan ham barqaror (o'lchandi: 30/30), zaxira — Chrome/shrift
// yangilanishi antialiasing'ni ozgina o'zgartirsa.
// «Qayta urinish» tugmasi xatosi 3–5 %
toHaveScreenshot: { maxDiffPixelRatio: 0.01 },
}, {
// 22/#13: visual regression. Oltin rasmlar platformaga
// bog'liq — CI (Linux) uchun Linux'da yaratiladi;
// lokal Windows'da — o'ziniki
name: "vizual",
testMatch: /vizual.spec.ts/,
use: {
...devices["Desktop Chrome"],
storageState: "e2e/.auth/foydalanuvchi.json",
},
dependencies: ["kirish"],
},chrome loyihasiga testIgnore: /vizual.spec.ts/ qo'shildi, skriptlar: "test:e2e": "playwright test --project=chrome", "test:vizual": "playwright test --project=vizual". Nega ajratildi: suratli testlar mashinaga bog'liq (keyingi bo'limlar) va ataylab dizayn o'zgarishida hammasi birga qizarishi mumkin — oddiy e2e testlar bundan aziyat chekmasin.
3.3 Oltin rasmlarni yaratish va yangilash
npx playwright test --project=vizual --update-snapshotsO'lchov: 15.9 s, 6 ta PNG, jami 312 KB. Ular test fayli yonidagi papkaga tushadi:
e2e/vizual.spec.ts-snapshots/
royxat-light-vizual-win32.png
royxat-dark-vizual-win32.png
sozlamalar-light-vizual-win32.png
sozlamalar-dark-vizual-win32.png
vazifa-light-vizual-win32.png
vazifa-dark-vizual-win32.pngNom shabloni: <siz bergan nom>-<loyiha>-<platforma>.png. Platforma (win32, linux, darwin) nomda — chunki bir sahifa har tizimda boshqacha chiziladi ("Windows va Linux" bo'limi). Rasmlar git'ga kiradi: ular test natijasi emas, testning "kutilgan qiymati".
--update-snapshots (qisqasi -u) to'rt rejimda ishlaydi:
| Rejim | Nima qiladi |
|---|---|
bayroqsiz (missing) |
faqat yo'q rasmni yozadi; test baribir yiqiladi ("snapshot doesn't exist") |
-u yoki changed |
farq qilganlarini qayta yozadi |
all |
hammasini qayta yozadi |
none |
hech narsa yozmaydi |
Qoida: -u ni faqat ataylab o'zgarishdan keyin bosing — va yangi rasmlarni commit'dan oldin ko'zingiz bilan ko'ring. "Test qizil — -u bosaman" degani xatoni oltin rasmga aylantirish degani. Snapshot darsidagi ogohlantirish bu yerda ikki barobar kuchli: rasm farqini kod ko'rib chiqishda o'qish matndan ham qiyin.
Tekshiruv uch marta: 17.1 / 14.2 / 13.7 s devor, 7 passed (6 surat + kirish).
4. Topilma: «Qayta urinish» tugmasi
4.1 Rasm nima ko'rsatdi
Birinchi oltin rasmlarni ko'zdan kechirganda ro'yxat suratida g'alati narsa ko'rindi: soxta server ishlayapti, ro'yxat chizilgan, aloqa matni bo'sh — tugma esa turibdi. Bu — oltin rasmni tasdiqlashdan oldin ko'rish qoidasining amaldagi foydasi: rasm "kutilgan qiymat" bo'lishidan oldin u to'g'rimi, deb so'rash kerak.
4.2 Sabab: hidden va display
hidden atributi o'zi hech narsani yashirmaydi. Uni brauzerning o'z CSS qoidasi yashiradi — brauzer standart stillari (UA stylesheet) ichida: [hidden] { display: none }. Bu qoida kaskadda eng kuchsiz — har qanday muallif CSS'i uni bosadi (kaskad).
Tugma ning sinflari orasida inline-flex bor — Ikonkalar darsida ikonka va matnni bir qatorga qo'yish uchun qo'shilgan. Tailwind'da u display: inline-flex beradi. Muallif CSS'i brauzer qoidasidan ustun — tugma ko'rinadi. Tailwind'ning o'z reset'i (preflight) bu holatni tuzatadigan qoida bilan keladi, lekin shadcn/ui I darsida preflight'ni ataylab ulamagan edik.
Bu holatni natija oynasida ko'ramiz — u haqiqiy Chrome, CSS haqiqatan qo'llanadi:
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
// Tailwind'dagidek: utilita sinfi qatlam (layer) ichida
const css = `
@layer base, utilities;
@layer utilities {
.inline-flex { display: inline-flex; }
}
`;
function Aloqa({ tarmoq }: { tarmoq: string }) {
return (
<div>
<style>{css}</style>
<p>Internet bor.</p>
<button className="inline-flex" hidden={tarmoq !== "xato"}>
Qayta urinish
</button>
</div>
);
}
test("hidden atributi bor", () => {
render(<Aloqa tarmoq="tayyor" />);
const btn = screen.getByText("Qayta urinish");
expect(btn).toHaveAttribute("hidden");
});
test("jest-dom: ko'rinmaydi", () => {
render(<Aloqa tarmoq="tayyor" />);
const btn = screen.getByText("Qayta urinish");
expect(btn).not.toBeVisible();
});
test("ekranda: brauzer chizdimi?", () => {
render(<Aloqa tarmoq="tayyor" />);
const btn = screen.getByText("Qayta urinish");
console.log(getComputedStyle(btn).display);
expect(btn.checkVisibility()).toBe(false);
});Natija oynasida (Chrome 155) konsol:
✓ hidden atributi bor
✓ jest-dom: ko'rinmaydi
inline-flex
× ekranda: brauzer chizdimi?
→ expected true to be false // Object.is equality
Testlar: 1 yiqildi, 2 o'tdi (jami 3)Uch test — uch xil javob:
- Atribut — bor. E2e testlar va kursning brauzer tekshiruvlari shuni so'ragan.
- jest-dom
toBeVisible— "ko'rinmaydi" deydi va adashadi. Biz jest-dom 7.0.1 manba kodiga qaradik:toBeVisibledisplayni ham tekshiradi, lekinhiddenatributi bor elementni CSS'dan qat'i nazar "ko'rinmas" deb hisoblaydi. jsdom'dagi komponent testlari bu xatoni hech qachon ko'rmas edi. - Brauzerning o'zi —
getComputedStyleinline-flexdeydi,checkVisibility()—true(brauzer elementni chizgan). Faqat shu test haqiqatni aytdi.
Yana bir qiziq farq: shu blokni jsdom'da (kursning tekshiruvchisi) ishga tushirsak, getComputedStyle none qaytaradi — jsdom qatlamli CSS'ni Chrome kabi hisoblamaydi. "jsdom — brauzer emas" (Browser Mode) bu yerda yana bir bor tasdiqlandi. Shuning uchun blok sayt bayrog'i bilan — natijasi faqat Chrome'da to'g'ri.
4.3 Tuzatish: [hidden] qoidasi !important bilan
asos.css ga — Tailwind preflight'idagi o'sha qoida:
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}Qatorma-qator:
[hidden]—hiddenatributli har element.:where(:not([hidden="until-found"]))—hidden="until-found"dan tashqari (bu qiymat sahifada qidiruv topguncha yashiradi, lekindisplay: noneemas — uni buzmaslik kerak).:wherespecificity'ni 0 qiladi (specificity).!important— eng muhimi.asos.css@layer baseichida, utilitalar esa@layer utilitiesda. Oddiy qoidalarda keyingi qatlam ustun — utilita yutadi. Muhim (!important) qoidalarda esa tartib teskari: oldingi qatlam ustun (@layer). Shunday qilib base'dagi!importantutilitadan kuchli.
Natija oynasida — o'sha blok, CSS'ga shu qoida qo'shilgan:
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
const css = `
@layer base, utilities;
@layer utilities {
.inline-flex { display: inline-flex; }
}
@layer base {
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
}
`;
function Aloqa({ tarmoq }: { tarmoq: string }) {
return (
<div>
<style>{css}</style>
<p>Internet bor.</p>
<button className="inline-flex" hidden={tarmoq !== "xato"}>
Qayta urinish
</button>
</div>
);
}
test("ekranda: brauzer chizdimi?", () => {
render(<Aloqa tarmoq="tayyor" />);
const btn = screen.getByText("Qayta urinish");
console.log(getComputedStyle(btn).display);
expect(btn.checkVisibility()).toBe(false);
});
test("xato holatida — ko'rinadi", () => {
render(<Aloqa tarmoq="xato" />);
const btn = screen.getByText("Qayta urinish");
expect(btn.checkVisibility()).toBe(true);
});Natija oynasida (Chrome 155) konsol:
none
✓ ekranda: brauzer chizdimi?
✓ xato holatida — ko'rinadi
Testlar: 2 o'tdi (jami 2)Ikkinchi test muhim: tuzatish tugmani "har doim yashiradigan" bo'lib qolmasin — xato holatida u kerak.
Tekshirib ko'ring:
!importantni olib tashlab, qoidani@layer basedan tashqariga (qatlamsiz) qo'ysak ham ishlaydimi?
Javob
Ha — qatlamsiz (unlayered) oddiy qoidalar har qanday qatlamdagi oddiy qoidalardan ustun. Lekin u holda hidden li elementga yozilgan har qanday qatlamsiz display qoidasi uni yana bosishi mumkin, specificity'ga qarab. Tailwind preflight'i va kanon !important ni tanlagan: "atribut bor — demak yashirin" qoidasi kaskadning boshqa joylariga bog'liq bo'lmasin.
4.4 E2e testi tasdiqlaydi
Xato topildi — endi uni qayta kelmaydigan qilish kerak. tarmoq.spec.ts dagi "server 500" testi oxiriga — Playwright'ning toBeHidden i:
// 22/#13: tugma hidden atributi bilan yashirinadi — Tailwind
// inline-flex uni bosib ketardi (visual regression topdi,
// asos.css da tuzatildi)
await expect(
page.getByRole("button", { name: "Qayta urinish" }),
).toBeHidden();Playwright jest-dom'dan farqli: u atributga emas, chizilganiga qaraydi (element bo'sh o'lchamli yoki visibility: hidden bo'lsa — yashirin). Tekshirdik: tuzatishsiz bu qator Received: visible bilan yiqildi, tuzatish bilan 4 passed. Endi xatoni ikkita test qo'riqlaydi: surat va aniq tekshiruv. Surat "nimadir o'zgardi" deydi, toBeHidden — "aynan nima".
Eski oltin rasm bilan solishtirganda farq katta chiqdi: kutilgan rasm 1280×1961, yangisi 1280×1917 (tugma yo'qolib, sahifa 44 px qisqardi), 61 501 farqli piksel (light) va 120 627 (dark):
- light (61 501 px)Playwright hisoboti3 %
- dark (120 627 px)Playwright hisoboti5 %
- chegaramaxDiffPixelRatio 0.011 %
Manba: O'lchandi: vazifalar kanoni (holat 13), Playwright 1.64.0, Chrome, 1280px fullPage, i5-12500H, Windows 11, 2026-10-20
Bitta tugma 3–5 % piksel farqi beradi: u o'zi kichik, lekin uning ostidagi hamma narsa 44 px yuqoriga siljidi — siljigan har piksel ham "farq". Dark'da farq ko'p, chunki fon va matn kontrasti boshqacha. Ikkalasi 1 % chegaradan ancha yuqori. Rasmlar yangilandi.
Narx: CSS 5.10 → 5.13 KB gzip. JS kodi baytma-bayt o'zgarmadi (634 255 B); gzip 206.73 → 206.74 KB — CSS fayl nomidagi hash o'zgargani uchun siqish natijasi ozgina boshqacha.
5. Beqaror piksellar: sabablar va davolar
Visual test eng injiq test turi: sahifada hech narsa buzilmagan, surat esa boshqacha. Ko'p uchraydigan sabablar:
| Sabab | Belgisi | Davosi |
|---|---|---|
| Animatsiya, transition | har safar boshqa kadr | animations: "disabled" |
| Kursor miltillashi | maydonda chiziq bor/yo'q | caret: "hide" |
| Sana, vaqt, tasodifiy ID | matn har kuni boshqa | soxta soat, mask |
| Tashqi ma'lumot | ro'yxat o'zgaradi | soxta API (page.route) |
| Shrift yuklanishi | matn kengligi o'zgaradi | shriftni lokal qilish, yuklanishni kutish |
| Boshqa OT yoki brauzer | butun sahifa siljigan | har platformaga o'z rasmi, bitta muhit (Docker) |
| Video, karta, reklama | doim har xil | mask yoki stylePath bilan yashirish |
animations va caret uchun Playwright hujjatiga ko'ra toHaveScreenshot da sukut ham "disabled" va "hide" — kanon ularni aniqlik uchun yozgan (page.screenshot() da sukut boshqacha).
mask — elementlar ustiga rangli to'rtburchak chizadi:
await expect(page).toHaveScreenshot("buyurtma.png", {
// Vaqt va buyurtma raqami har safar boshqa
mask: [page.getByTestId("vaqt"), page.locator(".buyurtma-raqami")],
maskColor: "#FF00FF",
});stylePath — surat oldidan qo'shimcha CSS fayl: video, .reklama { visibility: hidden }. Bu misollar vazifalar da yo'q — bizda soxta API yetarli bo'ldi.
Chegarani qanday tanlash kerak? Kanon o'lchadi: maxDiffPixelRatio: 0 bilan ham --repeat-each=5 (har test 5 marta) — 30/30 surat bir xil. Bitta mashinada, bitta Chrome'da shovqin yo'q. 0.01 — zaxira: Chrome yoki shrift yangilanganda yumshatish (antialiasing) ozgina o'zgarsa, testlar to'satdan qizarmasin. Xato 3–5 % edi — 1 % chegara uni bemalol ushlaydi. Qoida: chegarani "o'tib ketsin" deb emas, o'lchab tanlang — haqiqiy xato chegaradan bir necha baravar katta bo'lsin.
Tekshirib ko'ring: Bosh sahifada "Bugun: 20-oktabr" yozuvi bor. Visual test ertaga yiqiladi. Ikki yechim ayting.
Javob
(1) Vaqtni soxtalash — Playwright'da page.clock.setFixedTime(…) (Vitest II dagi soxta soat g'oyasi): har kuni bir xil sana. (2) O'sha elementni mask bilan yopish. Birinchisi yaxshiroq — sana joyi va uzunligi ham tekshiriladi; mask faqat chiqarib tashlaydi.
6. Windows va Linux: oltin rasm kimniki?
6.1 Muammo
Oltin rasmlar Windows'da yaratildi: nomida win32. GitHub Actions'dagi Linux runner *-vizual-linux.png ni qidiradi va topmaydi — test snapshot doesn't exist bilan yiqiladi. Linux'da rasm yaratib qo'yganda ham farq qoladi: Windows matnni Segoe UI bilan, Linux esa boshqa shriftlar (DejaVu, Liberation) bilan chizadi. Harf kengligi boshqacha — butun sahifa siljiydi.
Shuning uchun vazifalar da visual testlar CI'ga kirmagan. test.yml da izoh qoldirildi:
# Visual regression (npm run test:vizual) bu yerda
# YO'Q: oltin rasmlar Windows'da yaratilgan
# (*-vizual-win32.png), Linux runner ularni topmaydi
# va shriftni boshqacha chizadi. CI'ga qo'shish uchun
# avval Linux rasmlari: Playwright Docker obrazida
# --update-snapshots (#13)Halol holat: bizda visual testlar faqat lokal, bitta Windows mashinasida ishlaydi. Bu ham foyda (xatoni topdi), lekin jamoada har kim o'z mashinasida yurgizishi kerak.
6.2 Yo'l: Playwright Docker obrazi (hujjat bo'yicha)
Playwright rasmiy Docker obrazlarini chiqaradi: mcr.microsoft.com/playwright:v1.64.0-noble (Ubuntu 24.04, brauzerlar va shriftlar o'rnatilgan). G'oya: oltin rasmlarni ham, CI'dagi tekshiruvni ham bitta obrazda yurgizish — shunda Windows'dagi dasturchi ham, Linux runner ham bir xil piksellarni oladi. Hujjatga ko'ra taxminan shunday:
docker run --rm -it -v "$(pwd)":/work -w /work \
mcr.microsoft.com/playwright:v1.64.0-noble \
/bin/bash -c "npm ci && npx playwright test --project=vizual -u"Bu yo'l vazifalar da sinalmagan va o'lchanmagan — shu kompyuterda Docker bilan tekshirilmadi. Sinashdan oldin ikki narsani biling. Birinchisi hujjatda yozilgan: obrazdagi Playwright versiyasi package.json dagi bilan aynan bir xil bo'lsin, aks holda brauzer topilmaydi. Ikkinchisi — bizning sozlama: obrazda Playwright'ning o'z brauzerlari bor, vazifalar esa channel: "chrome" (Google Chrome) ishlatadi. Obrazda bu qanday ishlashini ham tekshirish kerak. O'z repongizda shu yo'lni sinab, natijani o'zingiz tekshirasiz.
7. Boshqa vositalar
7.1 Vitest: toMatchScreenshot
Browser Mode da Vitest'ning o'z visual tekshiruvi bor (Vitest 4 dan):
import { page } from "vitest/browser";
test("tugma — xavfli variant", async () => {
chiz(<Tugma variant="xavfli">O'chirish</Tugma>);
const tugma = page.getByRole("button", { name: "O'chirish" });
await expect.element(tugma).toMatchScreenshot("tugma-xavfli", {
comparatorName: "pixelmatch",
comparatorOptions: { allowedMismatchedPixelRatio: 0.01 },
});
});Hujjatga ko'ra: rasmlar __screenshots__/<test fayli>/ da, nomida brauzer va platforma (…-chromium-win32.png); threshold sukut 0.1; yangilash — vitest -u. Playwright'dan farqi: butun sahifa emas, odatda bitta komponent suratga olinadi. vazifalar da ishlatilmagan — bitta vosita (Playwright) yetarli bo'ldi, ikkinchi to'plam oltin rasm ikki barobar ishlov talab qiladi.
7.2 Bulut xizmatlari
| Xizmat | Nima qiladi | vazifalar da |
|---|---|---|
| Chromatic | Storybook story'larining suratlari, ko'rib chiqish ("qabul") | yo'q — hisob kerak |
| Percy (BrowserStack) | Playwright/Cypress/Storybook suratlari bulutda | yo'q — hisob kerak |
| Argos | Playwright suratlarini yig'ib solishtiradi, ochiq kodli | yo'q — hisob kerak |
| Lost Pixel | Storybook va sahifa suratlari, ochiq kodli rejimi bor | yo'q — sinalmagan |
Hammasi hujjat bo'yicha; kanonda birortasi ishlatilmagan. Ularning umumiy foydasi — bizdagi ikki muammoni hal qilish: suratlar bitta muhitda olinadi (platforma muammosi yo'q) va farqni jamoa veb-sahifada ko'rib, tugma bilan "qabul" qiladi (-u va commit o'rniga). Narxi — hisob, tashqi xizmatga kod suratlari yuboriladi, bepul rejalarida cheklovlar. npm'da (2026-10 holatiga): chromatic 18.13.0, @argos-ci/playwright 7.7.1, lost-pixel 3.22.0.
8. Vazifalar qadami: visual regression (holat 13)
8.1 Nima o'zgardi
| Fayl | O'zgarish |
|---|---|
e2e/vizual.spec.ts |
3 sahifa × light/dark = 6 surat |
e2e/vizual.spec.ts-snapshots/ |
6 PNG, 312 KB (git'da) |
playwright.config.ts |
expect.toHaveScreenshot, vizual loyihasi, testIgnore |
package.json |
test:e2e (--project=chrome), test:vizual |
src/asos.css |
[hidden] qoidasi |
e2e/tarmoq.spec.ts |
toBeHidden |
test.yml |
visual testlar nega yo'qligi haqida izoh |
Yangi paket yo'q — toHaveScreenshot @playwright/test ichida.
8.2 Natija va commit
Testlar: Vitest 348, brauzer 6, storybook 12, e2e 10, vizual 6 (+ kirish). npm run test:vizual — 7 passed (12.1s). Lint, format:check, tip, knip — toza. JS 206.7 KB.
git switch -c test/vizual
git add -A
git commitCommit (kanon): test: visual regression (toHaveScreenshot) — 3 sahifa light/dark; fix: hidden atributi Tailwind'dan ustun.
Commit xabari ikki ishni aytadi: test qo'shildi va xato tuzatildi. Bitta commit'da — chunki tuzatish suratlarga ta'sir qiladi: tuzatishsiz oltin rasm xatoni "kutilgan" deb saqlab qo'yardi.
9. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
A snapshot doesn't exist … writing actual |
birinchi ishga tushirish | rasmni ko'rib, commit qiling |
CI'da snapshot doesn't exist |
rasm boshqa platformada | o'sha platformada (Docker) yarating |
Expected an image 1280px by 1961px, received … |
sahifa balandligi o'zgardi | haqiqiy o'zgarishmi — tekshiring |
| har ishga tushirishda boshqa piksellar | animatsiya, sana, ma'lumot | animations, soxta soat, mask, soxta API |
| yarim chizilgan sahifa surati | tayyorlik kutilmagan | aniq "tayyor" elementni kuting |
| xato o'tib ketdi | chegara juda katta | o'lchab tanlang (xato chegaradan bir necha baravar katta) |
-u dan keyin xato oltin bo'ldi |
rasm ko'rilmagan | yangi rasmni commit'dan oldin oching |
10. Mashqlar
Mashqlar kurs/mashqlar/22/13-vizual/ da. Yechimlarni «Ishga tushir» bilan sinang.
1-mashq (oson): bo'sh joylarni to'ldiring
- Tasdiqlangan surat —
rasm - Oltin rasmlarni qayta yozish —
npx playwright test - Bitta piksel ranglari "bir xilmi" chegarasi —
- Elementni suratda yopish —
: [page.getByTestId("vaqt")] - Rasm nomidagi Windows belgisi —
Yechim
- oltin (golden, baseline) — u testning kutilgan qiymati, git'da saqlanadi.
- --update-snapshots (
-u) — faqat ataylab o'zgarishdan keyin. - threshold — piksel darajasi (sukut 0.2); butun rasm uchun —
maxDiffPixels/maxDiffPixelRatio. - mask — locator'lar massivi, ustiga rangli to'rtburchak.
- win32 — Linux'da
linux, macOS'dadarwin.
2-mashq (o'rta): maxDiffPixels va o'lcham
«Solishtirish — o'quv modeli» dagi compare ni kengaytiring: (1) maxDiffPixels sozlamasi (farqli piksellar soni chegarasi) — ikkala chegara berilsa, ikkalasi ham bajarilishi kerak; (2) rasmlar uzunligi har xil bo'lsa, solishtirmasdan { pass: false, reason: "o'lcham: 4 ≠ 3" } qaytarsin. maxDiffPixels berilmasa — cheklov yo'q (Infinity).
Ishora: standart qiymatlar destrukturizatsiyada (destrukturizatsiya); && bilan ikki shart.
Yechim
function colorDistance(a, b) {
const diffs = a.map((v, i) => Math.abs(v - b[i]));
return Math.max(...diffs) / 255;
}
function compare(expected, actual, options = {}) {
const {
threshold = 0.2,
maxDiffPixelRatio = 0,
maxDiffPixels = Infinity,
} = options;
if (expected.length !== actual.length) {
const reason = `o'lcham: ${expected.length} ≠ ${actual.length}`;
return { pass: false, reason };
}
let diff = 0;
for (let i = 0; i < expected.length; i++) {
if (colorDistance(expected[i], actual[i]) > threshold) diff++;
}
const ratio = diff / expected.length;
const pass = ratio <= maxDiffPixelRatio && diff <= maxDiffPixels;
return { diff, ratio, pass };
}
const white = [255, 255, 255];
const violet = [139, 130, 245];
const golden = [white, white, white, white];
const oneDot = [white, violet, white, white];
console.log(compare(golden, oneDot, { maxDiffPixelRatio: 1 }));
console.log(
compare(golden, oneDot, { maxDiffPixelRatio: 1, maxDiffPixels: 0 }),
);
console.log(compare(golden, [white, white, white]));Konsolda:
{ diff: 1, ratio: 0.25, pass: true }
{ diff: 1, ratio: 0.25, pass: false }
{ pass: false, reason: "o'lcham: 4 ≠ 3" }Ikkinchi chaqiruvda ulush bo'yicha hammasiga ruxsat bor, lekin son bo'yicha birortasiga ham yo'q — qattiqrog'i yutadi (Vitest toMatchScreenshot hujjati ham shunday deydi). Playwright o'lcham boshqa bo'lganda Expected an image … received … deb yozadi va farqni ham hisoblaydi — bizning model soddaroq: darhol to'xtaydi.
3-mashq (qiyin): niqob (mask)
compare ga mask sozlamasini qo'shing: indekslar massivi ([1, 2]) — bu piksellar solishtirilmaydi va ulush ham faqat qolgan piksellar bo'yicha hisoblanadi. Sinov: 6 pikselli "sahifa", 4- va 5-pikselda soat (12:00 → 12:01 — rangi o'zgaradi). Niqobsiz — yiqilsin, niqob bilan — o'tsin. Uchinchi holat: niqob ostida bo'lmagan 1-pikselda xato — niqob bo'lsa ham yiqilsin.
Ishora: new Set(mask) va has(i) (Set); ulushning maxraji — expected.length - mask.length.
Yechim
function colorDistance(a, b) {
const diffs = a.map((v, i) => Math.abs(v - b[i]));
return Math.max(...diffs) / 255;
}
function compare(expected, actual, options = {}) {
const { threshold = 0.2, maxDiffPixelRatio = 0 } = options;
const { mask = [] } = options;
const hidden = new Set(mask);
let diff = 0;
for (let i = 0; i < expected.length; i++) {
if (hidden.has(i)) continue;
if (colorDistance(expected[i], actual[i]) > threshold) diff++;
}
const ratio = diff / (expected.length - hidden.size);
return { diff, ratio, pass: ratio <= maxDiffPixelRatio };
}
const W = [255, 255, 255];
const D = [20, 23, 38];
const R = [220, 38, 38];
const golden = [W, D, W, W, D, D];
const nextMinute = [W, D, W, W, W, D];
const brokenButton = [W, R, W, W, W, D];
console.log(compare(golden, nextMinute).pass);
console.log(compare(golden, nextMinute, { mask: [4, 5] }).pass);
console.log(compare(golden, brokenButton, { mask: [4, 5] }));Konsolda:
false
true
{ diff: 1, ratio: 0.25, pass: false }Niqob soatni "ko'rmaslik" ga aylantirdi, lekin qolgan sahifani himoyada qoldirdi — 1-pikseldagi xato baribir ushlandi. Ulushning maxrajidan niqob piksellari olib tashlangani uchun 1 farq — 4 dan 1, ya'ni 0.25. Playwright'da niqob boshqacha ishlaydi: u surat olishda o'sha joyni bir xil rang bilan bo'yaydi (oltin rasm ham shunday olingan), maxraj o'zgarmaydi — natija bir xil, hisob boshqa.
4-mashq: Vazifalar qadami
test/vizual branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling.
Yechim
Tekshiruv ro'yxati:
- Avval tuzatishsiz:
vizual.spec.tsvaplaywright.config.tsni yozib,npx playwright test --project=vizual -u— 6 rasm.royxat-light-…pngni oching: «Qayta urinish» ko'rinadimi? asos.cssga[hidden]qoidasini qo'shing;npm run test:vizual— 2 ta (royxat light/dark) yiqiladi, farq rasmlariniplaywright-reportda ko'ring (o'lcham 1961 → 1917).- Farq ataylab —
-ubilan yangilang, rasmlarni yana oching. tarmoq.spec.tsgatoBeHidden; tuzatishni vaqtincha olib tashlab,Received: visibleni ko'ring. Qaytaring.npx playwright test --project=vizual --repeat-each=5— hammasi yashil (bitta mashinada shovqin yo'q).
11. Real ishda
- Dizayn tizimlari va katta saytlar visual testni komponent (Storybook story) va sahifa darajasida ishlatadi; farqni dizayner ko'rib "qabul" qiladi.
- CSS refaktoringi (Tailwind versiyasini ko'tarish, reset almashtirish) — visual testlarsiz xavfli: yuzlab sahifani qo'lda ko'rib chiqib bo'lmaydi. Bizning topilma aynan shunday: reset (preflight) yo'qligining oqibati.
- Narx: oltin rasmlar repoda joy egallaydi, platforma muammosi bor, beqaror bo'lishi oson. Shuning uchun visual testlar ko'pincha bir nechta muhim sahifa uchun yoziladi, hamma narsa uchun emas.
- Intervyu savollari: "visual test nimani ushlaydi, unit test nimani?", "oltin rasm CI'da nega yiqiladi?", "beqaror suratni qanday barqaror qilasiz?".
Xulosa
- Visual regression testi suratni oltin rasm bilan piksel bo'yicha solishtiradi; atribut va matn emas, ko'rinadigan natija tekshiriladi.
- Playwright:
toHaveScreenshot— ketma-ket ikki bir xil suratni kutadi;-u— faqat ataylab o'zgarishdan keyin va rasmni ko'rib. - Chegaralar:
threshold(piksel),maxDiffPixels/maxDiffPixelRatio(rasm); o'lchab tanlang. Beqarorlikka —animations,caret,mask, soxta ma'lumot va soat. - Rasm platformaga bog'liq (
win32/linux):vazifalarda visual testlar CI'siz; Linux yo'li — Playwright Docker obrazi (hujjat bo'yicha, sinalmagan). - Biz topdik: Tailwind
inline-flexhiddenni bosgan — e2e (atribut) va jest-domtoBeVisibleko'rmagan, surat ko'rgan;[hidden]qoidasi!importantbilan base qatlamida.
Keyingi dars: Accessibility testlari — axe bilan sahifalarni avtomatik tekshirish, uning chegarasi va klaviatura oqimini qo'lda va testda tekshirish.
Manbalar
- Playwright: Visual comparisons (playwright.dev/docs/test-snapshots),
PageAssertions.toHaveScreenshot,TestProject.expect, CLI--update-snapshots, Docker - Vitest: Visual regression testing,
toMatchScreenshot(vitest.dev/guide/browser/visual-regression-testing) - MDN:
hiddenatributi (until-found),Element.checkVisibility(),@layerva!important - Tailwind CSS v4 preflight (
[hidden]:where(:not([hidden="until-found"]))) - jest-dom 7.0.1 manba kodi (
toBeVisible—hiddenatributi) - Haqiqiy chiqishlar: Playwright 1.64.0, Chrome 155, Node 24.21.0, Windows 11, i5-12500H (2026-10-20)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!