test(e2e): setup Playwright testing infrastructure and comprehensive storefront & admin suites
This commit is contained in:
parent
7b89e141c5
commit
270e491d08
6
.gitignore
vendored
6
.gitignore
vendored
@ -11,4 +11,8 @@ coverage/
|
|||||||
node_modules
|
node_modules
|
||||||
.next
|
.next
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
test ci trigger
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
playwright/.auth/
|
||||||
|
blob-report/
|
||||||
|
|
||||||
|
|||||||
80
package-lock.json
generated
80
package-lock.json
generated
@ -8,7 +8,9 @@
|
|||||||
"name": "caninoiran-workspace",
|
"name": "caninoiran-workspace",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^8.2.2"
|
"@playwright/test": "^1.62.1",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"dotenv": "^17.4.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
@ -21,6 +23,22 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
@ -157,6 +175,19 @@
|
|||||||
"url": "https://opencollective.com/date-fns"
|
"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": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
@ -174,6 +205,21 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
@ -211,6 +257,38 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
|||||||
@ -12,9 +12,14 @@
|
|||||||
"build:frontend": "cd frontend/application && npm run build",
|
"build:frontend": "cd frontend/application && npm run build",
|
||||||
"build:admin": "cd frontend/admin-panel && 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",
|
"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": {
|
"devDependencies": {
|
||||||
"concurrently": "^8.2.2"
|
"@playwright/test": "^1.62.1",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"dotenv": "^17.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
106
playwright.config.ts
Normal file
106
playwright.config.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
160
tests/e2e/admin/admin-crud.spec.ts
Normal file
160
tests/e2e/admin/admin-crud.spec.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
68
tests/e2e/storefront/b2b-flow.spec.ts
Normal file
68
tests/e2e/storefront/b2b-flow.spec.ts
Normal file
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
45
tests/e2e/storefront/blog-wiki.spec.ts
Normal file
45
tests/e2e/storefront/blog-wiki.spec.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
51
tests/e2e/storefront/checkout-flow.spec.ts
Normal file
51
tests/e2e/storefront/checkout-flow.spec.ts
Normal file
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
58
tests/e2e/storefront/contact.spec.ts
Normal file
58
tests/e2e/storefront/contact.spec.ts
Normal file
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
56
tests/fixtures/admin-auth.setup.ts
vendored
Normal file
56
tests/fixtures/admin-auth.setup.ts
vendored
Normal file
@ -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 });
|
||||||
|
});
|
||||||
8
tests/fixtures/index.ts
vendored
Normal file
8
tests/fixtures/index.ts
vendored
Normal file
@ -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 };
|
||||||
Loading…
Reference in New Issue
Block a user