İçeriğe geç
academia.sh

Ders 22 / 27

Kullanıcı Merkezli Sorgular

Erişilebilir rol ve erişilebilir adın sorgu ölçütüne dönüştürülmesi, aynı belgenin rol sorgusuyla ve sınıf seçicisiyle sorgulanması, iki ölçütün yeniden düzenleme ve erişilebilirlik gerilemesi karşısındaki davranışı.

İçindekiler

Önceki ders davranışa bağlı testin uygulama ayrıntısına bağlı testten daha dayanıklı olduğunu gösterdi, ama sınanacak ögeyi bulmak için hâlâ ağacın etiket seçimine baktı. Sınıf adına dayanan bir seçici de aynı türden bir bağımlılıktır: her ikisi de yazarın serbestçe değiştirebilmesi gereken şeylere dayanır.

Kullanıcı ögeyi böyle bulmaz. Fareyle çalışan kullanıcı “Kaydet” yazan bir düğme arar; ekran okuyucuyla çalışan kullanıcı “Kaydet, düğme” diye duyurulan ögeye ulaşır. İkisinin ortak noktası iki bilgidir: ögenin rolü ve erişilebilir adı. Bu ders o iki bilgiyi bir sorgu ölçütüne dönüştürür.

Ölçütün Kaynağı

Web’in Temelleri ve HTML kursunda erişilebilirlik ağacı tanımlanmıştı: tarayıcının belge ağacından türettiği, yardımcı teknolojiye sunulan ikinci ağaç. Bu ağaçtaki her düğümün bir rolü, çoğunun bir adı vardır. Rol ögenin ne olduğunu söyler (düğme, bağlantı, arama kutusu, satır), ad ise hangisi olduğunu.

Testin bu iki bilgiye göre sorgu yapmasının iki sonucu vardır. Birincisi dayanıklılıktır: sınıf adları, sarmalayıcı kaplar ve biçem mimarisi kararları rolü değiştirmez. İkincisi ve daha önemlisi, sorgunun kendisi bir erişilebilirlik doğrulaması hâline gelir. Rolü olmayan bir ögeyi rol sorgusu bulamaz; test kırmızıya döner ve bildirdiği şey gerçek bir kusurdur.

Rol ve Ad Nasıl Hesaplanır

Aşağıdaki modül, hesabın öğretici bir alt kümesini kurar: yaygın ögelerin örtük rolleri, açık role özniteliğinin önceliği ve ad hesabının sırası.

// sorgu.mjs — erisilebilir rol ve ada gore oge secimi
export function o(tur, ozellikler = {}, ...cocuklar) {
  return { tur, ozellikler, cocuklar: cocuklar.flat() };
}

export function* dolas(dugum) {
  if (dugum === null || typeof dugum !== 'object') return;
  yield dugum;
  for (const cocuk of dugum.cocuklar ?? []) yield* dolas(cocuk);
}

function metin(dugum) {
  if (typeof dugum === 'string') return dugum;
  return (dugum.cocuklar ?? []).map(metin).join(' ').replace(/\s+/g, ' ').trim();
}

const ORTUK_ROL = {
  button: () => 'button',
  a: (oz) => (oz.href === undefined ? null : 'link'),
  table: () => 'table',
  tr: () => 'row',
  td: () => 'cell',
  th: (oz) => (oz.scope === 'row' ? 'rowheader' : 'columnheader'),
  ul: () => 'list',
  li: () => 'listitem',
  nav: () => 'navigation',
  main: () => 'main',
  h1: () => 'heading',
  h2: () => 'heading',
  h3: () => 'heading',
  form: (oz) => (oz['aria-label'] === undefined ? null : 'form'),
  input: (oz) => ({
    search: 'searchbox',
    text: 'textbox',
    number: 'spinbutton',
    checkbox: 'checkbox',
    submit: 'button',
  })[oz.type ?? 'text'] ?? null,
};

export function rol(dugum) {
  if (dugum.ozellikler?.role) return dugum.ozellikler.role;
  const kural = ORTUK_ROL[dugum.tur];
  return kural ? kural(dugum.ozellikler ?? {}) : null;
}

export function erisilebilirAd(agac, dugum) {
  const oz = dugum.ozellikler ?? {};
  if (oz['aria-labelledby']) {
    const hedef = [...dolas(agac)].find((d) => d.ozellikler?.id === oz['aria-labelledby']);
    if (hedef) return metin(hedef);
  }
  if (oz['aria-label']) return oz['aria-label'];
  if (oz.id) {
    const etiket = [...dolas(agac)].find((d) => d.tur === 'label' && d.ozellikler?.for === oz.id);
    if (etiket) return metin(etiket);
  }
  if (['button', 'a', 'h1', 'h2', 'h3', 'td', 'th'].includes(dugum.tur)) return metin(dugum);
  if (oz.title) return oz.title;
  if (oz.value !== undefined && dugum.tur === 'input' && oz.type === 'submit') return oz.value;
  return '';
}

