تخطَّ إلى المحتوى

Flutter و React Native

يقود Astur التطبيق الحقيقي على جهاز أو محاكي حقيقي، ولذلك لا يهمّ إطار الواجهة عادةً: فالتطبيقات الأصلية (‏Swift/Kotlin/Java/Obj-C) و React Native و Flutter كلها تعمل عبر واجهة @astur-mobile/test نفسها، وعبر الـ inspector وتوليد الكود نفسيهما.

والفرق هو في كيفية قراءة شجرة الواجهة:

نوع التطبيق Android iOS
‏SDK أصلي وكيل UIAutomator وكيل XCUITest
‏React Native وكيل UIAutomator (عروض أصلية) وكيل XCUITest (عروض أصلية)
‏Flutter خدمة Dart VM (شجرة الودجات) وكيل XCUITest (إمكانية الوصول/‏semantics)

القاعدة بسيطة: إذا كان الإطار يرسم عناصر أصلية (Native Views) حقيقية، فإن Astur يقوده — لأن الشجرة التي يقرأها هي شجرة المنصة نفسها، ولا حاجة لأي معالجة خاصة بالإطار. أما الأطر التي ترسم بكسلاتها الخاصة فهي وحدها التي تحتاج مساراً مخصصاً.

الإطار Android iOS ملاحظات
المنصة الأصلية (Kotlin/Java و Swift/Obj-C) نعم نعم الأساس الذي يُقاس عليه كل ما عداه
‏Jetpack Compose نعم يُصيّر إلى عقد إمكانية وصول أصلية
‏SwiftUI نعم يُطابَق مع أنواع XCUIElementType القياسية
‏React Native نعم نعم مكونات أصلية حقيقية، و testID يتحول إلى المُعرّف الأصلي
‏Expo نعم نعم يُبنى إلى React Native
‏Flutter نعم نعم تصيير مخصص، ولذلك له مساره الخاص — Dart VM على Android وشجرة الدلالات على iOS
‏.NET MAUI نعم نعم يُترجم إلى عناصر تحكم أصلية على المنصتين
‏NativeScript نعم نعم يُصيّر إلى عناصر أصلية
‏Cordova / Capacitor / Ionic نعم نعم الغلاف أصلي، ويُوصل إلى محتوى الويب عبر device.webContext()
‏Kotlin Multiplatform (واجهة مشتركة) نعم لم يجرِ التحقق جانب Android هو Compose اعتيادي، أما Compose Multiplatform على iOS فلم يُختبر
‏Unity ومحركات الألعاب غير مدعوم غير مدعوم سطح رسم واحد بلا شجرة عناصر — لا يوجد ما يمكن تحديده

لم يجرِ اختبار كل الصفوف مقابل تطبيق حقيقي عدا ما أُشير إليه، والباقي ينبني على كون الإطار يُصيّر إلى عناصر أصلية، وهو المعيار الحاسم فعلياً.

يرسم React Native عروضًا أصلية، فيؤتمته Astur تمامًا كتطبيق أصلي — دون إعداد إضافي ولا مشغّل خاص، على Android و iOS معًا.

  • أضف خاصية testID للعناصر التي تحتاجها اختباراتك. فيحوّل React Native قيمة testID إلى معرّف إمكانية الوصول الأصلي (resource-id على Android، و accessibilityIdentifier على iOS)، وهو ما تطابقه getById().
  • وتطابق getByText() و getByLabel() النصوص الظاهرة وتسميات إمكانية الوصول.
  • وتعمل أتمتة ‏DOM داخل WebView عبر device.webContext() — راجع WebViews (DOM) أدناه — على Android وعلى iOS (المحاكي والأجهزة الحقيقية) عبر ios-webkit-debug-proxy.
// React Native — اكشف معرّفات ثابتة للاختبارات
<TextInput testID="login-email-input" ... />
<Pressable testID="login-submit-button" ... />

