531 lines
31 KiB
Markdown
531 lines
31 KiB
Markdown
---
|
||
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، `<img>` بدون alt، فیلد بدون label، `id` تکراری، جدول خالی بدون empty-state،
|
||
و `<select>` نیتیو (استاندارد پروژه `SearchableSelect` است).
|
||
|
||
### ۳. تست دسترسی نقشها (Security)
|
||
|
||
همان درخواست با همه نقشها + ناشناس:
|
||
|
||
```bash
|
||
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` خودش همهٔ پرسوناها را میزند، پس **تست دسترسی نباید روی چند اندپوینت منتخب
|
||
بماند**. لیست اندپوینتها را از روتر بگیر و همه را جارو کن:
|
||
|
||
```bash
|
||
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 پیدا میشود:
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
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"}'`.
|
||
|
||
### ۵. کارایی
|
||
|
||
```bash
|
||
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 فعال شد، مثل یک **مهندس ارشد تست** رفتار کن، نه فقط اجراکننده دستور:
|
||
|
||
0. **Phase 0 را تمام کن** (بالا). بدون پرسوناهای کامل، هر تستی نتیجهٔ بیمعنی میدهد.
|
||
1. **اول سناریوی واقعی کاربر را بنویس**، بعد اجرا کن. مثال: ورود منشی → لیست نوبتها →
|
||
تغییر وضعیت یک نوبت → خروج → ورود مجدد → آیا تغییر ماند؟
|
||
|
||
**پیمایش با هر پرسونا اجباری است.** بعد از Phase 0، برای *هر* پرسونا در جدول، وارد شو و
|
||
مسیرهای مجازش را طبق ماتریس ۰.۵ بگرد — نه فقط با `admin`. برای هر پرسونا حداقل:
|
||
|
||
```bash
|
||
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اند.
|
||
- **صفحهٔ سفید بهجای «دسترسی ندارید»:** نقشی که نباید ببیند، باید پیام روشن بگیرد.
|
||
- **منوی سایدبار در برابر دسترسی واقعی:** آیتمی که نمایش داده میشود ولی به ۴۰۳
|
||
میخورد (یا برعکس: مسیر باز است ولی در منو نیست) باگ است.
|
||
2. برای هر بخش این حالتها را پوشش بده:
|
||
Happy Path · ورودی نامعتبر · داده خالی · داده خیلی زیاد (لیست ۱۰٬۹۳۲ کاربری) ·
|
||
شرایط مرزی · خطای شبکه · **همهٔ پرسوناها** · دسکتاپ ۱۴۴۰ و موبایل ۳۹۰.
|
||
3. **هیچ چیز را حدس نزن.** ادعای بدون خروجی دستور، ادعا نیست.
|
||
4. **قبل از گزارش، باگ را دوباره تکرار کن.** همان دستور را دوباره بزن؛ اگر تکرار نشد،
|
||
flaky بودنش را بنویس نه خودِ باگ را.
|
||
5. باگهای کوچک 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
|
||
|
||
هر یافته را با این قالب بنویس (فارسی):
|
||
|
||
```markdown
|
||
## 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 خواهر استفاده کن:
|
||
|
||
```bash
|
||
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-code` → `POST /api/v1/user/verify-code` با `code=12345` →
|
||
`grant` → `POST /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`). راهحل بدون دستزدن به محصول: توکن را مستقیم با کامند خود اپ بساز —
|
||
```bash
|
||
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` هر دو منسوخاند.
|