Semptom
28 Eylül'de kendi bileşen kütüphanemdeki parola gücü göstergesini ilk kez gerçek bir sayfaya, bir yönetim panelinin hesap ekranına yerleştirdim. Göstergenin küçük bir onay rozeti var: parola bütün koşulları sağladığında giriş kutusunun sağ ucunda yeşil bir onay işareti beliriyor. Zayıf bir parola yazdığımda da o yeşil işaret oradaydı.
Bir parola formunda yapılabilecek en kötü hatalardan biri bu: kullanıcıya "tamam" demek. Üstelik bileşen, kütüphanenin demo sayfasında kusursuz çalışıyordu ve kütüphaneye alınmadan önce kabul kontrolünden geçmişti.
Yanlış yollar
Bu hatanın iki güvencesi vardı ve ikisi de yanlış yere bakıyordu.
-
Kabul testi yeşildi. Ama test, kütüphanenin demo sayfasında yapılmıştı.
O sayfanın temel CSS'inde, birçok başlangıç şablonunda olduğu gibi, genel bir sıfırlama
satırı vardı:
[hidden]{display:none!important}. Bu satır hatayı demo sayfasında görünmez yapıyordu. Test, bileşeni değil, bileşen ile demo sayfasının toplamını sınamıştı. -
JS doğruydu. Onay rozetinin görünürlüğünü yöneten satır kusursuz
çalışıyordu:
ok.hidden = !checks.every(Boolean);Zayıf parolada özniteliktrueoluyordu. DOM'a bakan bir göz ya da öznitelik değerini doğrulayan bir test "gizli" der ve geçer.
İkisinin ortak noktası, ikisinin de niyete bakması: kod gizlemeyi istedi mi? İstedi. Kimse sonuca bakmıyordu: ekranda bir şey var mı?
Doğru test
Ayırt edici test, öğenin hesaplanmış stiline olmaması gereken durumda ve sıfırlaması olmayan bir sayfada bakmaktı:
const ok = document.querySelector('.pw-ok');
ok.hidden // true → JS doğru
getComputedStyle(ok).display // "grid" → ama ekranda
Öznitelik "gizli" diyor, hesaplanmış stil "görünür" diyor. Kullanıcının gördüğü ikincisi.
Kök neden
hidden özniteliğinin kendine ait bir gizleme gücü yok. Gizlemeyi yapan,
tarayıcının varsayılan stil sayfasındaki tek bir kural: [hidden] { display: none; }.
CSS'te kuralların hangi kaynaktan geldiği, seçicinin özgüllüğünden önce değerlendirilir; yazarın
stil sayfası tarayıcının varsayılanını her zaman yener. Bileşenin CSS'inde şu vardı:
.pw-ok {
position: absolute;
display: grid;
place-items: center;
/* ... */
}
Rozetin içindeki simgeyi ortalamak için yazılmış masum bir display: grid,
tarayıcının display: none'ını ezdi. Öznitelik yerinde, öğe ekranda.
Bu sitenin kendisi de aynı hatayla doğdu
Okuduğunuz sitenin ana sayfasındaki konu filtresi kartları hidden ile gizliyor.
Kartlar ise esnek kutu düzeninde. Filtrenin JS'i doğruydu; kartlar yine de gizlenmiyordu.
Filtre hata vermeden, sessizce ölüydü ve ancak yayından önceki testte yakalandı. Bugün
site CSS'inde o satır bir uyarıyla duruyor:
/* theme.js */
item.el.hidden = !show;
/* site.css */
.card { display: flex; flex-direction: column; /* ... */ }
/* ⚠️ Zorunlu: .card display:flex tanımlı olduğu için hidden
attribute'unun varsayılan display:none'ı sınıf kuralı
tarafından eziliyordu. Bu satır olmadan filtre ölür. */
.card[hidden] { display: none; }
Son satır silinirse filtre düğmeleri çalışıyor görünür, hiçbir kart kaybolmaz. Proje notlarında "silinmez" diye işaretli.
Aynı haftanın iki olayı bu kökün akrabasıydı. Üçünün ortak paydası şu: görünürlük, başka bir kuralın ya da başka bir kodun haber vermeden ezebileceği bir yerde tutuluyordu ve doğrulama, kullanıcının gördüğüne değil kodun niyetine bakıyordu.
Akraba 1: className gizleme sınıfını siliyor
30 Eylül'de bir yönetim panelini tek uzun sayfadan, her menü maddesinin kendi ekranı olduğu bir yapıya çeviriyordum. Ekranlar bir CSS sınıfıyla gizleniyordu. Aynı paneldeki durum bandı ise rengini her veri yenilemesinde şöyle alıyordu:
// Önce: sınıf listesinin TAMAMI yeniden yazılıyor
bant.className = 'kart' + (uyari ? ' uyari' : '');
// → ekran gizleme sınıfı (.ekran-kapali) da silindi
// Sonra: yalnızca kendi sınıfına dokun
bant.classList.toggle('uyari', uyari);
className = bir ekleme değil, değiştirmedir. Öğede o anda hangi sınıflar
varsa hepsi gider.
Sonuç: panel bir ekrana #vpn bağlantısıyla doğrudan açıldığında durum bandı
ait olmadığı ekrana sızdı. Normal açılışta fark edilmiyordu, çünkü gizleme sınıfı veri
çizildikten sonra uygulanıyordu; sızıntı ancak doğrudan açılışta ya da bir sonraki
veri yenilemesinde görünüyordu. Hatayı bir ekran testi yakaladı; 60 saniyelik yenileme
döngüsünü taklit eden ikinci bir senaryoyla da geri gelmesini kilitledim.
Akraba 2: gizli öğede hiçbir şey ölçmeyen test
Aynı dönüşümün ikinci sürprizi testlerdeydi. Panelin 390 piksel genişlikte taşma yapmadığını sınayan denetimlerim vardı: öğenin sağ kenarı pencere genişliğini aşıyor mu? Sayfa ekranlara bölünce bazı testler kırmızıya döndü. Bu iyiydi; değişikliği gördüler. Bazıları ise yeşil kaldı ve artık hiçbir şey sınamıyordu.
Gizli bir öğenin getBoundingClientRect() sonucu sıfırdır. Sağ kenarı 0 olan
bir kutu, 390 piksellik bir pencereyi asla aşmaz. VPN ve canlı akış bölümlerinin taşma
denetimleri, öğeler başka bir ekrandayken her koşuda geçti. Fark etmemin nedeni testlerin
kendisi değil, ekran görüntüsü almaya çalışmamdı; çağrı
clip height must be positive hatasıyla düştü. Kırpılacak alanın yüksekliği
sıfırdı, çünkü öğe hiç çizilmemişti.
// Önce: öğe gizliyse her zaman geçer
const r = el.getBoundingClientRect();
assert(r.right <= innerWidth); // r.right = 0 → PASS, ölçüm yok
// Sonra: önce ekrana geç, sonra görünürlüğü şart koş
await ekranaGec('vpn');
const r = el.getBoundingClientRect();
if (el.offsetParent === null || r.width === 0) {
fail('öğe görünmüyor, ölçülecek bir şey yok');
}
assert(r.right <= innerWidth);
Görünmeyen öğe için "taşma yok" demek doğru ama anlamsız. Ön şart sağlanmıyorsa test geçmemeli, düşmeli.
Çözüm
-
Bileşen kendi
[hidden]kuralını taşır. CSS'indedisplayverdiğim ve JS ilehiddenyaptığım her öğe için bileşenin kendi CSS'ine.pw-ok[hidden] { display: none; }biçiminde bir satır ekledim. Sayfanın genel[hidden]{display:none!important}sıfırlaması kendi sitende iyi bir sigortadır; ama bir bileşen, konulacağı sayfada o satırın olacağına güvenemez. -
Görünürlük ve veri durumu ayrı mekanizmalarla yönetilir. Sınıf ekranı
yönetir,
hiddenözniteliği "veri yok" durumunu. Durum sınıfları yalnızcaclassList.add,removeya datoggleile değişir; görünürlük sınıfı taşıyan bir öğeyeclassName =yazılmaz. Dönüşümden sonra kalan atamalar tek komutla taranır:grep -rn "\.className\s*=" . -
Ölçen test önce ölçebildiğini kanıtlar. Ekranlı bir arayüzde her ölçümden
önce test o ekrana geçer; öğe görünmüyorsa sonuç FAIL'dir. Bir uyarı:
position: fixedöğelerdeoffsetParentgörünür olsalar bilenulldöner; orada genişliğe ya da tarayıcı destekliyorsael.checkVisibility()sonucuna bakmak daha güvenli. -
Doğrulama, sıfırlaması olmayan bir sayfada da yapılır. Bileşen
doğrulaması demo sayfasıyla bitmez: çıplak bir sayfada, "olmaması gereken" durumda
(zayıf parola, boş liste) öğenin hesaplanmış
displaydeğerine ve ekran görüntüsüne bakılır.
Gizlemeyi doğrularken niyete değil sonuca bak. hidden === true ya da
"sınıf eklendi" bir iddiadır; getComputedStyle(el).display ve ekran
görüntüsü kanıttır.
Kalıcı ders
- Bileşen, konulduğu sayfanın sıfırlamasına güvenemez. Demo sayfası ile gerçek sayfa farklı temel CSS taşır; test ortamı hatayı saklayabilir.
- Ezilebilen tek bir mekanizmaya görünürlük emanet etme. Varsayılan stil ezilir, sınıf listesi üzerine yazılır. Görünürlüğü yöneten kural, onu ezebilecek kuralla aynı yerde ve ondan güçlü durmalı.
- Büyük bir yeniden düzenlemeden sonra kırmızıya dönen testler değil, yeşil kalanlar şüphelidir. Kırmızı test değişikliği gördü; yeşil kalanın hâlâ bir şey ölçtüğünü ayrıca kanıtlamak gerekir.
- Sessiz başarısızlık en pahalı türdür. Üç olayın hiçbiri hata mesajı üretmedi. Biri bir parola formunu, biri bir paneldeki ekran ayrımını, biri bir test takımını boşa düşürdü; bu sitenin filtresi de aynı yoldan, yayından önce yakalanmasa sessizce ölü gidecekti.