يرسم Flutter بكسلاته بنفسه بدل استخدام ودجات أصلية، ولذلك يتصل Astur على ‏Android بـ خدمة Dart VM ويقرأ شجرة الودجات الحيّة — معرّفات العناصر ونصوصها وتسمياتها وقيمها وأبعادها — بدل الاعتماد على طبقة إمكانية الوصول وحدها. وهذا يمنح فحصًا حقيقيًا على مستوى الودجة داخل الـ inspector وتوليد الكود.

  1. يكتشف Astur حزمة APK من نوع Flutter تلقائيًا (بالبحث عن libflutter.so / flutter_assets).
  2. يشغّل التطبيق بالأمر flutter run --use-application-binary=<apk>، وهو ما يربط مترجم تعابير Dart.
  3. يقرأ شجرة الودجات على الجهاز عبر خدمة VM ويحوّلها إلى محدِّدات Astur؛ بينما تُحقن النقرات والتعبئة والإيماءات عبر ADB.
  4. وبين الاختبارات يُعاد تشغيل التطبيق تشغيلًا سريعًا (‏hot restart) ويُعاد إلى المقدمة، لحالة نظيفة.
  • بناء debug (أو profile). فخدمة Dart VM موجودة في بنائي debug/profile فقط — ولا يمكن قيادة حزمة --release بهذه الطريقة.
  • ASTUR_FLUTTER_PROJECT — مسار مجلد مصدر تطبيق Flutter (المجلد الذي يحوي pubspec.yaml). ويُستخدم كمجلد عمل للأمر flutter run كي يتوفّر مترجم التعابير.
  • أمر flutter في PATH، أو ASTUR_FLUTTER_PATH مشيرًا إلى الملف التنفيذي. كما يفحص Astur مواقع التثبيت الشائعة (~/development/flutter/bin، ~/fvm/default/bin، Homebrew، …).
  • اختياري: ASTUR_FLUTTER=1 يفرض مسار Flutter، و ASTUR_FLUTTER=0 يعطّله (وإلا فالاكتشاف تلقائي).
Terminal window
# تشغيل مجموعة Flutter (Android)
ASTUR_FLUTTER_PROJECT=/path/to/flutter-app \
npx astur-mobile test --config ./config/android/playwright.flutter.config.ts
# فتح الـ inspector / توليد الكود مقابل حزمة Flutter
ASTUR_FLUTTER_PROJECT=/path/to/flutter-app \
npx astur-mobile codegen --android --device emulator-5554 \
--app ./assets/app-debug.apk --app-id com.example.app

جعل الودجات قابلة للإيجاد

Section titled “جعل الودجات قابلة للإيجاد”

امنح الودجات التي تحتاجها اختباراتك معرّف Semantics ثابتًا. فـ getById() تطابق ذلك المعرّف؛ بينما تطابق getByText() و getByLabel() محتوى Text والتسميات الدلالية.

// Flutter — اكشف معرّفًا ثابتًا للاختبارات
Semantics(
identifier: 'login-email-input',
child: TextField(controller: _email, decoration: const InputDecoration(hintText: 'pilot@astur.dev')),
)

وتبلّغ الحقول النصية عن قيمة: محتوى controller.text الحالي، أو النص البديل hintText حين تكون فارغة — فتعمل toHaveValue() كما تعمل على التطبيقات الأصلية.

ما الذي يجب أن يوفّره التطبيق

Section titled “ما الذي يجب أن يوفّره التطبيق”

تطبيق Flutter قابل للاختبار بقدر ما يكشف من semantics. وعمليًا تحتاج الشاشة كل ما يلي، لا البند الأول وحده:

  • معرّف على كل ودجة يلمسها الاختبار أو يؤكّد عليها. لُفّها بـ Semantics(identifier: 'some-id', child: …). وهذا يقابل getById() على Android و accessibilityIdentifier على iOS، فمعرّف واحد يخدم الاثنين.
  • قراءة معرَّفة لكل ما تتحقق منه. يدمج Flutter النصوص المتجاورة، فكثيرًا ما تنهار التسمية وقيمتها في عقدة واحدة. فإذا احتاج الاختبار تأكيد «أن العدّاد ارتفع»، فامنح القيمة معرّفًا خاصًا بها (native-lab-lane-a-count) بدل تحليل نص مدمج.
  • مراسٍ معرَّفة حول كل ما هو بلا معرّف عمدًا. فإذا شحنت الشاشة ودجات بلا معرّفات عن قصد (لتجريب by.native())، فضع عناصر تحمل معرّفات قبلها وبعدها مباشرةً كي تستطيع الاختبارات تمرير المجموعة إلى الشاشة بشكل حتمي.
  • تخطيط لا يُعاد تدفّقه عند التفاعل معه. احجز مساحة العدّادات والقراءات مسبقًا بدل إدراج صف عند أول تفاعل — فالتخطيط المتحرك يُبطل الإحداثيات وقد يدفع ودجة خارج الشاشة في منتصف الاختبار.
  • container: true على الأغلفة التي تريدها عقدًا مستقلة، وإلا فقد يدمجها Flutter في الأب فيختفي المعرّف من الشجرة.
// قراءة يستطيع الاختبار التأكيد عليها، بمعرّف خاص بها.
Semantics(
identifier: 'home-tap-single-count',
container: true,
child: Text('$_tapCount'),
)

ما يعمل (‏Android، عبر خدمة Dart VM)

Section titled “ما يعمل (‏Android، عبر خدمة Dart VM)”
  • التثبيت والتشغيل، والتصفير بإعادة التشغيل السريع، والاتجاه، ولقطات الشاشة
  • فحص شجرة الودجات المتداخلة في الـ inspector الحي وفي توليد الكود
  • getById (معرّف Semantics) و getByText و getByLabel
  • tap و doubleTap و longPress و fill و swipe
  • قراءة قيم الحقول النصية (toHaveValue)

Flutter على iOS (شجرة إمكانية الوصول في XCUITest)

Section titled “Flutter على iOS (شجرة إمكانية الوصول في XCUITest)”

لا توجد خدمة Dart VM على iOS، فيقرأ Astur تطبيق Flutter عبر شجرة إمكانية الوصول في XCUITest — وهو الوكيل نفسه المستخدم للتطبيقات الأصلية و React Native على iOS. وتعمل مجموعة العرض المشتركة على محاكي iOS بـ ‏11 ملفًا مفعّلًا كلها خضراء (تسجيل الدخول، والنماذج، والمنزلقة، والاتجاه/القائمة، والتمرير، ومختبر النقر، وملفات by.native() الخمسة). وأمران يجعلان هذا ممكنًا:

  • أضف Semantics(identifier:) للودجات التي تحتاجها اختباراتك. فـ Astur يقرأ المعرّفات، ويقرأ قيم العدّادات وموضع المنزلقة بالمعرّف حيث يدمج Flutter النص المحيط.
  • ‏Flutter يدمج نصوص الأبناء في تسمية إمكانية الوصول للحاوية على iOS، فلا يكون Text('Credentials') عنصرًا مستقلًا. ويعوّض وكيل iOS في Astur ذلك ببحث احتياطي بالنص الجزئي فوق التسميات المدمجة، فتظل getByText('Credentials') قادرة على الوصول — لكن فضّل المعرّفات لكل ما تؤكّد عليه.

المستثنى على iOS (حدود موثّقة، لا أخطاء):

  • السحب والإفلات — أول سحبة اصطناعية فقط من XCUITest تُسجَّل لدى مُميِّز الإيماءات في Flutter، فلا يمكن حل أحجية سحب متعددة القطع. (وهي تنجح على مشغّل Dart VM في Android الذي يحقن أحداث حركة حقيقية.)
  • رفع الوسائط و WebView — مطابقة لاستثناءات React Native على iOS (منتقي أصلي؛ ولا CDP لـ WKWebView — راجع WebViews).

كتابة اختبارات Flutter موثوقة

Section titled “كتابة اختبارات Flutter موثوقة”

كل ما يلي مقيس على مجموعة العرض بمحاكي Pixel 9. وهذه هي أنماط الفشل التي تؤذي فعلًا، مرتّبة تقريبًا بحسب الوقت الذي تكلّفه في التشخيص.

هذا أكبر مصدر منفرد لتذبذب اختبارات Flutter. فيزياء التمرير في Flutter تستجيب لـ سرعة الإفلات لا لمسافة الإيماءة. فالتمريرة نفسها على مدى 873 بكسل تتصرف بشكل مختلف تمامًا حسب durationMs:

durationMs النتيجة على صفحة بارتفاع كامل
300 يقذف إلى نهاية المحتوى تمامًا
600 يتجاوز الهدف بكثير
12001400 يسحب مسافة متوقّعة تقارب 1.3 ضعف مسافة الإيماءة ثم يتوقف

وحلقة بحث مبنيّة على تمريرات سريعة لا تستطيع التقارب — فهي تقذف إلى الأسفل، وتكتشف أنها تجاوزت، فتقذف إلى الأعلى، وتكرّر حتى تنفد محاولاتها. وعلى الشاشة يبدو ذلك كتطبيق يمرّر صعودًا وهبوطًا بلا نهاية. استخدم سحبًا بطيئًا (durationMs: 1_200) لأي تمريرة تحتاج التفكير في مسافتها. أما التمريرات السريعة فمناسبة فقط حين تريد قذفًا فعلًا.

الودجات المعروضة على الشاشة وحدها موجودة

Section titled “الودجات المعروضة على الشاشة وحدها موجودة”

تحوي شجرة الودجات ما هو مرسوم الآن. و«خارج الشاشة» لا تعني visible: false — بل إن العقدة غائبة، ومحدِّدها ببساطة لا يُحلّ أبدًا. والنتائج:

  • اكشف قبل أن تقرأ، دائمًا. لا تؤكّد شيئًا عن عنصر لم تمرّر إليه.
  • scrollIntoView() ضمانة أضعف مما تبدو: فهي تتوقف حالما تدخل العقدة الشجرة، ما يترك العنصر عادةً مقصوصًا بشريط سفلي عائم — موجودًا لكن غير قابل للاستخدام.
  • ارتكز على ما تحتاجه لا على الحاوية. فقد تقع أبعاد البطاقة داخل الشاشة بينما أحد أبنائها ما زال غائبًا. سمِّ العناصر التي يلمسها الاختبار فعلًا.
  • حاصِر الأهداف بلا معرّفات. لضمان وجود عنصر بلا معرّف على الشاشة، اشترط وجود العناصر ذات المعرّفات فوقه وتحته مباشرةً.
  • استنتج الحواف من الواجهة لا من ثوابت بكسلية. اقرأ أبعاد الشريط السفلي نفسه بدل حجز رقم سحري؛ فالثابت المضبوط يمرّر بشكل خاطئ بصمت على أي كثافة أخرى.

لا ترسل التطبيق إلى الخلفية ولا توقفه قسرًا في منتصف الجلسة

Section titled “لا ترسل التطبيق إلى الخلفية ولا توقفه قسرًا في منتصف الجلسة”

يُبقي المشغّل أمر flutter run متصلًا بـ Dart VM الخاص بالتطبيق، وتشغيل التطبيق بين الاختبارات هو إعادة تشغيل سريعة عبر ذلك الاتصال. ولذلك:

  • قتل العملية أولًا يدمّر الـ VM الذي تستهدفه إعادة التشغيل — فتتعلّق إعادة الارتباط حتى تنتهي مهلة getVM.
  • والضغط على HOME قد يعيد المحرّك بسطح صفري الحجم (Width is zero. 0,0)، ما يُسقط اتصال الخدمة بالطريقة نفسها.

صفّر جلسة Flutter بإعادة التشغيل السريعة وحدها. فهي المسار المدعوم والأسرع معًا. (ويتعافى Astur بإعادة تشغيل جلسة flutter run كاملةً إن فشل إعادة الارتباط، لكنها ثوانٍ لا داعي لإنفاقها.)

يجب أن يبقى التخطيط ثابتًا عبر التفاعل

Section titled “يجب أن يبقى التخطيط ثابتًا عبر التفاعل”

إذا غيّر التفاعل مع ودجة التخطيطَ، فإن الإحداثي الملتقط قبلها يصبح قديمًا وقت استخدامه — والعنصر المدفوع خارج الشاشة يغادر الشجرة كليًا. فضّل قيادة محدِّد (locator.tap()) الذي يُعيد الحل مباشرةً قبل التنفيذ، على التقاط bounds() مرة واحدة وإعادة استخدام النقطة.

تتجاوز by.native() شجرة الودجات عمدًا وتُحلّ عبر وكيل UiAutomator الذي يوصله Astur جنبًا إلى جنب مع خدمة VM. ويترتب على ذلك أمران:

  • يدمج Flutter تسميات أبناء الحاوية في content-desc واحد. فيبلّغ الصف بـ "Beta record\nChoose"، فطابقه مباشرةً بـ descriptionContains. ولا تلجأ إلى hasDescendant: فـ UiAutomator يعيد المرور على الشجرة الفرعية كاملةً لكل مرشّح، وهو أمر بطيء بما يكفي أمام شجرة Flutter لتنتهي مهلة الأمر — ولوحظ أنه يُسقط اتصال Dart VM معه.
  • الحل والتنفيذ منفصلان. يحلّ Astur عبر الوكيل لكنه ينفّذ عبر الصدفة، لأن غلاف Semantics في Flutter ليس قابلًا للنقر بذاته عادةً — فالعقدة القابلة للتنفيذ مدمجة تحته، ما يجعل نقرة من جانب الوكيل إما تُخطئ أو تبتلعها حاوية.

