معمارية تطبيقات المصادر المفتوحة › Selenium WebDriver 17 / 51 السابق التالي

Selenium WebDriver

معمارية تطبيقات المصادر المفتوحة (المجلد الأول)Selenium WebDriver

معمارية تطبيقات المصادر المفتوحة (المجلد الأول) Selenium WebDriver

Simon Stewart

إذا استمتعت بهذه الكتب، فقد يعجبك أيضًا التصميم البرمجي بالأمثلة في بايثون (Software Design by Example in Python) والتصميم البرمجي بالأمثلة في جافاسكربت (Software Design by Example in JavaScript).

‏Selenium أداة لأتمتة المتصفحات (browser automation)، تُستخدم عادةً لكتابة اختبارات من طرف إلى طرف (end-to-end) لتطبيقات الويب. وأداة أتمتة المتصفح تفعل تمامًا ما تتوقعه: تُؤتمت التحكم في المتصفح بحيث يمكن أتمتة المهام المتكررة. يبدو الأمر مسألة بسيطة الحل، لكن كما سنرى، فإن كثيرًا مما يجري في الكواليس حتى يعمل.

قبل وصف البنية المعمارية لـ Selenium، من المفيد أن نفهم كيف يتّسق الأجزاء المختلفة المرتبطة بالمشروع فيما بينها. وعلى مستوى عالٍ جدًّا، Selenium مجموعة من ثلاث أدوات. الأولى هذه الأدوات، وهي Selenium IDE، إضافة (extension) لـ Firefox تتيح للمستخدمين تسجيل الاختبارات وإعادة تشغيلها. وقد يكون نموذج التسجيل وإعادة التشغيل مقيّدًا ولا يناسب كثيرًا من المستخدمين، لذا تقدّم الأداة الثانية في المجموعة، وهي Selenium WebDriver، واجهات برمجية (APIs) بعدة لغات لتتيح قدرًا أكبر من التحكم وتطبيق ممارسات تطوير البرمجيات المعيارية. أما الأداة الأخيرة، Selenium Grid، فتتيح استخدام واجهات Selenium البرمجية للتحكم في نسخ المتصفح الموزّعة على شبكة (grid) من الآلات، مما يسمح بتشغيل مزيد من الاختبارات على التوازي. ويُشار إليها داخل المشروع باسم «IDE» و«WebDriver» و«Grid». ويستكشف هذا الفصل البنية المعمارية لـ Selenium WebDriver.

كُتب هذا الفصل أثناء إصدارات بيتا من Selenium 2.0 في أواخر عام 2010. فإن كنت تقرأ الكتاب بعد ذلك، فقد تقدّمت الأمور، وستتمكّن من رؤية كيف انبثقت الخيارات المعمارية الموصوفة هنا. أما إن كنت تقرأ قبل ذلك التاريخ: مبروك! فلديك آلة زمنية. هل يمكنني أن أحصل على بعض أرقام اليانصيب الرابحة؟

16.1. التاريخ

بدأ Jason Huggins مشروع Selenium عام 2004 أثناء عمله في ThoughtWorks على نظام «الأوقات والمصروفات» (Time and Expenses, T&E) الداخلي لديهم، وهو نظام يستخدم جافاسكربت (Javascript) على نطاق واسع. وعلى الرغم من أن Internet Explorer كان المتصفح المهيمن في ذلك الوقت، فقد استخدمت ThoughtWorks عددًا من المتصفحات البديلة (ولا سيما متغيّرات Mozilla)، وكانت تُبلغ عن العلل حين لا يعمل تطبيق T&E على المتصفح الذي تختاره. وكانت أدوات الاختبار مفتوحة المصدر في ذلك الوقت إما مركَّزة على متصفح واحد (غالبًا IE) أو محاكاة لمتصفح (مثل HttpUnit). وكان ثمن ترخيص أداة تجارية سيستنزف كامل الميزانية المحدودة لمشروع داخلي صغير، لذا لم تُدرَج حتى في الخيارات المجدية للاختبار.

وحيثما يصعب الأتمتة، من الشائع الاعتماد على الاختبار اليدوي. وهذا النهج لا يتّسع عندما يكون الفريق صغيرًا جدًّا أو عندما تكون الإصدارات متقاربة إلى حدٍّ كبير. كما أن مطالبة الناس بتنفيذ سيناريو (script) يمكن أتمتته إهدارٌ في حقّ البشرية. وبعبارة أكثر بساطة، الناس أبطأ وأكثر عرضة للخطأ من الآلة في المهام الرتيبة المتكررة. ولم يكن الاختبار اليدوي خيارًا مطروحًا.

ولحسن الحظ، دعمت جميع المتصفحات المختبَرة جافاسكربت. ورأى Jason والفريق الذي كان يعمل معه أن من المعقول كتابة أداة اختبار بهذه اللغة يمكن استخدامها للتحقق من سلوك التطبيق. واستلهامًا من العمل الجاري على FIT[1]، وُضعت صياغة قائمة على الجداول فوق جافاسكربت الخام، وهذا ما سمّح بكتابة الاختبارات من أشخاص ذوي خبرة برمجية محدودة، عبر مقاربة قائمة على الكلمات المفتاحية (keyword-driven) في ملفات HTML. وقد صدرت هذه الأداة، التي سُمّيت في الأصل «Selenium» ثم رُوجع إليها لاحقًا باسم «Selenium Core»، برخصة Apache 2 عام 2004.

بُني تنسيق Selenium الجدولي بشكل يشبه ActionFixture في FIT. ويُقسَّم كل صف من الجدول إلى ثلاثة أعمدة. يعطي العمود الأول اسم الأمر المراد تنفيذه، ويحتوي العمود الثاني عادةً على مُعرِّف للعنصر (element identifier)، ويحتوي العمود الثالث على قيمة اختيارية. على سبيل المثال، إليك كيفية كتابة السلسلة «Selenium WebDriver» في عنصر مُعرَّف بالاسم «q»:

type       name=q       Selenium WebDriver

ولأن Selenium كُتبت بجافاسكربت خالصة، تطلّب تصميمها الأولي من المطورين أن يستضيفوا Core واختباراتهم على الخادم نفسه الذي يعمل عليه التطبيق قيد الاختبار (application under test, AUT)، تفاديًا لخرق سياسات أمان المتصفح ولصندوق جافاسكربت الرملي (Javascript sandbox). ولم يكن ذلك عمليًّا أو ممكنًا في كل الأحوال. والأخطر من ذلك، أنه على الرغم من أن بيئة التطوير المتكاملة (IDE) الخاصة بالمطور تمنحه القدرة على التعامل السريع مع الشيفرة والتنقّل في قاعدة شيفرة كبيرة، فلا توجد أداة مماثلة لذلك في HTML. ثم تبيّن بسرعة أن صيانة مجموعة اختبارات متوسطة الحجم أمر مرهق ومؤلم.[2]

ولحلّ هذه المشكلة وغيرها، كُتب وسيط (proxy) حتى يتمكّن Selenium من اعتراض كل طلب HTTP. وقد أتاح استعمال هذا الوسيط تجاوز القيود العديدة لسياسة «نفس المضيف الأصل» (same host origin)، وهي السياسة التي لا تجيز للمتصفح أن يتيح لجافاسكربت إجراء أي نداء إلى غير الخادم الذي قُدّمت منه الصفحة الحالية، ومن ثمّ خُفِّف الضعف الأول. وفتح هذا التصميم الباب أمام إمكانية كتابة روابط Selenium (Selenium bindings) بلغات متعددة: لم يكن يتطلّب منها سوى قدرتها على إرسال طلبات HTTP إلى معرّف URL (URL) معيّن. وكان تنسيق البيانات على السلك (wire format) مبنيًّا عن قرب على الصياغة الجدولية لـ Selenium Core، وسُمّي هو وتلك الصياغة معًا باسم «Selenese». ولأن الروابط اللغوية كانت تتحكم في المتصفح عن بُعد، سُمّيت الأداة «Selenium Remote Control» أو «Selenium RC».

وفيما كان Selenium قيد التطوير، كان إطار آخر لأتمتة المتصفحات ينضج في ThoughtWorks: وهو WebDriver. وأُطلق الكود الأولي له في مطلع عام 2007. واستُخرج WebDriver من أعمال على مشاريع أرادت عزل اختباراتها من طرف إلى طرف عن أداة الاختبار الأساسية. وعادةً ما يتم هذا العزل عبر نمط المحوّل (Adapter pattern). ونما WebDriver من بصيرة نضجت بتطبيق هذا الأسلوب بثبات على مشاريع عديدة، وكان في البدء غلافًا (wrapper) حول HtmlUnit. ثم جاء دعم Internet Explorer وFirefox بسرعة بعد الإصدار.

وعند إصدار WebDriver كانت هناك فروق جوهرية بينه وبين Selenium RC، وإن كانا يشغلان المكان نفسه في الوصول: واجهة برمجية لأتمتة المتصفحات. وأوضح فرق كان يلاحظه المستخدم هو أن Selenium RC كانت له واجهة برمجية قائمة على قاموس (dictionary-based)، تُعرَض فيها جميع الدوال على صنف واحد، في حين كان لدى WebDriver واجهة برمجية أكثر توجّهًا كائنيًا (object-oriented). وإضافةً إلى ذلك، لم يكن WebDriver يدعم سوى Java، في حين كان Selenium RC يوفّر دعمًا لمجموعة واسعة من اللغات. وهناك أيضًا فروق تقنية كبيرة: فـ Selenium Core (الذي بُني عليه RC) كان في جوهره تطبيق جافاسكربت يعمل داخل الصندوق الرملي الأمني للمتصفح. في حين سعى WebDriver إلى الارتباط بالمتصفح ارتباطًا أصليًّا (natively)، متجاوزًا نموذج أمان المتصفح، على حساب زيادة كبيرة في جهد تطوير الإطار نفسه.

وفي أغسطس 2009 أُعلن أن المشروعين سيندمجان، وSelenium WebDriver هو ناتج هذا الاندماج. وأثناء كتابة هذا الفصل، يدعم WebDriver روابط لغوية لـ Java وC# وPython وRuby. وهو يوفّر دعمًا لـ Chrome وFirefox وInternet Explorer وOpera ومتصفحي Android وiPhone. وهناك مشاريع شقيقة، لا تُحفظ في مستودع الشيفرة نفسه لكنها تعمل عن قرب مع المشروع الرئيسي، توفّر روابط لـ Perl، وتنفيذًا لمتصفح BlackBerry، وآخرًا لمتصفح WebKit «بلا واجهة» (headless)—وهو مفيد في تلك الحالات التي تحتاج فيها الاختبارات إلى التشغيل على خادم تكامل مستمر (continuous integration) من دون شاشة عرض. ولا تزال آلية Selenium RC الأصلية قيد الصيانة، وتتيح لـ WebDriver أن يوفّر دعمًا للمتصفحات التي كانت ستُترك بلا دعم لولاها.

16.2. استطراد في المصطلحات

للأسف، يستخدم مشروع Selenium مصطلحات كثيرة. ولاستعراض ما قابلناه حتى الآن:

  • ‏Selenium Core هو قلب تنفيذ Selenium الأصلي، وهو مجموعة من نصوص جافاسكربت البرمجية تتحكم في المتصفح. ويُشار إليه أحيانًا بـ«Selenium» وأحيانًا بـ«Core».
  • ‏Selenium RC هو الاسم الذي أُعطي للروابط اللغوية الخاصة بـ Selenium Core، ويُشار إليه عادةً، على نحو مربك، باسم «Selenium» وحده أو «RC». وقد حلّ محله الآن Selenium WebDriver، حيث تُشار إلى واجهة RC البرمجية بـ«واجهة Selenium 1.x البرمجية».
  • ‏Selenium WebDriver يشغل المكان نفسه الذي شغله RC، وقد استوعب الروابط الأصلية للإصدار 1.x. وهو يشير إلى الروابط اللغوية وإلى تطبيقات شيفرة التحكم في كل متصفح على حدة. ويُشار إليه عادةً باسم «WebDriver» وحده، وأحيانًا باسم Selenium 2. ولا شكّ أن هذا الاسم سيُختصر مع الوقت إلى «Selenium».

ولن يجد القارئ الدقيق صعوبة في ملاحظة أن «Selenium» تُستخدم هنا بمعنى عام إلى حدٍّ كبير. والحمد لله، فإن السياق يكفي عادةً لتحديد أي Selenium بعينها يقصدها الناس.

