{"id":"l-rn-5-2","courseId":"react-native-apps","lang":"ar","kind":"lesson","summary":"اضبط Jest مع الـ preset المسمّى jest-expo، واكتب unit tests لمنطق Trailhead النقي، واختبر الـ components بالطريقة التي يستخدمها بها المستخدمون عبر React Native Testing Library، واصنع mock للوحدات الأصلية عند الحدود.","html":"<p>دفع كل درس في هذه الدورة المنطقَ خارج الـ components إلى دوال نقية صغيرة: <code dir=\"ltr\">parseDistanceKm</code>، و<code dir=\"ltr\">hikesReducer</code>، و<code dir=\"ltr\">validateHikeForm</code>، و<code dir=\"ltr\">permissionAction</code>، و<code dir=\"ltr\">trackDistanceKm</code>. ولم يكن ذلك من أجل الترتيب فقط. على الجوّال، التحقّق من تغيير يدويًا يعني إعادة البناء، والتشغيل، والتنقّل ثلاث شاشات إلى الداخل، والكتابة على لوحة مفاتيح محاكٍ. واختبار يتحقّق من الشيء نفسه في 20 جزءًا من الألف من الثانية، في كل مرة تحفظ فيها، أفضل مقايضة ستعقدها.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">إعداد Jest</h2>\n<p>يضبط الـ preset الخاص بـ Expo، <code dir=\"ltr\">jest-expo</code>، أداة Jest لـ React Native: يحوّل كود TypeScript وJSX، و<strong>يصنع mock للجانب الأصلي من وحدات Expo</strong>، فتعمل الاختبارات في Node دون جهاز. ثبّته مع React Native Testing Library:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> expo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> jest-expo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> jest</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @types/jest</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --dev</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> expo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @testing-library/react-native</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --dev</span></span></code></pre></div>\n<p>ثم وجّه Jest إلى الـ preset في <code dir=\"ltr\">package.json</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">package.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"scripts\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"test\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"jest --watchAll\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  },</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"jest\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"preset\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"jest-expo\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>يذكر دليل Expo أيضًا مُدخلًا لـ <code dir=\"ltr\">transformIgnorePatterns</code> للحزم التي تُشحن بكود غير محوَّل؛ انسخه من هناك حين يفشل استيراد برسالة “SyntaxError: Cannot use import statement outside a module”. وضع الاختبارات في مجلّدات <code dir=\"ltr\">__tests__</code> بجوار الكود الذي تغطّيه.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">ابدأ بالمنطق النقي</h2>\n<p>لأداة التحليل من القسم 1 قواعد صغيرة كثيرة. وجدول من الحالات يوثّقها أفضل من النثر:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/lib/__tests__/parseDistance.test.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { parseDistanceKm } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../parseDistance'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">test.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">each</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8.4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8.4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8,4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8.4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">' 12 km'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'abc'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'1.2.3'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'parseDistanceKm(%j) is %p'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">expected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parseDistanceKm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expected);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>لاحظ أن حالات الاختبار هي بالضبط المدخلات الغريبة التي قلقت بشأنها حين كتبت الدالة: الفاصلة العشرية، والوحدة اللاحقة، والصفر. كل بلاغ عن خطأ يصبح صفًّا إضافيًا في جدول كهذا، يُكتب قبل الإصلاح، فلا يستطيع الخطأ نفسه العودة بصمت أبدًا.</p>\n<p>والـ reducers تُعامَل بالطريقة نفسها: ابنِ state، وأرسل action، وتحقّق من النتيجة، وتحقّق أن المدخل لم يُعدَّل. هذه الاختبارات سريعة ومستقرّة ولا تحتاج إلى mocks، ولهذا يجب أن تشكّل معظم مجموعة اختباراتك.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">اختبار الـ components كما يستخدمها المستخدم</h2>\n<p>ترسم React Native Testing Library الـ components في الذاكرة، وتتيح لك الاستعلام عنها كما يدركها الناس: بالـ <strong>role</strong> والـ <strong>label</strong> والنص. هذا اختبار للـ component المسمّى <code dir=\"ltr\">QuickLog</code> من القسم 1:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/components/__tests__/QuickLog.test.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { render, screen, userEvent } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@testing-library/react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { QuickLog } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../QuickLog'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'saves a valid distance'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> user</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> userEvent.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setup</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">QuickLog</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> />);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> user.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByLabelText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Distance (km)'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8,4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> user.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">press</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Save'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }));</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(onSave).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8.4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'keeps Save disabled until the distance is valid'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> user</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> userEvent.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setup</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">QuickLog</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> />);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> user.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByLabelText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Distance (km)'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'far'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Save'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> })).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBeDisabled</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>لكي يعثر <code dir=\"ltr\">getByLabelText</code> على الحقل، يحتاج الـ <code dir=\"ltr\">TextInput</code> إلى <code dir=\"ltr\">accessibilityLabel=&quot;Distance (km)&quot;</code>. وهذا بيت القصيد: الاختبار الذي يستعلم بالـ role والـ label يفشل حين تكون الشاشة غير قابلة للوصول، فتصبح اختباراتك فحصًا لإمكانية الوصول أيضًا. يحاكي <code dir=\"ltr\">userEvent</code> التفاعلات الحقيقية (التركيز، وكتابة كل حرف، والضغط والإفلات)، وفي الإصدارات الحالية من المكتبة يكون كلٌّ من <code dir=\"ltr\">render</code> والتفاعلات <strong>غير متزامن</strong>، فانتظرها بـ await. والـ matchers مثل <code dir=\"ltr\">toBeOnTheScreen()</code> و<code dir=\"ltr\">toHaveTextContent()</code> و<code dir=\"ltr\">toBeDisabled()</code> تأتي مع المكتبة.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>اختبار تفاصيل التنفيذ</span></p>\n<p>كتابة assertion بأن component استدعى <code dir=\"ltr\">setState</code> مرتين، أو أخذ snapshot لشجرته كلها، يربط الاختبارات بطريقة كتابة الكود لا بما يفعله. أعِد هيكلة الـ component فتفشل الاختبارات مع أن التطبيق يعمل؛ واكسر المنطق فيسجّل الـ snapshot المخرجات المعطوبة بسرور. اكتب الـ assertions على ما يستطيع المستخدم ملاحظته: النص على الشاشة، وحالة الزر، وcallback استُدعي بالقيمة الصحيحة.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">صنع mock للجهاز عند الحدود</h2>\n<p>يضع <code dir=\"ltr\">jest-expo</code> بدائل للوحدات الأصلية، لكن هذه البدائل لا تُرجع شيئًا مفيدًا. وحين يعتمد component على إجابة وحدة ما، اصنع لها mock بالإجابة التي تريد اختبارها:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> *</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Location </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-location'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mock</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'expo-location'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  getForegroundPermissionsAsync: jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  requestForegroundPermissionsAsync: jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}));</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'offers Settings after a permanent denial'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mocked</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(Location.getForegroundPermissionsAsync).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockResolvedValue</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'denied'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, granted: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, canAskAgain: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, expires: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'never'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Location</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">LocationPermissionResponse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TrackScreen</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> />);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">findByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Open Settings'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> })).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBeOnTheScreen</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>استعلامات <code dir=\"ltr\">findBy…</code> تنتظر ظهور العنصر، وهذا يناسب الشاشات التي تحمّل بشكل غير متزامن. اصنع الـ mocks عند حافة تطبيقك (الوحدات الأصلية، و<code dir=\"ltr\">fetch</code>)، لا للـ components الخاصة بك، ليكون الكود قيد الاختبار هو الكود الذي تطلقه. والشاشات التي تستخدم hooks من Expo Router يمكن رسمها باستخدام <code dir=\"ltr\">renderRouter</code> من <code dir=\"ltr\">expo-router/testing-library</code>، الذي يجهّز router في الذاكرة بالمسارات التي تعطيه إياها.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">أين تقع اختبارات end-to-end</h2>\n<p>لا تستطيع اختبارات الوحدات واختبارات الـ components التقاط build أصلي معطوب، أو نافذة إذن لا تظهر أبدًا، أو لوحة مفاتيح تغطّي زرّ Save. بضعة اختبارات <strong>end-to-end</strong> على builds حقيقية تغطّي ذلك: أدوات مثل Maestro (التي يستطيع EAS تشغيلها في السحابة) أو Detox تقود التطبيق الفعلي على محاكٍ. احتفظ بها للمسارات التي سيؤلم كسرها أكثر من غيرها، مثل تسجيل الدخول وتسجيل رحلة، ودع الاختبارات السريعة تغطّي كل ما عدا ذلك.</p>\n<p>التالي: تحويل التطبيق المختبَر إلى builds تستطيع الهواتف الحقيقية تثبيتها.</p>\n","toc":[{"id":"sec-1","text":"إعداد Jest","level":2},{"id":"sec-2","text":"ابدأ بالمنطق النقي","level":2},{"id":"sec-3","text":"اختبار الـ components كما يستخدمها المستخدم","level":2},{"id":"sec-4","text":"صنع mock للجهاز عند الحدود","level":2},{"id":"sec-5","text":"أين تقع اختبارات end-to-end","level":2}],"takeaways":["يضبط الـ preset المسمّى jest-expo أداة Jest لـ React Native، ويصنع mock للجانب الأصلي من وحدات Expo، فتعمل الاختبارات في Node دون جهاز.","الدوال النقية مثل أدوات التحليل والـ reducers ودوال التحقّق تعطي أكبر قدر من الثقة مقابل كل دقيقة من كتابة الاختبارات؛ فاختبرها أولًا.","ترسم React Native Testing Library الـ components وتتيح لك العثور على العناصر بالدور (role) والتسمية (label) والنص، كما يفعل المستخدمون وقارئات الشاشة.","في الإصدارات الحالية من المكتبة، <code dir=\"ltr\">render</code> وتفاعلات <code dir=\"ltr\">userEvent</code> غير متزامنة؛ فانتظرها بـ await.","اصنع mock للوحدات الأصلية مثل expo-location عند الحدود باستخدام <code dir=\"ltr\">jest.mock</code>، واكتب الـ assertions على ما يراه المستخدم."],"further":[{"title":"Unit testing with Jest","url":"https://docs.expo.dev/develop/unit-testing/"},{"title":"React Native Testing Library","url":"https://testing-library.com/docs/react-native-testing-library/intro/"},{"title":"Jest mock functions","url":"https://jestjs.io/docs/mock-functions"}],"quiz":[{"q":"<p>أيّ اختبار في Trailhead يعطي أكبر قدر من الثقة بأقل جهد؟</p>","options":[{"text":"لقطة (snapshot) لشاشة سجلّ الرحلات كلها.","why":"اللقطات تنكسر مع كل تغيير غير ضار في البنية، وتنجح بسرور حين يكون المنطق خاطئًا. ويتعلّم الناس تحديثها دون قراءتها."},{"text":"اختبار end-to-end ينقر عبر التطبيق على محاكٍ للتحقّق من أن “8,4” تُحلَّل.","why":"اختبارات end-to-end قيّمة للمسارات الحرجة، لكنها بطيئة وغير مستقرّة للتحقّق من قاعدة تحليل واحدة."},{"text":"جدول من المدخلات والمخرجات المتوقّعة لـ <code dir=\"ltr\">parseDistanceKm</code> و<code dir=\"ltr\">hikesReducer</code>.","why":"صحيح. الدوال النقية تعمل في أجزاء من الثانية، ولا تحتاج إلى mocks، وتغطّي القواعد التي يعتمد عليها المستخدمون."}],"answer":2},{"q":"<p>كيف يجب أن يعثر اختبار component على زرّ Save في Trailhead؟</p>","options":[{"text":"<code dir=\"ltr\">screen.getByRole('button', { name: 'Save' })</code>","why":"صحيح. يعثر على ما يراه المستخدمون وقارئات الشاشة، ويفشل إن نسيت دور الزر، وهذا خطأ حقيقي في إمكانية الوصول."},{"text":"<code dir=\"ltr\">screen.getByTestId('save-btn-3')</code>","why":"معرّفات الاختبار تعمل، لكنها تختبر تفصيلة تنفيذية وتفوّت مشكلات إمكانية الوصول. استخدمها كملاذ أخير."},{"text":"بقراءة الابن الثالث للـ View الجذري في الشجرة المرسومة.","why":"هذا ينكسر كلما تغيّر التخطيط، حتى حين يظل التطبيق يعمل."}],"answer":0},{"q":"<p>يستدعي component الدالة <code dir=\"ltr\">Location.getForegroundPermissionsAsync()</code>. في اختبار Jest تُرجع <code dir=\"ltr\">undefined</code>، فيعرض الـ component مؤشّر تحميل إلى الأبد. ما الإصلاح الصحيح؟</p>","options":[{"text":"شغّل الاختبار على محاكٍ لتكون الوحدة الحقيقية متاحة.","why":"يعمل Jest في Node؛ لا يوجد جهاز. الحدود تحتاج إلى mock، لا إلى محاكٍ."},{"text":"احذف التحقّق من الإذن من الـ component حين تكون <code dir=\"ltr\">process.env.NODE_ENV</code> تساوي <code dir=\"ltr\">test</code>.","why":"التفرّعات الخاصة بالاختبار في كود الإنتاج تعني أنك لم تعد تختبر الكود الذي تطلقه."},{"text":"غلّف الاستدعاء بـ <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code> لينجح الاختبار.","why":"لا شيء يرمي خطأ هنا؛ الـ mock لا يُرجع شيئًا فحسب. والـ <code dir=\"ltr\">catch</code> لا يغيّر شيئًا ويُخفي الأخطاء الحقيقية."},{"text":"اصنع mock للوحدة باستخدام <code dir=\"ltr\">jest.mock('expo-location', …)</code> بحيث تُحَلّ الدالة باستجابة إذن تختارها أنت.","why":"صحيح. أنت تتحكّم في الحدود، وتستطيع اختبار كل حالة (الممنوح، والمرفوض، والمرفوض نهائيًا)."}],"answer":3}],"exercise":{"kind":"fill","language":"tsx","template":"import { render, screen, userEvent } from '@testing-library/react-native';\nimport { HikeForm } from '../HikeForm';\n\ntest('shows an error, then saves once the distance is fixed', async () => {\n  const onSave = jest.fn();\n  const user = userEvent.{{0}}();\n  await {{1}}(<HikeForm onSave={onSave} />);\n\n  const distance = screen.getByLabelText('Distance (km)');\n  await user.type(screen.getByLabelText('Name'), 'Ridge Loop');\n  await user.type(distance, 'far');\n  await user.{{2}}(screen.getByRole('{{3}}', { name: 'Save' }));\n\n  expect(screen.getByText('Enter a distance in km, like 8.4.')).{{4}}();\n  expect(onSave).not.toHaveBeenCalled();\n\n  await user.clear(distance);\n  await user.type(distance, '8,4');\n  await user.press(screen.getByRole('button', { name: 'Save' }));\n\n  expect(onSave).{{5}}(expect.objectContaining({ name: 'Ridge Loop', distanceKm: 8.4 }));\n});","blanks":[{"answers":["setup"]},{"answers":["render"]},{"answers":["press"]},{"answers":["button"]},{"answers":["toBeOnTheScreen","toBeVisible"]},{"answers":["toHaveBeenCalledWith","toHaveBeenLastCalledWith"]}],"title":"أكمل اختبار النموذج","prompt":"<p>املأ الفراغات في اختبار React Native Testing Library هذا للـ component المسمّى <code dir=\"ltr\">HikeForm</code> في Trailhead. يجب أن يجهّز أحداث المستخدم، ويرسم النموذج، ويضغط زرّ Save الذي يُعثر عليه <strong>بدوره (role)</strong>، ويتحقّق من ظهور خطأ المسافة على الشاشة، ثم يتحقّق أخيرًا من أن <code dir=\"ltr\">onSave</code> استقبلت الرحلة الصحيحة.</p>\n","hints":["يحتاج <code dir=\"ltr\">userEvent</code> إلى استدعاء لمرة واحدة يُرجع الكائن <code dir=\"ltr\">user</code>، ويُركَّب الـ component بالدالة الرئيسية في المكتبة.","المستخدمون يضغطون الأزرار؛ والاستعلام يبحث عن دور إمكانية الوصول الذي يحصل عليه Pressable من <code dir=\"ltr\">accessibilityRole</code>.","أحد الـ matchers يتحقّق أن العنصر على الشاشة؛ والآخر يتحقّق أن دالة mock استقبلت وسائط محدّدة."],"explanation":"<p>ينشئ <code dir=\"ltr\">userEvent.setup()</code> مستخدمًا يكتب ويضغط كإنسان حقيقي، و<code dir=\"ltr\">await render(...)</code> يركّب النموذج (والاثنان غير متزامنين في الإصدارات الحالية من المكتبة). ويعثر <code dir=\"ltr\">getByRole('button', { name: 'Save' })</code> على الزر بالطريقة التي يعثر بها قارئ الشاشة، فيحرس الاختبار إمكانية الوصول أيضًا. ويؤكّد <code dir=\"ltr\">toBeOnTheScreen()</code> أن رسالة التحقّق رُسمت، ويتحقّق <code dir=\"ltr\">toHaveBeenCalledWith(expect.objectContaining(...))</code> من الرحلة المحفوظة دون الاعتماد على حقول لا تهمّ الاختبار، مثل المعرّف المولَّد.</p>\n","checkLabels":{}},"words":591,"readMinutes":3}