teknikbakis

Zayıf parolaya yeşil onay: hidden'ı ezen tek CSS satırı ve 0 piksel ölçüp geçen testler

JS doğru çalışıyordu, öznitelik doğru değeri alıyordu, kabul testi yeşildi; kullanıcı ise zayıf parolanın yanında onay işareti görüyordu. Aynı hafta aynı kökten iki akraba hata daha çıktı. Bu sitenin kendisi de bu hatayla doğmuştu.

DOSYA TB-2026-015
SİSTEM HTML · CSS · JavaScript · headless Chrome
KATMAN Arayüz / görünürlük ve test
SÜRE 28–30 Eyl · 3 olay
DURUM ÇÖZÜLDÜ
KANIT 4 ek

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 öznitelik true oluyordu. 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ı:

EK-1 Zayıf parola girilmişken, gerçek sayfada
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:

EK-2 teknikbakis.com filtresi: JS ve CSS
/* 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:

EK-3 Durum bandı: önce ve sonra
// Ö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.

EK-4 Taşma denetimi: önce ve sonra
// Ö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

  1. Bileşen kendi [hidden] kuralını taşır. CSS'inde display verdiğim ve JS ile hidden yaptığı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.
  2. 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ızca classList.add, remove ya da toggle ile değişir; görünürlük sınıfı taşıyan bir öğeye className = yazılmaz. Dönüşümden sonra kalan atamalar tek komutla taranır:
    grep -rn "\.className\s*=" .
  3. Ö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 öğelerde offsetParent görünür olsalar bile null döner; orada genişliğe ya da tarayıcı destekliyorsa el.checkVisibility() sonucuna bakmak daha güvenli.
  4. 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ış display değerine ve ekran görüntüsüne bakılır.
ÇIKARIM

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.