اختبار إمكانية الوصول
اختبار إمكانية الوصول (a11y) يضمن أن تطبيق الويب الخاص بك قابل للاستخدام من قبل الأشخاص ذوي الإعاقة. ويشمل ذلك المستخدمين الذين يعتمدون على قارئات الشاشة، والتنقل بلوحة المفاتيح، والتحكم الصوتي، وغيرها من التقنيات المساعدة.
لماذا اختبار إمكانية الوصول مهم
- الامتثال القانوني: تتطلب العديد من الدول الامتثال لـ WCAG للمواقع الإلكترونية
- جمهور أوسع: أكثر من مليار شخص في جميع أنحاء العالم لديهم إعاقات
- تجربة مستخدم أفضل: المواقع التي يمكن الوصول إليها أسهل في الاستخدام للجميع
- فوائد تحسين محركات البحث: العديد من تحسينات إمكانية الوصول تساعد أيضًا محركات البحث
- سمعة العلامة التجارية: تظهر الالتزام بالشمولية
نظرة عامة على إرشادات WCAG
إرشادات إمكانية الوصول لمحتوى الويب (WCAG) منظمة حول أربعة مبادئ (POUR):
مبادئ POUR:
- قابل للإدراك: يجب أن تكون المعلومات قابلة للعرض للمستخدمين بطرق يمكنهم إدراكها (بدائل نصية، تسميات توضيحية، محتوى قابل للتكيف)
- قابل للتشغيل: يجب أن تكون مكونات واجهة المستخدم والتنقل قابلة للتشغيل (قابل للوصول عبر لوحة المفاتيح، وقت كافٍ، لا محفزات للنوبات)
- مفهوم: يجب أن تكون المعلومات وعملية واجهة المستخدم مفهومة (قابلة للقراءة، يمكن التنبؤ بها، مساعدة الإدخال)
- قوي: يجب أن يكون المحتوى قويًا بما يكفي للعمل مع التقنيات الحالية والمستقبلية
مستويات الامتثال لـ WCAG
المستوى A - المستوى الأدنى (الدعم الأساسي)
المستوى AA - المستوى المتوسط (معظم المواقع تستهدف هذا)
المستوى AAA - أعلى مستوى (المعيار الذهبي)
الاختبار التلقائي باستخدام axe-core
axe-core هو محرك اختبار إمكانية الوصول القوي الذي يمكنه العثور على ~57% من مشاكل WCAG تلقائيًا.
التثبيت
# تثبيت axe-core لـ JavaScript
npm install --save-dev @axe-core/cli axe-core
# تثبيت لـ Laravel Dusk
composer require --dev duskphp/a11y
استخدام axe-core مع Dusk
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Duskphp\A11y\AxeRunner;
class AccessibilityTest extends DuskTestCase
{
/** @test */
public function homepage_has_no_accessibility_violations()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertAccessible(); // يستخدم axe-core
$violations = $browser->getA11yViolations();
$this->assertCount(0, $violations,
'Found accessibility violations: ' . json_encode($violations, JSON_PRETTY_PRINT)
);
});
}
/** @test */
public function form_has_proper_labels()
{
$this->browse(function (Browser $browser) {
$browser->visit('/contact')
->assertAccessible([
'label', // جميع مدخلات النموذج يجب أن يكون لها تسميات
'color-contrast', // تباين لون كافٍ
'button-name' // يجب أن يكون للأزرار أسماء يمكن الوصول إليها
]);
});
}
}
تشغيل axe-core من سطر الأوامر
# اختبار صفحة واحدة
npx axe http://localhost:8000
# اختبار صفحات متعددة
npx axe http://localhost:8000 http://localhost:8000/about --save results.json
# اختبار مستوى WCAG محدد
npx axe http://localhost:8000 --tags wcag2a,wcag2aa
الاختبار باستخدام WAVE (أداة تقييم إمكانية الوصول للويب)
يوفر WAVE ملاحظات بصرية حول مشاكل إمكانية الوصول مباشرة على صفحتك.
استخدام امتداد متصفح WAVE
استخدام WAVE:
- قم بتثبيت امتداد WAVE لـ Chrome أو Firefox
- انتقل إلى صفحتك
- انقر على أيقونة WAVE
- راجع الأخطاء والتنبيهات والميزات
- أصلح المشاكل وأعد الاختبار
دمج واجهة برمجة تطبيقات WAVE في الاختبارات
<?php
use Illuminate\Support\Facades\Http;
/** @test */
public function check_accessibility_with_wave_api()
{
$url = 'https://yoursite.com/page';
$waveApiKey = env('WAVE_API_KEY');
$response = Http::get('https://wave.webaim.org/api/request', [
'key' => $waveApiKey,
'url' => $url,
'reporttype' => '4' // تنسيق JSON
]);
$result = $response->json();
$this->assertEquals(0, $result['categories']['error']['count'],
'Found ' . $result['categories']['error']['count'] . ' accessibility errors'
);
// فحص مشاكل محددة
$this->assertArrayNotHasKey('alt_missing', $result['categories']['error']['items']);
$this->assertArrayNotHasKey('label_missing', $result['categories']['error']['items']);
}
اختبار التنقل بلوحة المفاتيح
يجب أن تكون جميع الوظائف متاحة عبر لوحة المفاتيح فقط (لا حاجة للماوس).
اختبار ترتيب التنقل بالتبويب
<?php
/** @test */
public function form_has_logical_tab_order()
{
$this->browse(function (Browser $browser) {
$browser->visit('/register')
->keys('body', '{tab}')
->assertFocused('#name')
->keys('#name', '{tab}')
->assertFocused('#email')
->keys('#email', '{tab}')
->assertFocused('#password')
->keys('#password', '{tab}')
->assertFocused('#password_confirmation')
->keys('#password_confirmation', '{tab}')
->assertFocused('button[type="submit"]');
});
}
/** @test */
public function navigation_menu_is_keyboard_accessible()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->keys('body', '{tab}') // التركيز على الرابط الأول
->keys(null, '{enter}') // تنشيط الرابط
->assertPathIs('/about');
});
}
اختبار رؤية التركيز
<?php
/** @test */
public function focused_elements_are_visually_indicated()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->keys('body', '{tab}')
->assertScript(
'return window.getComputedStyle(document.activeElement).outline !== "none"',
'Focused element should have visible outline'
);
});
}
اختبار قارئ الشاشة
تعلن قارئات الشاشة عن المحتوى للمستخدمين ضعاف البصر. يضمن الاختبار البنية الدلالية المناسبة وسمات ARIA.
قارئات الشاشة الشائعة
- NVDA (Windows) - مجاني، مستخدم على نطاق واسع
- JAWS (Windows) - تجاري، معيار الصناعة
- VoiceOver (macOS/iOS) - قارئ شاشة Apple المدمج
- TalkBack (Android) - قارئ شاشة Android المدمج
اختبار HTML الدلالي
<?php
/** @test */
public function page_has_proper_heading_structure()
{
$this->browse(function (Browser $browser) {
$browser->visit('/blog');
// يجب أن يكون هناك h1 واحد بالضبط
$this->assertEquals(1, $browser->elements('h1')->count());
// يجب أن تكون العناوين بترتيب منطقي
$headings = $browser->script('
return Array.from(document.querySelectorAll("h1, h2, h3, h4, h5, h6"))
.map(h => parseInt(h.tagName.charAt(1)))
');
for ($i = 1; $i < count($headings); $i++) {
$this->assertLessThanOrEqual(
$headings[$i - 1] + 1,
$headings[$i],
'Heading levels should not skip (found h' . $headings[$i - 1] . ' then h' . $headings[$i] . ')'
);
}
});
}
/** @test */
public function images_have_alt_text()
{
$this->browse(function (Browser $browser) {
$browser->visit('/products');
$imagesWithoutAlt = $browser->script('
return Array.from(document.querySelectorAll("img"))
.filter(img => !img.hasAttribute("alt"))
.length
');
$this->assertEquals(0, $imagesWithoutAlt,
'All images must have alt attributes'
);
});
}
/** @test */
public function form_inputs_have_labels()
{
$this->browse(function (Browser $browser) {
$browser->visit('/contact');
$unlabeledInputs = $browser->script('
return Array.from(document.querySelectorAll("input, select, textarea"))
.filter(input => {
const id = input.id;
const ariaLabel = input.getAttribute("aria-label");
const ariaLabelledBy = input.getAttribute("aria-labelledby");
const label = id ? document.querySelector(`label[for="${id}"]`) : null;
return !label && !ariaLabel && !ariaLabelledBy;
})
.length
');
$this->assertEquals(0, $unlabeledInputs,
'All form inputs must have associated labels'
);
});
}
اختبار سمات ARIA
<?php
/** @test */
public function interactive_elements_have_roles()
{
$this->browse(function (Browser $browser) {
$browser->visit('/dashboard');
// يجب أن يكون للأزرار دور الزر
$nonButtonElements = $browser->script('
return Array.from(document.querySelectorAll("[role=\"button\"]"))
.filter(el => el.tagName !== "BUTTON")
.every(el => el.hasAttribute("tabindex") && el.hasAttribute("role"))
');
$this->assertTrue($nonButtonElements);
});
}
/** @test */
public function expandable_content_has_proper_aria()
{
$this->browse(function (Browser $browser) {
$browser->visit('/faq');
// فحص أزرار الأكورديون
$accordionButtons = $browser->elements('[aria-expanded]');
foreach ($accordionButtons as $button) {
$expanded = $button->getAttribute('aria-expanded');
$controls = $button->getAttribute('aria-controls');
$this->assertContains($expanded, ['true', 'false']);
$this->assertNotEmpty($controls);
// التحقق من وجود العنصر المتحكم فيه
$this->assertNotNull($browser->element('#' . $controls));
}
});
}
اختبار تباين الألوان
تتطلب WCAG نسب تباين دنيا بين النص والخلفية.
متطلبات تباين WCAG:
- المستوى AA: 4.5:1 للنص العادي، 3:1 للنص الكبير
- المستوى AAA: 7:1 للنص العادي، 4.5:1 للنص الكبير
- النص الكبير: 18pt+ أو 14pt+ عريض
اختبار التباين برمجيًا
<?php
/** @test */
public function text_has_sufficient_contrast()
{
$this->browse(function (Browser $browser) {
$browser->visit('/');
// استخدم axe-core للتحقق من التباين
$violations = $browser->script('
return new Promise((resolve) => {
axe.run(document, {
runOnly: ["color-contrast"]
}, (err, results) => {
resolve(results.violations);
});
});
');
$this->assertCount(0, $violations,
'Found color contrast violations: ' . json_encode($violations)
);
});
}
اختبار روابط التخطي
تسمح روابط التخطي لمستخدمي لوحة المفاتيح بتجاوز التنقل المتكرر.
<?php
/** @test */
public function page_has_skip_to_main_content_link()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertPresent('a[href="#main-content"]')
->keys('body', '{tab}') // التبويب الأول يجب أن يركز على رابط التخطي
->assertFocused('a[href="#main-content"]')
->keys(null, '{enter}')
->assertFocused('#main-content');
});
}
اختبار المحتوى الديناميكي
يجب إخطار قارئات الشاشة بالتغييرات الديناميكية.
اختبار مناطق ARIA الحية
<?php
/** @test */
public function notifications_use_aria_live_regions()
{
$this->browse(function (Browser $browser) {
$browser->visit('/dashboard');
// التحقق من وجود منطقة حية
$this->assertNotNull($browser->element('[aria-live="polite"]'));
// تشغيل الإشعار
$browser->press('@save-button');
// التحقق من ظهور الإشعار في المنطقة الحية
$browser->waitFor('[aria-live] .notification')
->assertSeeIn('[aria-live]', 'Changes saved successfully');
});
}
/** @test */
public function loading_states_are_announced()
{
$this->browse(function (Browser $browser) {
$browser->visit('/products');
$loadingRegion = $browser->element('[aria-busy="true"]');
$this->assertNotNull($loadingRegion);
$this->assertEquals('Loading products...',
$loadingRegion->getAttribute('aria-label')
);
});
}
اختبار مربعات الحوار المشروطة
<?php
/** @test */
public function modal_traps_focus()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->click('@open-modal-button')
->waitFor('[role="dialog"]')
->assertAttribute('[role="dialog"]', 'aria-modal', 'true');
// يجب حبس التركيز في المودال
$firstFocusable = $browser->element('[role="dialog"] button:first-of-type');
$lastFocusable = $browser->element('[role="dialog"] button:last-of-type');
// التبويب إلى العنصر الأخير
$browser->keys($lastFocusable, '{tab}');
// يجب العودة إلى العنصر الأول
$browser->assertFocused($firstFocusable);
// Escape يجب أن يغلق المودال
$browser->keys(null, '{escape}')
->waitUntilMissing('[role="dialog"]');
});
}
مجموعة اختبارات إمكانية الوصول الشاملة
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ComprehensiveA11yTest extends DuskTestCase
{
protected $pagesToTest = [
'/',
'/about',
'/products',
'/contact',
'/blog',
];
/** @test */
public function all_pages_pass_automated_checks()
{
$this->browse(function (Browser $browser) {
foreach ($this->pagesToTest as $url) {
$browser->visit($url);
$violations = $browser->getA11yViolations();
$this->assertCount(0, $violations,
"Page {$url} has violations: " . json_encode($violations, JSON_PRETTY_PRINT)
);
}
});
}
/** @test */
public function all_pages_are_keyboard_navigable()
{
$this->browse(function (Browser $browser) {
foreach ($this->pagesToTest as $url) {
$browser->visit($url);
// يجب أن تكون قادرًا على التنقل عبر جميع العناصر التفاعلية
$interactiveElements = $browser->script('
return document.querySelectorAll(
"a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])"
).length
');
$this->assertGreaterThan(0, $interactiveElements,
"Page {$url} has no interactive elements"
);
}
});
}
}
تمرين تطبيقي:
أنشئ مجموعة اختبارات إمكانية الوصول لنموذج تسجيل يتحقق من:
- جميع حقول النموذج لها تسميات مرتبطة
- يتم الإعلان عن رسائل الخطأ لقارئات الشاشة
- النموذج قابل للوصول إليه بالكامل عبر لوحة المفاتيح
- تباين الألوان يلبي معايير WCAG AA
- الحقول المطلوبة مشار إليها بشكل صحيح
- حالات النجاح/الخطأ تستخدم مناطق aria-live
قائمة التحقق من اختبار إمكانية الوصول:
- تشغيل الأدوات التلقائية (axe-core، WAVE)
- اختبار التنقل بلوحة المفاتيح
- التحقق من بنية HTML الدلالية
- فحص تباين الألوان
- الاختبار باستخدام قارئات الشاشة الفعلية
- التحقق من سمات ARIA
- اختبار إدارة التركيز
- فحص التصميم المتجاوب
- الاختبار مع المستخدمين ذوي الإعاقة (عندما يكون ذلك ممكنًا)
الخلاصة
اختبار إمكانية الوصول يضمن أن تطبيقك قابل للاستخدام من قبل الجميع:
- الأدوات التلقائية: axe-core و WAVE يمسكان ~57% من المشاكل
- الاختبار اليدوي: التنقل بلوحة المفاتيح وقارئات الشاشة يمسكان البقية
- امتثال WCAG: استهدف المستوى AA كحد أدنى
- الاختبار المستمر: دمج اختبارات إمكانية الوصول في خط CI/CD
- المستخدمون الحقيقيون: لا شيء يتفوق على الاختبار مع المستخدمين ذوي الإعاقة الفعليين