وأخيرًا، هناك عبارة أخرى سأستخدمها، ولا توجد طريقة مهذّبة لتقديمها: «المشغّل» (driver) هو الاسم الذي يُعطى لتنفيذ معيّن من واجهة WebDriver البرمجية. على سبيل المثال، هناك مشغّل لـ Firefox وآخر لـ Internet Explorer.

16.3. المحاور المعمارية

قبل أن نبدأ في النظر إلى الأجزاء المنفردة لفهم كيفية ترابطها، من المفيد أن نفهم المحاور العامة التي تنبثق منها بنية المشروع وطريقة تطويره. وباختصار شديد، فهذه هي:

  • خفِّض التكاليف.
  • حاكِ المستخدم.
  • أثبِت أن المشغِّلات تعمل…
  • …لكنك لا ينبغي أن تحتاج إلى فهم كيفية عمل كل شيء.
  • خفِّض «معامل الحافلة» (bus factor).
  • تعاطَف مع تنفيذ مكتوب بجافاسكربت.
  • كل نداء دالة هو نداء استدعاء إجراء عن بُعد (RPC).
  • نحن مشروع مفتوح المصدر.

16.3.1. خفِّض التكاليف

دعم X من المتصفحات على Y من المنصّات مسألة مكلفة بطبيعتها، سواء من حيث التطوير الأولي أو الصيانة. وإذا استطعنا إيجاد طريقة لإبقاء جودة المنتج مرتفعة من غير مخالفة عدد كبير من المبادئ الأخرى، فذاك هو المسار الذي نفضّله. ويتضح ذلك بأوضح صورة في تبنّينا لجافاسكربت حيثما أمكن، كما سترى بعد قليل.

16.3.2. حاكِ المستخدم

صُمِّم WebDriver لمحاكاة الطريقة التي سيتفاعل بها مستخدم مع تطبيق ويب. والمقاربة الشائعة لمحاكاة إدخال المستخدم هي الإفادة من جافاسكربت لتركيب (synthesize) سلسلة الأحداث وإطلاقها، أي الأحداث التي كان التطبيق سيراها لو أداها مستخدم حقيقي بالتفاعل نفسه. وهذه المقاربة القائمة على «الأحداث المركَّبة» (synthesized events) محفوفة بالمشكلات، إذ يُطلق كل متصفح — وأحيانًا الإصدارات المختلفة للمتصفح نفسه — أحداثًا تختلف قليلًا في نوعها وقيمها. ولزيادة الأمور تعقيدًا، لا تسمح معظم المتصفحات للمستخدم بالتفاعل بهذه الطريقة مع عناصر النماذج، مثل عناصر إدخال الملفات، لأسباب أمنية.

وحيثما أمكن، يستخدم WebDriver المقاربة البديلة، وهي إطلاق الأحداث على مستوى نظام التشغيل. ولأن هذه «الأحداث الأصلية» (native events) لا يولّدها المتصفح، فإن هذا الأسلوب يتجاوز القيود الأمنية المفروضة على الأحداث المركَّبة، ولأنها خاصة بنظام التشغيل، فبمجرد أن تعمل مع متصفح واحد على منصّة ما، يصبح إعادة استعمال الشيفرة في متصفح آخر نسبيًّا سهلًا. وللأسف، هذا الأسلوب ممكن فقط حيثما يستطيع WebDriver الارتباط عن قرب بالمتصفح، وحيثما حدد فريق التطوير أفضل طريقة لإرسال الأحداث الأصلية من دون أن يتطلّب ذلك أن تكون نافذة المتصفح مركَّزة (لأن اختبارات Selenium تستغرق وقتًا طويلًا في التشغيل، ومن المفيد أن يبقى بإمكانك استعمال الآلة لأغراض أخرى أثناء تشغيلها). وعند كتابة هذا الفصل، يعني هذا أن الأحداث الأصلية يمكن استخدامها على Linux وWindows، لكن ليس على Mac OS X.

وأيًّا كانت الطريقة التي يحاكي بها WebDriver إدخال المستخدم، فنحن نحاول بجدّ أن نحاكي سلوك المستخدم أقرب ما يمكن. ويختلف هذا عن RC، الذي كان يوفّر واجهات برمجية تعمل عند مستوى أدنى بكثير من المستوى الذي يعمل عنده المستخدم.

16.3.3. أثبِت أن المشغِّلات تعمل

قد يبدو الأمر بدهيًّا إلى حدٍّ كبير — من نوع «الأمومة وفطيرة التفاح» — لكني أعتقد أنه لا فائدة من كتابة شيفرة إن لم تعمل. والطريقة التي نثبت بها أن المشغِّلات تعمل في مشروع Selenium هي امتلاك مجموعة واسعة من حالات الاختبار المؤتمتة. وهذه عادةً «اختبارات تكامل» (integration tests)، تتطلّب تصريف الشيفرة (compilation) والإفادة من متصفح يتفاعل مع خادم ويب، لكننا نكتب، كلما أمكن، «اختبارات وحدات» (unit tests) التي — على خلاف اختبار التكامل — يمكن تشغيلها من غير تصريف كامل. وعند كتابة هذا الفصل، توجد نحو 500 حالة اختبار تكامل ونحو 250 حالة اختبار وحدات يمكن تشغيلها عبر كل متصفح من المتصفحات. ونضيف المزيد منها كلما أصلحنا مشكلات وكتبنا شيفرة جديدة، ويتّجه تركيزنا إلى كتابة مزيد من اختبارات الوحدات.

ولا يُشغَّل كل اختبار مع كل متصفح. فبعض الاختبارات يختبر قدرات خاصة لا تدعمها بعض المتصفحات، أو تُعالَج بطرق مختلفة في متصفحات مختلفة. ومن الأمثلة اختبارات مزايا HTML5 الجديدة التي لا تدعمها جميع المتصفحات. ومع ذلك، تُشغَّل مجموعة كبيرة من الاختبارات مع كل متصفح من متصفحات سطح المكتب الرئيسية. ومن المفهوم أن إيجاد طريقة لتشغيل أكثر من 500 اختبار لكل متصفح على عدّة منصّات تحدٍّ كبير، وهو تحدٍّ يواصل المشروع مصارعته.

16.3.4. لا ينبغي أن تحتاج إلى فهم كيفية عمل كل شيء

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

16.3.5. خفِّض «معامل الحافلة»

هناك مفهوم في هندسة البرمجيات (ليس جادًّا تمامًا) يُسمّى «معامل الحافلة» (bus factor). وهو يشير إلى عدد المطورين الرئيسيين الذين يلزم أن ينالوا نهاية قاتلة—على الأرجح أن تصيبهم حافلة—حتى يترك المشروع في حالة لا يمكنه الاستمرار منها. ومن المرجّح أن يكون شيء معقّد بهذا القدر كأتمتة المتصفحات عرضة لهذا الأمر بصفة خاصة، لذا تُتَّخذ قراراتنا المعمارية العديدة لرفع هذا الرقم إلى أعلى مستوى ممكن.

16.3.6. تعاطَف مع تنفيذ مكتوب بجافاسكربت

يلجأ WebDriver إلى استخدام جافاسكربت الخالصة لتشغيل المتصفح إذا لم تكن هناك طريقة أخرى للتحكم فيه. وهذا يعني أن أي واجهة برمجية نضيفها ينبغي أن تكون «متعاطفة» (sympathetic) مع تنفيذ مكتوب بجافاسكربت. ومثال ملموس: يقدّم HTML5 واجهة LocalStorage، وهي واجهة برمجية لتخزين بيانات منظّمة في جانب العميل. وهذه تُنفَّذ عادةً داخل المتصفح باستخدام SQLite. وكان التنفيذ الطبيعي أن نوفّر اتصالًا بقاعدة البيانات التي تخزّن البيانات الأساسية، باستخدام شيء مثل JDBC. لكننا استقرنا في النهاية على واجهة برمجية تحاكي عن قرب التنفيذ الأساسي المكتوب بجافاسكربت، لأن شيئًا يحاكي واجهات الوصول إلى قواعد البيانات التقليدية ليس متعاطفًا مع تنفيذ مكتوب بجافاسكربت.

16.3.7. كل نداء هو نداء RPC

يتحكم WebDriver في متصفحات تعمل في عمليات أخرى (processes). وعلى الرغم من أن هذا يسهل إغفاله، إلا أنه يعني أن كل نداء يتم عبر واجهته البرمجية هو نداء استدعاء إجراء عن بُعد (Remote Procedure Call, RPC)، ومن ثمّ يقف أداء الإطار رهنًا لزمن استجابة الشبكة. وفي التشغيل العادي، قد لا يكون هذا ملحوظًا إلى حدٍّ كبير—فمعظم أنظمة التشغيل تحسّن التوجيه إلى المضيف المحلي (localhost)—لكن كلما ازداد زمن الاستجابة بين المتصفح وشيفرة الاختبار، انخفضت كفاءة ما بدا كافيًّا في نظر مصمّمي الواجهات البرمجية وفي نظر مستخدميها على حدّ سواء.

وهذا يُدخل بعض التوتر في تصميم الواجهات البرمجية. فالواجهة الأكبر، ذات الدوال الأدقّ، تساعد على تقليل زمن الاستجابة عبر دمج النداءات المتعددة، لكن ينبغي أن يُوازن ذلك بالإبقاء على الواجهة معبِّرة وسهلة الاستخدام. فمثلًا، هناك عدة فحوص يلزم إجراؤها لتحديد ما إذا كان عنصر ما ظاهرًا لمستخدم من طرف إلى طرف. ولا يكتفينا بأخذ خصائص CSS المختلفة في الحسبان، والتي قد يلزم استنتاجها بالنظر إلى العناصر الأصلية، بل ينبغي لنا على الأرجح أن نفحص أبعاد العنصر أيضًا. أما الواجهة المصغّرة (minimalist) فستطلب إجراء كل فحص من هذه الفحوص على حدة. ويجمع WebDriver كلّها في دالة واحدة هي isDisplayed.

16.3.8. خاتمة: هذا مشروع مفتوح المصدر

على الرغم من أنها ليست مسألة معمارية بالمعنى الضيق، فإن Selenium مشروع مفتوح المصدر. والشريحة التي تصل كل النقاط السابقة ببعضها هي رغبنا في جعل المساهمة من مطوّر جديد ممكنة بميسورة تامة. فبإبقاء عمق المعرفة المطلوبة سطحيًّا قدر الإمكان، وباستخدام أقل عدد ممكن من اللغات، وبالاعتماد على اختبارات مؤتمتة للتحقق من أن شيئًا لم ينكسر، نأمل أن نتيح هذه الميسورة في المساهمة.

وكان المشروع في الأصل مقسّمًا إلى سلسلة من الوحدات (modules)، تمثّل كل وحدة منها متصفحًا بعينه، مع وحدات إضافية للشيفرة المشتركة ولدعم الأدوات المساعدة. وخُزِّنت شجرات المصدر (source trees) لكل ربط لغوي تحت هذه الوحدات. وكان هذا الأسلوب منطقيًّا إلى حدٍّ كبير للغات مثل Java وC#، لكنه مؤلم في العمل معه بالنسبة إلى متبنّي Ruby ومتبنّي Python. وقد ترجم ذلك تقريبًا إلى أرقام المساهمين النسبية، إذ لم يكن سوى حفنة من الناس قادرين ومهتمين بالعمل على الروابط اللغوية الخاصتين بـ Python وRuby. ولمعالجة ذلك، أُعيد تنظيم الشيفرة في أكتوبر ونوفمبر من عام 2010، وخُزّن شيفرتا Ruby وPython تحت دليل رئيسي واحد لكل لغة. وهذا يطابق توقعات مطوّري المصادر المفتوحة في هاتين اللغتين عن قرب أكبر، وأثره في المساهمات من المجتمع كان ملحوظًا تقريبًا على الفور.

16.4. التعامل مع التعقيد

البرمجيات نتاج بنية متكتّلة (lumpy construct). والكتل هي التعقيد، وكما نحن مصمّمو واجهة برمجية، لدينا خيار في موضع دفع هذا التعقيد. في أحد الطرفين، يمكننا نشر التعقيد بالتساوي قدر الإمكان، أي أن كل مستهلك للواجهة البرمجية يحتاج إلى أن يكون طرفًا فيه. ويقترح الطرف الآخر أخذ أكبر قدر ممكن من التعقيد وعزله في مكان واحد. وسيكون ذلك المكان الوحيد موضع ظلمة ورعب لكثيرين إن اضطروا إلى دخوله، لكن المقابل هو أن مستخدمي الواجهة البرمجية، الذين لا يحتاجون إلى الغوص في التنفيذ، يكونون قد دُفعت عنهم كلفة التعقيد مقدمًا.

