flutter_a11y_check 0.1.0
flutter_a11y_check: ^0.1.0 copied to clipboard
One-line accessibility audit for Flutter widget tests. Checks tap targets, labels, text contrast, images, 200% font size, duplicate labels and focus order, and prints a readable report (text, Markdown [...]
flutter_a11y_check
One line in a widget test audits a whole screen for accessibility and prints a report you can actually act on.
English summary.
flutter_a11y_checkis a dev dependency for widget tests. After you pump a screen,await expectAccessible(tester)checks it for tap targets under 48x48 (or 44x44 on iOS), tappable widgets with no label, text contrast below WCAG AA/AAA measured from the rendered frame, images with nosemanticLabel, layouts that overflow, cut text or ignore the setting at 200% font size, repeated or meaningless labels ("Remove" x3, "Click here"), and, if you turn it on, screen-reader order that jumps against the visual (RTL-aware) order. It collects every violation, not only the first, and each finding comes with a severity, a rule id, the widget and its ancestor path (BadScreen > Scaffold > AppBar > IconButton), its rect, the measured values and a concrete fix. The report prints as colored console text, Markdown (for PR comments) or JSON (for CI). You can ignore findings by rule, finder, key or label, run at several text scales and in RTL, and choose which severity fails the test. It builds on the logic offlutter_test'sandroidTapTargetGuideline,labeledTapTargetGuidelineandtextContrastGuideline, and adds the rest.
سطر واحد جوه الـ widget test بيفحص الشاشة كلها من ناحية الـ accessibility، وبدل
رسالة meetsGuideline المختصرة اللي بتقف عند أول غلطة، بيطلعلك تقرير فيه كل
المشاكل: الويدجت، ومكانه في الشجرة، ومقاسه، والأرقام اللي اتقاست، وتصلّحها إزاي.
✨ بيعمل إيه #
testWidgets('login is accessible', (tester) async {
await tester.pumpWidget(const MyApp());
await expectAccessible(tester); // بيفشل التست ومعاه التقرير كامل
});
أو لو عايز التقرير من غير ما التست يفشل:
final report = await auditAccessibility(tester);
debugPrint(report.toMarkdown()); // أو toText() أو toJsonString()
| القاعدة | الخطورة | بتمسك إيه |
|---|---|---|
tap-target |
🔴 error | أي حاجة بتتداس أصغر من 48x48 (أو 44x44 لو minTapTarget: 44) |
missing-label |
🔴 error | IconButton من غير tooltip، أو GestureDetector/InkWell حوالين أيقونة أو صورة من غير Semantics |
contrast |
🔴 error | نص التباين بتاعه أقل من WCAG AA (4.5:1 للنص العادي، 3:1 للكبير)، متقاس من الصورة اللي اترسمت فعلًا |
image-label |
🟡 warning | Image من غير semanticLabel ومش متعلّمة إنها ديكور (excludeFromSemantics) |
text-scale |
🔴 / 🟡 | الشاشة بتتكسر على 200%: overflow، أو نص بيتقص (ellipsis)، أو نص مش بيكبر خالص |
duplicate-label |
🟡 warning | كذا زرار على نفس الشاشة اسمهم واحد (تلات زراير «Remove») |
generic-label |
🟡 warning | label مبيقولش حاجة: «Button»، «Icon»، «Click here» |
focus-order |
🔵 info | (اختياري) ترتيب الـ screen reader / switch access بيرجع لورا عكس ترتيب القراية، ويعرف الـ RTL |
- 🧭 كل finding فيه: severity، و rule id، ورسالة بالإنجليزي، واقتراح تصليح، ونوع الويدجت،
والـ path بتاعه (
BadScreen > Scaffold > AppBar > IconButton)، والـ rect، والأرقام (المقاس، نسبة التباين، الألوان، اللون المقترح). - 🔁 بيلف على أكتر من حالة: كل
textScales(الافتراضي 100% و 200%)، وكل locale إضافي، وrtl: true. نفس المشكلة لو ظهرت في أكتر من حالة بتتكتب مرة واحدة، ومكتوب جنبها ظهرت فين. - 🙈 تجاهل بالقاعدة، أو بـ
Finder، أو بـKey، أو بالـ label. - 🎨 التقرير: نص ملوّن للـ console، و Markdown لكومنت الـ PR، و JSON للـ CI.
- ♻️ بيرجّع الـ text scale والـ locale زي ما كانوا بعد الفحص.
📦 التسطيب #
من GitHub كـ git dependency في dev_dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_a11y_check:
git:
url: https://github.com/youssef336/flutter_a11y_check.git
ref: v0.1.0
وبعدين flutter pub get، و import 'package:flutter_a11y_check/flutter_a11y_check.dart'; في التست.
⚙️ الإعدادات #
await expectAccessible(
tester,
config: A11yConfig(
minTapTarget: A11yConfig.iosTapTarget, // 44 (الافتراضي 48 بتاع Android)
contrast: ContrastLevel.aa, // aa | aaa | off
textScales: [1.0, 1.5, 2.0], // 1.0 بيتضاف دايمًا
rtl: true, // يعيد الفحص بـ Locale('ar')
rules: {...A11yRule.defaults, A11yRule.focusOrder},
failOn: A11ySeverity.error, // الافتراضي warning
severityOverrides: {A11yRule.imageLabel: A11ySeverity.error},
ignoreRules: {A11yRule.genericLabel},
ignore: [
A11yIgnore.key(const Key('brand-logo'), rules: {A11yRule.imageLabel}),
A11yIgnore.finder(find.byType(GoogleMap), reason: 'third-party view'),
A11yIgnore.label('Remove', rules: {A11yRule.duplicateLabel}),
],
pathLandmarks: {'ProductCard'}, // يظهر في الـ path
colors: false, // من غير ANSI
),
);
🧾 شكل التقرير #
ده جزء من التقرير بتاع الشاشة الوحشة اللي في example/ (بالـ toText(color: false)):
Accessibility audit FAILED: 6 errors, 5 warnings, 0 info
runs: 100%, 200%, 100% ar (rtl), 200% ar (rtl)
ERROR tap-target GestureDetector is 24x24, smaller than the 48x48 minimum tap target.
where BadScreen > Scaffold > ListView > Row > GestureDetector
rect (80, 188) 24x24 at 100%
fix Give the tappable area at least 48x48: add padding inside the gesture detector or wrap it in ConstrainedBox(constraints: BoxConstraints(minWidth: 48, minHeight: 48)).
ERROR missing-label IconButton can be tapped but has no label, so a screen reader announces only "button" (or nothing).
where BadScreen > Scaffold > AppBar > Row > IconButton
rect (752, 4) 48x48 at 100%
fix Add tooltip: 'Describe the action' - Flutter uses the tooltip as the label.
ERROR contrast "Prices include VAT" has contrast 1.79:1 (#BDBDBD on #FEF7FF); WCAG AA needs 4.5:1 for normal text (14px).
where BadScreen > Scaffold > ListView > Text
rect (16, 148) 768x20 at 100%
fix Use #737373 (or darker/stronger) for the text, or change the background, or make the text large (24px+, or 18.66px+ bold).
ERROR text-scale Breaks at 200% font size: Row overflows (A RenderFlex overflowed by 350 pixels on the right.).
where BadScreen > Scaffold > ListView > Row
rect (16, 72) 768x40 at 200%
fix Wrap the text children in Expanded/Flexible so they can wrap, or switch to Wrap when the content does not fit on one line.
WARN duplicate-label 3 tappable TextButton widgets on this screen are all labelled "Remove"; screen-reader users cannot tell them apart.
where BadScreen > Scaffold > ListView > Row > TextButton
rect (675.4, 224) 108.6x48 at 100%
also BadScreen > Scaffold > ListView > Row > TextButton
also BadScreen > Scaffold > ListView > Row > TextButton
fix Make each label unique by adding context, e.g. 'Delete Invoice #12', or merge the repeated controls into one.
🧪 المثال #
example/ فيه app بشاشتين: BadScreen فيها مثال لكل مشكلة، و GoodScreen نفس الشاشة بعد
التصليح. التستات في example/test/rules_test.dart بتثبت إن كل قاعدة بتمسك الشاشة الوحشة،
وإن الشاشة الكويسة بتعدّي كل القواعد على 100% و 200% وبالعربي RTL.
cd example && flutter test
❓ أسئلة #
ليه مش meetsGuideline وخلاص؟
meetsGuideline بيقولك «فيه مشكلة» ويديك SemanticsNode#42 ومقاس. هنا بتاخد كل المشاكل
مرة واحدة، والويدجت باسمه ومكانه في الشجرة، والتصليح. وفيه قواعد مش موجودة هناك: الصور، والـ
200%، والأسماء المكررة، وترتيب الـ focus.
التست بقى أبطأ؟
كل حالة (scale × locale) بتعمل pump وصورة للشاشة عشان التباين. لو عايز أسرع:
textScales: [1.0] أو contrast: ContrastLevel.off.
الـ RTL مش بيشتغل؟
الفحص بيغيّر الـ locale بتاع الـ test platform بس. لازم الـ app يكون فيه Locale('ar') في
supportedLocales ومعاه GlobalMaterialLocalizations.delegates. لو الشاشة فضلت LTR، التقرير
بيقولك في notes ومش بيعيد نفس الفحص على الفاضي.
عندي warning مقبول (زي «Delete» في كل صف في لستة)؟
A11yIgnore.label('Delete', rules: {A11yRule.duplicateLabel})، أو
failOn: A11ySeverity.error عشان الـ warnings تظهر من غير ما تفشّل التست.
هل بيشتغل مع Cupertino؟
أيوه، القواعد كلها على الـ semantics tree والـ render tree، مش على Material بس.
للـ iOS استخدم minTapTarget: A11yConfig.iosTapTarget.
⚖️ الحدود #
- التباين بيتقاس من الصورة اللي اترسمت في التست. خط التست بيرسم مربعات، فالنتيجة دقيقة للون النص على الخلفية، بس مش بتشوف anti-aliasing الخط الحقيقي. ولو النص فوق صورة أو gradient، الخلفية بتتاخد من أكتر لون حوالين النص.
- «نص كبير» حسب WCAG: 24px، أو 18.66px لو bold (بعد الـ scale). ده أشد شوية من
flutter_testاللي بيعتبر 18px كبير. - الـ
text-scaleبيشوف الـ overflow اللي Flutter بيبلّغ عنه (RenderFlex overflowed) والنص اللي اتقص بـmaxLines. نص بيتقص بـClipRectأو container ارتفاعه ثابت من غير error مش بيبان. وكلRenderFlexبيبلّغ عن الـ overflow مرة واحدة بس، فبيتكتب عند أول scale كسرته. - الـ widget path تقريبي: بيعرض widgets معروفة (Scaffold، AppBar، Row …) وأي widget اسمه
بيخلص بـ
Screen/Page/View/Dialog/Sheet. ضيف أسماءك فيpathLandmarks. focus-orderبيقارن ترتيب الـ semantics (اللي بيستخدمه TalkBack / VoiceOver / switch access) بترتيب القراية، ومش بيقراFocusTraversalPolicyبتاع الكيبورد. وهو info واختياري لإن التصميمات اللي فيها أعمدة جنب بعض ممكن تطلّع ملاحظات مقصودة.- مش بيفحص: الـ labels بلغة الـ app، ولا الـ live regions، ولا الـ custom actions، ولا تباين الأيقونات والعناصر اللي مش نص (WCAG 1.4.11).
🛠 Tech stack #
- Dart + Flutter, depends only on the Flutter SDK (
flutter,flutter_test). - Reads the semantics tree, the element/render trees, and the rendered frame (
OffsetLayer.toImage) inside the normalflutter testenvironment; no golden files, no device. - Tap-target, label and contrast logic adapted from
flutter_test's accessibility guidelines.
🏗 Project structure #
| File | Role |
|---|---|
lib/flutter_a11y_check.dart |
public API |
lib/src/audit.dart |
auditAccessibility / expectAccessible, runs per text scale and locale, ignores, dedupe |
lib/src/config.dart |
A11yConfig, ContrastLevel |
lib/src/finding.dart, lib/src/report.dart |
findings, rules, severities; text / Markdown / JSON report |
lib/src/contrast.dart |
WCAG luminance and ratio, pixel histogram, suggested color |
lib/src/context.dart, lib/src/widget_path.dart |
semantics node → widget, ancestor paths, merged gestures |
lib/src/rules/ |
one file per rule family |
example/ |
bad and good checkout screens + tests for every rule |
test/ |
unit tests: contrast math, report formatting, ignores, config |
🚀 Getting started #
git clone https://github.com/youssef336/flutter_a11y_check.git
cd flutter_a11y_check
flutter pub get
flutter test # package tests
cd example && flutter test # every rule on the bad screen, nothing on the good one
📄 License #
👤 Author #
Youssef Belal Zakaria · GitHub @youssef336
⭐ If you like this project, give it a star!