Files

31 KiB
Raw Permalink Blame History

name, description
name description
qa-clinicpro تست QA اپلیکیشن ClinicPro مثل یک کاربر واقعی — ابتدا ساخت همهٔ نقش‌ها و پروفایل‌های کامل (پزشک مستقل، پزشک عضو کلینیک، کلینیک، منشی، نماینده، بیمار، …) و تعیین ماتریس سطح دسترسی، سپس تست ماتریس دسترسی با تک‌تک آن‌ها. هر مانعی سر راه تست را مثل یک دولوپر ارشد Symfony/React خودش رفع می‌کند و تست را ادامه می‌دهد. اجرای اپ، ورود با هر نقش، پیمایش صفحات پنل ادمین، اسکرین‌شات، کشف خطاهای کنسول و شبکه، تست UI/UX و RTL، تست دسترسی نقش‌ها (authz)، تست قرارداد API و اندازه‌گیری کارایی، و تولید Bug Report. Use when asked to QA, test, smoke-test, find bugs in, screenshot, or verify ClinicPro's admin panel or API — «تست کن»، «باگ پیدا کن»، «QA کن»، «این صفحه را بررسی کن».

قبل از شروع، فایل clinicpro/.claude/guidelines.md را بخوان و همهٔ بخش‌های آن را اعمال کن — از جمله §۰ Grill که قبل از هر تغییر اجباری است.

QA ClinicPro