ويميل مطوّرو WebDriver أكثر إلى إيجاد التعقيد وعزله في مواضع قليلة بدل نشره. وأحد أسباب ذلك هو مستخدمونا. فهم استثنائيون في إيجاد المشكلات والعلل، كما تُظهر لمحة في قائمة عللنا، لكن لأن كثيرًا منهم ليسوا مطوّرين، فالواجهة المعقّدة لن تعمل معهم جيدًا. وسعينا إلى توفير واجهة برمجية توجّه الناس في الاتجاه الصحيح. ومثال على ذلك، الدوال التالية من واجهة Selenium البرمجية الأصلية، كلٌّ منها يمكن أن يُستخدم لتعيين قيمة عنصر إدخال:

  • type
  • typeKeys
  • typeKeysNative
  • keydown
  • keypress
  • keyup
  • keydownNative
  • keypressNative
  • keyupNative
  • attachFile

وهذا هو المكافئ في واجهة WebDriver البرمجية:

  • sendKeys

وكما ناقشنا سابقًا، فإن هذا يبرز أحد الاختلافات الفلسفية الكبرى بين RC وWebDriver: إذ يسعى WebDriver إلى محاكاة المستخدم، في حين يوفّر RC واجهات برمجية تتعامل مع مستوى أدنى يصعب على المستخدم بلوغه أو يستحيل. والفرق بين typeKeys وtypeKeysNative هو أن الأولى تستخدم دائمًا أحداثًا مركَّبة، في حين تحاول الثانية استخدام AWT Robot في كتابة المفاتيح. ومن المؤسف أن AWT Robot يرسل ضغطات المفاتيح إلى أي نافذة تكون مركَّزة، وهي النافذة التي ليس بالضرورة أن تكون نافذة المتصفح. أما أحداث WebDriver الأصلية، فعلى النقيض من ذلك، فتُرسَل مباشرة إلى مقبض النافذة (window handle)، فتتجنّب اشتراط أن تكون نافذة المتصفح مركَّزة.

16.4.1. تصميم WebDriver

يُشير الفريق إلى واجهة WebDriver البرمجية بأنها «قائمة على الكائنات» (object-based). والواجهات مُعرَّفة بوضوح وتحاول الالتزام بأن يكون لكل منها دور واحد فقط أو مسؤولية واحدة، لكننا بدلًا من نمذجة كل وسم HTML ممكن على حدة في صنف مستقل، لدينا واجهة واحدة فقط هي WebElement. وباتباع هذا الأسلوب، يمكن إعادة توجيه المطوّرين الذين يستخدمون IDE يدعم الإكمال التلقائي إلى الخطوة التالية الواجب أخذها. والنتيجة أن جلسات الترميز قد تبدو هكذا (في Java):

WebDriver driver = new FirefoxDriver();
driver.<user hits space>

وعند هذه النقطة، تظهر قائمة قصيرة نسبيًّا من 13 طريقة يختار منها المستخدم واحدة:

driver.findElement(<user hits space>)

وستُسقِط معظم بيئات التطوير المتكاملة الآن تلميحًا عن نوع الوسيط (argument) المتوقع، وهو في هذه الحالة «By». وهناك عدد من طرق المصنع (factory methods) المهيّأة مسبقًا لكائنات «By»، والمصرَّح عنها كدوال ساكنة (static methods) على By نفسه. وسيصل مستخدمنا سريعًا إلى سطر شيفرة يبدو هكذا:

driver.findElement(By.id("some_id"));

الواجهات القائمة على الأدوار

تخيّل صنف Shop مبسّطًا. كل يوم يحتاج إلى إعادة تخزين (restocking)، ويتعاون مع Stockist لتوصيل هذه البضاعة الجديدة. وكل شهر يحتاج إلى دفع رواتب الموظفين والضرائب. ولأغراض الجدال، نفترض أنه يفعل ذلك مستعينًا بـAccountant. وإحدى طرق نمذجة ذلك تبدو هكذا:

public interface Shop {
    void addStock(StockItem item, int quantity);
    Money getSalesTotal(Date startDate, Date endDate);
}

ولديك خياران حول أين تضع حدود الواجهة بين Shop وAccountant وStockist. يمكننا رسم خط نظري كما هو مبين في الشكل 16.1.

وهذا يعني أن كلًا من Accountant وStockist سيقبل Shop كوسيط لدواله. غير أن عيب ذلك هو أنه من المستبعد أن يكون لدى Accountant رغبة حقيقية في رصّ الرفوف، ومن المرجّح ألّا يكون من الحكمة أن يُدرك Stockist مقدار هامش الربح الكبير الذي يضيفه Shop على الأسعار. ومن ثمّ، فإن الموضع الأفضل لرسم الخط مبين في الشكل 16.2.

وسنحتاج إلى واجهتين على Shop أن ينفّذهما، لكن هاتين الواجهتين تُعرّفان بوضوح الدور الذي يؤديه Shop لكلٍّ من Accountant وStockist. وهما واجهات قائمة على الأدوار:

public interface HasBalance {
    Money getSalesTotal(Date startDate, Date endDate);
}

public interface Stockable {
    void addStock(StockItem item, int quantity);
}

public interface Shop extends HasBalance, Stockable {
}

أجد UnsupportedOperationExceptions وما شابهها غير مرضية البتّة، لكن لا بدّ من وجود شيء يتيح كشف الوظائف للمستخدمين الذين قد يحتاجون إليها، من دون أن تُثقل بقية الواجهات بالنسبة لأغلب المستخدمين. ولهذا الغرض، يستخدم WebDriver على نطاق واسع الواجهات القائمة على الأدوار. فمثلًا، هناك واجهة JavascriptExecutor التي توفّر القدرة على تنفيذ مقاطع اعتباطية من جافاسكربت في سياق الصفحة الحالية. ونجاح تحويل (cast) نسخة من WebDriver إلى تلك الواجهة يدل على أنه يمكنك توقّع أن تعمل دوالها.

المحاسب ومورّد البضاعة يعتمدان على المتجر

الشكل 16.1: المحاسب ومورّد البضاعة يعتمدان على المتجر

المتجر ينفّذ HasBalance وStockable

الشكل 16.2: المتجر ينفّذ HasBalance وStockable

16.4.2. التعامل مع الانفجار التوافقي

من أوّل ما يلوح للنظر عند التفكير لحظة في الطيف الواسع من المتصفحات واللغات التي يدعمها WebDriver، أن تكلفة الصيانة ستصاعد بسرعة ما لم يُتخذ الحذر. فمع X من المتصفحات وY من اللغات، من السهل جدًّا أن نقع في فخّ صيانة X×Y من التنفيذات.

وتقليل عدد اللغات التي يدعمها WebDriver سيكون إحدى وسائل الحدّ من هذه التكلفة، لكننا لا نريد سلوك هذا الطريق لسببين. أولهما، أن هناك حِملًا معرفيًّا (cognitive load) يُدفع عند الانتقال من لغة إلى أخرى، لذا من المفيد لمستخدمي الإطار أن يكونوا قادرين على كتابة اختباراتهم باللغة التي يمارسون بها معظم أعمالهم التطويرية. وثانيهما، أن خلط عدّة لغات في مشروع واحد أمر قد لا يريح الفرق العمل، والحقيقة أن معايير الترميز والمتطلبات المؤسسية تبدو وكأنها تطالب باحتكار تكنولوجي (monoculture) (مع أنني أعتقد — وهذا مبهجّ — أن النقطة الثانية لم تعد صحيحة إلى هذا الحدّ مع مرور الوقت)، ومن ثمّ فإن تقليل عدد اللغات المدعومة ليس خيارًا مطروحًا.

وتقليل عدد المتصفحات المدعومة ليس خيارًا أيضًا—فقد أُثار جدل صاخب حين استبعدنا دعم Firefox 2 في WebDriver، رغم أن هذا الاختيار لم يمثّل سوى أقل من 1% من سوق المتصفحات.

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

وعلى سبيل المثال، نجح مشغّل IE في نقل مسؤولية العثور على IE وتشغيله إلى منطق المشغّل الرئيسي. وعلى الرغم من أن هذا أدّى إلى عدد لافت من أسطر الشيفرة داخل المشغّل، فإن الربط اللغوي لإنشاء نسخة جديدة لا ينزل عن نداء واحد إلى ذلك المشغّل. وبالمقارنة، فشل مشغّل Firefox في إجراء هذا التغيير. وفي عالم Java وحدها، يعني ذلك أننا نملك ثلاثة أصناف رئيسية تتولى تهيئة Firefox وتشغيله، وتزن ما يقارب 1300 سطر شيفرة. وهذه الأصناف مكرَّرة في كل ربط لغوي يريد دعم FirefoxDriver من غير الاعتماد على تشغيل خادم Java. وهذه كمية كبيرة من الشيفرة الإضافية التي يلزم صيانتها.

16.4.3. عيوب تصميم WebDriver

الجانب السلبي لقرار كشف القدرات بهذه الطريقة هو أنه إلى أن يعرف أحدهم أن واجهة بعينها موجودة، قد لا يدرك أن WebDriver يدعم ذلك النوع من الوظائف؛ فهناك فقد في قابلية استكشاف الواجهة البرمجية. وبطبيعة الحال، حين كان WebDriver جديدًا، بدا أننا نضيّع وقتًا طويلًا في مجرد إحالة الناس إلى واجهات بعينها. أما الآن فقد بذلنا جهدًا أكبر بكثير في توثيقنا، ومع اتساع استخدام الواجهة البرمجية، يصبح العثور على المعلومات التي يحتاجها المستخدمون أسهل على نحو متزايد.

وهناك موضع واحد أرى فيه أن واجهتنا البرمجية رديئة إلى حدٍّ كبير. ولدينا واجهة تُسمّى RenderedWebElement، وهي خليط غريب من الدوال المتعلقة بالاستفسار عن حالة عرض العنصر (isDisplayed وgetSize وgetLocation)، وبإجراء العمليات عليه (دوال hover والسحب والإفلات)، فضلًا عن طريقة مفيدة للحصول على قيمة خاصية CSS بعينها. وقد أُنشئت لأن مشغّل HtmlUnit كان لا يكشف المعلومات المطلوبة، في حين كان مشغّل Firefox ومشغّل IE يكشفانها. وكانت في الأصل تحمل المجموعة الأولى من الدوال فقط، لكننا أضفنا الدوال الأخرى قبل أن أفكّر جادًّا في كيفية رغبتي في تطوّر الواجهة البرمجية. والواجهة معروفة الآن، والخيار الصعب هو: هل نحتفظ بهذا الركن القبيح من الواجهة البرمجية بما أنه مستعمل على نطاق واسع، أم نحاول حذفه؟ وتفضيلي ألا نترك «نافذة مكسورة» (broken window) خلفنا، لذا فإن إصلاح هذا قبل إصدار Selenium 2.0 أمر مهم. ونتيجةً لذلك، بحلول الوقت الذي تقرأ فيه هذا الفصل، من المرجّح أن يكون RenderedWebElement قد اختفى.

ومن منظور منفّذ، فإن الربط عن قرب بمتصفح هو أيضًا عيب في التصميم، وإن كان عيبًا لا مفرّ منه. فدعم متصفح جديد يتطلّب جهدًا كبيرًا، وكثيرًا ما يلزم عدّة محاولات حتى يُحسَن بالشكل الصحيح. ومثال ملموس: مرّ مشغّل Chrome بأربع عمليات إعادة كتابة كاملة، كما خضع مشغّل IE لثلاث عمليات إعادة كتابة رئيسية أيضًا. وميزة الربط عن قرب بالمتصفح هي أنه يوفّر قدرًا أكبر من التحكم.

16.5. الطبقات وجافاسكربت

أداة أتمتة المتصفح تُبنى في جوهرها من ثلاث قطع متحرّكة:

  • وسيلة لاستجواب DOM.
  • آلية لتنفيذ جافاسكربت.
  • وسيلة ما لمحاكاة إدخال المستخدم.

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

