{"id":"l-a11y-5-1","courseId":"web-accessibility","lang":"ar","kind":"lesson","summary":"شغّل axe في المتصفّح وفي اختبارات Playwright لتلتقط مبكرًا الإخفاقات الشائعة التي تستطيع الآلة رصدها، واعرف بالضبط أيّ المشكلات لن تجدها الأتمتة أبدًا.","html":"<p>لسمير مقولة: «الروبوت يجد الأخطاء الإملائية، والإنسان يجد الثغرات في الحبكة». أدوات إمكانية الوصول الآلية سريعة ورخيصة ولا تتعب، وتجد نسبة كبيرة من المشكلات في الصفحة المعتادة خلال ثوانٍ. لكنها عاجزة تمامًا عن معرفة إن كان النص البديل صادقًا، أو إن كان ترتيب التركيز منطقيًا، أو إن كانت نافذة الحوار قابلة للاستخدام. يوضّح هذا الدرس كيف تحصل منها على أقصى فائدة، وأين تتوقّف.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">axe وعائلته</h2>\n<p><strong>axe-core</strong>، الذي تصونه شركة Deque، مكتبة JavaScript مفتوحة المصدر لقواعد إمكانية الوصول. تعمل داخل الصفحة، وتفحص الـ DOM والأنماط المحسوبة، وتبلّغ عن المخالفات مع معيار WCAG المعنيّ والعنصر المخالف واقتراح للإصلاح. وهي حريصة على نحو غير معتاد على تجنّب الإنذارات الكاذبة: حين لا تكون متأكدة، تبلّغ عن العنصر على أنه “needs review” (يحتاج إلى مراجعة) لا على أنه مخالفة.</p>\n<p>ستقابلها في عدة أماكن:</p>\n<ul>\n<li><strong>axe DevTools</strong>، إضافة للمتصفّح، للفحص عند الطلب أثناء التطوير.</li>\n<li>فئة إمكانية الوصول في <strong>Lighthouse</strong> داخل أدوات المطوّر في Chrome، التي تشغّل جزءًا من قواعد axe وتعطي درجة.</li>\n<li><strong>تكاملات الاختبار</strong>: <code dir=\"ltr\">@axe-core/playwright</code> للاختبارات الشاملة (end-to-end)، وأغلفة مثل <code dir=\"ltr\">vitest-axe</code> لاختبارات الـ components.</li>\n</ul>\n<p>وإضافة <strong>WAVE</strong> من WebAIM و<strong>Accessibility Insights</strong> من Microsoft بدائل أو رفاق جيدون؛ وتضيف Accessibility Insights أيضًا فحوصًا يدوية موجَّهة.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">ما تلتقطه الأتمتة، وما لا تستطيعه</h2>\n<p>القواعد الآلية جيدة في الحقائق الموضوعية البنيوية:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"جدول قابل للتمرير\" tabindex=\"0\"><table><thead>\n<tr>\n<th>تلتقطه الأتمتة بشكل موثوق</th>\n<th>يحتاج إلى إنسان</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>غياب خاصية alt</td>\n<td>هل النص البديل دقيق ومفيد</td>\n</tr>\n<tr>\n<td>حقول وأزرار وروابط بلا اسم</td>\n<td>هل للاسم معنى، وهل يطابق التسمية المرئية</td>\n</tr>\n<tr>\n<td>تباين نصي أقل من 4.5:1 أو 3:1</td>\n<td>التباين فوق الصور والتدرّجات اللونية</td>\n</tr>\n<tr>\n<td>غياب <code dir=\"ltr\">lang</code>، وتكرار الـ ids، وقيم ARIA غير الصالحة</td>\n<td>هل اللغة صحيحة لكل جزء</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">aria-hidden</code> على عناصر قابلة للتركيز</td>\n<td>ترتيب التركيز، ومصائد لوحة المفاتيح، والتركيز بعد إغلاق نافذة حوار</td>\n</tr>\n<tr>\n<td>بعض مشكلات العناوين والـ landmarks</td>\n<td>هل العناوين تصف المحتوى</td>\n</tr>\n<tr>\n<td></td>\n<td>هل تُعلَن التحديثات الحيّة، وهل التعليمات واضحة</td>\n</tr>\n</tbody>\n</table></div><p>كم يمثّل ذلك إجمالًا؟ الدراسات تختلف لأنها تحسب بطرق مختلفة. حين زرعت دائرة الخدمات الرقمية الحكومية في المملكة المتحدة (GDS) عام 2017 ما مجموعه 142 عائقًا معروفًا في صفحة اختبار، وجدت أفضل أداة 40% منها، ووجد بعضها أقل من ذلك بكثير. أما Deque، التي تحصي المشكلات في آلاف عمليات التدقيق الحقيقية، فتقول إن الاختبارات الآلية التقطت نحو 57% من المشكلات من حيث العدد، ويعود ذلك أساسًا إلى أن بضع مشكلات قابلة للأتمتة، وخاصةً التباين، تتكرّر كثيرًا. الخلاصة المنصفة: الأتمتة تجد المشكلات الأكثر تكرارًا، لكنها لا تغطّي إلا أقلية مما تطلبه WCAG.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>مطاردة الدرجة</span></p>\n<p>أمضى فريق ذات مرة sprint كاملًا يرفع درجة Lighthouse في Ticketly من 86 إلى 100، معظم ذلك بإضافة <code dir=\"ltr\">aria-label</code> في كل مكان اشتكت منه قاعدة. ارتفعت الدرجة وساءت التجربة، لأن عدة تسميات لم تعد تطابق النص المرئي. أصلح المشكلة الحقيقية التي تشير إليها القاعدة، ولا تُضِف ARIA أبدًا لمجرد إسكات أداة.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">axe في اختبارات Playwright</h2>\n<p>الفحص في الـ CI يحوّل إمكانية الوصول إلى اختبار تراجع (regression test): بمجرد أن تصبح الصفحة نظيفة، يُفشل أي label مفقود جديد الـ build. مع Playwright و<code dir=\"ltr\">@axe-core/playwright</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">tests/a11y.spec.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { test, expect } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@playwright/test'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> AxeBuilder </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@axe-core/playwright'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> WCAG_AA</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'wcag2a'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'wcag2aa'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'wcag21a'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'wcag21aa'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'wcag22aa'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'event page has no detectable WCAG A/AA violations'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">goto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/events/friday'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> results</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> AxeBuilder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ page }).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">withTags</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">WCAG_AA</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">analyze</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(results.violations).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toEqual</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'booking dialog has no detectable violations when open'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">goto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/events/friday'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Book now Friday pass'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">click</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'dialog'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Book Friday pass'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> })).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBeVisible</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> results</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> AxeBuilder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ page })</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">include</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#booking'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">withTags</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">WCAG_AA</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">analyze</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(results.violations).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toEqual</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>الاختبار الثاني لا يقلّ أهمية عن الأول. لا يرى axe إلا ما هو معروض لحظة الفحص، فنوافذ الحوار المغلقة والقوائم المطويّة وحالات الخطأ والسمة الداكنة تبقى غير مختبَرة ما لم يضع اختبارك الصفحة في تلك الحالات أولًا.</p>\n<p>لاحظ أن الاختبار يجد العناصر أيضًا عبر <code dir=\"ltr\">getByRole</code> واسم متاح. تحديد العناصر بالدور والاسم هو الأسلوب الذي يوصي به Playwright أصلًا، ويمنحك فحصًا صغيرًا لإمكانية الوصول مجانًا: إن فقدت نافذة الحوار اسمها، يتوقّف الـ locator عن المطابقة.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>شدّ تدريجيًا ولا تحجب كل شيء</span></p>\n<p>في قاعدة كود قائمة، قد يبلّغ الفحص الأول عن مئات المشكلات. بدلًا من إيقاف الـ CI، سجّل المخالفات الحالية كخطّ أساس (baseline) واجعل الـ build يفشل على المخالفات الجديدة فقط. ثم أصلح خطّ الأساس مع الوقت، بدءًا بالصفحات التي يستخدمها أكبر عدد من الناس، مثل صفحة الدفع.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">الـ Linting يلتقط المشكلات أبكر</h2>\n<p>في مشاريع React، ينبّه <code dir=\"ltr\">eslint-plugin-jsx-a11y</code> إلى المشكلات وأنت تكتب: عنصر <code dir=\"ltr\">img</code> بلا <code dir=\"ltr\">alt</code>، أو <code dir=\"ltr\">div</code> عليه <code dir=\"ltr\">onClick</code> بلا دور أو معالج مفاتيح، أو خاصية <code dir=\"ltr\">aria-*</code> غير موجودة. يلتقط أقل مما يلتقطه axe لأنه لا يرى إلا الكود المصدري لا الصفحة المعروضة، لكنه يلتقطه بعد دقائق من الخطأ بدلًا من أيام.</p>\n<p>هكذا يبدو الإعداد متعدّد الطبقات: الـ linter في المحرّر، وaxe في اختبارات الـ components والاختبارات الشاملة، وإنسان بلوحة مفاتيح وقارئ شاشة قبل كل إصدار. والدرس التالي هو ذلك الإنسان.</p>\n","toc":[{"id":"sec-1","text":"axe وعائلته","level":2},{"id":"sec-2","text":"ما تلتقطه الأتمتة، وما لا تستطيعه","level":2},{"id":"sec-3","text":"axe في اختبارات Playwright","level":2},{"id":"sec-4","text":"الـ Linting يلتقط المشكلات أبكر","level":2}],"takeaways":["axe-core هو محرّك القواعد مفتوح المصدر الذي يقف خلف axe DevTools وتدقيق إمكانية الوصول في Lighthouse وكثير من تكاملات الاختبار.","تلتقط الأدوات الآلية بشكل موثوق غياب alt والـ labels والأسماء، وإخفاقات التباين، وقيم ARIA غير الصالحة، وغياب لغة الصفحة.","لا تستطيع هذه الأدوات الحكم على المعنى، فجودة alt وترتيب التركيز وسلوك لوحة المفاتيح والإعلانات الحيّة وعدم تطابق التسمية مع الاسم تحتاج إلى إنسان.","شغّل axe في الـ CI على الصفحات والحالات الأساسية، مثل نافذة حوار مفتوحة، لتُفشل التراجعاتُ الـ build، واعتبر الفحص النظيف الحدّ الأدنى لا خطّ النهاية."],"further":[{"title":"Playwright: Accessibility testing","url":"https://playwright.dev/docs/accessibility-testing"},{"title":"W3C WAI: Selecting Web Accessibility Evaluation Tools","url":"https://www.w3.org/WAI/test-evaluate/tools/selecting/"},{"title":"web.dev Learn Accessibility: Automated testing","url":"https://web.dev/learn/accessibility/test-automated"}],"quiz":[{"q":"<p>تحصل الصفحة الرئيسية في Ticketly على 100 في تدقيق إمكانية الوصول في Lighthouse. ماذا تستنتج؟</p>","options":[{"text":"الصفحة تحقّق WCAG 2.2 AA","why":"يشغّل Lighthouse جزءًا من قواعد axe. والدرجة الكاملة تعني أن أيًّا من تلك الفحوص الآلية لم يفشل، لا أن الصفحة متوافقة."},{"text":"لم يجد أيٌّ من الفحوص الآلية التي يشغّلها Lighthouse مشكلة؛ وما زالت مشكلات لوحة المفاتيح وقارئ الشاشة والمحتوى غير مختبَرة","why":"صحيح. إنها علامة جيدة وحماية مفيدة من التراجعات، لكن معظم WCAG يحتاج إلى حكم بشري."},{"text":"لن يواجه مستخدمو قارئ الشاشة أي صعوبة معها","why":"لا تستطيع الأدوات الآلية أن تعرف إن كانت الأسماء ذات معنى، أو إن كان التركيز ينتقل بشكل منطقي، أو إن كانت التحديثات تُعلَن. وحده الاختبار بقارئ شاشة يُظهر ذلك."}],"answer":1},{"q":"<p>أيّ هذه المشكلات سيرصدها axe افتراضيًا؟</p>","options":[{"text":"صورة عليها <code dir=\"ltr\">alt=&quot;image&quot;</code>","why":"خاصية alt موجودة، فلا يوجد ما ترصده القاعدة. أما إن كان النص ذا معنى فهذا يحتاج إلى إنسان."},{"text":"نافذة منبثقة تترك التركيز خلفها حين تُفتح","why":"إدارة التركيز تعتمد على التفاعل والقصد؛ ولا يستطيع فحص ثابت أن يعرف أين يجب أن يذهب التركيز."},{"text":"رسالة حالة في فقرة عادية لا تُعلنها قارئات الشاشة أبدًا","why":"لا يستطيع axe أن يعرف أيّ نص يتغيّر ديناميكيًا أو يجب أن يُعلَن. وهذا يُكتشف بالاختبار بقارئ شاشة."},{"text":"حقل نصي بلا label وبلا اسم متاح","why":"صحيح. غياب labels النماذج هو بالضبط نوع الإخفاقات الموضوعية البنيوية التي ترصدها القواعد الآلية بشكل موثوق."}],"answer":3},{"q":"<p>لماذا قد ينجح اختبار Playwright هذا مع أن نافذة الحجز في Ticketly فيها إخفاقات تباين؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">goto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/events/friday'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> results</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> AxeBuilder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ page }).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">analyze</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(results.violations).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toEqual</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([]);</span></span></code></pre></div>\n","options":[{"text":"يتجاهل AxeBuilder تباين الألوان افتراضيًا","why":"color-contrast من القواعد الافتراضية في axe. المشكلة في ما هو موجود في الصفحة لحظة الفحص."},{"text":"النافذة مغلقة حين يُجرى الفحص، فمحتواها غير معروض ولا يُختبر","why":"صحيح. افتح النافذة في الاختبار (انقر Book now) وأعد الفحص، ويُفضَّل استخدام ‎.include() على النافذة، لتختبر تلك الحالة."},{"text":"يعمل Playwright دون واجهة (headless)، ولا يمكن قياس التباين دون نافذة مرئية","why":"المتصفّحات التي تعمل دون واجهة تعرض الصفحات كاملة، وaxe يحسب التباين من الأنماط، فوضع headless ليس هو المشكلة."}],"answer":1}],"exercise":{"kind":"spot-bug","language":"html","code":"<main>\n  <h1>Harbour Lights 2026</h1>\n  <img src=\"crowd.jpg\" alt=\"image\">\n  <p class=\"section-title\">Tickets</p>\n  <label for=\"qty\">Quantity</label>\n  <input id=\"qty\" type=\"number\" min=\"1\" max=\"6\" value=\"1\">\n  <button type=\"button\" onclick=\"addToBasket()\">Add to basket</button>\n  <p id=\"basket-status\"></p>\n  <button type=\"button\" aria-label=\"Book\">Reserve seats</button>\n  <a href=\"/faq\">Festival FAQ</a>\n</main>","bugLines":[3,4,8,9],"title":"جِد ما لا يجده axe","prompt":"<p>هذا الجزء من Ticketly ينجح في فحص axe <strong>بلا أي مخالفة</strong>. ومع ذلك يصطدم مستخدم قارئ الشاشة بأربعة إخفاقات حقيقية في WCAG. اختر الأسطر التي سينبّه إليها مراجع بشري. (‎<code dir=\"ltr\">.section-title</code> منسّق ليبدو تمامًا مثل عناوين الأقسام الأخرى، و<code dir=\"ltr\">addToBasket()</code> تكتب “Ticket added” داخل <code dir=\"ltr\">#basket-status</code>.)</p>\n","hints":["يتحقّق axe من وجود الأشياء. ابحث عن أشياء موجودة لكنها تعني الشيء الخطأ.","قارن ما يراه المستخدم المبصر بما تقوله شجرة إمكانية الوصول، سطرًا سطرًا.","مشكلة تخصّ النص، وأخرى البنية، وثالثة التحديثات الديناميكية، ورابعة التحكّم الصوتي."],"explanation":"<p><strong>السطر 3:</strong> ‏<code dir=\"ltr\">alt=&quot;image&quot;</code> موجودة، فيرضى axe، لكنها لا تخبر مستخدم قارئ الشاشة بشيء (“image, image”). صِف الصورة، أو إن كانت زخرفية فاستخدم <code dir=\"ltr\">alt=&quot;&quot;</code>.</p>\n<p><strong>السطر 4:</strong> تبدو الفقرة عنوانَ قسم لكنها ليست كذلك، فهي غائبة عن التنقّل بين العناوين (1.3.1 Info and Relationships). استخدم <code dir=\"ltr\">&lt;h2&gt;Tickets&lt;/h2&gt;</code>.</p>\n<p><strong>السطر 8:</strong> يتغيّر <code dir=\"ltr\">#basket-status</code> بعد كل نقرة، لكنه ليس live region، فلا تُعلن قارئات الشاشة شيئًا (4.1.3 Status Messages). أضِف <code dir=\"ltr\">role=&quot;status&quot;</code>.</p>\n<p><strong>السطر 9:</strong> النص المرئي “Reserve seats” لكن الاسم المتاح “Book”، فمستخدم التحكّم الصوتي الذي يقول “click Reserve seats” لا يحصل على شيء (2.5.3 Label in Name). احذف الـ <code dir=\"ltr\">aria-label</code>. لدى axe قاعدة لهذا، لكنها تجريبية ومعطّلة افتراضيًا.</p>\n<p>بقية الأسطر سليمة: حقل الإدخال له label، وللأزرار والرابط أسماء واضحة.</p>\n","checkLabels":{}},"words":676,"readMinutes":4}