ClinicPro = بک‌اند Symfony 7.4 + یک SPA کلاینت‌ساید React 19 که از /admin/* سرو می‌شود. یعنی curl و فلگ --screenshot کروم به درد نمی‌خورند — هر دو روی فرم لاگین می‌نشینند، چون JWT در localStorage['clinicpro-auth'] است.

درایور این skill آن کار را انجام می‌دهد: با API لاگین می‌کند، localStorage را seed می‌کند، بعد ناوبری می‌کند و خطاهای کنسول، درخواست‌های شکست‌خورده، مسیری که واقعاً روی آن فرود آمده، و اسکرین‌شات را گزارش می‌دهد — با CDP روی WebSocket نیتیو Node 22، بدون هیچ وابستگی npm (نه playwright، نه puppeteer).

مسیرها نسبت به clinicpro/ هستند.

پیش‌نیازها

هیچ نصبی لازم نیست. فقط این دو:

ddev describe | head -3     # باید بالا باشد: https://clinic-pro.ddev.site
ls "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"

کروم جای دیگری است؟ CHROME_BIN را ست کن. بک‌اند جای دیگری است؟ CLINICPRO_BASE.

کاربران تست

TEST_USERS.md منسوخ است — هیچ‌کدام از کاربرانش (09100000001, 09100100000, …) در دیتابیس وجود ندارند و همه ERR_AUTH_005 می‌گیرند. اسکریپت‌های create_test_users.php و seed_realistic_data.php هم که آن فایل ارجاع می‌دهد در ریپو نیستند.

پرسوناهای QA در ROLES داخل درایور تعریف شده‌اند. واحد کار «پرسونا» است، نه ROLE_* — پزشک مستقل و پزشک عضو کلینیک هر دو ROLE_DOCTOR دارند ولی دادهٔ متفاوتی می‌بینند، پس هرکدام یک ردیف جداگانه‌اند.

پرسونا موبایل پسورد نقش‌ها تمایز
admin 09120671756 QaTest@1234 ROLE_ADMIN
clinic 09127000000 QaTest@1234 ROLE_CLINIC مالک کلینیک
secretary 09123456778 QaTest@1234 ROLE_SECRETARY منشیِ یک پزشک
doctor 09390039833 09390039833 ROLE_DOCTOR حساب قدیمی، وضعیت عضویتش نامعلوم
representation 09124000001 09124000001 ROLE_REPRESENTATION نماینده شهر
doctor_solo 09129000001 QaTest@1234 ROLE_DOCTOR پزشک مستقل — مطب شخصی، بدون کلینیک
doctor_member 09129000002 QaTest@1234 ROLE_DOCTOR پزشک عضو کلینیک
clinic_doctor 09129000003 QaTest@1234 ROLE_CLINIC+ROLE_DOCTOR چندنقشی
secretary_clinic 09129000004 QaTest@1234 ROLE_SECRETARY منشیِ کلینیک (نه پزشک)
unclaimed_doctor 09129000005 QaTest@1234 ROLE_UNCLAIMED_DOCTOR پروفایل ایمپورت‌شدهٔ تصاحب‌نشده
patient 09129000006 QaTest@1234 ROLE_USER کاربر عادی سایت
importer 09129000007 QaTest@1234 ROLE_IMPORTER

پنج ردیف اول موجودند. هفت ردیف آخر تا وقتی Phase 0 اجرا نشده وجود ندارند و driver.mjs roles برایشان می‌دهد — این دقیقاً چک آمادگی است.

اگر DB ریست شد، پسورد پنج‌تای اول را دوباره ست کن:

ddev exec php bin/console security:hash-password 'QaTest@1234'
# هش خروجی را در این کوئری بگذار:
ddev mysql -e "UPDATE users SET password_hash='<هش>' \
  WHERE mobile_number IN ('09120671756','09127000000','09123456778');"

اعتبارسنجی همه نقش‌ها:

node .claude/skills/qa-clinicpro/driver.mjs roles

خروجی واقعی:

admin            09120671756  ROLE_USER,ROLE_ADMIN  token 15min
clinic           09127000000  ROLE_USER,ROLE_CLINIC  token 15min
secretary        09123456778  ROLE_USER,ROLE_SECRETARY  token 15min
doctor           09390039833  ROLE_USER,ROLE_DOCTOR  token 15min
representation   09124000001  ROLE_USER,ROLE_REPRESENTATION  token 15min

می‌توانی به‌جای نام نقش، --as "0912xxxxxxx:password" هم بدهی.


مسیر اجرا (agent path)

۰. Phase 0 — ساخت نقش‌ها، پروفایل‌ها و ماتریس دسترسی (اجباری، قبل از هر تست)

هیچ تستی را قبل از تمام‌شدن این فاز شروع نکن. خروجی این فاز سه چیز است: همهٔ پرسوناها موجود · پروفایل هرکدام کامل · ماتریس دسترسی مکتوب.

۰.۱ — کشف نقش‌ها. لیست بالا را دوباره از روی کد بساز، به آن استناد نکن؛ ممکن است نقشی اضافه شده باشد:

grep -rhoE "ROLE_[A-Z_]+" src/ assets/admin/ config/ | sort -u
ddev mysql -e "SELECT roles, COUNT(*) c FROM users GROUP BY roles ORDER BY c DESC;"
grep -n "role_hierarchy" -A 10 config/packages/security.yaml

هر نقشی که در کد هست و در جدول پرسوناها نیست را به ROLES در driver.mjs اضافه کن.

۰.۲ — چک آمادگی. ببین کدام پرسونا هنوز نیست:

node .claude/skills/qa-clinicpro/driver.mjs roles

۰.۳ — ساخت پرسوناهای ناموجود. برای هرکدام، اول مسیر واقعی ساخت را در خود اپ پیدا کن و از همان استفاده کن — دست‌کاری مستقیم SQL پروفایل ناقص می‌سازد و تست را دروغین می‌کند. به این ترتیب بگرد:

ls src/*/Command/                         # آیا کامند کنسولی برای ساخت کاربر هست؟
grep -rn "IsGranted" src/Admin/Controller/ # اندپوینت‌های ادمینِ ساخت کاربر
sed -n '1,80p' docs/api/admin.md

فقط برای چیزی که هیچ مسیر اپلیکیشنی ندارد (مثلاً ست‌کردن ROLE_IMPORTER یا ساختن ROLE_UNCLAIMED_DOCTOR) به ddev mysql برگرد، و در گزارش بنویس که کدام پرسونا دستی ساخته شد.

ترتیب ساخت مهم است — وابستگی دارند:

کلینیک  →  doctor_member (عضو همان کلینیک)  →  secretary_clinic (منشیِ همان کلینیک)
پزشک    →  secretary (منشیِ همان پزشک)

۰.۴ — کامل‌کردن پروفایل. یک حسابِ بدون پروفایل، صفحات را خالی نشان می‌دهد و باگ‌های واقعی را پنهان می‌کند. برای هر پرسونا این‌ها باید پر باشند:

پرسونا حداقل پروفایل لازم
doctor_solo / doctor_member / clinic_doctor نام، تخصص، آدرس مطب، برنامهٔ کاری هفتگی، حداقل یک خدمت با تعرفه، حداقل یک بیمه
clinic نام کلینیک، شهر، آدرس، حداقل یک پزشک عضو، حداقل یک خدمت
secretary / secretary_clinic اتصال به پزشک/کلینیک + سطح دسترسی‌اش
representation شهر تخصیص‌یافته
patient نام، و حداقل یک نوبت رزروشده (برای اینکه صفحات خالی نباشند)
unclaimed_doctor پروفایل پزشک بدون کاربرِ تصاحب‌کننده

بعد از ساخت، پرشدن را تأیید کن — نه با حدس، با درخواست:

node .claude/skills/qa-clinicpro/driver.mjs api GET /api/v1/doctor/profile --as doctor_solo

۰.۵ — تعیین سطح دسترسی. ماتریس را از کد دربیاور، نه از ذهنت:

grep -n "RoleRoute\|allowedRoles\|element=" assets/admin/App.tsx   # مسیرهای فرانت
grep -rn "IsGranted" src/*/Controller/ | sed 's/.*IsGranted(//'    # گاردهای بک‌اند

از این دو، جدول مسیر → نقش‌های مجاز را بساز و در گزارش بیاور. بعد برای هر اندپوینت حساس با authz (بخش ۳) تأییدش کن. اختلاف بین ماتریسِ کد و خروجی authz = باگ، حتی اگر خروجی authz سخت‌گیرانه‌تر باشد.

۰.۶ — دروازهٔ خروج. تا وقتی roles برای همهٔ پرسوناها توکن برمی‌گرداند و ماتریس نوشته شده، به فاز بعد نرو. اگر پرسونایی ساخته نشد، طبق بخش «وقتی به مانع خوردی» خودت رفعش کن؛ رها کردنش یعنی آن نقش اصلاً تست نشده.

۱. بازدید از صفحه — اسکرین‌شات + خطاها

node .claude/skills/qa-clinicpro/driver.mjs visit \
  "https://clinic-pro.ddev.site/admin/dashboard" --as admin --out /tmp/qa-dash.png
✓ screenshot /tmp/qa-dash.png  (1440x900, as admin)

LANDING
  (none)

CONSOLE ERRORS
  (none)

NETWORK FAILURES
  (none)

بعد حتماً تصویر را با ابزار Read باز کن و نگاه کن. نیمی از باگ‌های UI فقط دیدنی‌اند، نه لاگ‌شدنی — همان یک اسکرین‌شات داشبورد دو باگ i18n لو داد (پایین را ببین).

فلگ‌ها: --w 1440 --h 900 (ویوپورت)، --wait 4000 (ms صبر برای رندر)، --full (کل صفحه).

موبایل را جدا تست کن — پنل RTL و پرجدول است و بیشتر مشکلات آنجاست:

node .claude/skills/qa-clinicpro/driver.mjs visit \
  "https://clinic-pro.ddev.site/admin/dashboard" --as admin --w 390 --h 844 --out /tmp/qa-m.png

بخش LANDING دو حالتی را می‌گیرد که اسکرین‌شات پنهان می‌کند:

⚠ WRONG PAGE: asked /admin/users, landed /admin/dashboard — role likely lacks access (RoleRoute in App.tsx)

۲. آدیت UI/UX و RTL

node .claude/skills/qa-clinicpro/driver.mjs ux \
  "https://clinic-pro.ddev.site/admin/dashboard" --as admin --w 390 --h 844
UX FINDINGS (390x844, as admin)
  5 tap target(s) under 36px on a mobile viewport

چک‌ها: RTL نبودن ریشه، lang غلط، سرریز افقی، رقم لاتین داخل متن فارسی، تارگت لمسی زیر ۳۶px، <img> بدون alt، فیلد بدون label، id تکراری، جدول خالی بدون empty-state، و <select> نیتیو (استاندارد پروژه SearchableSelect است).

۳. تست دسترسی نقش‌ها (Security)

همان درخواست با همه نقش‌ها + ناشناس:

node .claude/skills/qa-clinicpro/driver.mjs authz GET /api/v1/admin/users
AUTHZ GET /api/v1/admin/users

  anonymous        401
  admin            200
  clinic           403
  secretary        403
  doctor           403
  representation   403

  200 for: admin

هر ۲۰۰ غیرمنتظره در این جدول = یک باگ Critical. اگر anonymous هم ۲۰۰ گرفت، درایور هشدار می‌دهد.

۳.۱ جاروی کامل ماتریس — اجباری، نه نمونه‌ای

authz خودش همهٔ پرسوناها را می‌زند، پس تست دسترسی نباید روی چند اندپوینت منتخب بماند. لیست اندپوینت‌ها را از روتر بگیر و همه را جارو کن:

ddev exec php bin/console debug:router --format=json \
  | node -e 'const r=JSON.parse(require("fs").readFileSync(0));
      for (const [n,v] of Object.entries(r))
        if (v.path.startsWith("/api/v1") && !v.path.includes("{"))
          console.log(v.method.split("|")[0].replace("ANY","GET"), v.path);' \
  | while read m p; do
      node .claude/skills/qa-clinicpro/driver.mjs authz "$m" "$p"
    done | tee /tmp/qa-authz-matrix.txt

روی این DB حدود ۱۶۸ مسیر بدون پارامتر برمی‌گردد و authz برای هر مسیر به‌ازای هر پرسونا دوباره لاگین می‌کند (≈۲۲۰۰ درخواست) — چند دقیقه طول می‌کشد، پس در پس‌زمینه اجرایش کن و بعد فایل را بخوان. دو تله در خواندن خروجی:

  • ۴۲۲ روی مسیرهای POST طبیعی است (بدنه خالی فرستاده شده) و باگ نیست؛ چیزی که مهم است تمایز ۴۰۱/۴۰۳ از بقیه است. اگر نقشی به‌جای ۴۰۳ یک ۴۲۲ گرفت، یعنی گارد بعد از اعتبارسنجی اجرا شده — همان هم یافته است.
  • مسیرهای عمومی (لاگین، ثبت‌نام، لیست شهرها) قاعدتاً برای anonymous هم ۲۰۰‌اند؛ اول با config/packages/security.yaml تطبیق بده، بعد ادعای نشت کن.

اندپوینت‌های پارامتردار ({uuid}) از این حلقه می‌افتند — آن‌ها را دستی و با شناسهٔ متعلق به پرسونای دیگر بزن، چون همان‌جاست که IDOR پیدا می‌شود:

# uuid پزشکِ دیگری را به پرسونای doctor_solo بده — باید ۴۰۳/۴۰۴ بگیرد، نه ۲۰۰
node .claude/skills/qa-clinicpro/driver.mjs authz GET /api/v1/doctor/<uuid-of-another-doctor>

سه الگویی که باید در /tmp/qa-authz-matrix.txt دنبالشان بگردی:

یافته معنی
anonymous = ۲۰۰ روی مسیر غیرعمومی نشت داده — Critical
نقشی ۲۰۰ می‌گیرد که در ماتریس ۰.۵ نبود گارد جا افتاده — Critical
۲۰۰ روی uuidِ مستأجر دیگر IDOR — Critical
نقشی ۴۰۳ می‌گیرد که طبق ماتریس باید ۲۰۰ بگیرد یا گارد سخت‌گیر است یا ماتریس غلط — بررسی کن
۵۰۰ به‌جای ۴۰۳ گارد کار می‌کند ولی خطا مدیریت نشده — High

بدون این جدولِ کامل، فاز دسترسی تمام‌شده نیست. خروجی‌اش را در گزارش نهایی بیاور.

۴. تست قرارداد API

node .claude/skills/qa-clinicpro/driver.mjs api GET /api/v1/categorys/state --as admin
GET /api/v1/categorys/state  →  301  12ms  (as admin)

ENVELOPE
  (none)

BODY
{
  "success": false,
  "data": null,
  "errors": [
    { "code": "ERR_MOVED", "message": "این endpoint منتقل شده. لطفاً از /api/v1/provinces استفاده کنید." }
  ]
}

بخش ENVELOPE پاکت BaseController را چک می‌کند: نبودِ success، پاسخ خطای بدون errors، و دام معروف double/triple nesting (data.data.data).

POST هم می‌شود: --body '{"name":"x"}'.

۵. کارایی

node .claude/skills/qa-clinicpro/driver.mjs perf "https://clinic-pro.ddev.site/admin/doctors" --as admin
PERF https://clinic-pro.ddev.site/admin/doctors  (as admin)
  ttfb                    12ms
  domContentLoaded        232ms
  load                    233ms
  first-paint             180ms
  first-contentful-paint  248ms
  resources 24 · DOM nodes 1132

SLOWEST API CALLS
     18ms  2kb  v1/admin/doctors?page=1&limit=25
     17ms  1kb  v1/admin/doctors/stats
     17ms  5kb  v1/specialties
     13ms  1kb  v1/provinces

نقش QA و روش کار

وقتی این skill فعال شد، مثل یک مهندس ارشد تست رفتار کن، نه فقط اجراکننده دستور:

  1. Phase 0 را تمام کن (بالا). بدون پرسوناهای کامل، هر تستی نتیجهٔ بی‌معنی می‌دهد.
  2. اول سناریوی واقعی کاربر را بنویس، بعد اجرا کن. مثال: ورود منشی → لیست نوبت‌ها → تغییر وضعیت یک نوبت → خروج → ورود مجدد → آیا تغییر ماند؟

پیمایش با هر پرسونا اجباری است. بعد از Phase 0، برای هر پرسونا در جدول، وارد شو و مسیرهای مجازش را طبق ماتریس ۰.۵ بگرد — نه فقط با admin. برای هر پرسونا حداقل:

for p in admin clinic doctor_solo doctor_member clinic_doctor \
         secretary secretary_clinic representation patient; do
  node .claude/skills/qa-clinicpro/driver.mjs visit \
    "https://clinic-pro.ddev.site/admin/dashboard" --as "$p" --out "/tmp/qa-$p.png"
done

بعد هر اسکرین‌شات را با Read باز کن و ببین — و بخش LANDING را بخوان تا ریدایرکت بی‌صدای نقش را نگیری. سه چیزی که فقط با مقایسهٔ بین پرسوناها پیدا می‌شوند:

  • نشت داده بین مستأجرها: آیا doctor_solo دادهٔ بیمار پزشک دیگری را می‌بیند؟ آیا clinic نوبت‌های پزشک غیرعضو را می‌بیند؟ این‌ها همیشه Critical‌اند.
  • صفحهٔ سفید به‌جای «دسترسی ندارید»: نقشی که نباید ببیند، باید پیام روشن بگیرد.
  • منوی سایدبار در برابر دسترسی واقعی: آیتمی که نمایش داده می‌شود ولی به ۴۰۳ می‌خورد (یا برعکس: مسیر باز است ولی در منو نیست) باگ است.
  1. برای هر بخش این حالت‌ها را پوشش بده: Happy Path · ورودی نامعتبر · داده خالی · داده خیلی زیاد (لیست ۱۰٬۹۳۲ کاربری) · شرایط مرزی · خطای شبکه · همهٔ پرسوناها · دسکتاپ ۱۴۴۰ و موبایل ۳۹۰.
  2. هیچ چیز را حدس نزن. ادعای بدون خروجی دستور، ادعا نیست.
  3. قبل از گزارش، باگ را دوباره تکرار کن. همان دستور را دوباره بزن؛ اگر تکرار نشد، flaky بودنش را بنویس نه خودِ باگ را.
  4. باگ‌های کوچک UI را هم گزارش کن، ولی باگ‌های Business Logic اولویت بالاترند.

وقتی به مانع خوردی — رفعش کن، بعد برو تست بعدی

QA اینجا فقط گزارش‌نویس نیست. هر جا اجرای تست گیر کرد، مثل یک دولوپر ارشد Symfony/React خودت مشکل را حل کن، تأیید کن که حل شده، و تست را از همان‌جا ادامه بده. توقف روی اولین مانع یعنی بقیهٔ نقش‌ها هیچ‌وقت تست نمی‌شوند.

روال ثابت هر مانع:

بازتولید  →  ریشه‌یابی (نه علامت)  →  اصلاح  →  اثبات اصلاح  →  ثبت  →  ادامهٔ همان تست
  1. ریشه را پیدا کن، نه علامت را. visit صفحهٔ سفید داد؟ اول CONSOLE ERRORS و NETWORK FAILURES، بعد فایل سورس صفحه (redesign-page/driver.mjs inspect)، بعد کنترلر مربوطه. اصلاح باید در همان لایه‌ای باشد که علت آنجاست.
  2. طبق قواعد پروژه اصلاح کن، نه با وصلهٔ سریع:
    • بک‌اند: extends BaseController، خطا با AppException(ErrorCodes::…)، کد SOLID، تغییر entity ⟵ doctrine:migrations:diff + migrate.
    • فرانت: TanStack Query برای دادهٔ سرور، کامپوننت‌های components/ui/، توکن‌های styles.css (هیچ hex هاردکد)، رشته‌های فارسی.
    • اندپوینت عوض شد ⟵ همان جلسه docs/api/<domain>.md را به‌روز کن (قاعدهٔ ثابت پروژه).
  3. اثبات کن. همان دستوری که شکست خورده بود را دوباره بزن و خروجی سالمش را نشان بده. بعد ddev exec php bin/phpunit و در صورت تغییر فرانت npx tsc --noEmit را اجرا کن تا مطمئن شوی چیزی نشکسته‌ای.
  4. ثبت کن. هر اصلاح یک ورودی در بخش «Fixes Applied» گزارش نهایی می‌گیرد: مانع · ریشه · فایل‌های تغییریافته · دستور اثبات.

مرزهایی که رد نمی‌کنی:

  • باگ محصول را بی‌صدا رفع نکن. اگر مانع خودش یک باگ واقعی محصول است، هم Bug Report را بنویس هم اصلاح را — نه فقط اصلاح. گزارش، خروجی کار است.
  • هرگز برای سبزشدن تست، دسترسی را باز نکن. اگر نقشی ۴۰۳ می‌گیرد و تو انتظار ۲۰۰ داری، پیش‌فرض این است که انتظارت غلط است. IsGranted یا RoleRoute را فقط وقتی عوض کن که از روی کد ثابت کرده باشی آن نقش باید دسترسی داشته باشد، و دلیلش را بنویس. همین قاعده برای حذف اعتبارسنجی ورودی هم هست.
  • دادهٔ تست را با تغییر محصول نساز. کمبود دادهٔ پرسونا را با seed درست کن، نه با نرم‌کردن یک قاعدهٔ کسب‌وکار.
  • مهاجرت مخرب نزن. روی DB لوکالِ پر (۱۰٬۹۳۲ کاربر) doctrine:schema:drop یا مهاجرتی که ستون پرداده را می‌اندازد، ممنوع.
  • اگر اصلاح از تست بزرگ‌تر شد (بازطراحی معماری، تغییر شکست‌دهندهٔ قرارداد API که nobat724_front و clinic-pro-tauri هم مصرفش می‌کنند)، دست نگه دار: باگ را با اصلاح پیشنهادی گزارش کن، آن یک تست را SKIPPED علامت بزن، و برو تست بعدی.

فرمت Bug Report

هر یافته را با این قالب بنویس (فارسی):

## Title
<عنوان کوتاه و مشخص>

- **Severity:** Critical | High | Medium | Low
- **Priority:** فوری | مهم | معمولی | کم
- **Environment:** Chrome headless · macOS · ddev · نقش: <role> · ویوپورت: <w>x<h>

### Description
### Steps To Reproduce
1. `node .claude/skills/qa-clinicpro/driver.mjs …`   ← دستور دقیق، نه توضیح
2.
### Expected Behavior
### Actual Behavior
### Evidence
<خروجی درایور، مسیر اسکرین‌شات، پاسخ API>
### Impact
### Suggested Fix
<فایل:خط اگر پیدا کردی>

برای پیدا کردن فایل سورس یک صفحه از روی URL، از skill خواهر استفاده کن:

node .claude/skills/redesign-page/driver.mjs inspect "https://clinic-pro.ddev.site/admin/doctors"

گزارش نهایی

۱. خلاصه وضعیت کلی · ۲. تعداد باگ‌ها · ۳. لیست بر اساس Severity · ۴. باگ‌هایی که باید فوری رفع شوند · ۵. پیشنهاد بهبود کیفیت.

به‌علاوه این سه بخش که از قواعد بالا می‌آیند:

۶. Fixes Applied — هر مانعی که خودت رفع کردی:

مانع ریشه فایل‌های تغییریافته دستور اثبات

۷. ماتریس دسترسی — جدول کامل مسیر × پرسونا از بخش ۳.۱، با اختلاف‌های ماتریسِ کد و رفتار واقعی مشخص‌شده.

۸. پوشش — کدام پرسونا چه چیزی تست شد، و هر SKIPPED با دلیلش. اگر نقشی تست نشد باید اینجا صریح بیاید؛ گزارشِ ساکت بدتر از گزارش ناقص است.


Gotchas

  • SPA است، پس curl صفحه نمی‌دهد. curl /admin/doctors همیشه همان HTML پوسته را برمی‌گرداند. هر ادعایی درباره محتوای صفحه باید از visit بیاید.
  • ریدایرکت بی‌صدای نقش. RoleRoute در App.tsx کاربر بدون دسترسی را بی‌هیچ پیغامی به /dashboard می‌فرستد — اسکرین‌شات کاملاً سالم به‌نظر می‌رسد ولی صفحهٔ اشتباهی است. همیشه بخش LANDING را بخوان.
  • توکن فقط ۱۵ دقیقه اعتبار دارد. درایور برای هر دستور دوباره لاگین می‌کند، پس مسئله‌ای نیست؛ ولی اگر خودت توکن را جایی کش کردی، انتظار ۴۰۱ داشته باش.
  • TEST_USERS.md دروغ می‌گوید (بالا). به آن استناد نکن.
  • CLAUDE.md هم روی /api/v1/categorys/{bundle} منسوخ است — آن مسیر حالا ۳۰۱ با ERR_MOVED می‌دهد و مسیر واقعی /api/v1/provinces است.
  • کد OTP در محیط dev همیشه 12345 است (OtpService::sendCode — در غیر dev کد تصادفی ۵رقمی می‌سازد و SMS می‌کند). پس زنجیرهٔ کامل ورود بدون رمز اسکریپت‌پذیر است: POST /api/v1/user/send-codePOST /api/v1/user/verify-code با code=12345grantPOST /api/v1/user/otp-login.
  • patient و unclaimed_doctor با رمز وارد نمی‌شوند و این باگ نیست. PasswordAuthenticator::onAuthenticationSuccess هر کاربری که User::isStaff() نباشد را با ۴۰۳ و ERR_AUTH_006 رد می‌کند (staff = doctor/clinic/secretary/admin/representation/importer). این دو پرسونا فقط OTP-only هستند؛ درایور خودش به زنجیرهٔ OTP بالا fallback می‌کند. گاردش را برای سبزشدن تست باز نکن.
  • send-code سقف ۵ درخواست در ساعت به‌ازای هر IP دارد (config/packages/rate_limiter.yaml). یک جاروی کامل authz این سقف را می‌سوزاند و بعدش پرسوناهای OTP-only شکست می‌خورند (ERR_RATE_LIMIT_001). راه‌حل بدون دست‌زدن به محصول: توکن را مستقیم با کامند خود اپ بساز —
    ddev exec 'php bin/console lexik:jwt:generate-token 09129000006 --user-class="App\\Auth\\Entity\\User"'
    
    همان کلید و همان claimها؛ فقط محدودیت نرخ را دور می‌زند.
  • برای جاروی ماتریس، درایور را در حلقه صدا نزن. هر فراخوانی دوباره لاگین می‌کند (۱۶۸ مسیر × ۱۳ پرسونا ≈ ۲۲۰۰ لاگین) — هم چند ده دقیقه طول می‌کشد هم rate limit را می‌سوزاند. یک‌بار برای هر پرسونا توکن بگیر و همان را در همهٔ مسیرها استفاده کن.
  • گواهی TLS ddev را Node قبول نمی‌کند. درایور فقط برای هاست‌های *.ddev.site / localhost NODE_TLS_REJECT_UNAUTHORIZED=0 می‌گذارد و وارنینگ نویزی‌اش را خفه می‌کند.
  • خطاهای صفحهٔ لاگین به حساب صفحهٔ تحت تست نوشته نشوند. درایور بافر خطا را بعد از seed کردن localStorage و قبل از ناوبری به URL هدف پاک می‌کند.
  • دیتای لوکال واقعی و بزرگ است (۱۰٬۹۳۲ کاربر، ۲۰۲ کلینیک، ۱۷۹ پزشک) — برای تست «داده زیاد» لازم نیست چیزی seed کنی.
  • CDP روی پورت ۹۴۴۴ است تا با درایور redesign-page (پورت ۹۳۳۳) تداخل نکند؛ می‌توانی هر دو را هم‌زمان اجرا کنی. CDP_PORT قابل تغییر است.

Troubleshooting

نشانه علت / رفع
login as admin failed: … ERR_AUTH_005 DB ریست شده؛ پسورد QA را دوباره ست کن (بخش «کاربران تست»)
Chrome did not expose CDP on :9444 CHROME_BIN غلط است، یا نمونهٔ قبلی کروم روی همان پورت مانده — pkill -f clinicpro-qa
⚠ page text only N chars رندر SPA کرش کرده یا کند است؛ اول --wait 8000 را امتحان کن، بعد CONSOLE ERRORS را بخوان
⚠ redirected to /login توکن رد شده — با driver.mjs login <role> صحتش را چک کن
fetch failed / ECONNREFUSED ddev بالا نیست: ddev start

باگ‌های شناخته‌شده (در همین اجرا پیدا شدند)

نمونه‌هایی از خروجی واقعی همین درایور، به‌عنوان مرجعِ اینکه گزارش چطور باشد:

  1. Medium — در «وضعیت نوبت‌ها»ی داشبورد، برچسب‌های confirmed و expired انگلیسی مانده‌اند در حالی که بقیه فارسی‌اند («تکمیل شده»، «لغو پزشک»). بازتولید: visit https://clinic-pro.ddev.site/admin/dashboard --as admin، اسکرین‌شات.
  2. Low — کارت «درآمد این ماه» کلمهٔ «تومان» را دو بار نشان می‌دهد (یک‌بار کنار عدد، یک‌بار به‌عنوان زیرنویس کارت). همان اسکرین‌شات.
  3. Low — در ویوپورت ۳۹۰px داشبورد، ۵ تارگت لمسی زیر ۳۶px هستند. بازتولید: ux … --w 390 --h 844.
  4. Medium (مستندات)TEST_USERS.md و بخش Category در CLAUDE.md هر دو منسوخ‌اند.