From 270e491d0837db22a45ea40d483c7664670896aa Mon Sep 17 00:00:00 2001
From: parsa aghaei
Date: Tue, 25 Aug 2026 12:56:46 +0330
Subject: [PATCH] test(e2e): setup Playwright testing infrastructure and
comprehensive storefront & admin suites
---
.gitignore | 6 +-
package-lock.json | 80 ++++++++++-
package.json | 9 +-
playwright.config.ts | 106 ++++++++++++++
tests/e2e/admin/admin-crud.spec.ts | 160 +++++++++++++++++++++
tests/e2e/storefront/b2b-flow.spec.ts | 68 +++++++++
tests/e2e/storefront/blog-wiki.spec.ts | 45 ++++++
tests/e2e/storefront/checkout-flow.spec.ts | 51 +++++++
tests/e2e/storefront/contact.spec.ts | 58 ++++++++
tests/fixtures/admin-auth.setup.ts | 56 ++++++++
tests/fixtures/index.ts | 8 ++
11 files changed, 643 insertions(+), 4 deletions(-)
create mode 100644 playwright.config.ts
create mode 100644 tests/e2e/admin/admin-crud.spec.ts
create mode 100644 tests/e2e/storefront/b2b-flow.spec.ts
create mode 100644 tests/e2e/storefront/blog-wiki.spec.ts
create mode 100644 tests/e2e/storefront/checkout-flow.spec.ts
create mode 100644 tests/e2e/storefront/contact.spec.ts
create mode 100644 tests/fixtures/admin-auth.setup.ts
create mode 100644 tests/fixtures/index.ts
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 };