diff --git a/.gitignore b/.gitignore index a487b3a..a82c563 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ coverage/ node_modules .next *.tsbuildinfo -test ci trigger +test-results/ +playwright-report/ +playwright/.auth/ +blob-report/ + diff --git a/package-lock.json b/package-lock.json index 7195d5b..3a0417a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "caninoiran-workspace", "version": "1.0.0", "devDependencies": { - "concurrently": "^8.2.2" + "@playwright/test": "^1.62.1", + "concurrently": "^8.2.2", + "dotenv": "^17.4.2" } }, "node_modules/@babel/runtime": { @@ -21,6 +23,22 @@ "node": ">=6.9.0" } }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -157,6 +175,19 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -174,6 +205,21 @@ "node": ">=6" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -211,6 +257,38 @@ "dev": true, "license": "MIT" }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index 42bfedf..286bd30 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,14 @@ "build:frontend": "cd frontend/application && npm run build", "build:admin": "cd frontend/admin-panel && npm run build", "build": "npm run build:backend && npm run build:frontend && npm run build:admin", - "start": "concurrently -c \"blue,magenta,green\" -n \"backend,store,admin\" \"cd backend && npm run start:prod\" \"cd frontend/application && npm run start\" \"cd frontend/admin-panel && npm run preview\"" + "start": "concurrently -c \"blue,magenta,green\" -n \"backend,store,admin\" \"cd backend && npm run start:prod\" \"cd frontend/application && npm run start\" \"cd frontend/admin-panel && npm run preview\"", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:report": "playwright show-report" }, "devDependencies": { - "concurrently": "^8.2.2" + "@playwright/test": "^1.62.1", + "concurrently": "^8.2.2", + "dotenv": "^17.4.2" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..25e2094 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,106 @@ +import { defineConfig, devices } from '@playwright/test'; +import * as dotenv from 'dotenv'; +import * as path from 'path'; + +// Read from default .env.test file if present +dotenv.config({ path: path.resolve(__dirname, '.env.test') }); + +const STOREFRONT_URL = process.env.STOREFRONT_URL || 'http://localhost:4005'; +const ADMIN_URL = process.env.ADMIN_URL || 'http://localhost:5050'; +const BACKEND_URL = process.env.API_URL || 'http://localhost:4001/api'; + +/** + * Playwright E2E configuration for Canina Monorepo. + * Covers Next.js Storefront (port 4005) and Vite Admin Panel (port 5050). + */ +export default defineConfig({ + testDir: './tests', + timeout: 60000, + expect: { + timeout: 10000, + }, + fullyParallel: false, + workers: 2, + retries: 0, + reporter: [ + ['html', { open: 'never' }], + ['list'] + ], + use: { + baseURL: STOREFRONT_URL, + actionTimeout: 15000, + navigationTimeout: 45000, + screenshot: 'only-on-failure', + video: 'retain-on-failure', + trace: 'retain-on-failure', + locale: 'fa-IR', + timezoneId: 'Asia/Tehran', + }, + + projects: [ + // 1. Setup project for Admin Authentication State + { + name: 'admin-setup', + testMatch: /fixtures\/admin-auth\.setup\.ts/, + use: { + baseURL: ADMIN_URL, + }, + }, + + // 2. Storefront on Desktop Chromium + { + name: 'chromium-desktop', + testMatch: /storefront\/.*\.spec\.ts/, + use: { + ...devices['Desktop Chrome'], + baseURL: STOREFRONT_URL, + viewport: { width: 1440, height: 900 }, + }, + }, + + // 3. Storefront on Mobile Safari (iPhone 14) + { + name: 'mobile-safari', + testMatch: /storefront\/.*\.spec\.ts/, + use: { + ...devices['iPhone 14'], + baseURL: STOREFRONT_URL, + }, + }, + + // 4. Admin Panel Flow on Desktop Chromium (Requires admin-setup) + { + name: 'admin-chromium', + testMatch: /admin\/.*\.spec\.ts/, + dependencies: ['admin-setup'], + use: { + ...devices['Desktop Chrome'], + baseURL: ADMIN_URL, + viewport: { width: 1440, height: 900 }, + storageState: './playwright/.auth/admin.json', + }, + }, + ], + + // Auto-start webServers if not already running on local ports + webServer: [ + { + command: 'npm run dev:backend', + url: 'http://127.0.0.1:4001/api/docs', + reuseExistingServer: true, + timeout: 120000, + }, + { + command: 'npm run dev:frontend', + url: 'http://localhost:4005', + reuseExistingServer: true, + timeout: 120000, + }, + { + command: 'npm run dev:admin', + url: 'http://localhost:5050', + reuseExistingServer: true, + timeout: 120000, + }, + ], +}); diff --git a/tests/e2e/admin/admin-crud.spec.ts b/tests/e2e/admin/admin-crud.spec.ts new file mode 100644 index 0000000..cba9e9d --- /dev/null +++ b/tests/e2e/admin/admin-crud.spec.ts @@ -0,0 +1,160 @@ +import { test, expect } from '../../fixtures'; + +test.describe('سناریوی مدیریت محصول در پنل ادمین (Admin Products CRUD & SEO)', () => { + test('ورود به بخش محصولات -> باز کردن فرم ایجاد -> پر کردن فیلدها و سئو -> ذخیره', async ({ page }) => { + // Intercept and mock Admin Auth & Product API calls + 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_jwt_token', + user: { id: '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_jwt_token', + user: { id: '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({ + success: true, + data: { id: 'admin-1', email: 'admin@canina.ir', role: 'SUPER_ADMIN' }, + }), + }); + }); + + await page.route('**/api/admin/products*', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + success: true, + data: [ + { + id: 'prod-1', + artNo: 'ART-001', + nameFa: 'مکمل کلسیم و مفاصل سگ', + category: { id: 'cat-1', nameFa: 'مفاصل' }, + priceValue: 450000, + stockStatus: 'IN_STOCK', + }, + ], + meta: { total: 1, lastPage: 1 }, + }), + }); + } else if (route.request().method() === 'POST') { + await route.fulfill({ + status: 201, + contentType: 'application/json', + body: JSON.stringify({ + success: true, + data: { id: 'pw-mock-id', nameFa: 'محصول تستی پلی‌رایت', artNo: 'PW-TEST-001' }, + message: 'محصول با موفقیت ایجاد شد', + }), + }); + } else { + await route.continue(); + } + }); + + await page.route('**/api/admin/categories*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + success: true, + data: [{ id: 'cat-1', nameFa: 'مفاصل و استخوان' }], + }), + }); + }); + + await page.route('**/api/orders/stats*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ success: true, data: { pending: 0, processing: 0 } }), + }); + }); + + // 1. ورود به صفحه لاگین و انجام لاگین ادمین + await page.goto('/login', { waitUntil: 'domcontentloaded' }); + const emailInput = page.locator('input[type="email"], input[placeholder*="ایمیل"]').first(); + const passwordInput = page.locator('input[type="password"], input[placeholder*="رمز"]').first(); + if (await emailInput.isVisible()) { + 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('/products', { waitUntil: 'domcontentloaded' }); + + // 2. بررسی لود شدن ساختار صفحه مدیریت محصولات یا دکمه ایجاد محصول + const addProductBtn = page.getByRole('button', { name: /محصول جدید/i }).or( + page.locator('button').filter({ hasText: /محصول جدید/i }) + ).first(); + await expect(addProductBtn).toBeVisible({ timeout: 15000 }); + + // 3. کلیک روی دکمه افزودن محصول جدید + await addProductBtn.click(); + + // 4. بررسی باز شدن مودال فرم ایجاد محصول + const modalTitle = page.locator('h2, h3, div').filter({ hasText: /محصول جدید|افزودن محصول|اطلاعات پایه/i }).first(); + await expect(modalTitle).toBeVisible({ timeout: 10000 }); + + // 5. پر کردن اطلاعات پایه محصول + const nameFaInput = page.locator('input[placeholder*="نام فارسی"], input[name*="nameFa"]').or( + page.locator('input[type="text"]').first() + ).first(); + if (await nameFaInput.isVisible()) { + await nameFaInput.fill('محصول تستی پلی‌رایت برای بررسی سئو'); + } + + // 6. سوئیچ به تب یا بخش تنظیمات سئو (SEO) + const seoTab = page.locator('button, div').filter({ hasText: /سئو|SEO/i }).first(); + if (await seoTab.isVisible()) { + await seoTab.click(); + + // پر کردن فیلدهای سئو + const metaTitleInput = page.locator('input[placeholder*="عنوان سئو"], input[name*="metaTitle"]').first(); + const metaDescInput = page.locator('textarea[placeholder*="توضیحات سئو"], textarea[name*="metaDescription"]').first(); + + if (await metaTitleInput.isVisible()) { + await metaTitleInput.fill('خرید آنلاین محصول تستی با بهترین قیمت | کنینا ایران'); + } + if (await metaDescInput.isVisible()) { + await metaDescInput.fill('توضیحات متای تستی برای ارزیابی رندرینگ پیش‌نمایش SERP گوگل در پنل مدیریت پروژه کنینا.'); + } + } + + // 7. کلیک روی دکمه ذخیره / ثبت + const saveBtn = page.getByRole('button', { name: /ذخیره|ثبت نهایی|ایجاد محصول/i }).or( + page.locator('button').filter({ hasText: /ذخیره|ثبت/i }) + ).first(); + if (await saveBtn.isVisible()) { + await saveBtn.click(); + } + }); +}); diff --git a/tests/e2e/storefront/b2b-flow.spec.ts b/tests/e2e/storefront/b2b-flow.spec.ts new file mode 100644 index 0000000..8ab5701 --- /dev/null +++ b/tests/e2e/storefront/b2b-flow.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from '../../fixtures'; + +test.describe('سناریوی همکاری تجاری B2B (B2B Portal Flow)', () => { + test('ورود به /b2b -> بررسی تیتر H1 و متن‌های ارزش پیشنهادی -> ارسال فرم با اعتبارسنجی', async ({ page }) => { + // 1. ورود به /b2b + await page.goto('/b2b'); + await expect(page).toHaveURL(/\/b2b/); + + // 2. بررسی وجود تیتر H1 + const heading = page.locator('h1'); + await expect(heading).toBeVisible(); + await expect(heading).toContainText(/تأمین عمده|همکاری تجاری|کنینا/i); + + // بررسی متن‌های ارزش پیشنهادی (ارسال فاکتور رسمی، تخفیف، گواهی اصالت) + const valueProps = page.locator('section, div').filter({ hasText: /تخفیف|کلینیک|دامپزشکی|گواهی اصالت/i }); + await expect(valueProps.first()).toBeVisible(); + + // 3. ارسال فرم با داده‌های خالی و بررسی نمایش خطاهای اعتبارسنجی + const submitButton = page.getByRole('button', { name: /ارسال درخواست|ثبت درخواست|درخواست همکاری/i }).first(); + await expect(submitButton).toBeVisible(); + + // ارسال بدون وارد کردن فیلدها + await submitButton.click(); + + // بررسی پیام خطا یا اعتبارسنجی (toast یا متن ارور) + const toastError = page.locator('[data-sonner-toast], .toast, [role="alert"]').or( + page.getByText(/لطفاً نام مجموعه و شماره تماس را وارد کنید|الزامی است/i) + ).first(); + await expect(toastError).toBeVisible({ timeout: 5000 }); + + // 4. ارسال فرم با داده‌های معتبر (Mock کردن درخواست API برای جلوگیری از دیتای هرز) + await page.route('**/api/wholesale/apply', async (route) => { + await route.fulfill({ + status: 201, + contentType: 'application/json', + body: JSON.stringify({ success: true, message: 'درخواست همکاری با موفقیت ثبت شد' }), + }); + }); + + // پر کردن فیلدهای فرم + const businessNameInput = page.locator('input[placeholder*="نام کلینیک"], input[placeholder*="مجموعه"]').first(); + const contactNameInput = page.locator('input[placeholder*="نام و نام خانوادگی"], input[placeholder*="رابط"]').first(); + const phoneInput = page.locator('input[placeholder*="۰۹"], input[placeholder*="تماس"]').first(); + const cityInput = page.locator('input[placeholder*="شهر"]').first(); + + if (await businessNameInput.isVisible()) { + await businessNameInput.fill('پت شاپ آزمایشی پایتخت'); + } + if (await contactNameInput.isVisible()) { + await contactNameInput.fill('دکتر حسینی'); + } + if (await phoneInput.isVisible()) { + await phoneInput.fill('09121112233'); + } + if (await cityInput.isVisible()) { + await cityInput.fill('تهران'); + } + + // ارسال مجدد با داده‌های پر شده + await submitButton.click(); + + // 5. بررسی دریافت پیام موفقیت‌آمیز + const successFeedback = page.locator('[data-sonner-toast], [role="alert"]').or( + page.getByText(/با موفقیت ثبت شد|درخواست شما ثبت شد/i) + ).first(); + await expect(successFeedback).toBeVisible({ timeout: 7000 }); + }); +}); diff --git a/tests/e2e/storefront/blog-wiki.spec.ts b/tests/e2e/storefront/blog-wiki.spec.ts new file mode 100644 index 0000000..610ccd3 --- /dev/null +++ b/tests/e2e/storefront/blog-wiki.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '../../fixtures'; + +test.describe('سناریوی وبلاگ و دانشنامه (Blog & Wiki Flow)', () => { + test('ورود به /blog -> باز کردن مقاله -> بررسی اجزا -> رفتن به /wiki/[slug] و تست لینک‌ها', async ({ page }) => { + // 1. ورود به روت /blog + await page.goto('/blog'); + await expect(page).toHaveURL(/\/blog/); + + const blogHeading = page.locator('h1, h2').filter({ hasText: /مقالات|مجله|وبلاگ|سلامت/i }).first(); + await expect(blogHeading).toBeVisible({ timeout: 10000 }); + + // 2. کلیک روی اولین مقاله در لیست + const firstArticleLink = page.locator('a[href^="/blog/"]').filter({ hasNotText: 'وبلاگ' }).first(); + + if (await firstArticleLink.isVisible()) { + await firstArticleLink.click(); + + // 3. بررسی لود صفحه جزئیات مقاله + await expect(page).toHaveURL(/\/blog\/.+/); + const articleHeading = page.locator('h1').first(); + await expect(articleHeading).toBeVisible(); + + // بررسی وجود بخش‌های محتوایی یا سرفصل‌ها + const contentSection = page.locator('article, .prose, main').first(); + await expect(contentSection).toBeVisible(); + } + + // 4. رفتن به دانشنامه /wiki + // ابتدا روت wiki را باز می‌کنیم + await page.goto('/wiki/glucosamine'); + + // اگر اصطلاح پیدا نشود یا نام دیگری داشته باشد، چک می‌کنیم صفحه خطای 500 نداده باشد + const wikiHeading = page.locator('h1, h2').first(); + await expect(wikiHeading).toBeVisible(); + + // 5. بررسی وجود لینک مکمل‌ها یا محصولات مرتبط با ماده + const relatedProducts = page.locator('a[href^="/shop/"]').or( + page.locator('text=مکمل‌های حاوی').or(page.locator('text=محصولات مرتبط')) + ); + + if (await relatedProducts.count() > 0) { + await expect(relatedProducts.first()).toBeVisible(); + } + }); +}); diff --git a/tests/e2e/storefront/checkout-flow.spec.ts b/tests/e2e/storefront/checkout-flow.spec.ts new file mode 100644 index 0000000..00d954b --- /dev/null +++ b/tests/e2e/storefront/checkout-flow.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '../../fixtures'; + +test.describe('سناریوی خرید و سبد خرید (Storefront Checkout Flow)', () => { + test('ورود به صفحه اصلی -> رفتن به فروشگاه -> انتخاب محصول -> افزودن به سبد خرید -> تسویه حساب', async ({ page }) => { + // 1. ورود به صفحه اصلی + await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 45000 }); + await expect(page).toHaveTitle(/کنینا|Canina/i); + + // 2. رفتن به صفحه /shop + await page.goto('/shop', { waitUntil: 'domcontentloaded', timeout: 45000 }); + await expect(page).toHaveURL(/\/shop/); + + // بررسی وجود ساختار کاتالوگ فروشگاه و کارت‌های محصول + const productCard = page.locator('a[href^="/shop/"]').first(); + await expect(productCard).toBeVisible({ timeout: 20000 }); + + // 3. اعمال فیلتر دسته‌بندی یا دکمه‌های فیلتر + const categoryButton = page.locator('button, div').filter({ hasText: /مفاصل|ایمنی|ویتامین|همه|کاتالوگ/i }).first(); + if (await categoryButton.isVisible()) { + await categoryButton.click(); + } + + // 4. افزودن محصول به سبد خرید از کارت آرشیو یا صفحه محصول + const buyButtonOnCard = page.locator('button').filter({ hasText: /خرید/i }).first(); + if (await buyButtonOnCard.isVisible()) { + await buyButtonOnCard.click(); + } else { + const href = await productCard.getAttribute('href'); + if (href) { + await page.goto(href, { waitUntil: 'domcontentloaded' }); + } else { + await productCard.click(); + } + + // افزودن به سبد در صفحه جزئیات محصول + const addToCartBtn = page.getByRole('button', { name: /افزودن به سبد خرید/i }).or( + page.locator('button').filter({ hasText: /افزودن به سبد/i }) + ).first(); + await expect(addToCartBtn).toBeVisible({ timeout: 15000 }); + await addToCartBtn.click(); + } + + // 5. رفتن به صفحه تسویه حساب (Checkout) + await page.goto('/checkout', { waitUntil: 'domcontentloaded', timeout: 30000 }); + await expect(page).toHaveURL(/\/checkout/); + + // 6. بررسی لود شدن ساختار تسویه‌حساب (پرداخت یا کاتالوگ یا اطلاعات خریدار) + const checkoutPageElement = page.locator('button, div, h2').filter({ hasText: /پرداخت|تسویه‌حساب|سفارش|سبد خرید|کاتالوگ/i }).first(); + await expect(checkoutPageElement).toBeVisible({ timeout: 10000 }); + }); +}); diff --git a/tests/e2e/storefront/contact.spec.ts b/tests/e2e/storefront/contact.spec.ts new file mode 100644 index 0000000..192621a --- /dev/null +++ b/tests/e2e/storefront/contact.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '../../fixtures'; + +test.describe('سناریوی فرم تماس و استعلام (Contact Flow)', () => { + test('ورود به /contact -> اعتبارسنجی فیلدها -> پر کردن و ارسال پیام -> دریافت فیدبک', async ({ page }) => { + // 1. ورود به /contact + await page.goto('/contact'); + await expect(page).toHaveURL(/\/contact/); + + // 2. بررسی وجود تیتر صفحه تماس با ما + const contactHeading = page.locator('h1'); + await expect(contactHeading).toBeVisible(); + await expect(contactHeading).toContainText(/تماس با|کنینا/i); + + // 3. پیدا کردن فیلدهای فرم تماس + const nameInput = page.locator('input[placeholder*="نام"]').first(); + const phoneInput = page.locator('input[placeholder*="موبایل"], input[placeholder*="۰۹"]').first(); + const messageInput = page.locator('textarea[placeholder*="پیام"]').first(); + const submitBtn = page.getByRole('button', { name: /ارسال پیام|ثبت پیام|ارسال/i }).first(); + + await expect(submitBtn).toBeVisible(); + + // 4. تست اعتبارسنجی: ارسال فرم خالی + await submitBtn.click(); + const validationError = page.locator('text=لطفاً نام، شماره موبایل و متن پیام را وارد کنید') + .or(page.locator('[data-sonner-toast]')) + .or(page.locator('[role="alert"]')) + .or(page.locator('text=معتبر نیست')) + .first(); + await expect(validationError).toBeVisible({ timeout: 5000 }); + + // 5. Mock کردن پاسخ API ثبت پیام برای اطمینان از عدم ثبت دیتای هرز در دیتابیس اصلی + await page.route('**/api/contact', async (route) => { + await route.fulfill({ + status: 201, + contentType: 'application/json', + body: JSON.stringify({ success: true, message: 'پیام شما با موفقیت ثبت گردید.' }), + }); + }); + + // 6. پر کردن فرم با دیتای معتبر + await page.locator('#contact-name').fill('کاربر تستی'); + await page.locator('#contact-phone').fill('09123456789'); + if (await page.locator('#contact-subject').count() > 0) { + await page.locator('#contact-subject').fill('استعلام تستی محصول'); + } + await page.locator('#contact-message').fill('این یک پیام آزمایشی برای تست خودکار Playwright است.'); + + // 7. ارسال فرم + await submitBtn.click(); + + // 8. بررسی نمایش پیام موفقیت‌آمیز + const successAlert = page.locator('text=پیام شما با موفقیت ثبت شد') + .or(page.locator('text=موفقیت')) + .or(page.locator('[data-sonner-toast]')) + .first(); + await expect(successAlert).toBeVisible({ timeout: 7000 }); + }); +}); diff --git a/tests/fixtures/admin-auth.setup.ts b/tests/fixtures/admin-auth.setup.ts new file mode 100644 index 0000000..7010f8f --- /dev/null +++ b/tests/fixtures/admin-auth.setup.ts @@ -0,0 +1,56 @@ +import { test as setup, expect } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; + +const authFile = path.resolve(__dirname, '../../playwright/.auth/admin.json'); + +setup('authenticate as admin', async ({ page }) => { + const adminEmail = process.env.ADMIN_TEST_EMAIL || 'admin@canina.ir'; + const adminPassword = process.env.ADMIN_TEST_PASSWORD || 'Admin@123456'; + + // Ensure directory exists + const authDir = path.dirname(authFile); + if (!fs.existsSync(authDir)) { + fs.mkdirSync(authDir, { recursive: true }); + } + + // Go to admin login page + await page.goto('/login'); + + // Fill in email and password + const emailInput = page.locator('input[type="email"], input[placeholder*="ایمیل"]'); + const passwordInput = page.locator('input[type="password"], input[placeholder*="رمز"]'); + + if (await emailInput.count() > 0 && await passwordInput.count() > 0) { + await emailInput.first().fill(adminEmail); + await passwordInput.first().fill(adminPassword); + + // Submit form + const submitBtn = page.getByRole('button', { name: /ورود به پنل|ورود/i }); + if (await submitBtn.count() > 0) { + await submitBtn.first().click(); + } else { + await page.keyboard.press('Enter'); + } + + // Wait for navigation or localStorage auth token setting + await page.waitForTimeout(1000); + } + + // Ensure localStorage contains valid admin auth store + await page.evaluate(() => { + const authData = { + state: { + accessToken: 'mock_e2e_admin_jwt_token', + adminUser: { id: 'admin-1', email: 'admin@canina.ir', role: 'SUPER_ADMIN' }, + isAuthenticated: true, + }, + version: 0, + }; + localStorage.setItem('canina_admin_auth', JSON.stringify(authData)); + localStorage.setItem('token', 'mock_e2e_admin_jwt_token'); + }); + + // Save authentication storage state (cookies, localStorage) + await page.context().storageState({ path: authFile }); +}); diff --git a/tests/fixtures/index.ts b/tests/fixtures/index.ts new file mode 100644 index 0000000..8487700 --- /dev/null +++ b/tests/fixtures/index.ts @@ -0,0 +1,8 @@ +import { test as base, expect } from '@playwright/test'; + +// Declare custom fixtures if needed +export const test = base.extend({ + // Custom helpers can be attached here +}); + +export { expect };