رفتن به محتوا

آزمون

لایه کجا چه می‌گیرد اجرا
واحد packages/*/test/*.test.ts، apps/*/test/*.test.ts منطق دامنه، گزارش، صحنه، فرمان‌های CLI pnpm -r test
ویژگی‌محور (fast-check) packages/core/test/properties.test.ts، apps/web/test/facade.dom.test.tsx قانون‌هایی که برای صدها ورودی تصادفی باید برقرار باشد: distribute، cutSizeFor، nest، solveHomography، سیاستِ میلی‌مترِ generateParts (اندازهٔ صحیح، ماشین‌کاری روی گامِ ۰٫۵ و درونِ قطعه)، و نمای یونیت روی بوم برابرِ جعبه‌های layoutFront ِ هسته همان
DOM (jsdom) apps/web/test/*.dom.test.{ts,tsx}، packages/ui/test/*.dom.test.tsx رفتار پنل‌ها با Testing Library؛ پروژهٔ dom ِ vitest (بقیه در پروژهٔ nodetest/setup.ts چیزهایی را که jsdom ندارد پر می‌کند (setPointerCapture، ResizeObserver) همان
دود CLI apps/cli/test/smoke.test.ts، darz-open.test.ts بستهٔ tsdown، فرایند جدا، کد خروج. test/global-setup.ts بسته را از سورس می‌سازد؛ بی آن آزمون می‌افتد، کنار نمی‌رود همان
نگهبان‌ها scripts/test/*.test.mjs (node:test) خودِ اسکریپت‌های verify: قاعده‌های Stylelint و فهرستِ پایه‌اش، قاعدهٔ ESLint ِ رشتهٔ سخت‌کد و فهرستِ پایه‌اش، قاعده‌های دیگرِ ESLint (رنگ فقط از توکن، Dexie فقط در لایهٔ داده، «از کیت بگیر»، سازندهٔ نمونه با رندرگر، i18n ِ سراسری و مقدارِ بی‌تیپ در رابط)، متغیرِ CSS ِ تعریف‌نشده یا بازنشسته، چرخهٔ وارد کردن، چندزبانه و واژه‌نامه، سندِ قواعد، یادداشتِ انتشار، چرخ‌دندهٔ پوشش، بستهٔ npm و فونتِ سایت، پیکربندی Lingui، بودجهٔ اندازه، مجوزِ وابستگی‌ها، یک نمونه از Lingui و React، سرورِ serve-dist، نگهبانِ pre-commit pnpm test:scripts
مستندات apps/site/test/*.test.mjs (node:test) خودِ sync-reference (همگام‌سازیِ docs/reference با سایت؛ و پس از آزمون‌ها --check ِ خودش) و check-links (پیوند و لنگرِ سایتِ ساخته‌شده) همان
سرتاسری (Playwright) apps/web/e2e/*.spec.ts بستهٔ ساخته‌شده از زیرمسیر در مرورگر واقعی: مسیر دود، axe، پیش‌نمای رندر، مقایسهٔ تصویری، جاروی متن در شبه‌زبان و en-US pnpm e2e

بسته‌های ورک‌اسپیس یکدیگر را از src می‌خوانند (شرطِ صادراتِ @darzsaz/source در package.json هر بسته، customConditions در tsconfig.base.json، resolve.conditions در vitest.shared.ts و vite.config.ts). پس typecheck، lint و همهٔ آزمون‌ها بی pnpm -r build اجرا می‌شوند و هرگز dist ِ کهنه را نمی‌سنجند؛ بیلد فقط برای انتشار، pnpm e2e، سه نگهبانی که از dist می‌خوانند (darz-format، rules-doc و tokens-css با --check) و سنجش‌های خودِ بسته (size-budget، size-limit، release-check) لازم است. CI همین را اثبات می‌کند: کارِ verify خودش نمی‌سازد و typecheck و آزمون را پیش از پایین آوردنِ آرتیفکتِ dist ِ کارِ build می‌راند.

چیزهایی که فقط لایهٔ سرتاسری می‌بیند: مسیر مطلق دارایی روی زیرمسیر، چیدمانِ واقعی (jsdom چیدمان ندارد)، WebGL. Playwright روی دستگاه توسعه کرومِ نصب‌شده را می‌گیرد (channel: 'chrome')، در CI کرومیومِ تصویر رسمی را. scripts/serve-dist.mjs بسته را از /darzsaz/ روی درگاهِ E2E_PORT (پیش‌فرض ۸۷۹۱) بالا می‌آورد؛ دو worktree که هم‌زمان pnpm e2e می‌زنند هر کدام درگاهِ خودش را می‌خواهد، وگرنه یکی بستهٔ دیگری را می‌آزماید. نشانیِ درصدیِ بدشکل ۴۰۰ می‌گیرد؛ پیش‌تر پرتابِ decodeURIComponent کلِ سرور و باقیِ e2e را می‌انداخت. در CI، test.only ِ فراموش‌شده (forbidOnly) و آزمونی که فقط با تلاشِ دوباره سبز شد (failOnFlakyTests) اجرا را قرمز می‌کنند.

جاروی متن (۳.۹، ۳.۱۰). پروژهٔ Playwright ِ pseudo (e2e/pseudo-sweep.spec.ts) بیلدِ شبه‌زبان را می‌آزماید: pnpm e2e پیش از Playwright vite build --mode pseudo را در apps/web/pseudo/dist می‌سازد (۳ ثانیه) و همان سرور زیرِ /darzsaz-pseudo/ می‌دهدش — درگاهِ دوم با worktree ِ کناری یکی می‌شد. e2e/en-sweep.spec.ts در پروژهٔ app است. هر دو با e2e/sweep.ts متنِ دیده‌شدهٔ بیرون از کاتالوگ را با فهرستِ پایهٔ e2e/i18n-baseline/ می‌سنجند؛ متنِ تازه شکست است و فهرست فقط با pnpm e2e sweep -u و فقط رو به پایین نوشته می‌شود (i18n.md، «شبه‌زبان و جاروی en-US»). عکسِ هر متنِ بریده پیوستِ گزارش است.

پوسته در e2e. Playwright سیستم را روشن می‌نمایاند و پیش‌فرضِ برنامه «هماهنگ با سیستم» است، پس آزمونی که پوسته نمی‌گذارد پوستهٔ روشن را می‌بیند؛ آزمونی که به پوسته بسته است آن را در localStorage (darzsaz.theme) می‌نشاند. ماتریسِ axe (a11y.spec.ts، ۱۲.۱): هر مسیر (خانه، راهنما، اشتراک، ناشناخته، ویرایشگر و ویرایشگرِ با یونیتِ انتخاب‌شده، هزینه) و هر پنجرهٔ e2e/surfaces.ts × سه پوسته × دو زبان، و کارگاهِ گوشی در هر زبان — ۴۴ آزمون با یک آستانه (moderate و بالاتر). جداکنندهٔ SplitView با regionMatcher ِ axe از قاعدهٔ region بیرون است (react-resizable-panels آن را فرزندِ مستقیمِ گروه می‌خواهد و درونِ هیچ نشانه‌ای نمی‌نشیند)؛ axe روی WebGL و SVG «incomplete» می‌دهد، پس کنتراستِ نوارهای روی بوم و جوهرِ نقشه، کفِ قلم و زمینهٔ صحنه در theme.spec.ts با e2e/contrast.ts سنجیده می‌شوند. هر اسکنِ axe پیش از خود کاهشِ حرکت را می‌نمایاند (steady) تا اعلانِ در حالِ محو شدن نیمه‌شفاف خوانده نشود — و فقط همان‌جا: ریستِ کاهشِ حرکتِ styles.css هر تغییرِ حالت را گذارِ ۰٫۰۱ میلی‌ثانیه‌ای می‌کند که تا قابِ بعد می‌ایستد، و getComputedStyle تا آن قاب مقدارِ کهنه می‌دهد.

coverage-baseline.json پوششِ اندازه‌گیری‌شدهٔ هر بسته است و آستانهٔ vitest از آن درمی‌آید: نیم واحد پایین‌تر، برای نوسانِ مرزیِ v8 میان نسخه‌های Node (coverageOf در vitest.shared.ts). پس از pnpm coverage، pnpm coverage:ratchet سه چیز را می‌سنجد:

  1. پوشش زیرِ خطِ پایه نیامده؛
  2. پوشش از خطِ پایه جلو نزده — اگر زده، node scripts/coverage-ratchet.mjs --write و کامیتِ خطِ پایه، وگرنه سقوطِ بعدی تا همان فاصله دیده نمی‌شود؛
  3. خطِ پایه از همان پرونده روی main پایین‌تر نیست.
بسته خط / تابع / شاخه — coverage-baseline.json، Vitest ۴
core ۹۷٫۶ / ۹۷٫۸ / ۸۷٫۱
geometry ۹۸٫۹ / ۱۰۰ / ۸۷٫۵
i18n ۱۰۰ / ۱۰۰ / ۱۰۰
report ۹۸٫۹ / ۹۹٫۶ / ۸۵٫۷
ui ۹۶٫۷ / ۹۶٫۷ / ۹۰٫۵
web ۷۱٫۷ / ۶۸٫۸ / ۶۱٫۵
cli ۹۳٫۶ / ۹۷٫۱ / ۷۲٫۶

عددِ Vitest ۴ با ۳ مقایسه‌شدنی نیست. v8 در Vitest ۴ پوشش را از درختِ نحو می‌شمارد: «خط» فقط خطِ اجراشدنی است (نه کامنت، import و تیپ) و شاخهٔ تابعی که هرگز صدا زده نشده هم شمرده می‌شود. مخرج‌ها کوچک شدند — خطِ web از ۱۴٬۷۶۴ به ۵٬۰۵۸، Toast.tsx ِ ui از ۳۰ به ۱ — و شاخهٔ web از ۸۲٫۹ به ۵۸٫۸ آمد بی آنکه یک خط کد یا آزمون عوض شود. خطِ پایه یک بار، در کامیتی جدا با جدولِ پیش و پس، به اندازه‌گیریِ تازه رفت؛ از آنجا باز فقط بالا می‌رود.

در web دامنه همهٔ src است. بیرون فقط چیزی که jsdom نمی‌تواند اجرا کند، هر کدام با جای سنجشش کنار پیکربندی: main.tsx، Worker، و درختِ صحنهٔ r3f که بی WebGL ِ واقعی رندر نمی‌شود. تا نسل چهارم src/app هم بیرون بود و عدد ۷۵٫۳ می‌گفت در حالی که واقعیت ۷۲٫۰ بود.

strict جلوی as X، x! و any ِ پنهان (مثلاً درونِ Promise<any> ِ یک کتابخانه) را نمی‌گیرد؛ هر کدام جایی است که تیپ دیگر چیزی تضمین نمی‌کند. pnpm type-coverage (scripts/type-coverage.mjs) هر برنامهٔ تولیدی را با type-coverage --strict از پوشهٔ همان بسته می‌سنجد — از ریشه، سورسِ هسته در هر بسته‌ای که واردش می‌کند دوباره شمرده می‌شد — و زیرِ ۹۹٪ می‌افتد. آزمون‌ها بیرون‌اند؛ آنجا ! و as روی فیکسچر عمدی است.

برنامه (tsconfig.app.json، ۱۴۰۵/۰۶/۲۲) پوشش بی‌پوشش
core ۹۹٫۷۴٪ ۶۱ از ۲۳٬۴۶۵
geometry، report، Worker ِ وب ۱۰۰٪ ۰
ui ۹۹٫۸۲٪ ۴ از ۲٬۳۰۹
cli ۹۹٫۸۶٪ ۵ از ۳٬۷۲۲
web ۹۹٫۸۹٪ ۴۳ از ۴۱٬۸۱۱

packages/i18n پس از این اندازه‌گیری به فهرستِ scripts/type-coverage.mjs آمد و با همان آستانهٔ ۹۹٪ سنجیده می‌شود.

as unknown as در کدِ تولیدی یک جا مانده، با دلیل کنارش: دادهٔ JSON ِ کاتالوگ در packages/core/src/catalog/defaults.ts — اسکیمای کاتالوگ (parseCatalogData) هست، ولی irCatalog() در تکهٔ اولیهٔ خانه است و zod عمداً بیرون از آن؛ به‌جایش catalog-v2.test.ts همان داده را با اسکیمای کامل می‌سنجد. پنج جای دیگر رفت: اعلانِ اختیاری روی Window برای APIهایی که lib.dom ندارد (showSaveFilePicker، BarcodeDetector، قلابِ خودکارسازی) و کپیِ سطحیِ قلم‌های کاتالوگ به ردیفِ فرم (rowsOf).

پوششِ خط نمی‌گوید آزمون چیزی را ادعا کرد؛ خطی که اجرا شد و ادعایی نداشت هم «پوشیده» است. StrykerJS (stryker.config.mjs) کدِ packages/core/src/{nesting,parts,validate} را عمداً خراب می‌کند (< به <=، شرط به true، + به -) و می‌شمارد چند خرابی را آزمون‌های هسته گرفتند — تنها سنجهٔ مستقیمِ قاعدهٔ ۱.

پوشه (۱۴۰۵/۰۶/۲۴) جهش کشته مهلت زنده بی پوشش امتیاز بی مهلت آستانهٔ شکست زمان
nesting ۱٬۳۱۱ ۱٬۰۳۶ ۴۰ ۱۸۴ ۵۱ ۸۲٫۰۷ ۸۱٫۵۱ ۸۱ ۶۰ دقیقه، ۱۵
parts ۱٬۳۷۴ ۷۷۵ ۳۲۱ ۲۱۹ ۵۹ ۷۹٫۷۷ ۷۳٫۶۰ ۷۳ ۲۲ دقیقه، ۱۵
validate ۱٬۷۷۲ ۹۴۸ ۵۱۳ ۲۹۷ ۱۴ ۸۲٫۴۵ ۷۵٫۳۰ ۷۵ ۳۱ دقیقه، ۱۵

(«زمان»: دقیقه و شمارِ پردازه، روی دستگاهِ توسعه زیرِ بار — اجرای ۱۴۰۵/۰۶/۲۴ هم‌زمان با آزمون‌های دیگر بود و مهلت‌هایش زیاد.) آستانهٔ شکست امتیازِ همان روز بی مهلت‌ها است، رو به پایین — مهلت «گرفته» شمرده می‌شود و زیرِ بار بیشتر می‌شود — و مثل پوشش فقط بالا می‌رود. ۱۴۰۵/۰۶/۲۲ هر سه زیرِ ۷۰ ِ پلن بودند (۵۶٫۷ / ۶۰٫۶ / ۵۳٫۵) و ضعیف‌ترین پرونده‌ها usability.ts ۱۰٫۷، cost.ts ۲۵٫۰، merge.ts ۲۹٫۰، fit-run.ts ۳۰٫۶، sequence.ts ۲۰٫۰ و verify.ts ۴۳٫۶؛ فاز ۱ ِ نسل ششم (۱.۱۳) برای همین‌ها آزمونِ مرزی نوشت. جزئیاتِ هر جهشِ زنده در گزارشِ HTML.

Terminal window
MUTATE=nesting pnpm mutation # یک پوشه؛ گزارش در reports/mutation/nesting/index.html
pnpm mutation # هر سه، پشتِ هم

شبانه در .github/workflows/mutation.yml، نه در هر push: سه پوشه هم‌زمان در ماتریس، و نتیجهٔ هر شب در cache تا incremental شبِ بعد فقط جهشی را بیازماید که کد یا آزمونش عوض شده (اجرای دوبارهٔ parts ِ بی تغییر: ۱٬۲۰۳ از ۱٬۲۵۰ نتیجه بازخوانده، ۱۵ ثانیه). جهشِ «ایستای خالص» (ثابتی که فقط هنگامِ بارِ ماژول اجرا می‌شود) نادیده است (ignoreStatic): با همهٔ آزمون‌ها اجرا می‌شد (nesting ۱۱، parts ۴۷، validate ۲۷۲). پیکربندیِ vitest ِ جهش (packages/core/vitest.stryker.config.ts) فقط مهلتِ آزمون را بلند می‌کند: کدِ جهش‌خورده کندتر است و جلوی حلقهٔ بی‌پایان را timeoutMS ِ خودِ Stryker می‌گیرد.

apps/web/e2e/visual.spec.ts نمای سه‌بعدی آشپزخانهٔ نمونه را با عکسِ مرجعِ scene-3d.png می‌سنجد (maxDiffPixelRatio: 0.03)؛ عکس‌های مرجع در پوشهٔ همنامِ آزمون (…-snapshots/) می‌نشینند و در مخزن نیستند — کارِ دستیِ snapshots در CI می‌سازدشان (پایین‌تر، «عکسِ مرجع»)، و از فاز ۴ ِ نسل پنجم پوستهٔ رابط را: ویرایشگرِ با یونیتِ انتخاب‌شده و هزینه در سه پوسته × دو زبان (editor-<پوسته>-<زبان>.png، cost-…؛ بومِ WebGL پوشانده می‌شود) و کارگاه روی گوشی در دو زبان (workshop-<زبان>.png — صفحه پوستهٔ خودش را می‌نشاند). پانزده عکس؛ پوسته و زبان صریح‌اند، نه پیش‌فرضِ سیستم. هنوز هیچ‌کدام کامیت نشده: تا آرتیفکتِ کارِ snapshots در همین پوشه ننشیند، کارِ e2e ِ CI عمداً قرمز است. WebGL ِ نرم‌افزاری هر جا کمی فرق دارد، پس عکسِ مرجع فقط یک محیط دارد: تصویر رسمیِ Playwright در CI. نبودنِ عکس آنجا شکست است (updateSnapshots: 'none')، و روی دستگاه توسعه آزمون با یادداشت کنار می‌رود.

عکسِ مرجعِ تازه: Actions ← «بررسی» ← Run workflow با گزینهٔ snapshots؛ آرتیفکتِ visual-snapshots را در همان پوشه کامیت کن. عکسی که روی دستگاه توسعه ساخته شود هرگز کامیت نمی‌شود.

۱. هر تغییر رفتار، آزمونی دارد که بدون اصلاح می‌افتد. مطمئن نیستی؟ اصلاح را برگردان و ببین قرمز می‌شود. ۲. عدد، نه صفت. «بهتر شد» یعنی هیچ؛ «از ۲۸٫۸٪ به ۰٫۶٪» یعنی چیزی. ۳. آزمون فارسی نام می‌گیرد و می‌گوید چه چیزی را نگه می‌دارد. ۴. پیکره‌ها کد تایپ‌شده‌اند (packages/core/src/fixtures/): myKitchen()، lShapedProject()، الگوها. آزمون طلایی آشپزخانهٔ نمونه: ۵ ورق، ۵۸ قطعه، ۳۱ ردیف. ۵. هر آزمون با جهانِ دست‌نخورده شروع می‌شود: جاسوس، vi.fn() (شمار و رفتار)، جهانیِ جایگزین و متغیرِ محیطی پیش از آزمونِ بعد خودشان برمی‌گردند (restoreMocks، mockReset، unstubGlobals، unstubEnvsvi.restoreAllMocks() ِ دستی لازم نیست. در Vitest ۴ restoreMocks فقط spyOn را برمی‌گرداند — packages/core/test/test-isolation.test.ts همین را می‌سنجد. ۶. هشدارِ act خطاست. به‌روزرسانیِ React بیرون از act یعنی ادعای بعدی DOM ِ کهنه را می‌بیند؛ test/setup.ts آن را می‌اندازد. تغییرِ مستقیمِ فروشگاه در آزمون (useEditor.getState().select(…)) درونِ act(() => …) — تعویضِ زبان (i18n.load/activate) هم، چون هر جزئی که useLingui دارد از نو رندر می‌شود. همان جزء بی ارائه‌دهنده می‌افتد («useLingui hook was used without I18nProvider»): render ِ test/render.tsx. ۷. خوابِ ثابت نه. در e2e منتظرِ خودِ نشانه باش: untilStill (تصویر سه بارِ پیاپی یکی) و untilSettled (جعبهٔ عنصر سه بارِ پیاپی یکی) از e2e/helpers.ts.

قاعدهٔ ۱ را آزمونی که ادعا ندارد، .only ِ فراموش‌شده یا خوابِ ثابت بی‌صدا می‌شکند؛ سه ابزار، هر کدام روی آزمون‌های خودش (eslint.config.js):

ابزار کجا چه می‌گیرد
@vitest/eslint-plugin */test/** آزمونِ بی ادعا (کمکیِ ادعا نامش expect… است)، .only، expect ِ شرطی، عنوانِ تکراری
eslint-plugin-playwright apps/web/e2e waitForTimeout، شاخه درونِ آزمون (boxOf جایش)، ادعای غیرِ وب‌محور؛ test.skip فقط با شرط
eslint-plugin-testing-library آزمون‌های *.dom.test.{ts,tsx} و کمکی‌های test/render.tsx و select.ts ِ وب دسترسیِ مستقیم به DOM به‌جای پرس‌وجوی نقش، act ِ زائد، waitFor + getBy به‌جای findBy؛ استثناها (لایهٔ SVG، جای تمرکز) با دلیل کنار خط

react/jsx-key روی همهٔ *.tsx: React بی key فقط در کنسولِ توسعه هشدار می‌دهد.

Terminal window
pnpm verify # همه به ترتیب CI: typecheck، lint، format، knip، چرخهٔ وارد کردن، پوششِ تیپ، نگهبان‌ها و مجوزها، پوشش و چرخ‌دنده، build، سندها و توکن‌ها (`--check`)، بودجه و size-limit، بستهٔ npm و فونتِ سایت، e2e
pnpm typecheck # هر برنامهٔ هر بسته، اسکریپت‌ها (checkJs) و astro check ِ مستندات
pnpm type-coverage # type-coverage --strict، هر برنامهٔ تولیدی ≥ ۹۹٪
pnpm -r test # واحد، ویژگی‌محور، DOM و دود CLI — بی بیلد
pnpm test:scripts # آزمونِ خودِ نگهبان‌ها
pnpm coverage # با آستانه از coverage-baseline.json
pnpm coverage:ratchet # خطِ پایه با پوشش و با main می‌خواند
node scripts/licenses.mjs # مجوزِ وابستگی‌های تولیدیِ بسته‌های پخش‌شدنی از فهرستِ آزاد
pnpm size # size-limit روی هر تکه (پس از build)؛ pnpm size:report نقشهٔ ترکیب
pnpm e2e # Playwright روی بستهٔ ساخته‌شده (اول pnpm -r build؛ بیلدِ شبه‌زبان را خودش می‌سازد)؛ هیچ پروندهٔ ردیابی‌شده‌ای را عوض نمی‌کند
pnpm e2e sweep -u # فهرستِ پایهٔ جاروی شبه‌زبان و en-US پس از ترجمه، فقط رو به پایین
pnpm mutation # جهش‌آزمایی، شبانه در CI
SHOTS=1 pnpm --filter @darzsaz/web exec playwright test --project shots # عکس‌های README، فقط به‌درخواست

قلاب‌های گیت (lefthook): pre-commit پشتِ هم نگهبانِ پرونده، lint (--fix)، Stylelint (بی --fix)، prettier و typecheck (با اسکریپت‌ها) — هم‌زمان، اصلاح‌گرها یک پرونده را زیرِ دستِ هم می‌نوشتند؛ pre-push آزمون‌ها و آزمونِ نگهبان‌ها. نگهبانِ پرونده (scripts/staged-guard.mjs) پروندهٔ فونت بیرون از tools/fonts (و خروجیِ apps/web/public/fonts) و هر پروندهٔ بزرگ‌تر از ۱ MB را — جز tools/fonts/ و دو استثنای با دلیل (pnpm-lock.yaml، docs/journal/plans/v3/darzsaz-v3.html) — رد می‌کند؛ اندازه را از blob ِ staged می‌خواند، نه درختِ کاری. رد شدن برای یک بار: LEFTHOOK=0 git commit … (فقط نگهبانِ پرونده: LEFTHOOK_EXCLUDE=files).