تتدهور المحاكيات طويلة التشغيل بطرق تشبه أخطاء المنتج تمامًا: مهلات getVM، و device offline، و Width is zero. 0,0. فإذا بدأت عدة ملفات غير مترابطة بالفشل دفعةً واحدة، فأعد تشغيل المحاكي قبل تشخيص الكود — ففي هذه المجموعة وحدها أعاد ذلك نتيجة 12/14 المتكرّرة إلى 14/14.

وأيضًا: لا تشغّل adb shell uiautomator dump أبدًا أثناء عمل مجموعة اختبارات. فهو يفتح جلسة UiAutomation خاصة به ويفصل وكيل Astur في منتصف الاختبار. أما adb devices و logcat فآمنان؛ والـ dump ليس كذلك.

تبلّغ device.network عن حركة HTTP في التطبيق — وهي مفيدة لتشخيص ما استدعته الشاشة فعلًا، وللتأكيد على أنها استدعت الشيء الصحيح.

اقرأ حدّ التغطية أولًا. يبلّغ Astur عن حركة التطبيق المُجهَّزة للرصد، لا عن «كل حركة الجهاز». وما هو مُجهَّز يعتمد كليًا على الطبقة الخلفية، فاسأل بدل أن تفترض:

const capabilities = await device.network.capabilities();
// { observe, intercept, transports, responseBodies, coverage, adapterRequired }
test.skip(!capabilities.observe, capabilities.coverage);
await device.network.clear();
await app.networkLab.getProfile();
const [request] = await device.network.requests({ url: '/api/profile' });
expect(request).toMatchObject({ method: 'GET', status: 200 });
الرصد الاعتراض
‏Flutter على Android نعم — مُحلّل HTTP في Dart VM يحتاج المحوّل داخل التطبيق
‏Flutter على iOS (محاكي) نعم — مُحلّل HTTP في Dart VM يحتاج المحوّل داخل التطبيق
‏Flutter على iOS (جهاز حقيقي) لا — خدمة VM غير قابلة للوصول من المضيف يحتاج المحوّل داخل التطبيق
‏React Native (‏Android + iOS) نعم — نطاق Network في CDP، ببناء debug متصل بـ Metro يحتاج المحوّل داخل التطبيق
التطبيقات الأصلية على Android / iOS لا — لا يوجد خُطّاف مكافئ يحتاج المحوّل داخل التطبيق

تقرأ مراقبة Flutter خدمة Dart VM، فتحتاج بناء debug أو profile — إذ لا ينشر بناء release (‏AOT) واحدة، على أيٍّ من المنصتين. وعلى محاكي iOS هذا هو الشرط الوحيد: يجد Astur الخدمة التي يعلن عنها التطبيق أصلًا ويتصل بها، دون تغيير طريقة تثبيت التطبيق أو تشغيله أو قيادته.

وتقرأ مراقبة React Native نطاق Network نفسه في CDP الذي تستخدمه React Native DevTools. والمُبلِّغ يقيم في ReactCommon، طبقة C++ المشتركة، فيغطّي عميل واحد Android و iOS بالطريقة ذاتها — لكنه مُستبعَد وقت الترجمة من بناء release، فيحتاج هذا بناء debug يعمل مقابل Metro. والتغطية هي حركة XMLHttpRequest؛ وبالأخص فإن fetch الأصلي في Expo غير مرئي. راجع مراقبة الشبكة للإعداد وللحدود الكاملة.

وتشغّل مجموعة الأمثلة ملف الاختبار المشترك نفسه على الاثنين، فالفرق الوحيد هو البناء الذي تشير إليه:

Terminal window
npm run test:android:flutter # بناء Flutter من نوع debug/profile
npm run test:ios:flutter
npm run test:android:rn-debug # بناء React Native من نوع debug متصل بـ Metro
npm run test:ios:rn-debug

