Ders 24 / 27
Görsel Gerileme Testi
Referans görüntüyle aday görüntünün piksel düzeyinde karşılaştırılması, piksel toleransı ile oran eşiğinin ayrı işlevleri, maskeleme kararı ve karşılaştırmayı anlamlı kılan belirlenimcilik koşulları.
İçindekiler
Akış testi ölçüm satırının göründüğünü doğrular; nasıl göründüğünü doğrulamaz. Bir biçem kuralı yanlış kapsayıcıya uygulandığında, bir aralık değeri iki katına çıktığında ya da kaydet düğmesi arka planla aynı renge geldiğinde önceki iki dersin testleri geçmeyi sürdürür. Öge ağaçtadır, rolü yerindedir, tıklanabilir — ama ekranda yanlıştır.
Görsel gerileme testi bu boşluğu doldurur: bir ekran görüntüsünü kabul edilmiş bir referansla karşılaştırır ve farkı sayar.
Karşılaştırılan Şey
Üç görüntü vardır. Referans görüntü, daha önce incelenip onaylanmış hâldir ve depoda saklanır. Aday görüntü, o anki koddan üretilen hâldir. Fark görüntüsü ise ikisinin uyuşmadığı pikselleri işaretler.
Karşılaştırma üç ölçekte yapılabilir. Tam sayfa karşılaştırması en geniş kapsamı verir ama en gürültülü olanıdır: sayfanın herhangi bir yerindeki değişiklik testi düşürür. Bileşen karşılaştırması yalnız ilgili bileşenin çerçevesini alır ve kusuru daha dar bir alana bağlar. Öge düzeyinde karşılaştırma en dar kapsamdır.
Buradan ayrı bir yöntem, anlık görüntü testidir (snapshot test): çizilen ağacın metin biçimine dökülüp kaydedilmesi. Yapıdaki değişikliği yakalar, görünüşteki değişikliği yakalamaz — renk, aralık ve yazı tipi kararları ağaçta görünmez. İkisi farklı riskleri kapsar.
Fark Hesabı
Karşılaştırmanın çekirdeği kısa bir hesaptır: iki ızgaranın karşılıklı piksellerini gez, farkı bir toleransla kıyasla, uyuşmayanları say.
// gorsel-fark.mjs — iki gri tonlamali izgarayi karsilastirir export function fark(referans, aday, { tolerans = 0, maske = [] } = {}) { if (referans.genislik !== aday.genislik || referans.yukseklik !== aday.yukseklik) { return { boyutFarki: true, farkliPiksel: NaN, oran: NaN }; } const maskeli = (x, y) => maske.some((m) => x >= m.x && x < m.x + m.en && y >= m.y && y < m.y + m.boy); let farkli = 0; let sayilan = 0; for (let y = 0; y < referans.yukseklik; y += 1) { for (let x = 0; x < referans.genislik; x += 1) { if (maskeli(x, y)) continue; sayilan += 1; const i = y * referans.genislik + x; if (Math.abs(referans.pikseller[i] - aday.pikseller[i]) > tolerans) farkli += 1; } } return { boyutFarki: false, farkliPiksel: farkli, oran: farkli / sayilan }; } export function karar(sonuc, esik) { if (sonuc.boyutFarki) return 'kaldi (boyut degisti)'; return sonuc.oran > esik ? 'kaldi' : 'gecti'; }
Denemek için gerçek bir ekran görüntüsü gerekmez; ölçüm tablosu ekranını belirlenimci olarak çizen kırk sütunluk bir ızgara yeter. Aşağıdaki modül başlık şeridini, saat alanını, dört ölçüm satırını ve kaydet düğmesini gri tonlarla yerleştirir.
// goruntu.mjs — olcum tablosu ekraninin belirlenimci gri tonlamali cizimi const GENISLIK = 40; const YUKSEKLIK = 20; function bos() { return { genislik: GENISLIK, yukseklik: YUKSEKLIK, pikseller: new Uint8Array(GENISLIK * YUKSEKLIK).fill(255) }; } function kutu(g, x, y, en, boy, ton) { for (let sy = y; sy < y + boy; sy += 1) { for (let sx = x; sx < x + en; sx += 1) { if (sx >= 0 && sx < g.genislik && sy >= 0 && sy < g.yukseklik) g.pikseller[sy * g.genislik + sx] = ton; } } } export function ekran({ dugmeTonu = 60, saatDeseni = 0, kaydir = 0 } = {}) { const g = bos(); kutu(g, 0, 0 + kaydir, 40, 2, 200); // baslik seridi kutu(g, 30, 0 + kaydir, 8, 1, 120 + saatDeseni); // saat alani for (let s = 0; s < 4; s += 1) { // olcum satirlari kutu(g, 2, 4 + s * 3 + kaydir, 34, 2, 230); kutu(g, 3, 5 + s * 3 + kaydir, 10, 1, 90); } kutu(g, 26, 17 + kaydir, 12, 2, dugmeTonu); // kaydet dugmesi return g; } // cizim motorunun kenar yumusatma benzeri kucuk sapmasi export function gurultuEkle(g, tohum = 12345, genlik = 3) { const kopya = { ...g, pikseller: Uint8Array.from(g.pikseller) }; let s = tohum; for (let i = 0; i < kopya.pikseller.length; i += 1) { s = (s * 1103515245 + 12345) % 2147483648; const sapma = (s % (2 * genlik + 1)) - genlik; kopya.pikseller[i] = Math.min(255, Math.max(0, kopya.pikseller[i] + sapma)); } return kopya; }
// rapor.mjs — adaylarin referansla karsilastirilmasi import { fark, karar } from './gorsel-fark.mjs'; import { ekran, gurultuEkle } from './goruntu.mjs'; const referans = ekran(); const ESIK = 0.005; const SAAT_MASKESI = [{ x: 30, y: 0, en: 8, boy: 1 }]; const adaylar = [ ['ayni cizim', ekran(), {}], ['kenar yumusatma sapmasi', gurultuEkle(ekran()), {}], ['ayni sapma, tolerans 5', gurultuEkle(ekran()), { tolerans: 5 }], ['saat alani degisti', ekran({ saatDeseni: 40 }), {}], ['saat alani maskeli', ekran({ saatDeseni: 40 }), { maske: SAAT_MASKESI }], ['dugme rengi degisti', ekran({ dugmeTonu: 30 }), {}], ['icerik 1 piksel kaydi', ekran({ kaydir: 1 }), {}], ]; console.log('durum farkli oran karar'); for (const [ad, aday, secenek] of adaylar) { const s = fark(referans, aday, secenek); const oran = (s.oran * 100).toFixed(2).padStart(6); console.log(`${ad.padEnd(25)} ${String(s.farkliPiksel).padStart(6)} ${oran}% ${karar(s, ESIK)}`); }
node rapor.mjs
durum farkli oran karar ayni cizim 0 0.00% gecti kenar yumusatma sapmasi 502 62.75% kaldi ayni sapma, tolerans 5 0 0.00% gecti saat alani degisti 8 1.00% kaldi saat alani maskeli 0 0.00% gecti dugme rengi degisti 24 3.00% kaldi icerik 1 piksel kaydi 424 53.00% kaldi
Yedi satır, yöntemin bütün karar noktalarını içeriyor.
Tolerans, Eşik ve Maske
İkinci satır, sıfır toleransın neden kullanılamadığını gösteriyor. Her piksele en çok üç birimlik bir sapma eklendiğinde sekiz yüz pikselin beş yüz ikisi “farklı” sayıldı: yüzde 62,75. Gözle görülmeyen bir sapma, karşılaştırmanın tamamını anlamsız kıldı. Üçüncü satırda aynı görüntü beş birimlik toleransla karşılaştırıldı ve fark sıfıra indi.
Buradan iki ayrı kelepçe çıkar ve ikisi karıştırılmamalıdır. Piksel toleransı tek bir pikselin ne kadar sapabileceğini söyler; çizim motorunun kenar yumuşatma gürültüsünü soğurur. Oran eşiği kaç pikselin sapabileceğini söyler; kabul edilen toplam değişim miktarıdır. Tolerans gürültüye, eşik küçük ve önemsiz kaymalara karşıdır.
Dördüncü ve beşinci satırlar dinamik içeriği gösteriyor. Başlıktaki saat alanı her koşumda farklı bir değer taşır ve sekiz pikseli değiştirir: yüzde bir. Bu, kodla ilgisi olmayan bir farktır ve maskeyle karşılaştırma dışında bırakılır. Maske dikdörtgeni sayımdan tümüyle düşer; oran, kalan piksellere göre hesaplanır.
Altıncı satır gerçek bir gerilemedir: kaydet düğmesinin tonu değişti, yirmi dört piksel farklı, yüzde üç. Eşiğin üstünde olduğu için test kaldı.
Yedinci satır yöntemin en önemli sınırını gösteriyor. İçerik bir piksel aşağı kaydığında dört yüz yirmi dört piksel değişti — yüzde elli üç. Görsel karşılaştırma yerel değildir: bir kapsayıcının dış boşluğundaki tek birimlik değişiklik, altındaki her şeyi kaydırır ve farkı sayfanın tamamına yayar. Bu yüzden fark oranı kusurun büyüklüğünü ölçmez; yalnız “bir şey değişti” der. Kusurun yerini bulmak fark görüntüsüne bakmayı gerektirir.
Sonuncu ayrıntı boyut denetimidir. Görüntülerin ölçüleri farklıysa piksel karşılaştırması tanımsızdır; karşılaştırma başlamadan geçersiz sayılır.
Testin İçine Koymak
Karar ölçütü belirlendiğinde bu bir teste dönüşür.
// gorsel.test.mjs import { test } from 'node:test'; import assert from 'node:assert/strict'; import { fark, karar } from './gorsel-fark.mjs'; import { ekran, gurultuEkle } from './goruntu.mjs'; const ESIK = 0.005; const OLCUT = { tolerans: 5, maske: [{ x: 30, y: 0, en: 8, boy: 1 }] }; test('cizim sapmasi ve saat alani gerileme sayilmaz', () => { const aday = gurultuEkle(ekran({ saatDeseni: 40 })); assert.equal(karar(fark(ekran(), aday, OLCUT), ESIK), 'gecti'); }); test('dugme tonundaki degisiklik gerileme olarak bildirilir', () => { const sonuc = fark(ekran(), ekran({ dugmeTonu: 30 }), OLCUT); assert.equal(karar(sonuc, ESIK), 'kaldi'); assert.equal(sonuc.farkliPiksel, 24); }); test('boyut degisikligi karsilastirmayi gecersiz kilar', () => { const dar = { genislik: 39, yukseklik: 20, pikseller: new Uint8Array(39 * 20) }; assert.equal(karar(fark(ekran(), dar, OLCUT), ESIK), 'kaldi (boyut degisti)'); });
node --test gorsel.test.mjs
✔ cizim sapmasi ve saat alani gerileme sayilmaz (0.923334ms) ✔ dugme tonundaki degisiklik gerileme olarak bildirilir (0.115667ms) ✔ boyut degisikligi karsilastirmayi gecersiz kilar (0.053917ms) ℹ tests 3 ℹ suites 0 ℹ pass 3 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 32.929375
Belirlenimcilik Koşulları
Görsel karşılaştırma ancak aday görüntü belirlenimci üretildiğinde işe yarar. Beş koşul sağlanmalıdır.
Görüntü alanı ve piksel oranı sabitlenir; farklı genişlik farklı bir düzen üretir. Animasyon ve geçiş durdurulur, yoksa görüntü hangi karede alındığına bağlı olur — Düzen Sistemleri kursundaki hareket azaltma tercihi bu iş için de kullanılabilir. Yazı tipi yüklenene kadar beklenir; yedek yazı tipiyle çizilmiş bir kare her satırın genişliğini değiştirir. Saat, rastgele kimlik ve canlı veri sabitlenir ya da maskelenir. Kaydırma konumu sıfırlanır.
Bir koşul daha vardır ve dışarıdan gelir: çizim motoru ile işletim sistemi. Aynı kod, farklı bir ortamda kenar yumuşatmayı farklı yapar. Bu yüzden referans görüntüler üretildikleri ortama bağlıdır ve tek bir ortamda üretilip orada karşılaştırılır.
Referansların kendisi de yönetilmesi gereken bir varlıktır. Depoda ikili dosya olarak dururlar ve Sürüm Kontrolüne Giriş kursunda anlatıldığı gibi ikili dosyaların farkı okunamaz. Her kabul edilen değişiklik depoyu büyütür. Bu yüzden referans güncellemesi kendiliğinden yapılmaz: değişikliği bir kişi görür, onaylar ve yeni referans o onayla kaydedilir. Onaysız güncelleme, gerileme testini kendi kendini haklı çıkaran bir düzeneğe çevirir.
Özet
- Görsel gerileme testi referans ve aday görüntüyü piksel düzeyinde karşılaştırır; anlık görüntü testi ise yapıyı yakalar, görünüşü yakalamaz.
- Piksel toleransı tek pikselin sapmasını, oran eşiği toplam değişim miktarını sınırlar; ikisi farklı sorunlara karşıdır ve karıştırılmamalıdır.
- Dinamik alanlar maskelenir; maskelenen bölge sayımın tamamen dışında kalır.
- Fark oranı yerel değildir: bir piksellik kayma sayfanın yarısını farklı gösterdi, bu yüzden oran kusurun büyüklüğünü ölçmez.
- Karşılaştırma ancak görüntü alanı, animasyon, yazı tipi, saat ve kaydırma sabitlendiğinde anlamlıdır; referanslar üretildikleri ortama bağlıdır ve güncellemeleri onaya bağlanır.
Sonraki Adım
Dört ders boyunca kurulan testlerin hepsi bir varsayıma dayanıyordu: aynı kod aynı sonucu verir. Uygulamada bu varsayım sık sık bozulur. Bir test yirmi koşumun on dokuzunda geçer, birinde düşer; kimse bir şey değiştirmemiştir. Ekip önce yeniden çalıştırmayı dener, sonra o testi görmezden gelmeye başlar ve paketin verdiği işaret değersizleşir. Sonraki ders bu davranışın nedenlerini — zamanlama, sıraya bağımlılık ve paylaşılan durum — sayılabilir hâle getirir ve her birinin çözümünü gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.