{"id":"l-ds-4-4","courseId":"design-systems","lang":"ar","kind":"lesson","summary":"حوّل الـ stories في Storybook إلى مجموعة اختبارات لكل حالة من حالات المكوّنات، والتقط التغييرات البصرية غير المقصودة بمقارنة لقطات الشاشة عبر الـ themes، ومرّر الإصدارات عبر خط CI يفحص قبل أن ينشر.","html":"<p>غيّر إصدار 4.2.0 في Northwind سطرًا واحدًا: الـ token الخاص بـ <code dir=\"ltr\">line-height</code> لنص المتن، من 1.5 إلى 1.45. بدا كل شيء سليمًا في كل story فحصناها. لكنه دفع أيضًا آخر صف في جدول المسارات في تطبيق الـ dispatch إلى ما تحت حافة الشاشة، على أشيع دقّة شاشة للحواسيب المحمولة، في الكثافة المضغوطة، في الوضع الداكن. لم ينظر أحد إلى تلك التركيبة. ومنذ ذلك الحين صارت القاعدة بسيطة: الآلات تنظر إلى كل تركيبة؛ والبشر ينظرون إلى الفروقات.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الـ stories حالات اختبار</h2>\n<p>الـ story في Storybook تعرض مكوّنًا في حالة واحدة بـ props محدّدة. كتبت stories للتوثيق في القسم السابق؛ والـ stories نفسها هي العمود الفقري للاختبار:</p>\n<ul>\n<li><strong>العرض:</strong> يجب أن تُعرض كل story دون أخطاء.</li>\n<li><strong>التفاعل:</strong> يمكن أن تحمل الـ story دالة <code dir=\"ltr\">play</code> تنقر وتكتب وتتحقّق، مثل «الضغط على Escape يغلق الـ Dialog ويعيد التركيز إلى الـ trigger».</li>\n<li><strong>إمكانية الوصول:</strong> إضافة إمكانية الوصول (accessibility addon) تشغّل فحوص axe-core على كل story وتبلّغ عن المخالفات مثل التسميات الناقصة أو التباين الفاشل.</li>\n<li><strong>البصري:</strong> تُلتقط صورة لكل story وتُقارن بصورة مرجعية معتمدة.</li>\n</ul>\n<p>تستطيع إصدارات Storybook الحديثة تشغيل الـ stories كاختبارات عبر تكامل مع Vitest ‏(<code dir=\"ltr\">@storybook/addon-vitest</code>)، فتعمل الملفات نفسها في واجهة Storybook وفي الطرفية وفي الـ CI. وتشترط Northwind story واحدة لكل خانة في مصفوفة الحالات من درس إمكانية الوصول. هذه القاعدة تمنحك تغطية اختبارية شبه مجانية، لأن عمل تصميم الحالات أُنجز أصلًا.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">اختبار الانحدار البصري</h2>\n<p>اختبار الانحدار البصري يعرض story، ويلتقط صورة، ويقارنها بكسلًا بكسلًا بصورة مرجعية محفوظة. وإن اختلفتا بما يتجاوز حدًّا معيّنًا، يفشل الاختبار ويعرض الفرق. ثم يقرّر إنسان: إن كان التغيير مقصودًا، يقبله صورةً مرجعية جديدة؛ وإن لم يكن، يصلح الكود.</p>\n<p>تختار الفرق عادةً أحد إعدادين. خدمة مستضافة مثل Chromatic، التي يصنعها القائمون على Storybook، تعرض الـ stories في السحابة وتعطي المراجعين واجهة ويب لقبول الفروقات. أو تلتقط الصور بنفسك باستخدام Playwright:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">tests/visual.spec.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\"> { test, expect } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@playwright/test'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> stories</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'components-button--primary'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'components-button--danger'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'components-dialog--confirm'</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\"> themes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'light'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'dark'</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\"> densities</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'comfortable'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'compact'</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\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> stories) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> theme</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> themes) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> density</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> densities) {</span></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\">`${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">theme</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">density</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">goto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`/iframe.html?id=${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}&#x26;viewMode=story`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">evaluate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(([</span><span style=\"color:#953800;--shiki-dark:#FFA657\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">d</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\">          document.documentElement.dataset.theme </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">          document.documentElement.dataset.density </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> d;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        }, [theme, density]);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(page).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveScreenshot</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}-${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">theme</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}-${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">density</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}.png`</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 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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>كل story تُعرض في كل تركيبة من الـ theme والكثافة، ومعهما العلامة في مجموعة Northwind الحقيقية، وهكذا كان سيُلتقط انحدار ارتفاع السطر. والدالة <code dir=\"ltr\">toHaveScreenshot</code> في Playwright تعطّل حركات CSS افتراضيًا وتعيد المحاولة حتى تتطابق لقطتان متتاليتان، فتزيل أشيع مصادر الضجيج.</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>الخطوط وتنعيم الحواف تختلف بين macOS وWindows وLinux، فالصورة المرجعية الملتقطة على حاسوب محمول لن تطابق لقطة مأخوذة في الـ CI. ولّد الصور المرجعية وقارنها في بيئة واحدة ثابتة، وعادةً حاوية الـ CI، واترك المطوّرين يحدّثونها عبر الـ CI لا محليًا.</p>\n</aside>\n<p>والاختبارات البصرية تحرس طبقات الـ tokens أيضًا. تضيف Northwind قاعدة Stylelint في مهمّة الـ CI نفسها ترفض أي إشارة إلى primitive token، مثل <code dir=\"ltr\">var(--nw-blue-600)</code>، داخل أنماط المكوّنات. ثم تؤكّد المجموعة البصرية أن الـ CSS الملتزم بالقاعدة ما زال يعرض ما اعتمده المصمّمون.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">خط الإصدار</h2>\n<p>الاختبار لا يحمي المستخدمين إلا إذا كان النشر عاجزًا عن تخطّيه. لخط الإصدار في Northwind نصفان.</p>\n<p><strong>على كل pull request:</strong> التثبيت بـ <code dir=\"ltr\">npm ci</code>، وتشغيل فحوص الـ tokens (التسمية، والمراجع، والتباين)، وفحص الأنواع، والـ lint، واختبارات الوحدات والـ stories، وفحوص إمكانية الوصول، والاختبارات البصرية، واشتراط ملف changeset حين تتغيّر حزمة. ولا يمكن دمج الـ pull request حتى تنجح كل هذه، ويوافق عضو من النواة على الفروقات البصرية.</p>\n<p><strong>على main:</strong> يجمع إجراء Changesets الـ changesets المعلّقة في pull request باسم «Version Packages» يرفع الإصدارات ويحدّث <code dir=\"ltr\">CHANGELOG.md</code>. ودمج هذا الـ pull request يشغّل خطوة النشر:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">.github/workflows/release.yml</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:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">Release</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  push</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    branches</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">main</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">jobs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  release</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    runs-on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ubuntu-latest</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    permissions</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      contents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">write</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">        # push version commits and tags</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      pull-requests</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">write</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">   # open the Version Packages pull request</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      id-token</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">write</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">        # npm trusted publishing</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    steps</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:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/checkout@v7</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/setup-node@v7</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          node-version</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm ci</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm run build</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm test</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">changesets/action@v2</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          publish-script</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm run release</span></span></code></pre></div>\n<p>النشر يحدث أخيرًا، ومن <code dir=\"ltr\">main</code> وحده، وبعد البناء والاختبارات فقط. وبعد خروج الحزم، ينشر سير العمل نفسه موقع التوثيق ونسخة Storybook، ويستورد فريق النواة الـ tokens التي صدرت في الإصدار إلى مكتبة Figma.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الإصدارات التجريبية للتغييرات الخطرة</span></p>\n<p>للإصدار الـ major، انشر إصدارات تجريبية (prereleases) مثل <code dir=\"ltr\">5.0.0-next.1</code> تحت dist-tag منفصل في npm، حتى لا يلتقطها <code dir=\"ltr\">npm install</code> افتراضيًا. فريقان أو ثلاثة متطوّعون يثبّتونها قبل بضعة أسابيع، فيجدون مشكلات الانتقال التي فاتت الـ codemods.</p>\n</aside>\n<p>في التمرين تراجع سير عمل للإصدار يمكنه نشر كود معطوب. وبهذا يكتمل قسم التسليم؛ وبعده توسّع النظام ليخدم أربعين فريقًا.</p>\n","toc":[{"id":"sec-1","text":"الـ stories حالات اختبار","level":2},{"id":"sec-2","text":"اختبار الانحدار البصري","level":2},{"id":"sec-3","text":"خط الإصدار","level":2}],"takeaways":["الـ story حالة موثّقة للمكوّن، والـ story نفسها يمكن أن تقود اختبارات التفاعل وفحوص إمكانية الوصول واللقطات البصرية.","اختبارات الانحدار البصري (visual regression) تقارن لقطات الشاشة بصور مرجعية معتمدة (baselines)، فتظهر التغييرات غير المقصودة كفروقات يقبلها إنسان أو يرفضها.","اعرض كل مكوّن في كل تركيبة من الـ themes، لأن الزوايا التي لا تنظر إليها أبدًا هي التي تنكسر.","خط الإصدار يفحص، ثم يرقّم، ثم ينشر؛ ولا يُنشر شيء من branch لم يجتز كل الفحوص."],"further":[{"title":"Visual testing (Storybook docs, official repository)","url":"https://github.com/storybookjs/storybook/blob/next/docs/writing-tests/visual-testing.mdx"},{"title":"Visual comparisons (Playwright)","url":"https://playwright.dev/docs/test-snapshots"},{"title":"Changesets GitHub Action (official repository)","url":"https://github.com/changesets/action"}],"quiz":[{"q":"<p>فشل اختبار بصري في 46 story بعد pull request لم يغيّر إلا token الحشوة الخاص بـ Dialog. ماذا يجب أن يفعل المراجع أولًا؟</p>","options":[{"text":"قبول الصور المرجعية الجديدة الـ 46 كلها، لأن تغيير الـ token مقصود.","why":"بعض الـ 46 قد تكون آثارًا جانبية غير مقصودة؛ والقبول الأعمى يحوّل الاختبار إلى ختم آلي."},{"text":"النظر في المكوّنات التي تغيّرت؛ فإن تغيّر شيء غير Dialog وما يحتويه، فالـ token مستخدم على نطاق أوسع مما كان متوقّعًا.","why":"صحيح. الفروقات دليل على المدى الحقيقي للتغيير، وهو بالضبط ما يحتاجه المراجع ليحكم."},{"text":"إعادة تشغيل الاختبارات، لأن الاختبارات البصرية غير مستقرّة عادةً.","why":"إعادة تشغيل فرق ثابت في 46 story تعطي النتيجة نفسها؛ وعدم الاستقرار يظهر كإخفاقات عشوائية متغيّرة."},{"text":"حذف الـ stories الفاشلة حتى ينجح البناء.","why":"حذف التغطية يخفي المشكلة، ويخفي معها الانحدار الحقيقي التالي."}],"answer":1},{"q":"<p>تنجح الاختبارات البصرية على حاسوب المطوّر وتفشل في الـ CI بفروقات صغيرة في حواف النص. ما السبب الأرجح والإصلاح؟</p>","options":[{"text":"جهاز الـ CI أبطأ؛ زِد مهلة الانتظار.","why":"السرعة لا تغيّر تنعيم الحواف (anti-aliasing)؛ والمهلة تؤثّر في الانتظار لا في البكسلات."},{"text":"اللقطات تُلتقط قبل تحميل الخطوط؛ أضف انتظارًا ثابتًا لمدة ثانية إلى كل اختبار.","why":"الانتظار الثابت يبطئ المجموعة ويُبقيها غير مستقرّة؛ وهذا العرَض يشير إلى اختلافات في الرسم بين الأجهزة."},{"text":"أنظمة التشغيل المختلفة ترسم الخطوط بشكل مختلف؛ ولّد الصور المرجعية وقارنها في بيئة واحدة ثابتة، مثل حاوية الـ CI.","why":"صحيح. الصور المرجعية لا تكون قابلة للمقارنة إلا حين يولّدها المتصفّح ونظام التشغيل والخطوط نفسها."}],"answer":2},{"q":"<p>أين يجب أن يعمل <code dir=\"ltr\">npm publish</code> في سير عمل الإصدار لنظام التصميم؟</p>","options":[{"text":"بعد نجاح البناء والاختبارات والفحوص البصرية، على الـ branch الرئيسي، من خطوة الترقيم.","why":"صحيح. النشر هو الخطوة الأخيرة، ولا يحدث إلا لكود اجتاز كل الفحوص ودُمج."},{"text":"على كل pull request، حتى يستطيع المراجعون تثبيت التغيير.","why":"نشر كود غير مُراجَع في السجلّ يضعه أمام كل مستخدم؛ استخدم نسخ معاينة أو وسم prerelease بدلًا من ذلك."},{"text":"قبل الاختبارات، حتى لا يعطّل اختبار فاشل الإصلاحات العاجلة.","why":"الإصلاح العاجل الذي يكسر عمل أربعين فريقًا أسوأ من إصلاح يتأخّر قليلًا."},{"text":"يدويًا من حاسوب أحد القائمين على النظام بعد الدمج.","why":"النشر المحلي يتخطّى ضمانات الـ CI، ويعتمد على جهاز شخص واحد وبيانات اعتماده."}],"answer":0}],"exercise":{"kind":"spot-bug","language":"yaml","code":"name: Release\non:\n  pull_request:\n    branches: [main]\njobs:\n  release:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      id-token: write\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-node@v7\n        with:\n          node-version: 24\n          registry-url: https://registry.npmjs.org\n      - run: npm ci\n      - run: npm publish --workspace @northwind/ui\n      - run: npm run build\n      - run: npm test\n      - run: npm run test:visual","bugLines":[3,18],"title":"امنع سير العمل من نشر كود معطوب","prompt":"<p>سير العمل هذا في GitHub Actions يُفترض أن ينشر <code dir=\"ltr\">@northwind/ui</code> على npm بعد دمج التغييرات واختبارها. لكنه بصيغته الحالية قد يضع كودًا غير مختبَر وغير مُراجَع أمام الفرق الأربعين كلها. اختر السطر أو الأسطر المسبّبة لذلك.</p>\n","hints":["متى يعمل سير العمل هذا؟ وهل رُوجع الكود ودُمج في تلك اللحظة؟","اقرأ الخطوات من الأعلى إلى الأسفل: ماذا يحدث إن فشل البناء أو الاختبارات البصرية؟"],"explanation":"<p>سطران خاطئان.</p>\n<ul>\n<li><strong>السطر 3</strong> يطلق سير العمل على <code dir=\"ltr\">pull_request</code>، فينشر كل pull request مفتوح، قبل المراجعة أو الدمج. يجب أن يعمل سير عمل الإصدار على <code dir=\"ltr\">push</code> إلى <code dir=\"ltr\">main</code>.</li>\n<li><strong>السطر 18</strong> ينشر مباشرة بعد التثبيت، قبل <code dir=\"ltr\">npm run build</code> و<code dir=\"ltr\">npm test</code> والاختبارات البصرية. فإن فشل أيٌّ منها تكون الحزمة قد وصلت إلى npm بالفعل. يجب أن يكون النشر آخر خطوة.</li>\n</ul>\n<p>الإصلاح: <code dir=\"ltr\">on: push: branches: [main]</code>، ونقل خطوة النشر (أو إجراء Changesets الذي يشغّلها) إلى ما بعد البناء والاختبارات والاختبارات البصرية.</p>\n","checkLabels":{}},"words":612,"readMinutes":4}