وكما في معظم المشاريع الكبيرة، يستخدم Selenium مجموعة طبقية من المكتبات. وأدنى طبقة هي Closure Library من Google، وهي تقدّم بدائيات (primitives) وآلية للتقسيم إلى وحدات (modularization) تسمح بإبقاء ملفات المصدر مركَّزة وصغيرة قدر الإمكان. وفوق ذلك، توجد مكتبة أدوات تقدّم دوالًا تتدرّج من مهام بسيطة كالحصول على قيمة خاصية ما، إلى تحديد ما إذا كان عنصر ما سيكون ظاهرًا لمستخدم من طرف إلى طرف، وصولًا إلى إجراءات أكثر تعقيدًا بكثير مثل محاكاة نقرة باستخدام أحداث مركَّبة. وداخل المشروع، تُنظر إلى هذه العناصر باعتبارها أصغر وحدات لأتمتة المتصفح، ومن ثمّ تُسمّى ذرّات أتمتة المتصفح (Browser Automation Atoms) أو «الذرات». وأخيرًا، هناك طبقات محوّلات (adapter layers) تركّب الذرات لتستوفي عقود الواجهتين البرمجيتين الخاصتين بـ WebDriver وCore.

طبقات مكتبة جافاسكربت في Selenium

الشكل 16.3: طبقات مكتبة جافاسكربت في Selenium

اختيرت Closure Library لعدة أسباب. وكان السبب الرئيس هو أن Closure Compiler يفهم تقنية التقسيم إلى وحدات التي تستخدمها المكتبة. وClosure Compiler مُصرِّف يستهدف جافاسكربت كلغة إخراج. وقد يكون «التصريف» (compilation) بسيطًا جدًّا، مثل ترتيب ملفات الإدخال وفق تبعياتها ثم دمجها وطباعتها منسّقة، أو معقّدًا كإجراء تصغير متقدّم (minification) وإزالة الشيفرة الميتة. وميزة أخرى لا يمكن إنكارها أن عدة أعضاء من الفريق الذي يعمل على شيفرة جافاسكربت كانوا متمرّسين جيدًا على Closure Library.

وتُستخدم مكتبة الشيفرة «الذرّية» هذه على نطاق واسع في أنحاء المشروع كلما كانت الحاجة إلى استجواب DOM. وللأمر في حالتي RC وتلك المشغِّلات المكوّنة إلى حدٍّ كبير من جافاسكربت، تُستعمل المكتبة مباشرة، وعادةً بعد تصريفها كسكربت أحادي (monolithic script). أما المشغِّلات المكتوبة بـ Java فتُصرَّف فيها الدوال المفردة من طبقة محوّل WebDriver مع تفعيل التحسين الكامل، ويُدرَج جافاسكربت الناتج كموارد (resources) داخل حِزَم JAR. أما المشغِّلات المكتوبة بلغات من عائلة C، مثل مشغّل iPhone ومشغّل IE، فلا تُصرَّف الدوال المفردة فحسب مع التحسين الكامل، بل يُحوَّل الناتج إلى ثابت (constant) معرَّف في ترويسة (header)، يُنفَّذ عبر آلية تنفيذ جافاسكربت المعتادة في المشغّل عند الحاجة. وعلى الرغم من أن هذا يبدو أمرًا غريبًا، إلا أنه يتيح دفع جافاسكربت إلى داخل المشغّل الأساسي من دون الحاجة إلى كشف الشيفرة الخام في مواضع متعدّدة.

ولأن الذرات مستعملة على نطاق واسع، من الممكن ضمان سلوك متّسق بين المتصفحات المختلفة، ولأن المكتبة مكتوبة بجافاسكربت ولا تتطلّب صلاحيات مرتفعة كي تُنفَّذ، فإن دورة التطوير سهلة وسريعة. تستطيع Closure Library تحميل الاعتماديات (dependencies) ديناميكيًّا، فلا يحتاج مطوّر Selenium سوى كتابة اختبار وتحميله في متصفح، وتعديل الشيفرة والضغط على زر التحديث حسب الحاجة. وبمجرد أن ينجح الاختبار في متصفح واحد، من السهل تحميله في متصفح آخر والتأكد من نجاحه هناك. ولأن Closure Library تؤدّي عملًا جيدًا في تجريد الفروق بين المتصفحات، فغالبًا ما يكفي هذا، وإن كان من المُطمئِنّ وجود بناءات مستمرة (continuous builds) تشغّل مجموعة الاختبارات في كل متصفح مدعوم.

وكان لـ Core وWebDriver في الأصل مناطق كثيرة من شيفرة متقاربة—شيفرة تؤدّي الوظيفة نفسها بطرق تختلف قليلًا. وحين بدأنا العمل على الذرات، فُحصت هذه الشيفرة بالتدقيق في محاولة لإيجاد أفضل ما في كل نوع (best of breed). فبعد كل شيء، استُخدم المشروعان على نطاق واسع وكانت شيفرتهما شديدة المتانة، لذا فإن إطرح كل شيء والبدء من الصفر لم يكن مضيِّرًا فحسب بل ساذجًا. ومع استخراج كل ذرّة، حُدِّدت المواضع التي ستُستخدم فيها ثم تحويلها إلى استعمال الذرّة. فمثلًا، تقلّصت دالة getAttribute في مشغّل Firefox من نحو 50 سطر شيفرة إلى 6 أسطر، بما في ذلك الأسطر الفارغة:

FirefoxDriver.prototype.getElementAttribute =
  function(respond, parameters) {
  var element = Utils.getElementAt(parameters.id,
                                   respond.session.getDocument());
  var attributeName = parameters.name;

  respond.value = webdriver.element.getAttribute(element, attributeName);
  respond.send();
};

وذلك السطر الذي قبل الأخير، الذي تُسنَد إليه respond.value، هو استعمال لمكتبة WebDriver الذرّية.

وتُعِدّ الذرات تطبيقًا عمليًا لتجسيد عدة من المحاور المعمارية للمشروع. فهي بطبيعة الحال تفرض الشرط بأن يكون تنفيذ الواجهة البرمجية متعاطفًا مع تنفيذ مكتوب بجافاسكربت. والأفضل من ذلك أن المكتبة نفسها مشتركة في أنحاء قاعدة الشيفرة كلها؛ فحيثما كان على أي علة أن تُتحقَّق وتُصلَح عبر عدة تنفيذات، صارت من الكافي إصلاح العلة في موضع واحد، ما يخفّض كلفة التغيير ويحسّن الثبات والفاعلية. كما تجعل الذرات «معامل الحافلة» للمشروع أكثر مواتية. ولأن اختبار وحدات عاديًا مكتوبًا بجافاسكربت يمكن أن يُستعمل للتحقق من أن الإصلاح نجح، فإن عتبة الانضمام إلى المشروع المفتوح المصدر صارت أقلّ بكثير مما كانت حين كانت المعرفة بكيفية تنفيذ كل مشغّل شرطًا لازمًا.

وهناك ميزة أخرى لاستعمال الذرات. فالطبقة التي تحاكي تنفيذ RC القائم وترتكز إلى WebDriver أداة مهمة للفرق التي ترغب في الترحيل منضبطًا إلى واجهات WebDriver البرمجية الأحدث. ومع تحويل Selenium Core إلى ذرّات، تصبح إمكانية تصريف كل دالة من دواله على حدة، فيصبح تنفيذ طبقة المحاكاة هذه أسهل وأدقّ.

ولا حاجة إلى القول إن للمقاربة المتَّخذة جوانب سلبية. وأهمّها أن تصريف جافاسكربت إلى ثابت const بلغة C أمر غريب إلى حدٍّ كبير، وهو ما يحيّر دائمًا المساهمين الجدد في المشروع الذين يريدون العمل على شيفرة C. كما أن من النادر وجود مطوّر يملك كل إصدارات كل المتصفحات، وملتزمًا بما يكفي لتشغيل كل اختبار في جميع تلك المتصفحات—فمن الممكن أن يُخلِف أحدهم ارتدادًا (regression) في موضع غير متوقّع، وقد يستغرق بعض الوقت لتحديد المشكلة، ولا سيما إذا كانت البناءات المستمرة تعرّض لتذبذب.

ولأن الذرات توحّد القيم المعادة بين المتصفحات، من الممكن أيضًا وجود قيم معادة غير متوقّعة. فمثلًا، خُذ هذا HTML:

<input name="example" checked>

تعتمد قيمة الخاصية checked على المتصفح المستخدَم. وتُوحّد الذرات هذه القيمة، وكذلك سائر الخصائص المنطقية (Boolean attributes) المعرَّفة في مواصفة HTML5، لتصبح «true» أو «false». وحين أُدخلت هذه الذرّة إلى قاعدة الشيفرة، اكتشفنا مواضع كثيرة كان فيها الناس يفترضون ما يجب أن تكون عليه القيمة المعادة تبعًا للمتصفح. ومع أن القيمة صارت متّسقة الآن، فقد امتدّت فترة طويلة نشرح فيها للمجتمع ما حدث ولماذا.

16.6. المشغّل البعيد، وخاصة مشغّل Firefox

كان WebDriver البعيد في الأصل آلية RPC مموّهة. وقد تطوّر منذ ذلك الحين ليصبح إحدى الآليات الرئيسة التي نستعملها لخفض كلفة صيانة WebDriver، وذلك بتوفير واجهة موحّدة يمكن للروابط اللغوية أن تُبرمَج في ضوئها. وحتى مع دفعنا أكبر قدر ممكن من المنطق خارج الروابط اللغوية إلى داخل المشغّل، فإن كان على كل مشغّل أن يتواصل عبر بروتوكول فريد، لظل لدينا كم هائل من الشيفرة نكرّره في كل الروابط اللغوية.

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

ينقسم أي آلية RPC إلى قطعتين: وسيلة النقل (transport) والترميز (encoding). وكنا نعلم أن أيًّا كانت الطريقة التي ننفّذ بها بروتوكول WebDriver البعيد، فسنحتاج إلى دعم القطعتين معًا في اللغات التي نرغب في استعمالها عملاءً. وقد طُوّرت النسخة الأولى من هذا التصميم كجزء من مشغّل Firefox.

دأب مطوّرو Mozilla، وبالتالي Firefox، على اعتباره تطبيقًا متعدد المنصّات. ولتسهيل التطوير، أنشأت Mozilla إطارًا مستوحى من COM من Microsoft، يتيح بناء المكوّنات وتجميعها معًا، ويُسمّى XPCOM (أي COM متعدد المنصّات). وتُصرَّح واجهة XPCOM باستخدام IDL، وهناك روابط لغوية لـ C وJavascript فضلًا عن لغات أخرى. ولأن XPCOM يُستعمل في بناء Firefox، ولأن XPCOM يملك روابط لجافاسكربت، من الممكن الإفادة من كائنات XPCOM داخل إضافات Firefox.

تسمح إضافة Win32 العادية لـ COM بالوصول إلى الواجهات عن بُعد. وكانت هناك خطط لإضافة القدرة نفسها إلى XPCOM أيضًا، وقد أضاف Darin Fisher تنفيذًا لـ XPCOM ServerSocket لتسهيل ذلك. وعلى الرغم من أن خطط D-XPCOM لم تثمر قط، إلا أن البنية التحتية الزائدة ما تزال موجودة. واستفدنا من ذلك لإنشاء خادم بسيط جدًّا داخل إضافة Firefox مخصّصة تحتوي كل منطق التحكم في Firefox. وكان البروتوكول المستعمل في الأصل قائمًا على النص وموجَّهًا نحو الأسطر (line-oriented)، مع ترميز كل السلاسل بترميز UTF-2. وكان كل طلب أو استجابة يبدأ برقم، يدل على عدد الأسطر الجديدة التي ينبغي عدّها قبل اعتبار أن الطلب أو الرد قد أُرسل. والأمر الحاسم هنا أن هذا المخطّط سهل التنفيذ في جافاسكربت، لأن SeaMonkey (محرك جافاسكربت في Firefox آنذاك) يخزّن سلاسل جافاسكربت داخليًّا كأعداد صحيحة غير ساكنة بطول 16 بت.

