test(e2e): complete and verify 11 E2E test suites with strict mutation testing and CI integration
This commit is contained in:
parent
6ef293ea59
commit
e6018f3a37
45
.gitea/workflows/e2e.yml
Normal file
45
.gitea/workflows/e2e.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: E2E Playwright Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-e2e:
|
||||||
|
name: Run Full E2E & Security Suites
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install Monorepo Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Run E2E Test Suite
|
||||||
|
run: npx playwright test --project=chromium-desktop --project=admin-chromium
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
NODE_ENV: test
|
||||||
|
|
||||||
|
- name: Upload Test Report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 14
|
||||||
@ -169,6 +169,7 @@ export default function B2BManager() {
|
|||||||
{/* Tab Toggle */}
|
{/* Tab Toggle */}
|
||||||
<div className="flex items-center gap-2 bg-white p-1.5 rounded-2xl border border-gray-200 shadow-sm">
|
<div className="flex items-center gap-2 bg-white p-1.5 rounded-2xl border border-gray-200 shadow-sm">
|
||||||
<button
|
<button
|
||||||
|
data-testid="b2b-inquiries-tab"
|
||||||
onClick={() => setActiveTab('inquiries')}
|
onClick={() => setActiveTab('inquiries')}
|
||||||
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all ${
|
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all ${
|
||||||
activeTab === 'inquiries' ? 'bg-purple-600 text-white shadow-sm' : 'text-gray-600 hover:bg-gray-50'
|
activeTab === 'inquiries' ? 'bg-purple-600 text-white shadow-sm' : 'text-gray-600 hover:bg-gray-50'
|
||||||
@ -177,6 +178,7 @@ export default function B2BManager() {
|
|||||||
درخواستهای دریافت نمایندگی ({inquiries.length})
|
درخواستهای دریافت نمایندگی ({inquiries.length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
data-testid="b2b-partners-tab"
|
||||||
onClick={() => setActiveTab('partners')}
|
onClick={() => setActiveTab('partners')}
|
||||||
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all ${
|
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all ${
|
||||||
activeTab === 'partners' ? 'bg-purple-600 text-white shadow-sm' : 'text-gray-600 hover:bg-gray-50'
|
activeTab === 'partners' ? 'bg-purple-600 text-white shadow-sm' : 'text-gray-600 hover:bg-gray-50'
|
||||||
|
|||||||
@ -750,7 +750,7 @@ export default function Blogs() {
|
|||||||
<Button variant="secondary" size="sm" type="button" onClick={closeModal}>
|
<Button variant="secondary" size="sm" type="button" onClick={closeModal}>
|
||||||
انصراف
|
انصراف
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" size="sm" type="submit" form="blogMainForm">
|
<Button data-testid="blog-save-btn" variant="primary" size="sm" type="submit" form="blogMainForm">
|
||||||
ذخیره مقاله
|
ذخیره مقاله
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -794,6 +794,7 @@ export default function Blogs() {
|
|||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-xs font-black text-gray-700">عنوان اصلی مقاله *</label>
|
<label className="text-xs font-black text-gray-700">عنوان اصلی مقاله *</label>
|
||||||
<input
|
<input
|
||||||
|
data-testid="blog-title-input"
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.title}
|
value={formData.title}
|
||||||
|
|||||||
@ -774,6 +774,7 @@ export default function Orders() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6">
|
<td className="py-4 px-6">
|
||||||
<select
|
<select
|
||||||
|
data-testid="order-status-select"
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
value={order.status}
|
value={order.status}
|
||||||
onChange={(e) => handleUpdateStatus(order.id, e.target.value)}
|
onChange={(e) => handleUpdateStatus(order.id, e.target.value)}
|
||||||
|
|||||||
@ -110,6 +110,7 @@ const ArchiveProductCard: React.FC<{ product: Product }> = ({ product }) => {
|
|||||||
className="h-full"
|
className="h-full"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
|
data-testid="archive-product-card"
|
||||||
href={productUrl}
|
href={productUrl}
|
||||||
className="group bg-white rounded-[2.5rem] border border-medical-gray-200 overflow-hidden hover:shadow-2xl transition-all flex flex-col h-full relative"
|
className="group bg-white rounded-[2.5rem] border border-medical-gray-200 overflow-hidden hover:shadow-2xl transition-all flex flex-col h-full relative"
|
||||||
>
|
>
|
||||||
@ -294,8 +295,14 @@ export default function ArchivePage({
|
|||||||
return labels;
|
return labels;
|
||||||
}, [categories]);
|
}, [categories]);
|
||||||
|
|
||||||
// Sync state with prop change (e.g. from Header menu)
|
// Sync state with prop change (e.g. from Header menu / Direct navigation)
|
||||||
|
const prevInitialRef = React.useRef({ category: initialCategory, search: initialSearch });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (prevInitialRef.current.category === initialCategory && prevInitialRef.current.search === initialSearch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prevInitialRef.current = { category: initialCategory, search: initialSearch };
|
||||||
|
|
||||||
// Normalize Input (Trim and lowercase)
|
// Normalize Input (Trim and lowercase)
|
||||||
const normSearch = (initialSearch || "").trim().toLowerCase();
|
const normSearch = (initialSearch || "").trim().toLowerCase();
|
||||||
const normCat = initialCategory.trim().toLowerCase();
|
const normCat = initialCategory.trim().toLowerCase();
|
||||||
@ -303,14 +310,10 @@ export default function ArchivePage({
|
|||||||
const mapKey = normSearch || normCat;
|
const mapKey = normSearch || normCat;
|
||||||
const mapping = CATEGORY_MAP[mapKey];
|
const mapping = CATEGORY_MAP[mapKey];
|
||||||
|
|
||||||
if (selectedCategory === initialCategory && searchQuery === initialSearch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
|
|
||||||
// Reset ALL other filters when a new category/solution is selected from menu
|
// Reset other filters when initial URL route prop changes
|
||||||
setSelectedPet("all");
|
setSelectedPet("all");
|
||||||
setActiveSymptoms([]);
|
setActiveSymptoms([]);
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
@ -349,7 +352,9 @@ export default function ArchivePage({
|
|||||||
|
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const newUrl = `/shop${queryString ? `?${queryString}` : ''}`;
|
const newUrl = `/shop${queryString ? `?${queryString}` : ''}`;
|
||||||
router.push(newUrl, { scroll: false });
|
if (typeof window !== 'undefined' && window.location.pathname + window.location.search !== newUrl) {
|
||||||
|
router.replace(newUrl, { scroll: false });
|
||||||
|
}
|
||||||
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms, prescriptionFilter, router]);
|
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms, prescriptionFilter, router]);
|
||||||
|
|
||||||
const fetchProducts = useCallback(async () => {
|
const fetchProducts = useCallback(async () => {
|
||||||
@ -605,6 +610,7 @@ export default function ArchivePage({
|
|||||||
<div className="relative w-full sm:w-64">
|
<div className="relative w-full sm:w-64">
|
||||||
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" />
|
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" />
|
||||||
<input
|
<input
|
||||||
|
data-testid="shop-search-input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="جستجوی محصول..."
|
placeholder="جستجوی محصول..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
|
|||||||
@ -440,6 +440,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<label className="text-xs font-bold text-medical-gray-500">رمز عبور</label>
|
<label className="text-xs font-bold text-medical-gray-500">رمز عبور</label>
|
||||||
<button
|
<button
|
||||||
|
data-testid="forgot-password-link"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setView("forgot-password")}
|
onClick={() => setView("forgot-password")}
|
||||||
className="text-xs font-bold text-medical-gray-400 hover:text-canina-blue"
|
className="text-xs font-bold text-medical-gray-400 hover:text-canina-blue"
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import SafeImage from "./SafeImage";
|
|||||||
import { useUserStore } from "../lib/store/userStore";
|
import { useUserStore } from "../lib/store/userStore";
|
||||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||||
import AuthModal from "./AuthModal";
|
import AuthModal from "./AuthModal";
|
||||||
|
import DeleteConfirmModal from "./DeleteConfirmModal";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate }: {
|
export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate }: {
|
||||||
@ -136,7 +137,7 @@ export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate
|
|||||||
>
|
>
|
||||||
<Minus className="w-3 h-3" />
|
<Minus className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<span className="text-xs font-black min-w-[20px] text-center font-vazir">{toPersian(item.quantity)}</span>
|
<span data-testid="cart-item-quantity" className="text-xs font-black min-w-[20px] text-center font-vazir">{toPersian(item.quantity)}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
|
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
|
||||||
className="p-1 hover:text-canina-blue transition-colors"
|
className="p-1 hover:text-canina-blue transition-colors"
|
||||||
@ -146,6 +147,7 @@ export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
data-testid="cart-item-delete-btn"
|
||||||
onClick={() => setDeleteConfirmId(item.product.id)}
|
onClick={() => setDeleteConfirmId(item.product.id)}
|
||||||
className="text-medical-gray-300 hover:text-red-500 transition-colors"
|
className="text-medical-gray-300 hover:text-red-500 transition-colors"
|
||||||
>
|
>
|
||||||
@ -335,7 +337,7 @@ export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate
|
|||||||
<div className="flex items-center justify-between border-t border-medical-gray-200 pt-4 px-2">
|
<div className="flex items-center justify-between border-t border-medical-gray-200 pt-4 px-2">
|
||||||
<span className="text-lg font-black text-medical-gray-900 italic">مجموع قابل پرداخت</span>
|
<span className="text-lg font-black text-medical-gray-900 italic">مجموع قابل پرداخت</span>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<p className="text-2xl font-black text-canina-blue tracking-tighter leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
<p data-testid="cart-total-price" className="text-2xl font-black text-canina-blue tracking-tighter leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
||||||
<p className="text-[10px] text-medical-gray-400 font-bold">تومان</p>
|
<p className="text-[10px] text-medical-gray-400 font-bold">تومان</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -351,6 +353,20 @@ export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
<DeleteConfirmModal
|
||||||
|
isOpen={Boolean(deleteConfirmId)}
|
||||||
|
onClose={() => setDeleteConfirmId(null)}
|
||||||
|
onConfirm={() => {
|
||||||
|
if (deleteConfirmId) {
|
||||||
|
removeItem(deleteConfirmId);
|
||||||
|
setDeleteConfirmId(null);
|
||||||
|
toast.success("محصول از سبد خرید حذف شد");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="حذف از سبد خرید"
|
||||||
|
message="آیا از حذف این مکمل دارویی از سبد خرید خود اطمینان دارید؟"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@ -342,6 +342,7 @@ export default function Header({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
data-testid="header-login-btn"
|
||||||
onClick={() => setIsAuthModalOpen(true)}
|
onClick={() => setIsAuthModalOpen(true)}
|
||||||
className="flex items-center gap-1.5 px-2.5 py-2 sm:px-3.5 sm:py-2 bg-medical-gray-900 text-white hover:bg-canina-blue rounded-xl text-[11px] sm:text-xs font-black transition-all shadow-xs whitespace-nowrap"
|
className="flex items-center gap-1.5 px-2.5 py-2 sm:px-3.5 sm:py-2 bg-medical-gray-900 text-white hover:bg-canina-blue rounded-xl text-[11px] sm:text-xs font-black transition-all shadow-xs whitespace-nowrap"
|
||||||
>
|
>
|
||||||
@ -354,6 +355,7 @@ export default function Header({
|
|||||||
{/* Shopping Cart Button */}
|
{/* Shopping Cart Button */}
|
||||||
{!isCartDisabled && (
|
{!isCartDisabled && (
|
||||||
<button
|
<button
|
||||||
|
data-testid="cart-header-btn"
|
||||||
onClick={onCartOpen}
|
onClick={onCartOpen}
|
||||||
className="relative px-3.5 py-2 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-medical-gray-900 transition-all flex items-center gap-2 shadow-sm"
|
className="relative px-3.5 py-2 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-medical-gray-900 transition-all flex items-center gap-2 shadow-sm"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -888,6 +888,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
</button>
|
</button>
|
||||||
) : (product.packageSize || 0) <= 0 ? (
|
) : (product.packageSize || 0) <= 0 ? (
|
||||||
<button
|
<button
|
||||||
|
data-testid="product-desktop-out-of-stock-btn"
|
||||||
onClick={() => toast.info("درخواست اطلاعرسانی موجودی ثبت شد")}
|
onClick={() => toast.info("درخواست اطلاعرسانی موجودی ثبت شد")}
|
||||||
className="w-[60%] flex items-center justify-center gap-1.5 px-4 bg-amber-500 text-white rounded-2xl h-16 font-black text-sm hover:bg-amber-600 transition-all shadow-xl shadow-amber-500/20 font-vazir whitespace-nowrap"
|
className="w-[60%] flex items-center justify-center gap-1.5 px-4 bg-amber-500 text-white rounded-2xl h-16 font-black text-sm hover:bg-amber-600 transition-all shadow-xl shadow-amber-500/20 font-vazir whitespace-nowrap"
|
||||||
>
|
>
|
||||||
@ -896,6 +897,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
data-testid="product-desktop-add-to-cart-btn"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addItem(product, itemQuantity, { quantity: calculation.dailyDose, unit: calculation.unit });
|
addItem(product, itemQuantity, { quantity: calculation.dailyDose, unit: calculation.unit });
|
||||||
toast.success(`${toPersian(itemQuantity)} عدد ${product.name} به سبد خرید اضافه شد`);
|
toast.success(`${toPersian(itemQuantity)} عدد ${product.name} به سبد خرید اضافه شد`);
|
||||||
@ -1074,6 +1076,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
</a>
|
</a>
|
||||||
) : isOutOfStock ? (
|
) : isOutOfStock ? (
|
||||||
<button
|
<button
|
||||||
|
data-testid="product-out-of-stock-btn"
|
||||||
onClick={() => toast.info("درخواست اطلاعرسانی موجودی ثبت شد")}
|
onClick={() => toast.info("درخواست اطلاعرسانی موجودی ثبت شد")}
|
||||||
className="flex-1 bg-amber-500 text-white py-2.5 px-3 rounded-xl font-black text-xs hover:bg-amber-600 transition-all flex items-center justify-center gap-1.5 shadow-md shadow-amber-500/20 whitespace-nowrap"
|
className="flex-1 bg-amber-500 text-white py-2.5 px-3 rounded-xl font-black text-xs hover:bg-amber-600 transition-all flex items-center justify-center gap-1.5 shadow-md shadow-amber-500/20 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
@ -1101,6 +1104,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
data-testid="product-add-to-cart-btn"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addItem(product, itemQuantity, { quantity: calculation.dailyDose, unit: calculation.unit });
|
addItem(product, itemQuantity, { quantity: calculation.dailyDose, unit: calculation.unit });
|
||||||
toast.success(`${toPersian(itemQuantity)} عدد به سبد خرید اضافه شد`);
|
toast.success(`${toPersian(itemQuantity)} عدد به سبد خرید اضافه شد`);
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export default defineConfig({
|
|||||||
// 2. Storefront on Desktop Chromium
|
// 2. Storefront on Desktop Chromium
|
||||||
{
|
{
|
||||||
name: 'chromium-desktop',
|
name: 'chromium-desktop',
|
||||||
testMatch: /storefront\/.*\.spec\.ts/,
|
testMatch: /(storefront|security)\/.*\.spec\.ts/,
|
||||||
use: {
|
use: {
|
||||||
...devices['Desktop Chrome'],
|
...devices['Desktop Chrome'],
|
||||||
baseURL: STOREFRONT_URL,
|
baseURL: STOREFRONT_URL,
|
||||||
@ -61,7 +61,7 @@ export default defineConfig({
|
|||||||
// 3. Storefront on Mobile Safari (iPhone 14)
|
// 3. Storefront on Mobile Safari (iPhone 14)
|
||||||
{
|
{
|
||||||
name: 'mobile-safari',
|
name: 'mobile-safari',
|
||||||
testMatch: /storefront\/.*\.spec\.ts/,
|
testMatch: /(storefront|security)\/.*\.spec\.ts/,
|
||||||
use: {
|
use: {
|
||||||
...devices['iPhone 14'],
|
...devices['iPhone 14'],
|
||||||
baseURL: STOREFRONT_URL,
|
baseURL: STOREFRONT_URL,
|
||||||
|
|||||||
@ -66,13 +66,37 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ۳. برنامه تکمیل و توسعه تستهای مفقود (Action Plan)
|
## ۳. وضعیت پوشش نهایی سوئیت ۱۱گانه (Final 11 Test Suites Status)
|
||||||
برای رسیدن به پوشش ۱۰۰٪ سیستم، فایلهای تست جدید زیر باید ایجاد و تستهای موجود با رویکرد پایداری انتخابگرها و پوشش سناریوهای منفی بازنویسی شوند:
|
تمام ۱۱ سوئیت تست زیر با استانداردهای بدون `if-isVisible`، حذف سلکتورهای فالبک، assertion دقیق روی مقادیر رشتهای و محاسبات ریاضی، و اعتبارسنجی با Mutation Testing پیادهسازی و در CI ادغام شدند:
|
||||||
|
|
||||||
1. `tests/e2e/storefront/shop-filter-search.spec.ts`: تست کامل جستجو، فیلتر دستهها، ماشینحساب دوز و حالتهای بدون نتیجه.
|
1. `tests/e2e/storefront/shop-filter-search.spec.ts`: تست کامل جستجو، فیلتر دستهها، ماشینحساب دوز و حالتهای بدون نتیجه.
|
||||||
2. `tests/e2e/storefront/cart-operations.spec.ts`: تست تعاملی سبد خرید، تغییر تعداد، حذف، کوپن تخفیف و سقف خرید.
|
2. `tests/e2e/storefront/order-tracking.spec.ts`: تست پیگیری سفارشات با سناریوهای موفق و ناموفق.
|
||||||
3. `tests/e2e/storefront/order-tracking.spec.ts`: تست پیگیری سفارشات با سناریوهای موفق و ناموفق.
|
3. `tests/e2e/storefront/cart-operations.spec.ts`: تست تعاملی سبد خرید، محاسبه دقیق ریاضی `unitPrice * 2`، تغییر تعداد، حذف و سبد خالی.
|
||||||
4. `tests/e2e/storefront/user-auth-pets.spec.ts`: تست ورود کاربر، OTP، اضافه کردن پت جدید و پروفایل.
|
4. `tests/e2e/admin/admin-orders-flow.spec.ts`: تست مدیریت سفارشات، تغییر وضعیت فاکتور و فیلترها در پنل مدیریت.
|
||||||
5. `tests/e2e/admin/admin-orders-flow.spec.ts`: تست مدیریت سفارشات، تغییر وضعیت فاکتور و فیلترها در پنل مدیریت.
|
5. `tests/e2e/admin/admin-blogs-crud.spec.ts`: تست ایجاد و ویرایش مقالات وبلاگ و دانشنامه در پنل ادمین.
|
||||||
6. `tests/e2e/admin/admin-blogs-crud.spec.ts`: تست ایجاد و ویرایش مقالات وبلاگ و دانشنامه در پنل ادمین.
|
6. `tests/e2e/admin/admin-b2b-submissions.spec.ts`: تست مشاهده و مدیریت فرمهای ثبت شده B2B و تماس.
|
||||||
7. `tests/e2e/admin/admin-b2b-submissions.spec.ts`: تست مشاهده و مدیریت فرمهای ثبت شده B2B و تماس.
|
7. `tests/e2e/storefront/auth-otp-recovery.spec.ts`: تست جریان OTP و بازیابی و تنظیم رمز عبور جدید.
|
||||||
|
8. `tests/e2e/admin/admin-rbac-roles.spec.ts`: تفکیک سطوح دسترسی، نقشهای کاربران و ادمین ارشد.
|
||||||
|
9. `tests/e2e/storefront/error-pages-404-500.spec.ts`: رندر و ناوبری صفحات خطای ۴۰۴ و ۵۰۰.
|
||||||
|
10. `tests/e2e/security/xss-ratelimit.spec.ts`: تست مقاومت امنیتی در برابر حملات XSS و محدودیت نرخ ارسال پیامک (Cooldown Timer).
|
||||||
|
11. `tests/e2e/storefront/concurrency-stock.spec.ts`: رقابت موازی همزمان (Parallel Race با `Promise.all`) روی آخرین موجودی کالا در دو کانتکست مجزا.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۴. راهنمای نگهداری و افزودن فیچرهای جدید بدون شکستن تستها (Developer Maintenance Guide)
|
||||||
|
|
||||||
|
برای اینکه توسعهدهندگان جدید یا تغییرات آینده باعث رگرسیون یا شکست بیدلیل سوئیت نشوند، قوانین زیر الزامی است:
|
||||||
|
|
||||||
|
1. **استفاده اجباری از `data-testid`**:
|
||||||
|
- برای هر المان قابل تعامل جدید (دکمه، اینپوت، کارت، فیلتر)، به جای اتکا به کلاس CSS یا متن فارسی، حتماً `data-testid="..."` اضافه کنید.
|
||||||
|
2. **پرهیز از Assertionهای شکننده متن یا کلاس**:
|
||||||
|
- از چک کردن کلاسهای ظاهری (مانند `text-canina-blue`) خودداری کنید. به جای آن، وضعیت بیزینسی (مقدار عددی، مقدار فیلد فرم با `.toHaveValue()`, یا URL با `.toHaveURL()`) را راستیآزمایی کنید.
|
||||||
|
3. **ممنوعیت استفاده از الگوی کاذب `if (await element.isVisible())`**:
|
||||||
|
- اگر حضور یک المان بخشی از کارکرد صفحه است، آن را مستقیماً با `await expect(locator).toBeVisible()` بنویسید تا در صورت بروز باگ، تست فیل شود و بیصدا عبور نکند.
|
||||||
|
4. **اجرای محلی قبل از Push**:
|
||||||
|
- همیشه قبل از ایجاد PR یا Push، دستور زیر را اجرا کنید:
|
||||||
|
```bash
|
||||||
|
npx playwright test --project=chromium-desktop --project=admin-chromium
|
||||||
|
```
|
||||||
|
5. **بهروزرسانی خودکار در گیتهاب/گیتیا (CI Pipeline)**:
|
||||||
|
- فایل `.gitea/workflows/e2e.yml` روی هر Push و Pull Request روی برنچهای `main` و `develop` تمام سوئیتها را به طور خودکار اجرا میکند.
|
||||||
|
|||||||
@ -51,9 +51,12 @@ test.describe('سناریوی مدیریت درخواستهای B2B و فرم
|
|||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: 'b2b-1',
|
id: 'b2b-1',
|
||||||
clinicName: 'کلینیک تخصصی دامپزشکی پایتخت',
|
companyName: 'کلینیک تخصصی دامپزشکی پایتخت',
|
||||||
managerName: 'دکتر محمدی',
|
contactName: 'دکتر محمدی',
|
||||||
phone: '09123334455',
|
phone: '09123334455',
|
||||||
|
email: 'clinic@paytakht.ir',
|
||||||
|
businessType: 'کلینیک دامپزشکی',
|
||||||
|
message: 'درخواست خرید عمده مکملهای مفاصل کنینا',
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
@ -63,6 +66,27 @@ test.describe('سناریوی مدیریت درخواستهای B2B و فرم
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.route('**/api/b2b/partners*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'partner-1',
|
||||||
|
companyName: 'پت شاپ مرکزی ارکیده',
|
||||||
|
userId: 'usr-901',
|
||||||
|
taxId: '1400889922',
|
||||||
|
creditLimit: 50000000,
|
||||||
|
discountTier: 'طلایی (۲۵٪)',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 1. لاگین ادمین
|
// 1. لاگین ادمین
|
||||||
await page.goto('/login', { waitUntil: 'domcontentloaded' });
|
await page.goto('/login', { waitUntil: 'domcontentloaded' });
|
||||||
const emailInput = page.locator('input[type="email"]');
|
const emailInput = page.locator('input[type="email"]');
|
||||||
@ -79,17 +103,25 @@ test.describe('سناریوی مدیریت درخواستهای B2B و فرم
|
|||||||
await page.goto('/b2b', { waitUntil: 'domcontentloaded' });
|
await page.goto('/b2b', { waitUntil: 'domcontentloaded' });
|
||||||
await expect(page).toHaveURL(/\/b2b/);
|
await expect(page).toHaveURL(/\/b2b/);
|
||||||
|
|
||||||
// 3. راستیآزمایی تیتر اختصاصی مدیریت B2B
|
// 3. راستیآزمایی تیتر اختصاصی مدیریت B2B و درخواست متناظر
|
||||||
const b2bHeader = page.locator('h2').filter({ hasText: 'مدیریت همکاران عمدهفروشی و B2B' });
|
const b2bHeader = page.locator('h2').filter({ hasText: 'مدیریت همکاران عمدهفروشی و B2B' });
|
||||||
await expect(b2bHeader).toBeVisible({ timeout: 15000 });
|
await expect(b2bHeader).toBeVisible({ timeout: 15000 });
|
||||||
|
await expect(page.locator('span').filter({ hasText: 'کلینیک تخصصی دامپزشکی پایتخت' }).first()).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
// 4. راستیآزمایی تبهای درخواستها و همکاران
|
// 4. سوئیچ به تب حسابهای تاییدشده همکار
|
||||||
const partnerTab = page.getByRole('button', { name: /حسابهای تاییدشده همکار/i });
|
const partnerTab = page.locator('[data-testid="b2b-partners-tab"]').first();
|
||||||
await expect(partnerTab).toBeVisible();
|
await expect(partnerTab).toBeVisible();
|
||||||
await partnerTab.click();
|
await partnerTab.click();
|
||||||
|
|
||||||
const inquiriesTab = page.getByRole('button', { name: /درخواستهای دریافت نمایندگی/i });
|
// راستیآزمایی دقیق رندر شدن جدول حسابهای همکار
|
||||||
|
const partnerRowText = page.locator('td').filter({ hasText: 'پت شاپ مرکزی ارکیده' }).first();
|
||||||
|
await expect(partnerRowText).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
// 5. سوئیچ مجدد به تب درخواستها
|
||||||
|
const inquiriesTab = page.locator('[data-testid="b2b-inquiries-tab"]').first();
|
||||||
await expect(inquiriesTab).toBeVisible();
|
await expect(inquiriesTab).toBeVisible();
|
||||||
await inquiriesTab.click();
|
await inquiriesTab.click();
|
||||||
|
|
||||||
|
await expect(page.locator('span').filter({ hasText: 'کلینیک تخصصی دامپزشکی پایتخت' }).first()).toBeVisible({ timeout: 10000 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { test, expect } from '../../fixtures';
|
|||||||
|
|
||||||
test.describe('سناریوی مدیریت مقالات و وبلاگ در پنل ادمین (Admin Blogs & CMS Flow)', () => {
|
test.describe('سناریوی مدیریت مقالات و وبلاگ در پنل ادمین (Admin Blogs & CMS Flow)', () => {
|
||||||
test('ورود به بخش مقالات -> باز کردن فرم ایجاد -> پر کردن عنوان و ذخیره نهایی', async ({ page }) => {
|
test('ورود به بخش مقالات -> باز کردن فرم ایجاد -> پر کردن عنوان و ذخیره نهایی', async ({ page }) => {
|
||||||
|
let blogCreated = false;
|
||||||
|
|
||||||
// Intercept and mock Admin Auth & Blogs API calls
|
// Intercept and mock Admin Auth & Blogs API calls
|
||||||
await page.route('**/api/auth/admin-login*', async (route) => {
|
await page.route('**/api/auth/admin-login*', async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
@ -42,6 +44,22 @@ test.describe('سناریوی مدیریت مقالات و وبلاگ در پن
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.route('**/api/admin/blogs/categories*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ success: true, data: [] }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/api/admin/blogs/tags*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ success: true, data: [] }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
await page.route('**/api/admin/blogs*', async (route) => {
|
await page.route('**/api/admin/blogs*', async (route) => {
|
||||||
if (route.request().method() === 'GET') {
|
if (route.request().method() === 'GET') {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
@ -61,12 +79,15 @@ test.describe('سناریوی مدیریت مقالات و وبلاگ در پن
|
|||||||
meta: { total: 1, lastPage: 1 },
|
meta: { total: 1, lastPage: 1 },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else if (route.request().method() === 'POST') {
|
||||||
|
blogCreated = true;
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 201,
|
status: 201,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
body: JSON.stringify({ success: true, message: 'مقاله جدید با موفقیت ذخیره شد' }),
|
body: JSON.stringify({ success: true, message: 'مقاله جدید با موفقیت ذخیره شد' }),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,14 +115,17 @@ test.describe('سناریوی مدیریت مقالات و وبلاگ در پن
|
|||||||
await expect(newBlogBtn).toBeVisible();
|
await expect(newBlogBtn).toBeVisible();
|
||||||
await newBlogBtn.click();
|
await newBlogBtn.click();
|
||||||
|
|
||||||
// 4. راستیآزمایی باز شدن فرم ایجاد و پر کردن عنوان
|
// 4. پر کردن عنوان در فرم مودال
|
||||||
const blogTitleInput = page.locator('input[placeholder*="عنوان"]').first();
|
const blogTitleInput = page.locator('[data-testid="blog-title-input"]').first();
|
||||||
await expect(blogTitleInput).toBeVisible({ timeout: 10000 });
|
await expect(blogTitleInput).toBeVisible({ timeout: 10000 });
|
||||||
await blogTitleInput.fill('مقاله آموزشی تستی پلیرایت');
|
await blogTitleInput.fill('مقاله آموزشی تستی پلیرایت');
|
||||||
|
|
||||||
// 5. ذخیره فرم و راستیآزمایی پیام موفقیت یا فراخوانی موفقیتآمیز
|
// 5. ذخیره فرم با دکمه ذخیره مقاله
|
||||||
const saveBlogBtn = page.getByRole('button', { name: /ذخیره|ثبت|انتشار/i }).first();
|
const saveBlogBtn = page.locator('[data-testid="blog-save-btn"]').first();
|
||||||
await expect(saveBlogBtn).toBeVisible();
|
await expect(saveBlogBtn).toBeVisible({ timeout: 10000 });
|
||||||
await saveBlogBtn.click();
|
await saveBlogBtn.click();
|
||||||
|
|
||||||
|
// راستیآزمایی دقیق ارسال موفقیتآمیز درخواست ساخت مقاله به سرور
|
||||||
|
await expect.poll(() => blogCreated, { timeout: 10000 }).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { test, expect } from '../../fixtures';
|
import { test, expect } from '../../fixtures';
|
||||||
|
|
||||||
test.describe('سناریوی مدیریت سفارشات و فیلترها در پنل ادمین (Admin Orders Management)', () => {
|
test.describe('سناریوی مدیریت و فیلتر سفارشات در پنل ادمین (Admin Orders Flow & Exact Value Assertion)', () => {
|
||||||
test('ورود به بخش سفارشات -> راستیآزمایی رندر جدول فاکتورها -> فیلتر وضعیت و مشاهده جزییات', async ({ page }) => {
|
test('ورود به بخش سفارشات -> راستیآزمایی رندر جدول فاکتورها -> راستیآزمایی مقدار دقیق وضعیت سفارش -> فیلتر وضعیت', async ({ page }) => {
|
||||||
// Intercept and mock Admin Auth & Orders API calls
|
// Intercept and mock Admin Auth & Orders API calls
|
||||||
await page.route('**/api/auth/admin-login*', async (route) => {
|
await page.route('**/api/auth/admin-login*', async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
@ -50,13 +50,13 @@ test.describe('سناریوی مدیریت سفارشات و فیلترها در
|
|||||||
success: true,
|
success: true,
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
id: 'ord-101',
|
id: 'ord-12345',
|
||||||
orderNumber: 'CN-10001',
|
trackingNumber: 'CN-88990',
|
||||||
customerName: 'علی رضایی',
|
|
||||||
customerPhone: '09121112233',
|
|
||||||
totalAmount: 1850000,
|
totalAmount: 1850000,
|
||||||
status: 'processing',
|
status: 'shipped',
|
||||||
|
paymentMethod: 'ONLINE',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
user: { firstName: 'علی', lastName: 'رضایی', phone: '09121112233' },
|
||||||
items: [{ id: 'item-1', name: 'مکمل مفاصل سگ', quantity: 2, priceValue: 925000 }],
|
items: [{ id: 'item-1', name: 'مکمل مفاصل سگ', quantity: 2, priceValue: 925000 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -85,10 +85,15 @@ test.describe('سناریوی مدیریت سفارشات و فیلترها در
|
|||||||
const ordersHeading = page.locator('h2').filter({ hasText: 'مدیریت سفارشات' });
|
const ordersHeading = page.locator('h2').filter({ hasText: 'مدیریت سفارشات' });
|
||||||
await expect(ordersHeading).toBeVisible({ timeout: 15000 });
|
await expect(ordersHeading).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
// 4. راستیآزمایی دراپداون فیلتر وضعیت و انتخاب وضعیت جدید
|
// 4. راستیآزمایی مقدار دقیق متنی فیلد وضعیت سفارش در سطر جدول (ارسال شده)
|
||||||
const statusSelect = page.locator('select').first();
|
const orderStatusSelect = page.locator('[data-testid="order-status-select"]').first();
|
||||||
await expect(statusSelect).toBeVisible();
|
await expect(orderStatusSelect).toBeVisible({ timeout: 10000 });
|
||||||
await statusSelect.selectOption({ label: 'در حال پردازش' });
|
await expect(orderStatusSelect).toHaveValue('shipped');
|
||||||
|
|
||||||
|
// 5. راستیآزمایی دراپداون فیلتر وضعیت در بالای صفحه و انتخاب وضعیت جدید
|
||||||
|
const topFilterSelect = page.locator('select').first();
|
||||||
|
await expect(topFilterSelect).toBeVisible();
|
||||||
|
await topFilterSelect.selectOption({ label: 'ارسال شده' });
|
||||||
await expect(page).toHaveURL(/status=/);
|
await expect(page).toHaveURL(/status=/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
109
tests/e2e/admin/admin-rbac-roles.spec.ts
Normal file
109
tests/e2e/admin/admin-rbac-roles.spec.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { test, expect } from '../../fixtures';
|
||||||
|
|
||||||
|
test.describe('سناریوی سطوح دسترسی و تفکیک نقشهای کاربران و ادمین (Admin RBAC Roles)', () => {
|
||||||
|
test('لاگین سوپر ادمین -> مدیریت کاربران -> مشاهده تفکیک دقیق نقشهای کاربری، همکار B2B و مدیر کل', async ({ page }) => {
|
||||||
|
// Intercept and mock Admin Auth & Users API
|
||||||
|
await page.route('**/api/auth/admin-login*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
accessToken: 'mock_e2e_superadmin_jwt',
|
||||||
|
user: { id: 'super-admin-1', email: 'admin@canina.ir', role: 'SUPER_ADMIN' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/api/auth/refresh*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
accessToken: 'mock_e2e_superadmin_jwt',
|
||||||
|
user: { id: 'super-admin-1', email: 'admin@canina.ir', role: 'SUPER_ADMIN' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/api/users/profile*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: 'super-admin-1',
|
||||||
|
email: 'admin@canina.ir',
|
||||||
|
role: 'SUPER_ADMIN',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/api/admin/users*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'user-regular',
|
||||||
|
firstName: 'مهرداد',
|
||||||
|
lastName: 'صادقی',
|
||||||
|
mobile: '09121110001',
|
||||||
|
role: 'User_PetOwner',
|
||||||
|
walletBalance: 150000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user-b2b',
|
||||||
|
firstName: 'دکتر علوی',
|
||||||
|
lastName: 'کلینیک پارس',
|
||||||
|
mobile: '09121110002',
|
||||||
|
role: 'User_Wholesale',
|
||||||
|
walletBalance: 5000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user-admin',
|
||||||
|
firstName: 'مدیر ارشد',
|
||||||
|
lastName: 'سیستم',
|
||||||
|
mobile: '09121110003',
|
||||||
|
role: 'ADMIN',
|
||||||
|
walletBalance: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: { total: 3, lastPage: 1 },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. لاگین ادمین
|
||||||
|
await page.goto('/login', { waitUntil: 'domcontentloaded' });
|
||||||
|
const emailInput = page.locator('input[type="email"]');
|
||||||
|
const passwordInput = page.locator('input[type="password"]');
|
||||||
|
await expect(emailInput).toBeVisible({ timeout: 10000 });
|
||||||
|
await emailInput.fill('admin@canina.ir');
|
||||||
|
await passwordInput.fill('Admin@123456');
|
||||||
|
|
||||||
|
const submitBtn = page.getByRole('button', { name: /ورود با رمز عبور|ورود به پنل|ورود/i }).first();
|
||||||
|
await submitBtn.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// 2. ورود به صفحه مدیریت کاربران
|
||||||
|
await page.goto('/users', { waitUntil: 'domcontentloaded' });
|
||||||
|
await expect(page).toHaveURL(/\/users/);
|
||||||
|
|
||||||
|
// 3. راستیآزمایی تفکیک قطعی نقشهای RBAC در ردیفهای جدول
|
||||||
|
const regularBadge = page.locator('span').filter({ hasText: 'مشتری عادی' }).first();
|
||||||
|
await expect(regularBadge).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
const wholesaleBadge = page.locator('span').filter({ hasText: 'خریدار عمده' }).first();
|
||||||
|
await expect(wholesaleBadge).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
const adminBadge = page.locator('span').filter({ hasText: 'مدیر سیستم' }).first();
|
||||||
|
await expect(adminBadge).toBeVisible({ timeout: 10000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
110
tests/e2e/security/xss-ratelimit.spec.ts
Normal file
110
tests/e2e/security/xss-ratelimit.spec.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { test, expect } from '../../fixtures';
|
||||||
|
|
||||||
|
test.describe('سناریوی اعتبارسنجی امنیتی XSS و محدودیت نرخ پیامک (Security XSS & Rate-Limiting)', () => {
|
||||||
|
test('تزریق اسکریپت مخرب در فیلدهای فرم تماس و راستیآزمایی عدم اجرای XSS و پاکسازی دادهها', async ({ page }) => {
|
||||||
|
let capturedPayload: Record<string, string> = {};
|
||||||
|
|
||||||
|
await page.route('**/api/contact*', async (route) => {
|
||||||
|
if (route.request().method() === 'POST') {
|
||||||
|
capturedPayload = route.request().postDataJSON?.() || {};
|
||||||
|
await route.fulfill({
|
||||||
|
status: 201,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'پیام شما با موفقیت ثبت شد',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let dialogFired = false;
|
||||||
|
page.on('dialog', async (dialog) => {
|
||||||
|
dialogFired = true;
|
||||||
|
await dialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. ورود به صفحه تماس با ما
|
||||||
|
await page.goto('/contact', { waitUntil: 'domcontentloaded' });
|
||||||
|
await expect(page).toHaveURL(/\/contact/);
|
||||||
|
|
||||||
|
// 2. پر کردن فیلدها با پیلود خطرناک XSS
|
||||||
|
const xssPayload = '<img src=x onerror=alert("XSS_ATTACK_CANINA") />';
|
||||||
|
const nameInput = page.locator('#contact-name');
|
||||||
|
const phoneInput = page.locator('#contact-phone');
|
||||||
|
const subjectInput = page.locator('#contact-subject');
|
||||||
|
const messageInput = page.locator('#contact-message');
|
||||||
|
|
||||||
|
await expect(nameInput).toBeVisible({ timeout: 10000 });
|
||||||
|
await nameInput.fill(xssPayload);
|
||||||
|
await phoneInput.fill('09121112233');
|
||||||
|
await subjectInput.fill('XSS Test Subject');
|
||||||
|
await messageInput.fill('<script>alert("XSS")</script> متن درخواست تستی');
|
||||||
|
|
||||||
|
// 3. ارسال فرم تماس
|
||||||
|
const submitBtn = page.locator('button[type="submit"]').filter({ hasText: /ارسال پیام/i }).first();
|
||||||
|
await expect(submitBtn).toBeVisible();
|
||||||
|
await submitBtn.click();
|
||||||
|
|
||||||
|
// 4. راستیآزمایی نمایش پیام موفقیت بدون فعال شدن هیچ alert/dialog مخرب
|
||||||
|
const successBox = page.locator('h4').filter({ hasText: 'پیام شما با موفقیت ثبت شد' });
|
||||||
|
await expect(successBox).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
// راستیآزمایی قطعی عدم اجرای alert جاوااسکریپت در مرورگر
|
||||||
|
expect(dialogFired).toBe(false);
|
||||||
|
|
||||||
|
// راستیآزمایی ارسال امن دادهها
|
||||||
|
expect(capturedPayload.name).toBe(xssPayload);
|
||||||
|
expect(capturedPayload.phone).toBe('09121112233');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('شبیهسازی محدودیت نرخ و تایمر معکوس OTP (Rate-Limiting & Cooldown Timer)', async ({ page }) => {
|
||||||
|
// Intercept send-otp to return 429 when rate limited
|
||||||
|
let sendOtpAttempts = 0;
|
||||||
|
await page.route('**/api/auth/send-otp*', async (route) => {
|
||||||
|
sendOtpAttempts++;
|
||||||
|
if (sendOtpAttempts === 1) {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ success: true, code: '12345' }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 429,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: 'تعداد درخواستهای شما بیش از حد مجاز است. لطفاً ۲ دقیقه صبر کنید.',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. باز کردن مودال ورود با OTP
|
||||||
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
|
const headerLoginBtn = page.locator('[data-testid="header-login-btn"]').first();
|
||||||
|
await expect(headerLoginBtn).toBeVisible({ timeout: 15000 });
|
||||||
|
await headerLoginBtn.click();
|
||||||
|
|
||||||
|
// 2. ورود با کد پیامکی (OTP)
|
||||||
|
const otpOptionBtn = page.locator('button').filter({ hasText: /ورود با کد پیامکی \(OTP\)/i }).first();
|
||||||
|
await expect(otpOptionBtn).toBeVisible({ timeout: 10000 });
|
||||||
|
await otpOptionBtn.click();
|
||||||
|
|
||||||
|
// 3. وارد کردن شماره موبایل و ارسال اولین OTP
|
||||||
|
const phoneInput = page.locator('input[type="tel"]').first();
|
||||||
|
await expect(phoneInput).toBeVisible({ timeout: 10000 });
|
||||||
|
await phoneInput.fill('09129998877');
|
||||||
|
|
||||||
|
const sendOtpBtn = page.locator('button[type="submit"]').filter({ hasText: /ارسال کد تایید/i }).first();
|
||||||
|
await expect(sendOtpBtn).toBeVisible();
|
||||||
|
await sendOtpBtn.click();
|
||||||
|
|
||||||
|
// 4. راستیآزمایی فعال بودن متن تایمر معکوس (Cooldown Timer) و عدم امکان کلیک زودهنگام
|
||||||
|
const timerText = page.locator('span').filter({ hasText: /ارسال مجدد کد تا/i }).first();
|
||||||
|
await expect(timerText).toBeVisible({ timeout: 10000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
109
tests/e2e/storefront/auth-otp-recovery.spec.ts
Normal file
109
tests/e2e/storefront/auth-otp-recovery.spec.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { test, expect } from '../../fixtures';
|
||||||
|
|
||||||
|
test.describe('سناریوی اعتبارسنجی احراز هویت پیامکی و بازیابی رمز عبور (Auth OTP & Password Recovery)', () => {
|
||||||
|
test('درخواست کد بازیابی پیامکی -> ورود کد OTP -> تنظیم رمز عبور جدید -> لاگین موفق با رمز جدید', async ({ page }) => {
|
||||||
|
let resetPasswordCalled = false;
|
||||||
|
|
||||||
|
// Intercept and mock Auth API endpoints
|
||||||
|
await page.route('**/api/auth/send-otp*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'کد تایید ارسال شد',
|
||||||
|
code: '54321',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/api/auth/verify-otp*', async (route) => {
|
||||||
|
const postData = route.request().postDataJSON?.() || {};
|
||||||
|
if (postData.code === '54321') {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
data: { resetToken: 'mock_reset_token_xyz' },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 400,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: 'کد تایید نامعتبر است',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/api/users/profile*', async (route) => {
|
||||||
|
if (route.request().method() === 'PUT' || route.request().method() === 'PATCH') {
|
||||||
|
resetPasswordCalled = true;
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
data: { id: 'usr-1', mobile: '09123456789', firstName: 'پارسا' },
|
||||||
|
message: 'رمز عبور با موفقیت تغییر کرد',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: 'usr-1',
|
||||||
|
mobile: '09123456789',
|
||||||
|
firstName: 'پارسا',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. ورود به صفحه اصلی و باز کردن مودال ورود
|
||||||
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
|
const headerLoginBtn = page.locator('[data-testid="header-login-btn"]').first();
|
||||||
|
await expect(headerLoginBtn).toBeVisible({ timeout: 15000 });
|
||||||
|
await headerLoginBtn.click();
|
||||||
|
|
||||||
|
// 2. کلیک روی لینک فراموشی رمز عبور
|
||||||
|
const forgotLink = page.locator('[data-testid="forgot-password-link"]').first();
|
||||||
|
await expect(forgotLink).toBeVisible({ timeout: 10000 });
|
||||||
|
await forgotLink.click();
|
||||||
|
|
||||||
|
// 3. پر کردن شماره موبایل و ارسال درخواست OTP
|
||||||
|
const phoneInput = page.locator('input[placeholder="۰۹۱۲۳۴۵۶۷۸۹"]').first();
|
||||||
|
await expect(phoneInput).toBeVisible({ timeout: 10000 });
|
||||||
|
await phoneInput.fill('09123456789');
|
||||||
|
|
||||||
|
const sendOtpBtn = page.locator('button[type="submit"]').filter({ hasText: /ارسال کد بازیابی/i }).first();
|
||||||
|
await expect(sendOtpBtn).toBeVisible();
|
||||||
|
await sendOtpBtn.click();
|
||||||
|
|
||||||
|
// 4. راستیآزمایی ورود به مرحله forgot-otp و وارد کردن کد OTP
|
||||||
|
const otpInput = page.locator('input[placeholder="کد ۵ رقمی"]').first();
|
||||||
|
await expect(otpInput).toBeVisible({ timeout: 10000 });
|
||||||
|
await otpInput.fill('54321');
|
||||||
|
|
||||||
|
const verifyOtpBtn = page.locator('button[type="submit"]').filter({ hasText: /تایید و ادامه/i }).first();
|
||||||
|
await expect(verifyOtpBtn).toBeVisible();
|
||||||
|
await verifyOtpBtn.click();
|
||||||
|
|
||||||
|
// 5. راستیآزمایی ورود به مرحله تنظیم رمز عبور جدید
|
||||||
|
const newPassInput = page.locator('input[placeholder="حداقل ۶ کاراکتر"]').first();
|
||||||
|
await expect(newPassInput).toBeVisible({ timeout: 10000 });
|
||||||
|
await newPassInput.fill('NewSecret@2026');
|
||||||
|
|
||||||
|
const submitNewPassBtn = page.locator('button[type="submit"]').filter({ hasText: /ثبت رمز عبور جدید/i }).first();
|
||||||
|
await expect(submitNewPassBtn).toBeVisible();
|
||||||
|
await submitNewPassBtn.click();
|
||||||
|
|
||||||
|
// 6. راستیآزمایی قطعی فراخوانی موفق API ریست پسورد
|
||||||
|
await expect.poll(() => resetPasswordCalled, { timeout: 10000 }).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,8 +1,14 @@
|
|||||||
import { test, expect } from '../../fixtures';
|
import { test, expect } from '../../fixtures';
|
||||||
|
|
||||||
test.describe('سناریوی عملیات پیشرفته سبد خرید (Cart Operations & State Synchronization)', () => {
|
function parsePersianPrice(text: string): number {
|
||||||
test('ورود به صفحه محصول -> افزودن مستقیم به سبد خرید -> راستیآزمایی در تسویهحساب', async ({ page, isMobile }) => {
|
const english = text.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d).toString());
|
||||||
// 1. ورود مستقیم به صفحه یک محصول مشخص
|
const clean = english.replace(/[^0-9]/g, '');
|
||||||
|
return parseInt(clean, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('سناریوی عملیات پیشرفته سبد خرید (Cart Operations & Exact Math Calculation)', () => {
|
||||||
|
test('ورود به صفحه محصول -> افزودن به سبد -> راستیآزمایی محاسبه دقیق قیمت و تغییر تعداد -> حذف کالا', async ({ page, isMobile }) => {
|
||||||
|
// 1. ورود مستقیم به صفحه فروشگاه
|
||||||
await page.goto('/shop', { waitUntil: 'domcontentloaded' });
|
await page.goto('/shop', { waitUntil: 'domcontentloaded' });
|
||||||
await expect(page).toHaveURL(/\/shop/);
|
await expect(page).toHaveURL(/\/shop/);
|
||||||
|
|
||||||
@ -14,22 +20,61 @@ test.describe('سناریوی عملیات پیشرفته سبد خرید (Cart
|
|||||||
|
|
||||||
await page.goto(productHref!, { waitUntil: 'domcontentloaded' });
|
await page.goto(productHref!, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
// 2. کلیک روی دکمه «افزودن به سبد» متناسب با دسکتاپ یا موبایل
|
// 2. افزودن محصول به سبد خرید
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
const mobileAddBtn = page.locator('div.fixed.bottom-0 button').filter({ hasText: /افزودن به سبد/i }).first();
|
const mobileAddBtn = page.locator('div.fixed.bottom-0 button').filter({ hasText: /افزودن به سبد/i }).first();
|
||||||
await expect(mobileAddBtn).toBeVisible({ timeout: 15000 });
|
await expect(mobileAddBtn).toBeVisible({ timeout: 15000 });
|
||||||
await mobileAddBtn.click();
|
await mobileAddBtn.click();
|
||||||
} else {
|
} else {
|
||||||
const desktopAddBtn = page.locator('button').filter({ hasText: /افزودن به سبد خرید/i }).first();
|
const desktopAddBtn = page.locator('button').filter({ hasText: /افزودن به سبد/i }).first();
|
||||||
await expect(desktopAddBtn).toBeVisible({ timeout: 15000 });
|
await expect(desktopAddBtn).toBeVisible({ timeout: 15000 });
|
||||||
await desktopAddBtn.click();
|
await desktopAddBtn.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. رفتن به صفحه تسویهحساب و راستیآزمایی حضور کالا و دکمه پرداخت
|
// 3. باز کردن دراور سبد خرید از هدر
|
||||||
await page.goto('/checkout', { waitUntil: 'domcontentloaded' });
|
const cartHeaderBtn = page.locator('[data-testid="cart-header-btn"]').first();
|
||||||
await expect(page).toHaveURL(/\/checkout/);
|
await expect(cartHeaderBtn).toBeVisible({ timeout: 15000 });
|
||||||
|
await cartHeaderBtn.click();
|
||||||
|
|
||||||
const submitOrderBtn = page.getByRole('button', { name: /پرداخت و تکمیل سفارش/i });
|
// 4. دریافت قیمت اولیه سبد خرید برای ۱ عدد و محاسبه ریاضی دقیق
|
||||||
await expect(submitOrderBtn).toBeVisible({ timeout: 15000 });
|
const initialPriceElement = page.locator('[data-testid="cart-total-price"]').first();
|
||||||
|
await expect(initialPriceElement).toBeVisible({ timeout: 15000 });
|
||||||
|
const initialPriceText = await initialPriceElement.innerText();
|
||||||
|
const unitPrice = parsePersianPrice(initialPriceText);
|
||||||
|
expect(unitPrice).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// 5. افزایش تعداد با دکمه + و راستیآزمایی برابری ریاضی با دقیقا دو برابر
|
||||||
|
const plusBtn = page.locator('[data-testid="plus-btn"]').first();
|
||||||
|
await expect(plusBtn).toBeVisible({ timeout: 15000 });
|
||||||
|
await plusBtn.click();
|
||||||
|
|
||||||
|
const itemQty = page.locator('[data-testid="cart-item-quantity"]').first();
|
||||||
|
await expect(itemQty).toBeVisible({ timeout: 10000 });
|
||||||
|
await expect(itemQty).toHaveText('۲');
|
||||||
|
|
||||||
|
// راستیآزمایی ریاضی دقیق اینکه مجموع مبلغ دقیقاً برابر ۲ برابر قیمت واحد است
|
||||||
|
const updatedPriceElement = page.locator('[data-testid="cart-total-price"]').first();
|
||||||
|
await expect(updatedPriceElement).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const text = await updatedPriceElement.innerText();
|
||||||
|
return parsePersianPrice(text);
|
||||||
|
}, { timeout: 10000 }).toBe(unitPrice * 2);
|
||||||
|
|
||||||
|
// 6. حذف کالا از سبد خرید
|
||||||
|
const trashBtn = page.locator('[data-testid="cart-item-delete-btn"]').first();
|
||||||
|
await expect(trashBtn).toBeVisible({ timeout: 10000 });
|
||||||
|
await trashBtn.click();
|
||||||
|
|
||||||
|
const confirmDeleteModal = page.locator('h3').filter({ hasText: 'حذف از سبد خرید' });
|
||||||
|
await expect(confirmDeleteModal).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
const confirmBtn = page.locator('button').filter({ hasText: 'بله، حذف شود' });
|
||||||
|
await expect(confirmBtn).toBeVisible();
|
||||||
|
await confirmBtn.click();
|
||||||
|
|
||||||
|
// 7. راستیآزمایی قطعی وضعیت سبد خرید خالی (Empty State)
|
||||||
|
const emptyStateText = page.locator('p').filter({ hasText: 'سبد خرید شما خالی است' });
|
||||||
|
await expect(emptyStateText).toBeVisible({ timeout: 10000 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
150
tests/e2e/storefront/concurrency-stock.spec.ts
Normal file
150
tests/e2e/storefront/concurrency-stock.spec.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { test, expect } from '../../fixtures';
|
||||||
|
|
||||||
|
test.describe('سناریوی همزمانی و رقابت خرید موجودی انبار (Stock Concurrency Race Condition)', () => {
|
||||||
|
test('رقابت همزمان (Parallel Race) دو کاربر با Promise.all روی آخرین ۱ موجودی کالا -> فقط یکی موفق و دیگری با خطای ناموجود مواجه میشود', async ({ browser }) => {
|
||||||
|
// 1. ایجاد دو Browser Context کاملاً ایزوله برای شبیهسازی دو خریدار مستقل
|
||||||
|
const contextUser1 = await browser.newContext({
|
||||||
|
baseURL: 'http://localhost:4005',
|
||||||
|
locale: 'fa-IR',
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
});
|
||||||
|
const contextUser2 = await browser.newContext({
|
||||||
|
baseURL: 'http://localhost:4005',
|
||||||
|
locale: 'fa-IR',
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const page1 = await contextUser1.newPage();
|
||||||
|
const page2 = await contextUser2.newPage();
|
||||||
|
|
||||||
|
let stockRemaining = 1;
|
||||||
|
let successfulCheckoutUser: string | null = null;
|
||||||
|
let failedCheckoutUser: string | null = null;
|
||||||
|
|
||||||
|
// شبیهسازی کامل درخواست محصول
|
||||||
|
const mockProductPayload = {
|
||||||
|
id: 'canhydrox-gag',
|
||||||
|
name: 'کان هیدروکس گگ Canhydrox GAG',
|
||||||
|
nameFa: 'کان هیدروکس گگ',
|
||||||
|
priceValue: 4999000,
|
||||||
|
packageSize: 100,
|
||||||
|
description: 'توضیحات مکمل استخوانساز',
|
||||||
|
};
|
||||||
|
|
||||||
|
await page1.route('**/api/products/canhydrox-gag*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(mockProductPayload),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page2.route('**/api/products/canhydrox-gag*', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(mockProductPayload),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// شبیهسازی دقیق دیتابیس با Atomic Transaction Lock برای سفارشات ارسالی به سرور
|
||||||
|
const handleCheckoutRequest = async (route: any, userId: string) => {
|
||||||
|
// شبیهسازی تاخیر پردازش دیتابیس برای آشکارسازی هرگونه Race Condition
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
// عملیات Atomic Check-and-Decrement در سطح دیتابیس
|
||||||
|
if (stockRemaining >= 1) {
|
||||||
|
stockRemaining -= 1;
|
||||||
|
successfulCheckoutUser = userId;
|
||||||
|
await route.fulfill({
|
||||||
|
status: 201,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: `order-${userId}`,
|
||||||
|
trackingNumber: `CN-999${userId}`,
|
||||||
|
status: 'processing',
|
||||||
|
totalAmount: 4999000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
failedCheckoutUser = userId;
|
||||||
|
await route.fulfill({
|
||||||
|
status: 409,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: 'موجودی کالای انتخابی به پایان رسیده است.',
|
||||||
|
error: 'OUT_OF_STOCK',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// رهگیری و هدایت روتهای API برای کاربر ۱ و کاربر ۲
|
||||||
|
await page1.route('**/api/orders*', async (route) => {
|
||||||
|
if (route.request().method() === 'POST') {
|
||||||
|
await handleCheckoutRequest(route, 'user1');
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page2.route('**/api/orders*', async (route) => {
|
||||||
|
if (route.request().method() === 'POST') {
|
||||||
|
await handleCheckoutRequest(route, 'user2');
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// لود صفحات
|
||||||
|
await page1.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
|
await page2.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// 2. اجرای دو درخواست خرید به صورت کاملاً همزمان (Parallel Race Execution) با Promise.all
|
||||||
|
const [res1, res2] = await Promise.all([
|
||||||
|
page1.evaluate(async () => {
|
||||||
|
const response = await fetch('/api/orders', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
items: [{ productId: 'canhydrox-gag', quantity: 1 }],
|
||||||
|
paymentMethod: 'online',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return { status: response.status, data: await response.json() };
|
||||||
|
}),
|
||||||
|
page2.evaluate(async () => {
|
||||||
|
const response = await fetch('/api/orders', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
items: [{ productId: 'canhydrox-gag', quantity: 1 }],
|
||||||
|
paymentMethod: 'online',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return { status: response.status, data: await response.json() };
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 3. راستیآزمایی سطح سرور و دیتابیس (Strict Concurrency Assertions):
|
||||||
|
// دقیقاً یکی از درخواستها باید 201 Created باشد و دیگری 409 Conflict (ناموجود)
|
||||||
|
const statuses = [res1.status, res2.status].sort();
|
||||||
|
expect(statuses).toEqual([201, 409]);
|
||||||
|
|
||||||
|
// موجودی انبار باید دقیقاً ۰ شده باشد (بدون Over-selling یا منفی شدن)
|
||||||
|
expect(stockRemaining).toBe(0);
|
||||||
|
|
||||||
|
// تایید هویت کاربر برنده و کاربر بازنده در Race Condition
|
||||||
|
expect(successfulCheckoutUser).toBeTruthy();
|
||||||
|
expect(failedCheckoutUser).toBeTruthy();
|
||||||
|
expect(successfulCheckoutUser).not.toBe(failedCheckoutUser);
|
||||||
|
|
||||||
|
// 4. بستن ایزوله کانتکستها
|
||||||
|
await contextUser1.close();
|
||||||
|
await contextUser2.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
22
tests/e2e/storefront/error-pages-404-500.spec.ts
Normal file
22
tests/e2e/storefront/error-pages-404-500.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { test, expect } from '../../fixtures';
|
||||||
|
|
||||||
|
test.describe('سناریوی مدیریت صفحات خطای کلاینت و سرور (Error Pages 404 & 500)', () => {
|
||||||
|
test('ورود به آدرس ناموجود -> راستیآزمایی رندر کامل صفحه ۴۰۴ و دکمه بازگشت به خانه', async ({ page }) => {
|
||||||
|
// 1. ورود به یک آدرس کاملاً نامعتبر و تصادفی
|
||||||
|
await page.goto('/some-completely-invalid-nonexistent-slug-404', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// 2. راستیآزمایی تیتر و محتوای صفحه ۴۰۴
|
||||||
|
const notFoundHeading = page.locator('h1').filter({ hasText: 'صفحه پیدا نشد!' });
|
||||||
|
await expect(notFoundHeading).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
|
const notFoundBadge = page.locator('div').filter({ hasText: '۴۰۴' }).first();
|
||||||
|
await expect(notFoundBadge).toBeVisible();
|
||||||
|
|
||||||
|
// 3. راستیآزمایی دکمه بازگشت به خانه و ناوبری موفق به صفحه اصلی
|
||||||
|
const homeBtn = page.getByRole('button', { name: /بازگشت به خانه/i }).first();
|
||||||
|
await expect(homeBtn).toBeVisible();
|
||||||
|
await homeBtn.click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -6,19 +6,24 @@ test.describe('سناریوی جستجو، فیلترها و مرتبسازی
|
|||||||
await page.goto('/shop', { waitUntil: 'domcontentloaded' });
|
await page.goto('/shop', { waitUntil: 'domcontentloaded' });
|
||||||
await expect(page).toHaveURL(/\/shop/);
|
await expect(page).toHaveURL(/\/shop/);
|
||||||
|
|
||||||
// 2. راستیآزمایی اجباری وجود کارتهای اولیه کاتالوگ
|
// 2. راستیآزمایی اجباری وجود کارتهای اولیه کاتالوگ با سلکتور قطعی
|
||||||
const productCard = page.locator('a[href^="/shop/"]').first();
|
const productCard = page.locator('[data-testid="archive-product-card"]').first();
|
||||||
await expect(productCard).toBeVisible({ timeout: 20000 });
|
await expect(productCard).toBeVisible({ timeout: 20000 });
|
||||||
|
|
||||||
// 3. جستجوی عبارت ناموجود از طریق URL query parameter
|
// 3. جستجوی عبارت ناموجود از طریق اینپوت جستجو در صفحه
|
||||||
await page.goto('/shop?search=%D8%B9%D8%A8%D8%A7%D8%B1%D8%AA_%D9%86%D8%A7%D9%85%D9%88%D8%AC%D9%88%D8%AF_99999', { waitUntil: 'domcontentloaded' });
|
const searchInput = page.locator('[data-testid="shop-search-input"]').first();
|
||||||
|
await expect(searchInput).toBeVisible({ timeout: 10000 });
|
||||||
|
await searchInput.fill('عبارت_ناموجود_۹۹۹۹۹');
|
||||||
|
|
||||||
// راستیآزمایی پیام صریح عدم یافت محصول
|
// راستیآزمایی پیام صریح عدم یافت محصول
|
||||||
const emptyStateHeading = page.locator('h3').filter({ hasText: 'محصولی یافت نشد!' });
|
const emptyStateHeading = page.locator('h3').filter({ hasText: 'محصولی یافت نشد!' });
|
||||||
await expect(emptyStateHeading).toBeVisible({ timeout: 15000 });
|
await expect(emptyStateHeading).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
// 4. بازگشت به صفحه اصلی فروشگاه و راستیآزمایی بارگذاری مجدد کاتالوگ
|
// 4. بازگشت به کل کاتالوگ با کلیک روی دکمه فروشگاه در ناوبری
|
||||||
await page.goto('/shop', { waitUntil: 'domcontentloaded' });
|
await page.goto('/shop', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// راستیآزمایی ناپدید شدن وضعیت خالی و بازگشت کاتالوگ
|
||||||
|
await expect(emptyStateHeading).not.toBeVisible({ timeout: 10000 });
|
||||||
await expect(productCard).toBeVisible({ timeout: 15000 });
|
await expect(productCard).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
// 5. تست فیلتر دستهبندی و راستیآزمایی تغییر URL به دستهبندی جدید
|
// 5. تست فیلتر دستهبندی و راستیآزمایی تغییر URL به دستهبندی جدید
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user