export function roleGore(agac, aranan, { ad } = {}) {
  return [...dolas(agac)].filter(
    (d) => rol(d) === aranan && (ad === undefined || erisilebilirAd(agac, d) === ad),
  );
}

export function sinifaGore(agac, sinif) {
  return [...dolas(agac)].filter((d) => (d.ozellikler?.class ?? '').split(' ').includes(sinif));
}

Üç ayrıntı gerçek hesapla aynıdır. Açık role özniteliği örtük rolü ezer. Ad hesabı bir öncelik dizisidir ve ilk bulunan kazanır: başka bir ögeye yapılan başvuru, doğrudan verilen etiket dizgisi, yerel etiket ögesi, ögenin kendi içeriği, en sonda title. Bazı ögeler niteliğe bağlı rol alır — bağlantı yalnız href taşıdığında bağlantıdır, bir bölüm ögesi ancak adı olduğunda yer imi sayılır.

Gerçek hesap bundan geniştir: gizli ögelerin dışlanması, ad hesabının özyinelemesi ve rol eşlemesinin tamamı standartta tanımlıdır. Buradaki alt küme, sorgunun neye dayandığını göstermeye yeter.

Aynı Belgeyi İki Ölçütle Sorgulamak

Sınanacak belge ölçüm giriş formudur. Üç sürüm var: başlangıç işaretlemesi, yalnız sınıf adları değişmiş hâli ve kaydet düğmesinin rolsüz bir kaba dönüştüğü hâli.

// giris-formu.mjs — olcum giris formunun uc surumu
import { o } from './sorgu.mjs';

// Surum 1: baslangic isaretlemesi
export const surum1 = () =>
  o('form', { 'aria-label': 'Olcum girisi' },
    o('h2', { class: 'form__baslik' }, 'Yeni olcum'),
    o('label', { for: 'istasyon', class: 'form__etiket' }, 'Istasyon'),
    o('input', { id: 'istasyon', type: 'text', class: 'form__girdi' }),
    o('label', { for: 'deger', class: 'form__etiket' }, 'Sicaklik'),
    o('input', { id: 'deger', type: 'number', class: 'form__girdi' }),
    o('button', { type: 'submit', class: 'dugme dugme--birincil' }, 'Kaydet'));

// Surum 2: yalniz sinif adlari ve sarmalayicilar degisti
export const surum2 = () =>
  o('form', { 'aria-label': 'Olcum girisi' },
    o('h2', { class: 'yigin__baslik' }, 'Yeni olcum'),
    o('div', { class: 'alan' },
      o('label', { for: 'istasyon', class: 'alan__ad' }, 'Istasyon'),
      o('input', { id: 'istasyon', type: 'text', class: 'alan__deger' })),
    o('div', { class: 'alan' },
      o('label', { for: 'deger', class: 'alan__ad' }, 'Sicaklik'),
      o('input', { id: 'deger', type: 'number', class: 'alan__deger' })),
    o('button', { type: 'submit', class: 'eylem eylem--birincil' }, 'Kaydet'));

// Surum 3: kaydet dugmesi rolsuz bir kaba donustu
export const surum3 = () =>
  o('form', { 'aria-label': 'Olcum girisi' },
    o('h2', { class: 'yigin__baslik' }, 'Yeni olcum'),
    o('div', { class: 'alan' },
      o('label', { for: 'istasyon', class: 'alan__ad' }, 'Istasyon'),
      o('input', { id: 'istasyon', type: 'text', class: 'alan__deger' })),
    o('div', { class: 'alan' },
      o('label', { for: 'deger', class: 'alan__ad' }, 'Sicaklik'),
      o('input', { id: 'deger', type: 'number', class: 'alan__deger' })),
    o('div', { class: 'eylem eylem--birincil', onclick: 'gonder()' }, 'Kaydet'));
// sorgu.test.mjs
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { roleGore, sinifaGore, erisilebilirAd } from './sorgu.mjs';
import { surum1, surum2, surum3 } from './giris-formu.mjs';

describe('surum 1', () => {
  test('rol sorgusu kaydet dugmesini bulur', () => {
    const agac = surum1();
    assert.equal(roleGore(agac, 'button', { ad: 'Kaydet' }).length, 1);
  });

  test('sinif sorgusu ayni dugmeyi bulur', () => {
    assert.equal(sinifaGore(surum1(), 'dugme').length, 1);
  });

  test('etiketli girdinin erisilebilir adi etiket metnidir', () => {
    const agac = surum1();
    const [girdi] = roleGore(agac, 'spinbutton');
    assert.equal(erisilebilirAd(agac, girdi), 'Sicaklik');
  });
});

describe('surum 2 — sinif adlari degisti', () => {
  test('rol sorgusu hala bulur', () => {
    const agac = surum2();
    assert.equal(roleGore(agac, 'button', { ad: 'Kaydet' }).length, 1);
  });

  test('sinif sorgusu artik bulamaz', () => {
    assert.equal(sinifaGore(surum2(), 'dugme').length, 0);
  });
});

