---
name: qa-clinicpro
description: تست 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 کن»، «این صفحه را بررسی کن».
---
# 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/` هستند.
## پیشنیازها
هیچ نصبی لازم نیست. فقط این دو:
```bash
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 ریست شد، پسورد پنجتای اول را دوباره ست کن:
```bash
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');"
```
اعتبارسنجی همه نقشها:
```bash
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 — ساخت نقشها، پروفایلها و ماتریس دسترسی (اجباری، قبل از هر تست)
هیچ تستی را قبل از تمامشدن این فاز شروع نکن. خروجی این فاز سه چیز است:
**همهٔ پرسوناها موجود** · **پروفایل هرکدام کامل** · **ماتریس دسترسی مکتوب**.
**۰.۱ — کشف نقشها.** لیست بالا را دوباره از روی کد بساز، به آن استناد نکن؛ ممکن است
نقشی اضافه شده باشد:
```bash
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` اضافه کن.
**۰.۲ — چک آمادگی.** ببین کدام پرسونا هنوز نیست:
```bash
node .claude/skills/qa-clinicpro/driver.mjs roles
```
**۰.۳ — ساخت پرسوناهای ناموجود.** برای هرکدام، **اول مسیر واقعی ساخت را در خود اپ پیدا
کن** و از همان استفاده کن — دستکاری مستقیم SQL پروفایل ناقص میسازد و تست را دروغین
میکند. به این ترتیب بگرد:
```bash
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` | پروفایل پزشک بدون کاربرِ تصاحبکننده |
بعد از ساخت، پرشدن را تأیید کن — نه با حدس، با درخواست:
```bash
node .claude/skills/qa-clinicpro/driver.mjs api GET /api/v1/doctor/profile --as doctor_solo
```
**۰.۵ — تعیین سطح دسترسی.** ماتریس را از کد دربیاور، نه از ذهنت:
```bash
grep -n "RoleRoute\|allowedRoles\|element=" assets/admin/App.tsx # مسیرهای فرانت
grep -rn "IsGranted" src/*/Controller/ | sed 's/.*IsGranted(//' # گاردهای بکاند
```
از این دو، جدول `مسیر → نقشهای مجاز` را بساز و در گزارش بیاور. بعد برای هر اندپوینت
حساس با `authz` (بخش ۳) تأییدش کن. **اختلاف بین ماتریسِ کد و خروجی `authz` = باگ**،
حتی اگر خروجی `authz` سختگیرانهتر باشد.
**۰.۶ — دروازهٔ خروج.** تا وقتی `roles` برای همهٔ پرسوناها توکن برمیگرداند و ماتریس
نوشته شده، به فاز بعد نرو. اگر پرسونایی ساخته نشد، طبق بخش «وقتی به مانع خوردی»
خودت رفعش کن؛ رها کردنش یعنی آن نقش اصلاً تست نشده.
### ۱. بازدید از صفحه — اسکرینشات + خطاها
```bash
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 و پرجدول است و بیشتر مشکلات آنجاست:
```bash
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
```bash
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، `` بدون alt، فیلد بدون label، `id` تکراری، جدول خالی بدون empty-state،
و `