في Flutter المصدر هو مُحلّل HTTP الخاص بـ dart:io في Dart VM — وهو المصدر نفسه الذي تستخدمه شاشة Network في Flutter DevTools. ويغطي HttpClient من dart:io، وبالتالي package:http و Dio، لأن كليهما مبني عليه. وهو لا يغطي طلبات WebView الخاصة، ولا نداءات حِزم SDK الأصلية، ولا حركة قنوات المنصة. ويُكتشف الدعم أثناء التشغيل بفحص الامتدادات المسجّلة في الـ isolate، لا يُستنتج من كون التطبيق «تطبيق Flutter».

وثلاثة قرارات تصميمية مقصودة تستحق المعرفة:

  • تُطلق requests() خطأً بدل إعادة [] حين تعجز الجلسة عن الرصد (NETWORK_OBSERVATION_UNSUPPORTED). فالمصفوفة الفارغة يجب أن تعني «لم تحدث حركة»، وإلا لنجح تأكيد عليها لسبب خاطئ.
  • تُحجب ترويسات الاعتماد افتراضيًا — إذ تتحوّل authorization و cookie و set-cookie و x-api-key إلى <redacted> قبل إعادة أي سجل، فلا تصل الأسرار إلى سجل CI أو تقرير HTML.
  • يُحدّ حجم أجسام الاستجابات (‏64 كيبي بايت افتراضيًا) وتُسقَط مع bodyOmittedReason: 'too-large'. فالمُحلّل يحتفظ بكل ما يلتقطه طوال عمر الـ isolate، ولذلك كان تشغيل بلا حدّ سينمو بلا نهاية. ويفعّل Astur التحليل عند أول استخدام، وتفرّغ تجهيزة الاختبار المخزنَ بين الاختبارات، فلا يستطيع اختبار التأكيد على حركة اختبار آخر أبدًا.

ويمكن تعديل كلا الافتراضين عند كل نداء:

await device.network.requests({ url: '/api' }, { maxBodyBytes: 4096, redactHeaders: ['x-tenant'] });

الاعتراض غير متاح بعد. فقيمة capabilities().intercept هي false في كل مكان، و adapterRequired يوضّح السبب: إذ إن تزييف الطلب أو إفشاله يعني إمساكه مفتوحًا، وهو ما لا يستطيعه المُحلّل — فهو يبلّغ عمّا حدث فعلًا. وهذا يحتاج محوّلًا صغيرًا اختياريًا داخل التطبيق، وهي المرحلة التالية. ولا يشحن Astur وسيط MITM لهذا الغرض عن قصد: فـ Android 7 فأعلى يتجاهل شهادات الجذر المثبّتة من المستخدم ما لم يوافق التطبيق عبر network_security_config، و HttpClient في Dart يتجاهل وسيط النظام تمامًا ما لم يضبط التطبيق findProxy — أي أن الوسيط يحتاج تعديلات في التطبيق على أي حال، ويضيف فوقها أعطال الشهادات و TLS كأسباب جديدة لتعطّل اختبارات لا علاقة لها بالموضوع.

  • مشغّل خدمة Dart VM خاص بـ Android. فعلى ‏iOS يُقرأ Flutter عبر شجرة إمكانية الوصول في XCUITest — راجع Flutter على iOS أعلاه لما يعمل اليوم وما هو مستثنى.
  • بناء debug/profile مطلوب (لا خدمة VM في بناء release).
  • ASTUR_FLUTTER_PROJECT مطلوب — إذ يحتاج Astur مصدر Flutter لربط مترجم التعابير؛ ولا تكفي الإشارة إلى حزمة APK وحدها.
  • العناصر المعروضة على الشاشة فقط. فكما في شجرة إمكانية الوصول على iOS، تكشف شجرة الودجات ما هو مرسوم حاليًا على الشاشة — فمرّر الهدف إلى داخل الشاشة قبل قراءته أو التأكيد عليه.
  • واجهات النظام خارج Flutter غير مرئية لخدمة VM. فنوافذ أذونات النظام، ومنتقي الصور أو الملفات الأصلي، وقوائم المشاركة منفصلة عن عرض Flutter، فلن تجدها getBy* في شجرة Flutter. تعامل معها بالإحداثيات، أو قُدها عبر مسار الوكيل الأصلي.
  • قد تكون الإيماءات الاصطناعية على ودجات pan المخصّصة غير دقيقة. فالسحب والإفلات الدقيق على GestureDetector/Listener مخصّص قد لا يقع تمامًا؛ فضّل النقر والتعبئة الثابتين وأبقِ الأهداف القابلة للسحب خارج عروض التمرير المتنافسة.
  • حِزم Flutter من نوع debug كبيرة. فبناء debug يحمل نواة JIT (kernel_blob.bin)، ومحرّك debug، وطبقة تحقق Vulkan، فلن يكون صغيرًا — لكن مجموعة معماريات المعالج ما زالت تضاعف حجمه تقريبًا. وبالقياس على تطبيق العرض: ‏154 ميغابايت لكل المعماريات مقابل 86 ميغابايت مع --target-platform android-arm64. ولاحظ أن الراية تُتجاهل في البناء التزايدي — فنفّذ flutter clean أولًا وإلا أعاد Gradle استخدام الحزمة الضخمة السابقة وبدت الراية بلا أثر.
  • مساحة تخزين المحاكي مجمع منفصل عن قرص المضيف. فالخطأ INSTALL_FAILED_INSUFFICIENT_STORAGE يعني عادةً أن قسم بيانات الـ AVD ممتلئ، لا أن الحزمة كبيرة جدًا. امسح بيانات المحاكي (وهو ما يستعيد مساحة المضيف أيضًا، إذ يتقلّص ملف qcow2) قبل تقليص حجم الملف.