describe('surum 3 — dugme rolsuz kaba dondu', () => {
  test('sinif sorgusu bulmayi surdurur', () => {
    assert.equal(sinifaGore(surum3(), 'eylem').length, 1);
  });

  test('rol sorgusu bulamaz', () => {
    const agac = surum3();
    assert.equal(roleGore(agac, 'button', { ad: 'Kaydet' }).length, 0);
  });
});
node --test sorgu.test.mjs
▶ surum 1
  ✔ rol sorgusu kaydet dugmesini bulur (0.631ms)
  ✔ sinif sorgusu ayni dugmeyi bulur (0.071459ms)
  ✔ etiketli girdinin erisilebilir adi etiket metnidir (0.069417ms)
✔ surum 1 (1.141792ms)
▶ surum 2 — sinif adlari degisti
  ✔ rol sorgusu hala bulur (0.075042ms)
  ✔ sinif sorgusu artik bulamaz (0.049541ms)
✔ surum 2 — sinif adlari degisti (0.184916ms)
▶ surum 3 — dugme rolsuz kaba dondu
  ✔ sinif sorgusu bulmayi surdurur (0.077958ms)
  ✔ rol sorgusu bulamaz (0.061166ms)
✔ surum 3 — dugme rolsuz kaba dondu (0.214625ms)
ℹ tests 7
ℹ suites 3
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 36.906584

İki ölçütün üç sürümdeki davranışı bir tabloda toplanabilir.

Değişiklik Rol ve ad sorgusu Sınıf seçicisi
Sınıf adları ve sarmalayıcılar değişti bulur bulamaz
Düğme rolsüz bir kaba dönüştü bulamaz bulur

İki satır iki farklı türde bilgi verir. Birinci satırda sınıf seçicisi yanlış alarm üretir: davranış değişmemişken test kırmızıya döner. İkinci satırda rol sorgusu bulamaz, çünkü bulunacak bir düğme kalmamıştır — o kap klavyeyle odaklanamaz, boşluk tuşuyla çalıştırılamaz ve ekran okuyucuya düğme diye duyurulmaz. Sınıf seçicisinin bu sürümde hâlâ “bulması” iyi bir haber değildir; testin gerçek bir gerilemeyi görmediğini gösterir.

Ölçütlerin Önceliği

Pratikte bir sıra kurulabilir. En üstte rol ve erişilebilir ad vardır: her etkileşimli öge için birincil ölçüt budur. Sonra etiket metni gelir; form alanlarında rolle birlikte zaten aynı bilgiyi verir. Üçüncü sırada, etkileşimli olmayan içerik için görünen metin bulunur — bir uyarı bölgesinin duyurduğu ileti gibi.

En sonda test özniteliği durur: yalnız sınama için eklenen bir veri özniteliği. Bunun maliyeti, kullanıcının hiç görmediği bir sözleşme yaratmasıdır; öge erişilebilirlik ağacından tümüyle düşse bile test yeşil kalır. Gerekçesi olan iki durum vardır: erişilebilir adı doğal olarak bulunmayan yapısal bir kap ve aynı rol ile aynı adı taşıyan çok sayıda öge arasından belirli birini ayırma ihtiyacı.

Sorgunun birden çok öge bulması da bir bilgidir. Aynı sayfada “Kaydet” adlı iki düğme varsa, testin belirsizliği kullanıcının belirsizliğidir; çözüm sorguyu daraltmak değil, adları ayırmaktır.

Özet

  • Kullanıcı ögeyi rolüyle ve adıyla tanır; sorgunun ölçütü de bu ikisi olduğunda test kullanıcının bakış açısını taşır.
  • Örtük rol öge türünden ve niteliklerinden türer; açık role özniteliği onu ezer. Ad hesabı bir öncelik dizisidir ve ilk bulunan kazanır.
  • Sınıf adları ve sarmalayıcılar değiştiğinde rol sorgusu ögeyi bulmayı sürdürdü, sınıf seçicisi yanlış alarm üretti.
  • Düğme rolsüz bir kaba dönüştüğünde rol sorgusu bulamadı; bu, gerçek bir erişilebilirlik gerilemesinin testte görünmesidir.
  • Ölçüt sırası rol ve ad, etiket metni, görünen metin, en sonda test özniteliğidir; test özniteliği kullanıcının görmediği bir sözleşme yarattığı için son çaredir.

Sonraki Adım

Bileşen testi bileşenin kendi ağacıyla sınırlıdır. Ölçüm listesinin gerçekten sunucudan gelmesi, adres çubuğunun güncellenmesi, formun gönderilmesi ve listenin yenilenmesi bu sınırın dışındadır. Kullanıcının yaşadığı şey ise tam olarak bu zincirdir: parçalar tek tek doğru çalışırken akış bir yerde kopabilir. Sonraki ders uygulamayı gerçek bir tarayıcıda baştan sona çalıştıran testleri, akışın bir durum makinesi olarak modellenmesini ve bu testlerin neden az sayıda tutulduğunu ele alır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat