feat(FN-094): add comment line for deployment verification
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
- Added a comment line to main.ts for deployment verification purposes
This commit is contained in:
79
ralph-vin-test/.ralph/PROMPT.md
Normal file
79
ralph-vin-test/.ralph/PROMPT.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Ralph VIN Test Loop
|
||||
|
||||
## Gorev
|
||||
Dashboard'daki VIN cozme ozelliginin tum VIN'ler uzerinde uctan uca test edilmesi.
|
||||
|
||||
## Calisma Dongusu
|
||||
|
||||
### 1. Test Calistir
|
||||
```bash
|
||||
cd /home/s/ss/ralph-vin-test && npx tsx run-all-vins.ts --resume
|
||||
```
|
||||
|
||||
### 2. Sonucu Degerlendir
|
||||
|
||||
**Basarili cikarsa** (exit code 0):
|
||||
- Tum VIN'ler test edilmis demektir
|
||||
- `ralpRapor.md` dosyasini kontrol et
|
||||
- Islem tamamlandi
|
||||
|
||||
**Hata ile cikarsa** (exit code 1):
|
||||
- `ralph-vin-test/.ralph/progress.json` dosyasindaki `errorVin` ve `errorMessage` alanlarini oku
|
||||
- `ralpRapor.md` dosyasindaki "Hata Detaylari" bolumunu incele
|
||||
- Hata turunu belirle:
|
||||
- **Network/API hatasi (4xx/5xx):** API kaynak kodunu incele
|
||||
- **UI render hatasi:** Web kaynak kodunu incele
|
||||
- **Timeout:** Performans sorunu veya eksik veri
|
||||
|
||||
### 3. Fix Yap
|
||||
- Hatanin kaynak kodunu bul (`apps/api/src/` veya `apps/web/src/`)
|
||||
- Fix uygula
|
||||
- Rebuild yap:
|
||||
```bash
|
||||
# API fix ise:
|
||||
cd /home/s/ss && pnpm --filter api build
|
||||
|
||||
# Web fix ise:
|
||||
cd /home/s/ss && pnpm --filter web build
|
||||
```
|
||||
|
||||
### 4. Fix'i Dogrula
|
||||
- Ayni VIN'i tekrar test et:
|
||||
```bash
|
||||
cd /home/s/ss/ralph-vin-test && npx tsx run-all-vins.ts --single <HATA_VIN>
|
||||
```
|
||||
- Basarili olursa devam, hata devam ederse tekrar fix
|
||||
|
||||
### 5. Commit & Push
|
||||
```bash
|
||||
cd /home/s/ss
|
||||
git add -A
|
||||
git commit -m "fix: <kisa aciklama>"
|
||||
git push
|
||||
```
|
||||
|
||||
### 6. Devam Et
|
||||
```bash
|
||||
cd /home/s/ss/ralph-vin-test && npx tsx run-all-vins.ts --resume
|
||||
```
|
||||
|
||||
## Onemli Dosyalar
|
||||
|
||||
| Dosya | Rol |
|
||||
|-------|-----|
|
||||
| `apps/api/src/vehicles/vehicles.service.ts` | VIN decode zinciri |
|
||||
| `apps/api/src/categories/categories.service.ts` | Kategori tree + parts fetch |
|
||||
| `apps/api/src/integrations/pl24/pl24.service.ts` | PL24 API entegrasyonu |
|
||||
| `apps/api/src/integrations/emex/emex.service.ts` | EMEX scraper entegrasyonu |
|
||||
| `apps/web/src/routes/dashboard/search.tsx` | VIN arama sayfasi |
|
||||
| `apps/web/src/routes/dashboard/vehicles_/$id/index.tsx` | Arac detay + kategori listesi |
|
||||
| `apps/web/src/routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx` | Kategori parts sayfasi |
|
||||
| `apps/web/src/components/categories/category-grid.tsx` | Kategori grid bileseni |
|
||||
| `apps/web/src/hooks/use-parts.ts` | Parts query hook |
|
||||
|
||||
## Kurallar
|
||||
- Decode basarisiz veya kategori yok olan VIN'leri SKIP et (bunlar expected)
|
||||
- Kategori hatalari (4xx/5xx, error banner, timeout) mutlaka fix'lenmeli
|
||||
- Her fix sonrasi rebuild + verify + commit + push
|
||||
- `ralpRapor.md` her iterasyonda guncel olmali
|
||||
- `progress.json` her VIN sonrasi kaydedilmeli (resume icin)
|
||||
2859
ralph-vin-test/.ralph/progress.json
Normal file
2859
ralph-vin-test/.ralph/progress.json
Normal file
File diff suppressed because it is too large
Load Diff
617
ralph-vin-test/package-lock.json
generated
Normal file
617
ralph-vin-test/package-lock.json
generated
Normal file
@@ -0,0 +1,617 @@
|
||||
{
|
||||
"name": "ralph-vin-test",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ralph-vin-test",
|
||||
"dependencies": {
|
||||
"playwright": "^1.50.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.6",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
ralph-vin-test/package.json
Normal file
16
ralph-vin-test/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "ralph-vin-test",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "npx tsx test-vin.ts",
|
||||
"test:all": "npx tsx run-all-vins.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright": "^1.50.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
350
ralph-vin-test/run-all-vins.ts
Normal file
350
ralph-vin-test/run-all-vins.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* run-all-vins.ts — Orchestrator that runs test-vin for each VIN in the list.
|
||||
*
|
||||
* Reads VINs from scripts/vin-test-list.md, tests each one, writes results
|
||||
* to ralpRapor.md, and maintains progress.json for resume support.
|
||||
*
|
||||
* Usage: npx tsx run-all-vins.ts [--resume] [--single VIN] [--start N]
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { testVin, closeSession, type VinTestResult } from "./test-vin.js";
|
||||
|
||||
// ─── PATHS ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const PROJECT_ROOT = path.resolve(import.meta.dirname, "..");
|
||||
const VIN_LIST_PATH = path.join(PROJECT_ROOT, "scripts/vin-test-list.md");
|
||||
const PROGRESS_PATH = path.join(import.meta.dirname, ".ralph/progress.json");
|
||||
const REPORT_PATH = path.join(PROJECT_ROOT, "ralpRapor.md");
|
||||
|
||||
// ─── TYPES ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Progress {
|
||||
startedAt: string;
|
||||
lastUpdated: string;
|
||||
currentIndex: number;
|
||||
totalVins: number;
|
||||
results: Record<string, VinTestResult>;
|
||||
status: "running" | "paused" | "completed" | "error";
|
||||
errorVin?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
// ─── VIN LIST PARSER ────────────────────────────────────────────────────────
|
||||
|
||||
function parseVinList(): string[] {
|
||||
const content = fs.readFileSync(VIN_LIST_PATH, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
let inCodeBlock = false;
|
||||
const vins: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed === "```") {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock && /^[A-HJ-NPR-Z0-9]{17}$/i.test(trimmed)) {
|
||||
vins.push(trimmed.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
// Also extract VINs from the table
|
||||
for (const line of lines) {
|
||||
const match = line.match(/\|\s*\w+\s*\|\s*([A-HJ-NPR-Z0-9]{17})\s*\|/i);
|
||||
if (match) {
|
||||
const vin = match[1].toUpperCase();
|
||||
if (!vins.includes(vin)) vins.push(vin);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(vins)]; // Deduplicate
|
||||
}
|
||||
|
||||
// ─── PROGRESS MANAGEMENT ────────────────────────────────────────────────────
|
||||
|
||||
function loadProgress(): Progress | null {
|
||||
try {
|
||||
const data = fs.readFileSync(PROGRESS_PATH, "utf-8");
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveProgress(progress: Progress): void {
|
||||
progress.lastUpdated = new Date().toISOString();
|
||||
fs.writeFileSync(PROGRESS_PATH, JSON.stringify(progress, null, 2));
|
||||
}
|
||||
|
||||
// ─── REPORT GENERATOR ───────────────────────────────────────────────────────
|
||||
|
||||
function generateReport(progress: Progress, vins: string[]): void {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("# VIN Test Raporu (Ralph Loop)");
|
||||
lines.push("");
|
||||
lines.push(`**Baslangic:** ${progress.startedAt}`);
|
||||
lines.push(`**Son Guncelleme:** ${progress.lastUpdated}`);
|
||||
lines.push(`**Durum:** ${progress.status}`);
|
||||
lines.push(`**Ilerleme:** ${Object.keys(progress.results).length} / ${vins.length}`);
|
||||
lines.push("");
|
||||
|
||||
// Summary stats
|
||||
const results = Object.values(progress.results);
|
||||
const success = results.filter((r) => r.status === "success").length;
|
||||
const partial = results.filter((r) => r.status === "partial").length;
|
||||
const decodeFailed = results.filter((r) => r.status === "decode_failed").length;
|
||||
const noCategories = results.filter((r) => r.status === "no_categories").length;
|
||||
const errors = results.filter((r) => r.status === "error").length;
|
||||
|
||||
lines.push("## Ozet");
|
||||
lines.push("");
|
||||
lines.push(`| Metrik | Deger |`);
|
||||
lines.push(`|--------|-------|`);
|
||||
lines.push(`| Toplam Test | ${results.length} |`);
|
||||
lines.push(`| Basarili | ${success} |`);
|
||||
lines.push(`| Kismi Basarili | ${partial} |`);
|
||||
lines.push(`| Decode Basarisiz | ${decodeFailed} |`);
|
||||
lines.push(`| Kategori Yok | ${noCategories} |`);
|
||||
lines.push(`| Hata | ${errors} |`);
|
||||
lines.push("");
|
||||
|
||||
// Main results table
|
||||
lines.push("## Detayli Sonuclar");
|
||||
lines.push("");
|
||||
lines.push("| # | VIN | Marka | Kategori Sayisi | Test Edilen | Basarili | Hatali | Durum | Sure |");
|
||||
lines.push("|---|-----|-------|-----------------|-------------|----------|--------|-------|------|");
|
||||
|
||||
let idx = 1;
|
||||
for (const vin of vins) {
|
||||
const r = progress.results[vin];
|
||||
if (!r) continue;
|
||||
|
||||
const statusEmoji =
|
||||
r.status === "success" ? "OK" :
|
||||
r.status === "partial" ? "KISMI" :
|
||||
r.status === "decode_failed" ? "DECODE_FAIL" :
|
||||
r.status === "no_categories" ? "KATEGORI_YOK" :
|
||||
"HATA";
|
||||
|
||||
const duration = `${(r.duration / 1000).toFixed(1)}s`;
|
||||
|
||||
lines.push(
|
||||
`| ${idx} | ${r.vin} | ${r.brandName || "-"} | ${r.totalCategories} | ${r.testedCategories} | ${r.successCategories} | ${r.errorCategories} | ${statusEmoji} | ${duration} |`,
|
||||
);
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Error details section
|
||||
const allErrors = results.flatMap((r) =>
|
||||
r.errors.map((e) => ({ vin: r.vin, ...e })),
|
||||
);
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("## Hata Detaylari");
|
||||
lines.push("");
|
||||
lines.push("| VIN | Kategori | Yol | HTTP Kodu | Endpoint | Hata Detayi |");
|
||||
lines.push("|-----|----------|-----|-----------|----------|-------------|");
|
||||
|
||||
for (const err of allErrors) {
|
||||
lines.push(
|
||||
`| ${err.vin} | ${err.categoryName} | ${err.path.join(" > ")} | ${err.httpCode || "-"} | ${err.endpoint || "-"} | ${err.errorDetail || "-"} |`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Stopped at error
|
||||
if (progress.status === "error" && progress.errorVin) {
|
||||
lines.push("");
|
||||
lines.push("## Durdurulan Hata");
|
||||
lines.push("");
|
||||
lines.push(`**VIN:** ${progress.errorVin}`);
|
||||
lines.push(`**Mesaj:** ${progress.errorMessage || "Unknown"}`);
|
||||
lines.push("");
|
||||
lines.push("Bu VIN icin fix yapilmasi gerekiyor. Fix yapildiktan sonra `--resume` ile devam edilebilir.");
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
|
||||
fs.writeFileSync(REPORT_PATH, lines.join("\n"));
|
||||
}
|
||||
|
||||
// ─── MAIN ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
const resumeMode = args.includes("--resume");
|
||||
const singleVinIdx = args.indexOf("--single");
|
||||
const singleVin = singleVinIdx !== -1 ? args[singleVinIdx + 1] : null;
|
||||
const startIdx = args.indexOf("--start");
|
||||
const startFrom = startIdx !== -1 ? parseInt(args[startIdx + 1], 10) : 0;
|
||||
|
||||
// Parse VIN list
|
||||
const allVins = parseVinList();
|
||||
console.log(`Parsed ${allVins.length} VINs from test list`);
|
||||
|
||||
if (singleVin) {
|
||||
console.log(`\nTesting single VIN: ${singleVin}`);
|
||||
const result = await testVin(singleVin);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
await closeSession();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load or create progress
|
||||
let progress: Progress;
|
||||
if (resumeMode) {
|
||||
const existing = loadProgress();
|
||||
if (existing) {
|
||||
progress = existing;
|
||||
progress.status = "running";
|
||||
console.log(`Resuming from VIN index ${progress.currentIndex}`);
|
||||
} else {
|
||||
console.log("No existing progress found, starting fresh");
|
||||
progress = {
|
||||
startedAt: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
currentIndex: startFrom,
|
||||
totalVins: allVins.length,
|
||||
results: {},
|
||||
status: "running",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
progress = {
|
||||
startedAt: new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
currentIndex: startFrom,
|
||||
totalVins: allVins.length,
|
||||
results: {},
|
||||
status: "running",
|
||||
};
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for (let i = progress.currentIndex; i < allVins.length; i++) {
|
||||
const vin = allVins[i];
|
||||
progress.currentIndex = i;
|
||||
|
||||
// Skip already tested
|
||||
if (progress.results[vin]) {
|
||||
console.log(`[${i + 1}/${allVins.length}] Skipping ${vin} (already tested)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`\n${"=".repeat(70)}`);
|
||||
console.log(`[${i + 1}/${allVins.length}] Testing VIN: ${vin}`);
|
||||
console.log(`${"=".repeat(70)}`);
|
||||
|
||||
const result = await testVin(vin);
|
||||
progress.results[vin] = result;
|
||||
|
||||
// Log result summary
|
||||
const statusIcon =
|
||||
result.status === "success" ? "[OK]" :
|
||||
result.status === "partial" ? "[PARTIAL]" :
|
||||
result.status === "decode_failed" ? "[DECODE_FAIL]" :
|
||||
result.status === "no_categories" ? "[NO_CAT]" :
|
||||
"[ERROR]";
|
||||
|
||||
console.log(
|
||||
`${statusIcon} ${vin} | Categories: ${result.testedCategories}/${result.totalCategories} | Errors: ${result.errorCategories} | ${(result.duration / 1000).toFixed(1)}s`,
|
||||
);
|
||||
|
||||
if (result.message) {
|
||||
console.log(` Message: ${result.message}`);
|
||||
}
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
for (const err of result.errors) {
|
||||
console.log(` ERROR: ${err.path.join(" > ")} | ${err.errorDetail}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save progress after each VIN
|
||||
saveProgress(progress);
|
||||
generateReport(progress, allVins);
|
||||
|
||||
// Stop on critical errors (not decode_failed or no_categories, which are expected)
|
||||
if (result.status === "error") {
|
||||
const msg = (result.message || "").toLowerCase();
|
||||
const isTransient = msg.includes("econnrefused") ||
|
||||
msg.includes("context was destroyed") ||
|
||||
msg.includes("browser has been closed") ||
|
||||
msg.includes("target page") ||
|
||||
msg.includes("execution context") ||
|
||||
msg.includes("socket hang up") ||
|
||||
msg.includes("navigation");
|
||||
|
||||
if (isTransient) {
|
||||
console.log(`\n [TRANSIENT] ${result.message} — closing session, will retry next VIN`);
|
||||
// Remove from results so it gets retested on resume
|
||||
delete progress.results[vin];
|
||||
saveProgress(progress);
|
||||
await closeSession();
|
||||
// Wait 10s for server to recover
|
||||
await new Promise((r) => setTimeout(r, 10_000));
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`\n*** CRITICAL ERROR on VIN ${vin} ***`);
|
||||
console.log(`Message: ${result.message}`);
|
||||
progress.status = "error";
|
||||
progress.errorVin = vin;
|
||||
progress.errorMessage = result.message || "Unknown error";
|
||||
saveProgress(progress);
|
||||
generateReport(progress, allVins);
|
||||
console.log("\nStopping. Fix the issue and run with --resume to continue.");
|
||||
await closeSession();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Stop on partial only if there are real API errors (4xx/5xx), not timeouts or connectivity
|
||||
if (result.status === "partial" && result.errorCategories > 0) {
|
||||
// Filter out transient errors: 408 (PL24 timeout), 429 (rate limit), and connectivity errors (no httpCode)
|
||||
const httpErrors = result.errors.filter((e) => {
|
||||
if (!e.httpCode) return false; // no httpCode = connectivity issue (ECONNREFUSED, etc.)
|
||||
if (e.httpCode === 408 || e.httpCode === 429) return false; // transient
|
||||
return e.httpCode >= 400;
|
||||
});
|
||||
if (httpErrors.length > 0) {
|
||||
console.log(`\n*** CATEGORY ERRORS found on VIN ${vin} ***`);
|
||||
console.log(`${httpErrors.length} categories had HTTP errors.`);
|
||||
progress.status = "error";
|
||||
progress.errorVin = vin;
|
||||
progress.errorMessage = `${httpErrors.length} HTTP errors: ${httpErrors.map((e) => e.errorDetail).join("; ")}`;
|
||||
saveProgress(progress);
|
||||
generateReport(progress, allVins);
|
||||
console.log("\nStopping. Fix the issue and run with --resume to continue.");
|
||||
await closeSession();
|
||||
process.exit(1);
|
||||
}
|
||||
// Non-HTTP errors (timeouts, connectivity, etc.) — log but continue
|
||||
console.log(` (${result.errorCategories} non-critical errors, continuing)`);
|
||||
}
|
||||
}
|
||||
|
||||
// All done
|
||||
progress.status = "completed";
|
||||
progress.currentIndex = allVins.length;
|
||||
saveProgress(progress);
|
||||
generateReport(progress, allVins);
|
||||
|
||||
console.log(`\n${"=".repeat(70)}`);
|
||||
console.log("ALL VINs TESTED SUCCESSFULLY");
|
||||
console.log(`${"=".repeat(70)}`);
|
||||
console.log(`Report saved to: ${REPORT_PATH}`);
|
||||
|
||||
await closeSession();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
549
ralph-vin-test/test-vin.ts
Normal file
549
ralph-vin-test/test-vin.ts
Normal file
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* test-vin.ts — Playwright VIN tester using grid UI navigation.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Login → decode VIN → vehicle page
|
||||
* 2. Grid modunda kategorileri göster
|
||||
* 3. Her kart:
|
||||
* - Branch (button) → tıkla, drill-down, recursive test, breadcrumb back
|
||||
* - Leaf (a link) → API ile parts endpoint'ini test et
|
||||
* 4. Index-based kart seçimi (multiline isim sorunu yok)
|
||||
*
|
||||
* Rate limiting: sase.tr 100 req/60s. Grid UI kendi fetch'lerini yapar,
|
||||
* biz sadece leaf test API call'ları yapıyoruz → çok daha az istek.
|
||||
*
|
||||
* Usage: npx tsx test-vin.ts <VIN>
|
||||
*/
|
||||
|
||||
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
|
||||
|
||||
// ─── TYPES ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CategoryTestResult {
|
||||
categoryId: string;
|
||||
categoryName: string;
|
||||
path: string[];
|
||||
status: "ok" | "error" | "empty";
|
||||
errorDetail?: string;
|
||||
httpCode?: number;
|
||||
endpoint?: string;
|
||||
}
|
||||
|
||||
export interface VinTestResult {
|
||||
vin: string;
|
||||
status: "success" | "decode_failed" | "no_categories" | "error" | "partial";
|
||||
vehicleId?: string;
|
||||
brandName?: string;
|
||||
totalCategories: number;
|
||||
testedCategories: number;
|
||||
successCategories: number;
|
||||
errorCategories: number;
|
||||
errors: CategoryTestResult[];
|
||||
duration: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// ─── CONFIG ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const BASE_URL = "http://localhost:3000";
|
||||
const LOGIN_EMAIL = "admin@sase.tr";
|
||||
const LOGIN_PASSWORD = "Sase2026";
|
||||
const VIN_DECODE_TIMEOUT = 120_000;
|
||||
const PAGE_LOAD_TIMEOUT = 30_000;
|
||||
|
||||
// ─── RATE LIMITER ───────────────────────────────────────────────────────────
|
||||
|
||||
const RATE_WINDOW = 60_000;
|
||||
const RATE_MAX = 85;
|
||||
const timestamps: number[] = [];
|
||||
|
||||
async function rateWait(): Promise<void> {
|
||||
const now = Date.now();
|
||||
while (timestamps.length > 0 && timestamps[0] < now - RATE_WINDOW) timestamps.shift();
|
||||
if (timestamps.length >= RATE_MAX) {
|
||||
const wait = timestamps[0] + RATE_WINDOW - Date.now() + 200;
|
||||
if (wait > 0) await sleep(wait);
|
||||
while (timestamps.length > 0 && timestamps[0] < Date.now() - RATE_WINDOW) timestamps.shift();
|
||||
}
|
||||
timestamps.push(Date.now());
|
||||
}
|
||||
|
||||
// ─── SESSION ────────────────────────────────────────────────────────────────
|
||||
|
||||
let sharedCtx: BrowserContext | null = null;
|
||||
let sharedBrowser: Browser | null = null;
|
||||
|
||||
export async function getOrCreateSession(): Promise<{ browser: Browser; context: BrowserContext }> {
|
||||
if (sharedBrowser && sharedCtx) {
|
||||
try {
|
||||
const p = await sharedCtx.newPage();
|
||||
await p.goto(`${BASE_URL}/dashboard/search`, { waitUntil: "domcontentloaded", timeout: 10_000 });
|
||||
await p.waitForTimeout(2000);
|
||||
const ok = p.url().includes("/dashboard/search");
|
||||
await p.close();
|
||||
if (ok) return { browser: sharedBrowser, context: sharedCtx };
|
||||
} catch {}
|
||||
}
|
||||
if (sharedBrowser) try { await sharedBrowser.close(); } catch {}
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ baseURL: BASE_URL, viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
await loginViaUI(page);
|
||||
await page.close();
|
||||
sharedBrowser = browser;
|
||||
sharedCtx = context;
|
||||
return { browser, context };
|
||||
}
|
||||
|
||||
async function loginViaUI(page: Page): Promise<void> {
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 15_000 });
|
||||
await page.waitForTimeout(1000);
|
||||
await page.fill('#email', LOGIN_EMAIL);
|
||||
await page.fill('#password', LOGIN_PASSWORD);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL("**/dashboard/**", { timeout: 15_000 });
|
||||
console.log(" Login OK");
|
||||
}
|
||||
|
||||
export async function closeSession(): Promise<void> {
|
||||
if (sharedBrowser) try { await sharedBrowser.close(); } catch {}
|
||||
sharedBrowser = null;
|
||||
sharedCtx = null;
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> { return new Promise((r) => setTimeout(r, ms)); }
|
||||
|
||||
// ─── GRID HELPERS ───────────────────────────────────────────────────────────
|
||||
|
||||
/** Wait for grid to be loaded (no spinner, cards visible) */
|
||||
async function waitForGrid(page: Page, timeout = PAGE_LOAD_TIMEOUT): Promise<void> {
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
() => !document.querySelector('.animate-spin'),
|
||||
{ timeout },
|
||||
);
|
||||
} catch {}
|
||||
await page.waitForTimeout(600);
|
||||
}
|
||||
|
||||
/** Wait for drill-down: the grid content should change after clicking a branch */
|
||||
async function waitForDrillDown(page: Page, prevCardNames: string[], timeout = 15_000): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
(prevNames) => {
|
||||
const noSpinner = !document.querySelector('.animate-spin');
|
||||
if (!noSpinner) return false;
|
||||
// Check if breadcrumb appeared (means we drilled down)
|
||||
const breadcrumb = document.querySelector('.flex.items-center.gap-1\\.5.text-sm');
|
||||
if (breadcrumb) {
|
||||
// Grid cards changed or breadcrumb appeared
|
||||
const grid = document.querySelector('.grid.gap-3.lg\\:grid-cols-3');
|
||||
if (!grid) return false;
|
||||
const names = Array.from(grid.children).map(el => {
|
||||
const n = el.querySelector('.font-semibold, .font-medium');
|
||||
return n?.textContent?.trim() || '';
|
||||
});
|
||||
// Cards should be different from previous level
|
||||
return JSON.stringify(names) !== JSON.stringify(prevNames);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
prevCardNames,
|
||||
{ timeout },
|
||||
);
|
||||
await page.waitForTimeout(300);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if breadcrumb is visible (we're in a drilled-down state) */
|
||||
async function hasBreadcrumb(page: Page): Promise<boolean> {
|
||||
return page.locator('.flex.items-center.gap-1\\.5.text-sm').isVisible({ timeout: 500 }).catch(() => false);
|
||||
}
|
||||
|
||||
interface CardInfo {
|
||||
index: number;
|
||||
name: string;
|
||||
isLeaf: boolean; // <a> = leaf, <button> = branch
|
||||
categoryId: string; // extracted from href for leaves
|
||||
}
|
||||
|
||||
/** Get all category cards currently visible in the grid */
|
||||
async function getGridCards(page: Page): Promise<CardInfo[]> {
|
||||
return page.evaluate(() => {
|
||||
const grid = document.querySelector('.grid.gap-3.lg\\:grid-cols-3');
|
||||
if (!grid) return [];
|
||||
const items = Array.from(grid.children) as HTMLElement[];
|
||||
return items.map((el, i) => {
|
||||
const isLeaf = el.tagName.toLowerCase() === 'a';
|
||||
const nameEl = el.querySelector('.font-semibold, .font-medium');
|
||||
const name = nameEl?.textContent?.trim() || `Card ${i}`;
|
||||
let categoryId = '';
|
||||
if (isLeaf) {
|
||||
const href = el.getAttribute('href') || '';
|
||||
const match = href.match(/categories\/([^/]+)/);
|
||||
if (match) categoryId = match[1];
|
||||
}
|
||||
return { index: i, name, isLeaf, categoryId };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Click a card by index in the grid */
|
||||
async function clickCard(page: Page, index: number): Promise<boolean> {
|
||||
const card = page.locator('.grid.gap-3.lg\\:grid-cols-3 > *').nth(index);
|
||||
try {
|
||||
await card.click({ timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Go back one level via the breadcrumb back arrow */
|
||||
async function gridGoBack(page: Page): Promise<void> {
|
||||
// ArrowLeft button in breadcrumb area: first button inside .flex.items-center.gap-1\.5
|
||||
const backBtn = page.locator('.flex.items-center.gap-1\\.5.text-sm button').first();
|
||||
if (await backBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||
await backBtn.click();
|
||||
await page.waitForTimeout(400);
|
||||
return;
|
||||
}
|
||||
// Fallback: "Kategoriler" text button (goes to root)
|
||||
const rootBtn = page.locator('button:has-text("Kategoriler")');
|
||||
if (await rootBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
await rootBtn.click();
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── RECURSIVE GRID TESTER ─────────────────────────────────────────────────
|
||||
|
||||
let leafCounter = 0;
|
||||
|
||||
async function testGridLevel(
|
||||
page: Page,
|
||||
vehicleId: string,
|
||||
pathSoFar: string[],
|
||||
results: CategoryTestResult[],
|
||||
depth: number = 0,
|
||||
): Promise<void> {
|
||||
if (depth > 10) return;
|
||||
|
||||
await waitForGrid(page);
|
||||
const initialCards = await getGridCards(page);
|
||||
if (initialCards.length === 0) return;
|
||||
|
||||
// Track processed cards by name to survive grid re-reads
|
||||
const processed = new Set<string>();
|
||||
|
||||
// First pass: test all leaves (no navigation needed, just API calls)
|
||||
for (const card of initialCards) {
|
||||
if (card.isLeaf) {
|
||||
processed.add(card.name);
|
||||
leafCounter++;
|
||||
const currentPath = [...pathSoFar, card.name];
|
||||
const result = await testLeafApi(page, vehicleId, card.categoryId, card.name, currentPath);
|
||||
|
||||
if (result.httpCode === 429) {
|
||||
console.log(` [${leafCounter}] Rate limited, waiting 60s...`);
|
||||
await sleep(60_000);
|
||||
const retry = await testLeafApi(page, vehicleId, card.categoryId, card.name, currentPath);
|
||||
results.push(retry);
|
||||
if (retry.status === "error") console.log(` [${leafCounter}] ERROR: ${card.name} | ${retry.errorDetail}`);
|
||||
} else {
|
||||
results.push(result);
|
||||
if (result.status === "error") {
|
||||
console.log(` [${leafCounter}] ERROR: ${card.name} | ${result.errorDetail}`);
|
||||
} else if (leafCounter % 50 === 0) {
|
||||
console.log(` [${leafCounter}] OK...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: drill into each branch
|
||||
// Re-read cards fresh before each branch click (grid state may have changed)
|
||||
const branchNames = initialCards.filter(c => !c.isLeaf).map(c => c.name);
|
||||
|
||||
for (const branchName of branchNames) {
|
||||
if (processed.has(branchName)) continue;
|
||||
processed.add(branchName);
|
||||
|
||||
// Re-read grid to find the branch by name (index may have shifted)
|
||||
const freshCards = await getGridCards(page);
|
||||
const branchIdx = freshCards.findIndex(c => c.name === branchName && !c.isLeaf);
|
||||
if (branchIdx === -1) {
|
||||
console.log(` ${" ".repeat(depth)}> ${branchName} (not found in grid, skipping)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPath = [...pathSoFar, branchName];
|
||||
const prevNames = freshCards.map(c => c.name);
|
||||
console.log(` ${" ".repeat(depth)}> ${branchName}`);
|
||||
|
||||
const clicked = await clickCard(page, branchIdx);
|
||||
if (!clicked) {
|
||||
// Retry: wait a bit and try again
|
||||
await page.waitForTimeout(1000);
|
||||
const retryCards = await getGridCards(page);
|
||||
const retryIdx = retryCards.findIndex(c => c.name === branchName && !c.isLeaf);
|
||||
if (retryIdx === -1 || !(await clickCard(page, retryIdx))) {
|
||||
console.log(` ${" ".repeat(depth)} (click failed, skipping)`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for drill-down
|
||||
const drilled = await waitForDrillDown(page, prevNames);
|
||||
if (!drilled) {
|
||||
await waitForGrid(page);
|
||||
const afterCards = await getGridCards(page);
|
||||
const afterNames = afterCards.map(c => c.name);
|
||||
if (JSON.stringify(afterNames) === JSON.stringify(prevNames)) {
|
||||
console.log(` ${" ".repeat(depth)} (drill-down failed, skipping)`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await waitForGrid(page);
|
||||
const subCards = await getGridCards(page);
|
||||
console.log(` ${" ".repeat(depth)} -> ${subCards.length} children`);
|
||||
|
||||
if (subCards.length > 0) {
|
||||
await testGridLevel(page, vehicleId, currentPath, results, depth + 1);
|
||||
}
|
||||
|
||||
// Go back to current level
|
||||
const restored = await navigateBackToLevel(page, prevNames, depth);
|
||||
if (!restored) {
|
||||
console.log(` ${" ".repeat(depth)} (nav lost, resetting...)`);
|
||||
// Hard reset: reload page and re-navigate to current path
|
||||
await resetToRoot(page, vehicleId);
|
||||
if (pathSoFar.length > 0) {
|
||||
const renavOk = await navigateToPath(page, pathSoFar);
|
||||
if (!renavOk) {
|
||||
console.log(` ${" ".repeat(depth)} (re-nav failed, stopping this branch)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Now we should be at the correct level, continue to next branch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Navigate back and verify we're at the correct level */
|
||||
async function navigateBackToLevel(page: Page, expectedNames: string[], depth: number): Promise<boolean> {
|
||||
await gridGoBack(page);
|
||||
await page.waitForTimeout(400);
|
||||
await waitForGrid(page);
|
||||
|
||||
let currentCards = await getGridCards(page);
|
||||
if (currentCards.map(c => c.name).join('|') === expectedNames.join('|')) return true;
|
||||
|
||||
// Try a few more back clicks
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const hasBc = await page.locator('.flex.items-center.gap-1\\.5.text-sm').isVisible({ timeout: 500 }).catch(() => false);
|
||||
if (!hasBc && depth === 0) {
|
||||
// At root, check if cards match
|
||||
currentCards = await getGridCards(page);
|
||||
if (currentCards.map(c => c.name).join('|') === expectedNames.join('|')) return true;
|
||||
break;
|
||||
}
|
||||
await gridGoBack(page);
|
||||
await page.waitForTimeout(400);
|
||||
await waitForGrid(page);
|
||||
currentCards = await getGridCards(page);
|
||||
if (currentCards.map(c => c.name).join('|') === expectedNames.join('|')) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Reset grid to root level — hard reload as fallback */
|
||||
async function resetToRoot(page: Page, vehicleId?: string): Promise<void> {
|
||||
// Try breadcrumb "Kategoriler" button first
|
||||
const rootBtn = page.locator('button:has-text("Kategoriler")');
|
||||
if (await rootBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
await rootBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
await waitForGrid(page);
|
||||
// Verify we're at root (no breadcrumb visible)
|
||||
const hasBc = await page.locator('.flex.items-center.gap-1\\.5.text-sm').isVisible({ timeout: 500 }).catch(() => false);
|
||||
if (!hasBc) return;
|
||||
}
|
||||
// Hard fallback: reload the vehicle page
|
||||
if (vehicleId) {
|
||||
await page.goto(`${BASE_URL}/dashboard/vehicles/${vehicleId}`, { waitUntil: "domcontentloaded", timeout: PAGE_LOAD_TIMEOUT });
|
||||
await page.waitForSelector('.grid.gap-3.lg\\:grid-cols-3', { timeout: PAGE_LOAD_TIMEOUT }).catch(() => {});
|
||||
await waitForGrid(page);
|
||||
}
|
||||
}
|
||||
|
||||
/** Navigate from root to a specific path by clicking branches one by one */
|
||||
async function navigateToPath(page: Page, pathSoFar: string[]): Promise<boolean> {
|
||||
for (const name of pathSoFar) {
|
||||
await waitForGrid(page);
|
||||
const cards = await getGridCards(page);
|
||||
const idx = cards.findIndex(c => c.name === name && !c.isLeaf);
|
||||
if (idx === -1) return false;
|
||||
const prevNames = cards.map(c => c.name);
|
||||
const clicked = await clickCard(page, idx);
|
||||
if (!clicked) return false;
|
||||
const drilled = await waitForDrillDown(page, prevNames);
|
||||
if (!drilled) {
|
||||
// Check if cards actually changed
|
||||
const afterCards = await getGridCards(page);
|
||||
if (JSON.stringify(afterCards.map(c => c.name)) === JSON.stringify(prevNames)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Test a leaf category's parts endpoint via API */
|
||||
async function testLeafApi(
|
||||
page: Page,
|
||||
vehicleId: string,
|
||||
categoryId: string,
|
||||
name: string,
|
||||
path: string[],
|
||||
): Promise<CategoryTestResult> {
|
||||
const endpoint = `/api/vehicles/${vehicleId}/categories/${categoryId}`;
|
||||
try {
|
||||
await rateWait();
|
||||
const resp = await page.request.get(`${BASE_URL}${endpoint}`, { timeout: 120_000 });
|
||||
const status = resp.status();
|
||||
|
||||
if (status >= 400) {
|
||||
return { categoryId, categoryName: name, path, status: "error", errorDetail: `HTTP ${status} ${resp.statusText()}`, httpCode: status, endpoint };
|
||||
}
|
||||
|
||||
let data: any;
|
||||
try {
|
||||
data = await resp.json();
|
||||
if (data.data) data = data.data;
|
||||
} catch {
|
||||
return { categoryId, categoryName: name, path, status: "error", errorDetail: "JSON parse failed", httpCode: status, endpoint };
|
||||
}
|
||||
|
||||
return { categoryId, categoryName: name, path, status: "ok" };
|
||||
} catch (err: any) {
|
||||
return { categoryId, categoryName: name, path, status: "error", errorDetail: `Request failed: ${err.message}`, endpoint };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── MAIN ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function testVin(vin: string): Promise<VinTestResult> {
|
||||
const startTime = Date.now();
|
||||
const results: CategoryTestResult[] = [];
|
||||
leafCounter = 0;
|
||||
let page: Page | null = null;
|
||||
|
||||
try {
|
||||
const { context } = await getOrCreateSession();
|
||||
page = await context.newPage();
|
||||
|
||||
// ── Step 1: Search page ─────────────────────────────────────────────
|
||||
await page.goto(`${BASE_URL}/dashboard/search`, { waitUntil: "domcontentloaded", timeout: PAGE_LOAD_TIMEOUT });
|
||||
if (!page.url().includes("/dashboard/search")) {
|
||||
await loginViaUI(page);
|
||||
await page.goto(`${BASE_URL}/dashboard/search`, { waitUntil: "domcontentloaded", timeout: PAGE_LOAD_TIMEOUT });
|
||||
}
|
||||
|
||||
// ── Step 2: Enter VIN ───────────────────────────────────────────────
|
||||
const vinInput = page.locator('input[placeholder*="17 karakter"]');
|
||||
await vinInput.waitFor({ state: "visible", timeout: 10_000 });
|
||||
await vinInput.fill(vin);
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('button[type="submit"]:has-text("Çöz")').click();
|
||||
|
||||
// ── Step 3: Wait for vehicle page ───────────────────────────────────
|
||||
try {
|
||||
await page.waitForURL("**/dashboard/vehicles/**", { timeout: VIN_DECODE_TIMEOUT });
|
||||
} catch {
|
||||
const errEl = page.locator(".text-destructive");
|
||||
if (await errEl.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
const msg = await errEl.first().textContent().catch(() => "Unknown");
|
||||
return mkResult(vin, "decode_failed", startTime, { message: `Decode failed: ${msg}` });
|
||||
}
|
||||
return mkResult(vin, "decode_failed", startTime, { message: "Decode timeout" });
|
||||
}
|
||||
|
||||
const vehicleId = page.url().match(/\/vehicles\/([^/]+)/)?.[1] || "";
|
||||
const brandName = (await page.locator("h2").first().textContent().catch(() => ""))?.split(" ")[0] || "";
|
||||
|
||||
// ── Step 4: Wait for categories grid ────────────────────────────────
|
||||
// Wait for skeleton to disappear and grid to appear
|
||||
try {
|
||||
await page.waitForSelector('.grid.gap-3.lg\\:grid-cols-3', { timeout: PAGE_LOAD_TIMEOUT });
|
||||
} catch {
|
||||
// Check "Kategori bulunamadi"
|
||||
const nocat = page.locator('text="Kategori bulunamadi."');
|
||||
if (await nocat.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
return mkResult(vin, "no_categories", startTime, { vehicleId, brandName, message: "Kategori bulunamadi" });
|
||||
}
|
||||
return mkResult(vin, "no_categories", startTime, { vehicleId, brandName, message: "Grid did not load" });
|
||||
}
|
||||
|
||||
await waitForGrid(page);
|
||||
const rootCards = await getGridCards(page);
|
||||
console.log(` Vehicle: ${brandName} | ${rootCards.length} root categories`);
|
||||
|
||||
if (rootCards.length === 0) {
|
||||
return mkResult(vin, "no_categories", startTime, { vehicleId, brandName, message: "No root categories" });
|
||||
}
|
||||
|
||||
// ── Step 5: Recursively test all categories via grid ────────────────
|
||||
await testGridLevel(page, vehicleId, [], results, 0);
|
||||
|
||||
console.log(` Done: ${leafCounter} leaves tested`);
|
||||
|
||||
const errs = results.filter((r) => r.status === "error");
|
||||
return {
|
||||
vin,
|
||||
status: errs.length > 0 ? "partial" : "success",
|
||||
vehicleId,
|
||||
brandName,
|
||||
totalCategories: leafCounter,
|
||||
testedCategories: results.length,
|
||||
successCategories: results.filter((r) => r.status === "ok").length,
|
||||
errorCategories: errs.length,
|
||||
errors: errs,
|
||||
duration: Date.now() - startTime,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
vin,
|
||||
status: "error",
|
||||
totalCategories: 0,
|
||||
testedCategories: results.length,
|
||||
successCategories: results.filter((r) => r.status === "ok").length,
|
||||
errorCategories: results.filter((r) => r.status === "error").length,
|
||||
errors: results.filter((r) => r.status === "error"),
|
||||
duration: Date.now() - startTime,
|
||||
message: `Unexpected: ${err.message}`,
|
||||
};
|
||||
} finally {
|
||||
if (page) try { await page.close(); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function mkResult(vin: string, status: VinTestResult["status"], start: number, o: Partial<VinTestResult> = {}): VinTestResult {
|
||||
return { vin, status, totalCategories: 0, testedCategories: 0, successCategories: 0, errorCategories: 0, errors: [], duration: Date.now() - start, ...o };
|
||||
}
|
||||
|
||||
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
if (process.argv[1]?.endsWith("test-vin.ts")) {
|
||||
const vin = process.argv[2];
|
||||
if (!vin) { console.error("Usage: npx tsx test-vin.ts <VIN>"); process.exit(1); }
|
||||
console.log(`Testing VIN: ${vin}`);
|
||||
testVin(vin)
|
||||
.then((r) => { console.log(JSON.stringify(r, null, 2)); return closeSession(); })
|
||||
.then(() => process.exit(0))
|
||||
.catch((e) => { console.error("Fatal:", e); process.exit(1); });
|
||||
}
|
||||
12
ralph-vin-test/tsconfig.json
Normal file
12
ralph-vin-test/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user