على الرغم من أن العبث ببروتوكولات ترميز مخصّصة فوق المقابس الخام طريقة ممتعة لقضاء الوقت، إلا أنها لها عدة عيوب. فلم تكن هناك مكتبات متاحة على نطاق واسع للبروتوكول المخصّص، ولذلك كان يلزم تنفيذه من الصفر لكل لغة نرغب في دعمها. وهذا الشرط — وهو تنفيذ مزيد من الشيفرة — يجعل من المستبعد أن يشارك المساهمون السخيون من المصادر المفتوحة في تطوير روابط لغوية جديدة. وإضافةً إلى ذلك، وعلى الرغم من أن البروتوكول الموجَّه نحو الأسطر كان مقبولًا حين كنا نرسل بيانات نصية فحسب، إلا أنه أثار مشكلات حين أردنا إرسال صور، مثل لقطات الشاشة.

وتبيّن بسرعة ووضوح أن آلية RPC الأصلية هذه غير عملية. ولحسن الحظ، كانت هناك وسيلة نقل معروفة جيدًا ذات انتشار واسع ودعم في كل اللغات تقريبًا، تتيح لنا ما نريد: HTTP.

ومتى قرّرنا استعمال HTTP كوسيلة نقل، كانت الخطوة التالية هي تحديد ما إذا كنا سنستعمل نقطة نهاية واحدة (à la SOAP) أم نقاط نهاية متعدّدة (على نمط REST). وقد استعمل بروتوكول Selenese الأصلي نقطة نهاية واحدة، وكان يشفّر الأوامر والوسائط في سلسلة الاستعلام (query string). ورغم أن هذا الأسلوب كان ناجحًا، إلا أنه لم يكن «يشعر» بالصواب: كنا نحلم بامتلاك القدرة على الاتصال بنسخة WebDriver بعيدة داخل متصفح لعرض حالة الخادم. وانتهينا إلى اختيار أسلوب نسمّيه «شبه REST» (REST-ish): عناوين URL متعدّدة لنقاط النهاية تستخدم أفعال HTTP لتضمين المعنى، لكنها تكسر عددًا من القيود اللازمة لنظام RESTful حقًّا، ولا سيما قيود موقع الحالة وإمكانية التخزين المؤقت (caching)، وذلك في الدرجة الأولى لأن هناك موضعًا واحدًا فقط يمكن أن توجد فيه حالة التطبيق بصورة ذات معنى.

ورغم أن HTTP يجعل من السهل دعم طرائق متعددة لترميز البيانات بالاستناد إلى تفاوض نوع المحتوى (content negotiation)، فقد قرّرنا أننا بحاجة إلى شكل قانوني معياري (canonical form) يمكن لكل تطبيقات بروتوكول WebDriver البعيد أن تعمل معه. وكانت هناك بضعة خيارات بديهية: HTML أو XML أو JSON. وقد استُبعد XML بسرعة: فرغم أنه تنسيق بيانات معقول تمامًا وهناك مكتبات تدعمه في كل لغة تقريبًا، فإن انطباعي عن مدى محبّيته في مجتمع المصادر المفتوحة هو أن الناس لا يستمتعون بالعمل معه. وإضافةً إلى ذلك، من الممكن تمامًا أنه حتى لو كانت البيانات المعادة تتشارك «شكلًا» مشتركًا، فسيكون من السهل إضافة حقول إضافية[3]. كانت هذه الإضافات لـ XML يمكن نمذجتها باستخدام مساحات أسماء XML (XML namespaces)، لكن ذلك سيبدأ في إدخال مزيد من التعقيد في شيفرة العميل: وهو أمر كنت حريصًا على تجنّبه. واستُبعد XML كخيار. ولم يكن HTML خيارًا جيدًا حقًّا، لأننا احتجنا إلى القدرة على تعريف تنسيق بيانات خاص بنا، ورغم أن من الممكن ابتكار تنسيق مصغّر (micro-format) مدمج واستعماله، إلا أنه يبدو كمحاولة دقّ مسمار بضرس.

وآخر الاحتمالات التي جُرِّبت هو Javascript Object Notation أي JSON. فيمكن للمتصفحات تحويل سلسلة نصية إلى كائن إما باستدعاء eval مباشرة، أو، في المتصفحات الأحدث، عبر بدائيات (primitives) صُمِّمت لتحويل كائن جافاسكربت من سلسلة وإليه بأمان ومن دون آثار جانبية. ومن الناحية العملية، JSON تنسيق بيانات شعبي، وله مكتبات متوفرة له في كل لغة تقريبًا، وكل الأولاد الأذكياء يحبّونه. خيار سهل.

وعليه، استعملت النسخة الثانية من بروتوكول WebDriver البعيد HTTP كوسيلة نقل، وترميز JSON بترميز UTF-8 كصيغة ترميز افتراضية. واختير UTF-8 صيغةً افتراضية للترميز حتى يتمكّن العملاء من الكتابة بسهولة في اللغات التي محدودية دعمها لـ Unicode، لأن UTF-8 متوافق إلى الوراء مع ASCII. وكانت الأوامر المرسلة إلى الخادم تستعمل عنوان URL لتحديد الأمر المُرسَل، وتشفّر وسائط الأمر في مصفوفة.

فمثلًا، نداء WebDriver.get("http://www.example.com") يقابله طلب POST إلى عنوان URL يشفّر معرّف الجلسة (session ID) وينتهي بـ"/url"، بوصف مصفوفة الوسائط هكذا {[}'http://www.example.com'{]}. وكانت النتيجة المعادة أكثر تنظيمًا قليلًا، وكانت تحوي عناصر نائبة لقيمة معادة ورمز خطأ. ولم يمض وقت طويل قبل أن تأتي النسخة الثالثة من البروتوكول البعيد، التي استبدلت مصفوفة وسائط الطلب بقاموس من الوسائط المسمّاة. وقد كان لهذا ميزة جعل تصحيح أخطاء الطلبات أسهل بكثير، وألغت احتمال أن يخطئ العملاء في ترتيب الوسائط، مما جعل النظام ككل أكثر متانة. ومن الطبيعي أنه قُرّر استعمال رموز خطأ HTTP العادية للدلالة على قيم معيّنة واستجابات كانت رموز الخطأ أنسب طريقة للدلالة عليها؛ فمثلًا، إذا حاول مستخدم نداء عنوان URL لا يقابله شيء، أو حين نرغب في الدلالة على «الاستجابة الفارغة».

ولبروتوكول WebDriver البعيد مستويان من معالجة الأخطاء: أحدهما للطلبات غير الصالحة، والآخر للأوامر الفاشلة. ومن أمثلة الطلبات غير الصالحة أن يكون المورد المطلوب غير موجود على الخادم، أو ربما فعل HTTP (verb) لا يفهمه المورد (مثل إرسال أمر DELETE إلى المورد المخصّص للتعامل مع عنوان URL الصفحة الحالية). وفي تلك الحالات، تُرسَل استجابة HTTP 4xx عادية. أما الأمر الفاشل، فيُضبط رمز خطأ الاستجابة فيه على 500 («Internal Server Error»)، وتحتوي البيانات المعادة على تفصيل أدقّ لما حدث خطأً.

وعند إرسال استجابة تحتوي بيانات من الخادم، تأخذ شكل كائن JSON:

المفتاح الوصف
sessionId مقبض غامض (opaque) يستعمله الخادم لتحديد أين يوجّه الأوامر الخاصة بالجلسة.
status رمز حالة رقمي يلخّص نتيجة الأمر. وتشير القيمة غير الصفرية إلى أن الأمر قد فشل.
value قيمة استجابة JSON.

وهذا مثال على استجابة:

{
  sessionId: 'BD204170-1A52-49C2-A6F8-872D127E7AE8',
  status: 7,
  value: 'Unable to locate element with id: foo'
}

وكما يتضح، فإننا نشفّر رموز الحالة في الاستجابة، وتشير القيمة غير الصفرية إلى أن شيئًا ما سار على نحو سيئ حقًّا. وكان مشغّل IE أول من استعمل رموز الحالة، وتطابق القيم المستعملة في بروتوكول السلك هذه القيم. ولأن جميع رموز الأخطاء متّسقة بين المشغِّلات، من الممكن أن تتشارك شيفرة معالجة الأخطاء بين جميع المشغِّلات المكتوبة بلغة بعينها، مما يجعل مهمة منفّذي جانب العميل أسهل.

وخادم WebDriver البعيد ليس سوى سِرْفلت (servlet) جافا يعمل كمُوجِّه (multiplexer)، ويوجّه أي أوامر يتلقّاها إلى نسخة WebDriver مناسبة. وهو من نوع الأشياء التي يستطيع طالب في السنة الثانية أن يكتبه. كما ينفّذ مشغّل Firefox بروتوكول WebDriver البعيد، وبنيته أكثر إثارة بكثير للاهتمام، فدعنا نتّبع طلبًا من النداء في الروابط اللغوية وصولًا إلى تلك الواجهة الخلفية (back-end) حتى يعود إلى المستخدم.

ونفترض أننا نستعمل Java وأن «element» نسخة من WebElement، فيبدأ كل شيء هنا:

element.getAttribute("row");

وداخليًّا، يملك العنصر مُعرِّفًا «غامضًا» (opaque) يستعمله جانب الخادم لتحديد العنصر الذي نتحدث عنه. ولأغراض هذا العرض، سنفترض أنه يحمل القيمة "some_opaque_id". ويُشفَّر هذا في كائن Command جافا يحتوي على Map يحمل الوسيطَين (المسمّيين الآن) id لمعرّف العنصر وname لاسم الخاصية التي يتم الاستفسار عنها.

ويُظهر البحث السريع في جدول أن عنوان URL الصحيح هو:

/session/:sessionId/element/:id/attribute/:name

وأي جزء من عنوان URL يبدأ بنقطتين يُفترض أنه متغيّر يحتاج إلى استبدال. وقد مُنحنا الوسيطَين id وname من قبل، وsessionId هو مقبض غامض آخر يُستعمل للتوجيه عندما يستطيع الخادم معالجة أكثر من جلسة في الوقت نفسه (وهو ما لا يستطيعه مشغّل Firefox). ومن ثمّ يتمدّد هذا العنوان عادةً إلى شيء من الشكل:

http://localhost:7055/hub/session/XXX/element/some_opaque_id/attribute/row

وبمناسبة الحديث عنه، طُوِّر بروتوكول السلك البعيد لـ WebDriver في الوقت نفسه الذي اقتُرح فيه قوالب URL (URL Templates) كمسودة لـ RFC. فكلٌّ من مخطّطنا لتحديد عناوين URL وقوالب URL يسمح بتوسيع المتغيّرات (ومن ثمّ اشتقاقها) داخل عنوان URL. وللأسف، وعلى الرغم من أن قوالب URL اقتُرح في الوقت نفسه، لم نعلم بها إلا متأخرًا نسبيًّا، ومن ثمّ لم تُستعمل لوصف بروتوكول السلك.

ولأن الدالة التي ننفّذها عديمة الأثر (idempotent)[4]، فإن طريقة HTTP الصحيحة المستعملة هي GET. ونفوّض الأمر إلى مكتبة جافا قادرة على التعامل مع HTTP (وهي Apache HTTP Client) لإجراء النداء إلى الخادم.

نظرة عامة على بنية مشغّل Firefox

الشكل 16.4: نظرة عامة على بنية مشغّل Firefox

نُفِّذ مشغّل Firefox بوصفه إضافة لـ Firefox، ويبيّن التصميم الأساسي له في الشكل 16.4. ومن غير المعتاد نوعًا ما أن فيه خادم HTTP مدمجًا. فرغم أننا استعملنا في الأصل خادمًا بنيناه بأنفسنا، فإن كتابة خوادم HTTP في XPCOM لم تكن إحدى كفاءاتنا الأساسية، لذا حين توفّرت الفرصة استبدلناه بخادم HTTPD أساسي كتبته Mozilla بنفسها. وتتلقّى HTTPD الطلبات، وتُمرّرها تقريبًا فورًا إلى كائن dispatcher.

يأخذ المُوجِّه الطلب ويمرّره على قائمة معروفة بالعناوين URL المدعومة، فيحاول أن يجد واحدًا يطابق الطلب. ويتم هذا التطابق بمعرفة الاستبدال بين المتغيّرات الذي جرى في جانب العميل. وبمجرد العثور على مطابقة تامة، بما في ذلك الفعل المستعمل، يُنشأ كائن JSON يمثّل الأمر المراد تنفيذه. وفي حالتنا يبدو هكذا:

{
  'name': 'getElementAttribute',
  'sessionId': { 'value': 'XXX' },
  'parameters': {
    'id': 'some_opaque_key',
    'name': 'rows'
  }
}