صفحات الويب على الجوال، لا WebView وحده

Section titled “صفحات الويب على الجوال، لا WebView وحده”

تقود device.webContext() واجهة WebView داخل تطبيقك. أما لاختبار موقع ويب داخل متصفح الجهاز نفسه — Chrome على Android و Safari على iOS — فاستخدم device.browser؛ راجع الويب على الجوال. وما دون مستوى الصفحة فالآلية واحدة، وما يضيفه المتصفح هو الهدف والتنقل.

تضمّن التطبيقات الهجينة WebView تكون شجرة DOM فيه غير مرئية لشجرة إمكانية الوصول الأصلية. وتفتح device.webContext() تلك الشجرة وتقودها بـ محدِّدات ويب ثابتة — بالسهولة نفسها التي توفّرها الواجهة الأصلية:

const web = await device.webContext();
await web.getByTestId('astur-submit').tap();
await web.getById('astur-email').fill('qa@astur.dev');
const status = await web.getById('astur-result').textContent();
const tree = await web.snapshot(); // شجرة DOM كاملة مع أفضل المحدِّدات والأبعاد

وهي محايدة تجاه المحرّك بحكم التصميم: إذ يجري كل الاستعلام وتوليد المحدِّدات والتفاعل داخل الصفحة عبر جسر محقون فوق ناقل واحد evaluate(js) → JSON، فيكون السلوك متطابقًا في ‏Flutter و React Native. وتُرتَّب أفضل المحدِّدات هكذا: getByTestIdgetByIdgetByRolegetByText ‹ CSS. ويعرض الـ inspector شجرة DOM نفسها مدمجةً تحت العقدة الأصلية المضيفة لـ WebView، مع إمكانية التعبئة والنقر على عناصر الويب.

الناقل حسب المنصة:

المنصة الناقل الحالة
‏Android (‏Flutter + RN) ‏Chromium WebView · Chrome DevTools Protocol يعمل — فعّل setWebContentsDebuggingEnabled(true) (‏WebView.enableDebugging على Android / بناءات debug في RN)
جهاز iOS حقيقي ‏WKWebView · WebKit RWI عبر ios-webkit-debug-proxy يعمل — ‏WKWebView.isInspectable = true (‏iOS 16.4 فأعلى) + الإعدادات ▸ Safari ▸ متقدم ▸ Web Inspector، مع brew install ios-webkit-debug-proxy
محاكي iOS ‏WKWebView · webinspectord_sim يعمل — يتطلب WKWebView.isInspectable = true (‏iOS 16.4+) و brew install ios-webkit-debug-proxy. ويحدد Astur مقبس المحاكي الخاص به ويشغّله عبر الوضع -s في iwdp تلقائياً، فلا حاجة لأي إعداد إضافي

راجع حدود المنصات للمرجع الكامل لحدود Android و iOS، والـ Inspector وتوليد الكود لحلقة التأليف الحيّة.