ثم يُمرَّر هذا كسلسلة JSON إلى مكوّن XPCOM مخصّص كتبناه اسمه CommandProcessor. وهذه هي الشيفرة:

var jsonResponseString = JSON.stringify(json);
var callback = function(jsonResponseString) {
  var jsonResponse = JSON.parse(jsonResponseString);

  if (jsonResponse.status != ErrorCode.SUCCESS) {
    response.setStatus(Response.INTERNAL_ERROR);
  }

  response.setContentType('application/json');
  response.setBody(jsonResponseString);
  response.commit();
};

// Dispatch the command.
Components.classes['@googlecode.com/webdriver/command-processor;1'].
    getService(Components.interfaces.nsICommandProcessor).
    execute(jsonString, callback);

هناك قدر كبير من الشيفرة هنا، لكن هناك نقطتان رئيسيتان. الأولى، أننا حوّلنا الكائن أعلاه إلى سلسلة JSON. والثانية، أننا نمرّر دالة استدعاء (callback) إلى دالة execute، مما يجعل الاستجابة HTTP تُرسَل.

تنظر دالة execute في مُعالِج الأوامر (command processor) إلى «name» لتحديد الدالة التي يلزم استدعاؤها، ثم تستدعيها. وأول وسيط يُعطى لهذه الدالة المنفِّذة هو كائن «respond» (وهذا الاسم من سُمّي به لأنه كان في الأصل الدالة المستعملة وحدها لإرسال الاستجابة إلى المستخدم)، وهو يغلّف القيم الممكنة التي قد تُرسَل، فضلًا عن امتلاكه دالة تتيح إرسال الاستجابة إلى المستخدم، وآليات للحصول على معلومات عن DOM. وأما الوسيط الثاني فهو قيمة كائن parameters الذي رأيناه أعلاه (في هذه الحالة، id وname). وميزة هذا المخطّط أن لكل دالة واجهة موحّدة تعكس البنية المستعملة في جانب العميل. وهذا يعني أن النماذج الذهنية المستعملة في التفكير في الشيفرة على كل جانب متشابهة. وإليك التنفيذ الأساسي لدالة getAttribute، التي رأينها من قبل في القسم 16.5:

FirefoxDriver.prototype.getElementAttribute = function(respond, parameters) {
  var element = Utils.getElementAt(parameters.id,
                                  respond.session.getDocument());
  var attributeName = parameters.name;

  respond.value = webdriver.element.getAttribute(element, attributeName);
  respond.send();
};

ولكي تكون مراجع العناصر متّسقة، فإن السطر الأول يكتفي بالبحث في ذاكرة تخزين مؤقت (cache) عن العنصر المشار إليه بالمُعرِّف الغامض. وفي مشغّل Firefox، يكون هذا المُعرِّف الغامض قيمة UUID، و«ذاكرة التخزين المؤقت» ليست سوى خريطة (map). ويفحص أيضًا getElementAt ما إذا كان العنصر المشار إليه معروفًا ومُرفقًا بـ DOM. فإن أخفق أيّ من الفحصين، يُزال المُعرِّف من ذاكرة التخزين المؤقت (عند الحاجة) ويُطلَق استثناء يُعاد إلى المستخدم.

ويستفيد السطر الذي قبل الأخير من ذرّات أتمتة المتصفح التي ناقشناها سابقًا، وهذه المرة بعد تصريفها كسكربت أحادي (monolithic script) وتحميلها كجزء من الإضافة.

وفي السطر الأخير، تُستدعى دالة send. وهي تجري فحصًا بسيطًا للتأكد من أننا لا نرسل الاستجابة إلا مرة واحدة، قبل أن تستدعي دالة الاستدعاء المعطاة إلى دالة execute. وتُعاد الاستجابة إلى المستخدم في شكل سلسلة JSON، يُفرَغ محتواها في كائن يبدو هكذا (مع افتراض أن getAttribute أعادت "7"، أي أن العنصر لم يُعثر عليه):

{
  'value': '7',
  'status': 0,
  'sessionId': 'XXX'
}

ثم يفحص عميل Java قيمة حقل status. فإذا كانت تلك القيمة غير صفرية، حوّل رمز الحالة الرقمي إلى استثناء من النوع الصحيح وألقاه، مستعينًا بحقل «value» للمساعدة في تعيين الرسالة المرسلة إلى المستخدم. وإذا كانت الحالة صفرًا، تُعاد قيمة حقل «value» إلى المستخدم.

ومعظم هذا له معنى معيّن، لكن هناك قطعة واحدة سيطرح عليها القارئ الدقيق أسئلة: لماذا حوّل المُوجِّه الكائن الذي كان يملكه إلى سلسلة قبل استدعاء دالة execute؟

والسبب في ذلك أن مشغّل Firefox يدعم أيضًا تشغيل اختبارات مكتوبة بجافاسكربت خالصة. وطبيعيًا، كان دعم هذا سيكون أمرًا عسيرًا إلى حدٍّ استثنائي: فالاختبارات تعمل في سياق الصندوق الرملي الأمني لجافاسكربت داخل المتصفح، ومن ثمّ قد لا تفعل أشياء مفيدة في الاختبارات، مثل الانتقال بين النطاقات أو رفع الملفات. غير أن إضافة WebDriver في Firefox توفّر مخرجًا من هذا الصندوق (escape hatch). وهي تُعلن عن وجودها بإضافة خاصية webdriver إلى عنصر المستند. وتستعمل واجهة WebDriver البرمجية لجافاسكربت هذا المؤشر لتتمكّن من إضافة كائنات أوامر مرمّزة بصيغة JSON كقيمة لخاصية command على عنصر المستند، وإطلاق حدث webdriverCommand مخصّص، ثم الاستماع إلى حدث webdriverResponse على العنصر نفسه لإعلامها بأنه قد عُيِّنت الخاصية response.

وهذا يوحي بأن تصفّح الويب في نسخة من Firefox مع إضافة WebDriver مركّبة فكرة سيئة إلى حدٍّ كبير، لأنه يجعل التحكم بالمتصفح عن بُعد أمرًا سهلًا إلى حدٍّ تافه.

خلف الكواليس، هناك «رسول DOM» (DOM messenger) ينتظر الحدث webdriverCommand، ويقرأ كائن JSON المرمَّز ثم يستدعي دالة execute في مُعالِج الأوامر. وهذه المرة تكون دالة الاستدعاء مجرّد دالة تعيّن الخاصية response على عنصر المستند ثم تطلق حدث webdriverResponse المتوقّع.

16.7. مشغّل IE

‏Internet Explorer متصفح مثير للاهتمام. فهو مبني من عدد من واجهات COM تعمل معًا في تناغم. وهذا يمتد إلى داخل محرك جافاسكربت، حيث تشير متغيّرات جافاسكربت المألوفة في الواقع إلى نسخ COM الكامنة خلفها. فـ window في جافاسكربت هو IHTMLWindow، وdocument نسخة من الواجهة البرمجية IHTMLDocument. وقد أحسنت Microsoft مهمة صيانة السلوك القائم إلى حدٍّ كبير وهي تطوّر متصفحها. وهذا يعني أن أي تطبيق كان يعمل مع أصناف COM التي يعرضها IE6 سيظل يعمل مع IE9.

وتطوّر بنية مشغّل Internet Explorer مع الزمن. وقد كان أحد القوى المؤثّرة الرئيسة في تصميمه شرط تجنّب أداة تثبيت (installer). وهذا شرط غير معتاد نوعًا ما، لذا قد يحتاج إلى بعض التوضيح. وأول سبب لاستبعاد أداة التثبيت هو أنها تجعل مرور WebDriver في «اختبار الخمس دقائق» أصعب، حيث يُنزّل المطوّر حزمة ويجرّبها لفترة وجيزة. والأهم من ذلك، من المتكرر نسبيًّا أن يكون مستخدمو WebDriver عاجزين عن تثبيت البرمجيات على أجهزتهم بأنفسهم. كما يعني ذلك أنه لا يحتاج أحد إلى تذكّر تسجيل الدخول إلى خوادم التكامل المستمر لتشغيل أداة تثبيت حين يريد مشروع بدء الاختبار باستخدام IE. وأخيرًا، تشغيل أدوات التثبيت ليس جاريًا في ثقافة بعض اللغات. فالعرف الشائع في Java هو ببساطة وضع ملفات JAR في مسار CLASSPATH، ومن تجربتي، فإن المكتبات التي تتطلب أدوات تثبيت لا تحظى عادةً بنفس القبول أو الاستعمال.

إذن، لا أداة تثبيت. ولهذا الاختيار عواقبه.

واللغة الطبيعية التي يُستعمل للبرمجة على Windows ستكون لغة تعمل على ‎.Net‎، وعلى الأرجح C#. ويتكامل مشغّل IE عن قرب مع IE بالإفادة من واجهات أتمتة COM الخاصة بـ IE التي تصحب كل إصدار من Windows. وعلى وجه التحديد، نستعمل واجهات COM من مكتبتي MSHTML وShDocVw الأصليتين، اللتين تشكلان جزءًا من IE. وقبل C# 4، كان التوافق البيني (interoperability) بين CLR وCOM يتحقق باستعمال مجموعات تجميع التوافق الأولي المنفصلة (Primary Interop Assemblies, PIAs). ومجموعة PIA هي في جوهرها جسر مولَّد بين عالم CLR المُدار وعالم COM.

وللأسف، كان استعمال C# 4 سيعني استعمال نسخة حديثة جدًّا من بيئة تشغيل ‎.Net‎، وكثير من الشركات تتجنّب العيش على الحافة، مفضّلةً استقرار الإصدارات القديمة ومشكلاتها المعروفة. ولو استعملنا C# 4 لاستبعدنا تلقائيًّا نسبة معقولة من قاعدة مستخدمينا. وهناك أيضًا عيوب أخرى لاستعمال PIA. فكّر في قيود الترخيص. وبعد التشاور مع Microsoft، اتضح أن مشروع Selenium لن يملك الحقوق اللازمة لتوزيع مجموعتَي PIA الخاصتين بمكتبتي MSHTML أو ShDocVw. وحتى لو مُنحت تلك الحقوق، فإن كل تثبيت لـ Windows وIE يملك تركيبة فريدة من هذه المكتبات، مما يعني أننا كنا سنحتاج إلى شحنة ضخمة من هذه الأشياء. وبناء مجموعات PIAs على جهاز العميل عند الطلب ليس خيارًا مطروحًا أيضًا، لأنها تتطلب أدوات تطوير قد لا توجد على جهاز مستخدم عادي.

إذن، ورغم أن C# كانت ستكون لغة جذابة للقيام بمعظم الترميز، إلا أنها لم تكن خيارًا مطروحًا. احتجنا إلى استعمال شيء أصلي، على الأقل في التواصل مع IE. وكان الخيار الطبيعي التالي لذلك هو C++، وهذه هي اللغة التي اخترناها في النهاية. ولاستعمال C++ ميزة أننا لا نحتاج إلى استعمال مجموعات PIAs، لكنه يعني أننا نحتاج إلى إعادة توزيع مكتبة Visual Studio C++ بصيغة DLL ما لم نربط بها ربطًا ثابتًا. ولأننا سنحتاج إلى تشغيل أداة تثبيت لإتاحة تلك المكتبة، فإننا نربط مكتبتنا للتواصل مع IE ربطًا ثابتًا.

تلك كلفة مرتفعة إلى حدٍّ ما ندفعها مقابل شرط عدم استعمال أداة تثبيت. غير أنه، إذا عدنا إلى موضوع أين ينبغي أن تقيم التعقيد، فإن هذا الاستثمار يستحق ثمنه لأنه يجعل حياة مستخدمينا أسهل بكثير. وهو قرار نعيد تقييمه على أساس مستمر، لأن الفائدة التي تعود على المستخدم تقابلها حقيقة أن مجموعة من يستطيعون المساهمة في مشروع مفتوح المصدر متقدّم بلغة C++ تبدو أصغر بكثير ممن يستطيعون المساهمة في مشروع مكافئ بلغة C#.

ويبيّن التصميم الأولي لمشغّل IE في الشكل 16.5.

مشغّل IE الأصلي

الشكل 16.5: مشغّل IE الأصلي

انطلاقًا من قاعدة ذلك التكديس (stack)، ترى أننا نستعمل واجهات أتمتة COM الخاصة بـ IE. ولمكيّف التعامل معها بسهولة على المستوى المفاهيمي، لفّنا تلك الواجهات الخام بمجموعة من أصناف C++ تحاكي عن قرب واجهة WebDriver البرمجية الرئيسة. ولمتابعة تواصل أصناف Java مع C++، استعملنا JNI، مع تنفيذ دوال JNI باستعمال تجريدات C++ لواجهات COM.

نجح هذا الأسلوب إلى حدٍّ معقول ما دامت Java هي لغة العميل الوحيدة، لكنه كان سيكون مصدر ألم وتعقيد لو كان على كل لغة ندعمها أن نغيّر بها المكتبة الأساسية. وهكذا، ورغم أن JNI نجح، إلا أنه لم يوفّر مستوى التجريد الصحيح.

وما مستوى التجريد الصحيح؟ كل لغة نرغب في دعمها كانت تملك آلية للاستدعاء نزولًا إلى شيفرة C خالصة. في C#، يأتي ذلك في شكل PInvoke. وفي Ruby هناك FFI، وفي Python هناك ctypes. وفي عالم Java، هناك مكتبة ممتازة اسمها JNA (بنية Java الأصلية). احتجنا إلى كشف واجهتنا البرمجية باستعمال هذا القاسم المشترك الأدنى. وقد تم ذلك بأخذ نموذج كائناتنا وتبسيطه، باستعمال بادئة من حرفين أو ثلاثة للدلالة على «الواجهة الأصل» (home interface) للطريقة: «wd» لـ«WebDriver» و«wde» لعنصر WebDriver. وهكذا صارت WebDriver.get هي wdGet، وصارت WebElement.getText هي wdeGetText. وتُعيد كل دالة عددًا صحيحًا يمثّل رمز حالة، مع استعمال وسائط «out» للسماح للدوال بإعادة بيانات أكثر دلالة. وهكذا انتهينا إلى تواقيع دوال من الشكل:

int wdeGetAttribute(WebDriver*, WebElement*, const wchar_t*, StringWrapper**)

بالنسبة إلى شيفرة الاستدعاء، فإن WebDriver وWebElement وStringWrapper هي أنواع غامضة (opaque): فعزونا الفروق في الواجهة البرمجية لتوضيح أي قيمة ينبغي استعمالها كوسيط، مع أننا كان من الممكن تمامًا أن تكون «void *». ويمكنك أيضًا أن ترى أننا كنّا نستعمل محارف عريضة (wide characters) للنص، لأننا أردنا التعامل مع النصوص الدولية على الوجه الصحيح.

وفي جانب Java، كشفنا مكتبة هذه الدوال عبر واجهة، ثم هيّئناها لتبدو كواجهة كائنية التوجه المعتادة التي يقدّمها WebDriver. فمثلًا، يبدو تعريف جافا لدالة getAttribute هكذا:

public String getAttribute(String name) {
  PointerByReference wrapper = new PointerByReference();
  int result = lib.wdeGetAttribute(
      parent.getDriverPointer(), element, new WString(name), wrapper);

  errors.verifyErrorCode(result, "get attribute of");

  return wrapper.getValue() == null ? null : new StringWrapper(lib, wrapper).toString();
}

وأدى هذا إلى التصميم المبين في الشكل 16.6.

مشغّل IE المعدَّل

الشكل 16.6: مشغّل IE المعدَّل

بينما كانت جميع الاختبارات تعمل على الآلة المحلية، سار هذا الأمر على ما يرام، لكن ما إن بدأنا في استعمال مشغّل IE داخل WebDriver البعيد حتى بدأنا نواجه انسدادًا عشوائيًّا. وأرجعنا هذه المشكلة إلى قيد في واجهات أتمتة COM الخاصة بـ IE. فهي مصمَّمة للاستعمال ضمن نموذج «شقة الخيط المفرد» (Single Thread Apartment). أي أن هذا في جوهره إلى شرط استدعاء الواجهة من الخيط نفسه في كل مرة. وفي التشغيل المحلي، يحدث ذلك تلقائيًّا. لكن خوادم تطبيقات Java تطلق بدورها عدة خيوط للتعامل مع الحمل المتوقّع. وما النتيجة؟ لم يكن لدينا أي وسيلة للتأكد من أن الخيط نفسه سيُستعمل للوصول إلى مشغّل IE في جميع الحالات.

كان أحد الحلول لهذه المشكلة هو تشغيل مشغّل IE في منفّذ أحادي الخيط (single-threaded executor) وتسلسل كل الوصول عبر Futures في خادم التطبيقات، وقد كان هذا هو التصميم الذي اخترناه لفترة. غير أنه بدا من غير الإنصاف أن ندفع هذا التعقيد إلى شيفرة الاستدعاء، ومن السهل جدًّا تخيّل حالات يسيء فيها الناس استعمال مشغّل IE من عدة خيوط عن غير قصد. فقرّرنا غرس هذا التعقيد في داخل المشغّل نفسه. وقد فعلنا ذلك بالإبقاء على نسخة IE في خيط منفصل، واستعمال واجهة PostThreadMessage البرمجية في Win32 للتواصل عبر حدود الخيط. وهكذا، عند كتابة هذا الفصل، يبدو تصميم مشغّل IE كما في الشكل 16.7.

مشغّل IE في Selenium 2.0 alpha 7

الشكل 16.7: مشغّل IE في Selenium 2.0 alpha 7

هذا ليس نوع التصميم الذي كنت سأختاره من تلقاء نفسي، لكنه يتمتع بميزة أنه يعمل ويصمد أمام الأهوال التي قد يختار مستخدمونا إلحاقها به.

أحد عيوب هذا التصميم أنه قد يصعب تحديد ما إذا كانت نسخة IE قد أقفلت نفسها إغلاقًا تامًّا. وقد يحدث هذا إذا فُتح نافذة حوار مشروطة (modal) بينما نتعامل مع DOM، أو إذا وقع فشل ذريع في الطرف الآخر من حدود الخيط. ومن ثمّ فإن لكل رسالة خيط نرسلها مهلة مرتبطة بها، وقد ضُبطت على مدة لا تقلّ عن دقيقتين، وهو مقدّر سخيًا إلى حدٍّ كبير. ومن ردود الفعل الواردة في قوائم البريد، يتبيّن أن هذا الافتراض صحيح عمومًا لكنه ليس صحيحًا دائمًا، وقد تجعل الإصدارات اللاحقة من مشغّل IE المهلة قابلة للضبط.

ومن عيب آخر أن تنقيح أخطاء الداخل قد يكون عميق المشكلة، إذ يتطلب مزيجًا من السرعة (بعد كل شيء، أمامك دقيقتان لتتبّع الشيفرة إلى أبعد ما يمكن)، والاستخدام المتعذّر لنقاط التوقف (break points)، وفهم مسار الشيفرة المتوقّع الذي سيُسلك عبر حدود الخيط. ولا حاجة إلى القول إن المشروع المفتوح المصدر، وفيه مشكلات مثيرة أخرى كثيرة يجب حلّها، لا تكاد توجد فيه رغبة في هذا النوع من العمل القذر. وهذا يقلّل «معامل الحافلة» للنظام إلى حدٍّ كبير، وكأمين على المشروع، فإن هذا ما يقلقني.

ولمعالجة ذلك، يُنقل المزيد والمزيد من مشغّل IE ليُركَب فوق الذرّات نفسها التي يستند إليها مشغّل Firefox وSelenium Core. ونفعل ذلك بتصريف كل ذرّة نزمع استعمالها وتجهيزها في ملف ترويسة C++، مع كشف كل دالة كثابت. وعند وقت التشغيل، نجهّز جافاسكربت المراد تنفيذه من هذه الثوابت. وهذا الأسلوب يعني أننا نستطيع تطوير واختبار نسبة معقولة من شيفرة مشغّل IE من دون الحاجة إلى مُصرِّف C، مما يسمح لكثير أكبر من الناس بالمساهمة في اكتشاف العلل وحلّها. وفي النهاية، الهدف هو أن نُبقي واجهات التفاعل وحدها في الشيفرة الأصلية، وأن نعتمد على الذرّات قدر الإمكان.

وهناك مقاربة أخرى نستكشفها الآن، وهي إعادة كتابة مشغّل IE ليعتمد خادم HTTP خفيف الوزن، مما يتيح لنا معاملته بوصفه WebDriver بعيدًا. وإذا حدث ذلك، يمكننا إزالة كثير من التعقيد الذي أدخلته حدود الخيط، فيقلّص ذلك كثيرًا شيفرة المصدر المطلوبة، ويجعل مسار التنفيذ أسهل في التتبّع إلى حدٍّ كبير.

16.8. Selenium RC

من غير الممكن دائمًا الارتباط عن قرب بمتصفح بعينه. وفي تلك الحالات، يعود WebDriver إلى الآلية الأصلية المستعملة في Selenium. وهذا يعني استعمال Selenium Core، وهو إطار مكتوب بجافاسكربت خالصة، وهو ما يُدخل عددًا من العيوب لأنه يُنفَّذ تمامًا في سياق صندوق جافاسكربت الرملي. ومن منظور مستخدمي واجهات WebDriver البرمجية، يعني هذا أن قائمة المتصفحات المدعومة تنقسم إلى طبقات، بعضها متكامل عن قرب ويوفّر تحكمًا استثنائيًّا، وبعضها يُقاد عبر جافاسكربت ويوفّر المستوى نفسه من التحكم الذي كان يوفّره Selenium RC الأصلي.

من المفاهيمي، التصميم المستعمل بسيط إلى حدٍّ كبير، كما ترى في الشكل 16.8.

مخطط بنية Selenium RC

الشكل 16.8: مخطط بنية Selenium RC

وكما ترى، هناك ثلاث قطع متحرّكة هنا: شيفرة العميل، والخادم الوسيط، وشيفرة جافاسكربت الخاصة بـ Selenium Core التي تعمل داخل المتصفح. فجانب العميل ليس سوى عميل HTTP يشفّر الأوامر إلى قطعة موجودة في جانب الخادم. وعلى خلاف WebDriver البعيد، هناك نقطة نهاية واحدة فقط، وفعل HTTP المستعمل لا يكاد يهم. ويرجع ذلك جزئيًّا إلى أن بروتوكول Selenium RC مشتق من الواجهة البرمجية الجدولية التي يقدّمها Selenium Core، وهذا يعني أن الواجهة كلها يمكن وصفها بوسائط استعلام URL الثلاثة.

عندما يبدأ العميل جلسة جديدة، يبحث خادم Selenium عن «سلسلة المتصفح» (browser string) المطلوبة لتحديد مشغّل متصفح مطابق. والمشغّل هو المسؤول عن تهيئة نسخة المتصفح المطلوبة وإطلاقها. وفي حالة Firefox، الأمر ببساطة هو توسيع ملف تهيئة (profile) جاهز مع عدد قليل من الإضافات المثبّتة مسبقًا (إضافة للتعامل مع أمر «quit»، وأخرى لنمذجة «document.readyState» التي لم تكن موجودة في إصدارات Firefox الأقدم التي ما زلنا ندعمها). وأهم جزء من التهيئة المنفَّذة هو أن الخادم يهيّئ نفسه بوصفه وسيطًا للمتصفح، ما يعني أن بعض الطلبات على الأقل (تلك الموجّهة إلى ‎"/selenium-server"‎) تمرّ عبره. ويمكن أن يعمل Selenium RC في أحد ثلاثة أوضاع: التحكم في إطار داخل نافذة واحدة (وضع «singlewindow»)، أو التحكم في نافذة منفصلة مع التحكم في التطبيق قيد الاختبار في نافذة ثانية (وضع «multiwindow»)، أو حقن نفسه في الصفحة عبر وسيط (وضع «proxyinjection»). وبحسب وضع التشغيل، قد تمرّ جميع الطلبات عبر الوسيط.

ومتى جُهِّز المتصفح، يُطلَق، مع عنوان URL ابتدائي يشير إلى صفحة مستضافة على خادم Selenium—RemoteRunner.html. وهذه الصفحة مسؤولة عن تهيئة الانطلاق (bootstrapping) للعملية عبر تحميل جميع ملفات جافاسكربت المطلوبة لـ Selenium Core. وبمجرد اكتمال ذلك، تُستدعى الدالة «runSeleniumTest». وهي تستعمل الانعكاس (reflection) على كائن Selenium لتهيئة قائمة الأوامر المتاحة قبل بدء حلقة معالجة الأوامر الرئيسة.

تفتح جافاسكربت العاملة في المتصفح طلب XMLHttpRequest إلى عنوان URL على خادم الانتظار (‎/selenium-server/driver‎)، معتمدةً على أن الخادم يمرّر جميع الطلبات كي يضمن أن يذهب الطلب فعلًا إلى موضع صالح. وبدلًا من تقديم طلب، فإن أول ما تفعله هو إرسال الاستجابة عن الأمر الذي نُفّذ سابقًا، أو «OK» في حالة ما إذا كان المتصفح في طور الإقلاع فقط. ثم يبقي الخادم الطلب مفتوحًا حتى يصل أمر جديد من اختبار المستخدم عبر العميل، وعندها يُرسَل كاستجابة لجافاسكربت المنتظرة. وقد سُمّيت هذه الآلية في الأصل «استجابة/طلب» (Response/Request)، أما اليوم فمن المرجّح أن تُسمّى «Comet مع استطلاع طويل عبر AJAX».

ولماذا يعمل RC بهذه الطريقة؟ يحتاج الخادم إلى أن يهيّأ نفسه بوصفه وسيطًا كي يتمكّن من اعتراض أي طلبات تُرسَل إليه، من دون أن تقع جافاسكربت المستدعية في مخالفة سياسة «المضيف الأصل الواحد» (Single Host Origin)، التي تنص على أن الموارد التي تأتي من الخادم نفسه الذي قُدّم السكربت منه هي وحدها التي يمكن طلبها عبر جافاسكربت. وهذا ضابط أمني، لكنه من منظور مطوّر إطار لأتمتة المتصفحات محبط إلى حدٍّ كبير، ويتطلّب حيلة من هذا النوع.

وسبب إجراء نداء XmlHttpRequest إلى الخادم ذو شقّين. أولهما، وهو الأهم، أنه ما لم تتوفر WebSockets، وهي جزء من HTML5، في غالبية المتصفحات، فلا سبيل إلى تشغيل عملية خادم بشكل موثوق داخل متصفح. وهذا يعني أن الخادم كان لا بدّ أن يقيم في مكان آخر. وثانيهما، أن XMLHttpRequest تستدعي دالة استدعاء الاستجابة بصورة غير متزامنة، ما يعني أنه أثناء انتظارنا للأمر التالي لا يتأثر التنفيذ المعتاد للمتصفح. أما الطريقتان الأخريان للانتظار حتى الأمر التالي فكانا سيفترضان، أو الاستطلاع على الخادم على أساس منتظم لمعرفة ما إذا كان هناك أمر آخر لتنفيذه، وهو ما كان سيضيف زمن استجابة (latency) إلى اختبارات المستخدمين، أو إدخال جافاسكربت في حلقة مفرغة (busy loop) كانت سترفع استهلاك المعالج إلى السماء وتمنع جافاسكربت أخرى من التنفيذ داخل المتصفح (لأن هناك خيط جافاسكربت واحدًا فقط يُنفَّذ في سياق نافذة واحدة).

وداخل Selenium Core توجد قطعتان رئيسيتان متحرّكتان. الأولى هي الكائن الرئيس selenium، الذي يعمل كـ«مُضيف» (host) لجميع الأوامر المتاحة ويحاكي الواجهة البرمجية المقدَّمة للمستخدمين. والقطعة الثانية هي browserbot. ويُستعمل هذا من كائن Selenium لإخفاء الفروق الموجودة في كل متصفح وتقديم رؤية مثالية للوظائف الشائعة في المتصفحات. وهذا يعني أن الدوال في selenium أوضح وأسهل في الصيانة، في حين أن browserbot شديد التركيز.

وتجري حاليًا عملية تحويل Core تدريجيًا إلى الإفادة من ذرّات الأتمتة. وسيحتاج selenium وbrowserbot على الأرجح إلى البقاء، لأن هناك قدرًا كبيرًا من الشيفرة يعتمد على الواجهات التي يكشفانها، لكن من المتوقّع أنهما سيتحوّلان في النهاية إلى أصناف قشرة (shell classes) تفوّض العمل إلى الذرّات في أقرب وقت ممكن.

16.9. نظرة إلى الوراء

بناء إطار لأتمتة المتصفحات يشبه إلى حدٍّ كبير طلاء غرفة؛ فعند النظر الأول يبدو الأمر شيئًا ينبغي أن يكون سهلًا جدًّا. لا يحتاج الأمر سوى طبقات قليلة من الطلاء، وينتهي العمل. والمشكلة أن كلما اقتربت، تطلع مهام وتفاصيل أكثر، ويطول التنفيذ. وفي حالة الغرفة، فالأشياء من نوع التفاف حول وحدات الإضاءة والمشعات وشرائط القاعدة الأرضية هي ما يبدأ في ابتلاع الوقت. أما في إطار لأتمتة المتصفحات، فالمشاكل والقدرات المتباينة بين المتصفحات هي ما يجعل الوضع أكثر تعقيدًا. وقد عبّر عن هذه الحالة المتطرفة Daniel Wagner-Hall وهو جالس بجانبي أثناء عملنا على مشغّل Chrome؛ فقد وضع يديه على المكتب وغرم وهو يقول: «كل الحالات حدّية!» (edge cases). وكان من المفيد لو استطعت العودة وقلت لنفسي هذا، وأن المشروع سيستغرق وقتًا أطول بكثير مما توقعت.

ولا أستطيع كذلك إلا أن أتساءل أين كان المشروع لو أننا حدّدنا الحاجة إلى طبقة مثل ذرّات الأتمتة وتصرّفنا بناءً عليها في وقت أقرب مما فعلنا. ومن المؤكد أن ذلك كان سيجعل بعض التحديات التي واجهها المشروع، داخلية كانت أم خارجية، تقنية كانت أم اجتماعية، أسهل في المعالجة. فقد نُفِّذ Core وRC في مجموعة مركَّزة من اللغات—في جوهرها جافاسكربت وJava فحسب. وكان Jason Huggins يصف هذا بأنه منح Selenium مستوى من «قابلية الاختراق» (hackability)، وهو ما جعل من اليسير على الناس الانخراط في المشروع. ولم يتوفّر هذا المستوى من قابلية الاختراق على نطاق واسع في WebDriver إلا مع الذرّات. وفي مقابل ذلك، فإن سبب اتساع نطاق تطبيق الذرّات إلى هذا الحد هو مُصرِّف Closure، الذي تبنّيناه فور إتاحته تقريبًا كمشروع مفتوح المصدر.

ومن المفيد أيضًا أن نتأمل الأشياء التي أصبنا فيها. فالقرار المتّخذ في كتابة الإطار من منظور المستخدم هو قرار ما زلت أعتقد أنه صحيح. وقد أثمر ذلك في البداية، حين أبرز المستخدمون الأوائل مواضع التحسين، مما سمح بزيادة نفع الأداة بسرعة. ثم إن WebDriver، مع توسّع المهام المطلوبة منه وتشدّدها وارتفاع عدد المطوّرين الذين يستعملونه، يعني إضافة واجهات برمجية جديدة بعناية واهتمام، مع إبقاء تركيز المشروع مضبوطًا. وإذا أخذنا في الحسبان نطاق ما نحاول فعله، فإن هذا التركيز حيوي.

أما الربط عن قرب بالمتصفح، فهو أمر صحيح وخاطئ في آنٍ واحد. فهو صحيح، لأنه أتاح لنا محاكاة المستخدم بدقة فائقة، والتحكم في المتصفح تحكمًا ممتازًا. وهو خاطئ لأن هذا الأسلوب متطلّب تقنيًا إلى حدٍّ كبير، ولا سيما عند العثور على نقطة الارتباط الضرورية داخل المتصفح. والتطوّر المستمر لمشغّل IE نموذج على ذلك، وعلى الرغم من أنه غير مغطّى هنا، فإن الأمر نفسه ينطبق على مشغّل Chrome الذي له تاريخ طويل مليء بالأحداث. وسنحتاج في وقتٍ ما إلى إيجاد طريقة للتعامل مع هذا التعقيد.

16.10. النظر إلى الأمام

هناك دائمًا متصفحات لا يستطيع WebDriver التكامل معها عن قرب، لذا هناك دائمًا حاجة إلى Selenium Core. ويُجري العمل على نقلها من تصميمها التقليدي الحالي إلى تصميم أكثر وحداتية قائمًا على Closure Library نفسها التي تستعملها الذرّات. ونتوقّع أيضًا أن نُدرج الذرّات بعمق أكبر داخل تطبيقات WebDriver القائمة.

كان أحد أهداف WebDriver الأولى أن يعمل لبنة بناء (building block) لواجهات وأدوات أخرى. وبالطبع، لا يعيش Selenium في فراغ: فهناك الكثير من أدوات أتمتة المتصفحات الأخرى مفتوحة المصدر. إحداها هي Watir (Web Application Testing In Ruby)، وقد بدأ العمل، بمحاولة مشتركة بين مطوّري Selenium وWatir، على وضع واجهة Watir البرمجية فوق نواة WebDriver. ونحن متحمّسون للعمل مع مشاريع أخرى أيضًا، فتشغيل كل المتصفحات الموجودة بنجاح عمل شاق. سيكون من الجميل أن تتوفّر نواة متينة يستطيع غيرنا البناء عليها. وأملنا أن تكون تلك النواة هي WebDriver.

تقدّم Opera Software لمحة عن هذا المستقبل، إذ نفّذت بشكل مستقل واجهة WebDriver البرمجية، مستعملةً مجموعات اختبار WebDriver للتحقق من سلوك شيفرتها، وستطلق مشغّل OperaDriver الخاص بها. كما يعمل أعضاء في فريق Selenium مع أعضاء في فريق Chromium لإضافة نقاط ارتباط (hooks) أفضل ودعم لـ WebDriver في ذلك المتصفح، ومن ثمّ في Chrome أيضًا. ولدينا علاقة ودّية مع Mozilla، التي ساهمت بشيفرة لـ FirefoxDriver، ومع مطوّري محاكي المتصفح Java الشهير HtmlUnit.

تَرى إحدى رؤى المستقبل أن يستمر هذا الاتجاه، مع كشف نقاط الأتمتة (automation hooks) بطريقة موحّدة عبر متصفحات مختلفة كثيرة. والمزايا لمن يحبّ كتابة اختبارات لتطبيقات الويب واضحة، والمزايا لمصنّعي المتصفحات واضحة أيضًا. فمثلًا، نظرًا للكلفة النسبية المرتفعة للاختبار اليدوي، تعتمد كثير من المشاريع الكبيرة اعتمادًا شديدًا على الاختبار المؤتمت. فإذا لم يكن من الممكن، أو حتى إذا كان «فقط» متعبًا إلى حدٍّ كبير، اختبار ما بمتصفح بعينه، فإن الاختبارات لا تُشغَّل له أصلًا، ما ينعكس سلبًا على مدى جودة عمل التطبيقات المعقّدة مع ذلك المتصفح. أما ما إذا كانت نقاط الأتمتة هذه ستكون قائمة على WebDriver فهو سؤال مفتوح، ولكننا نأمل!

ستكون السنوات القليلة قادمة مثيرة للاهتمام إلى حدٍّ كبير. وبما أننا مشروع مفتوح المصدر، فنحن نرحّب بك بأن تنضمّ إلينا في هذه الرحلة على http://selenium.googlecode.com/.

الحواشي

  1. http://fit.c2.com
  2. هذا قريب جدًّا من FIT، ويشرح James Shore، أحد منسّقي ذلك المشروع، بعض عيوبه في http://jamesshore.com/Blog/The-Problems-With-Acceptance-Testing.html.
  3. فمثلًا، يُعيد الخادم البعيد لقطة شاشة مرمّزة بـ base64 مع كل استثناء كمعين على التنقيح، لكن مشغّل Firefox لا يفعل ذلك.
  4. أي أنها تُعيد النتيجة نفسها دائمًا.

  1. http://fit.c2.com ↩︎

  2. هذا قريب جدًّا من FIT، ويشرح James Shore، أحد منسّقي ذلك المشروع، بعض عيوبه في http://jamesshore.com/Blog/The-Problems-With-Acceptance-Testing.html. ↩︎

  3. فمثلًا، يُعيد الخادم البعيد لقطة شاشة مرمّزة بـ base64 مع كل استثناء كمعين على التنقيح، لكن مشغّل Firefox لا يفعل ذلك. ↩︎

  4. أي أنها تُعيد النتيجة نفسها دائمًا. ↩︎