QA: unit tests for API logic, Playwright smoke tests, pre-push check, manual QA checklist

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-08 20:11:07 +03:30
co-authored by Cursor
parent e6755d0c2e
commit 46a7693ea0
17 changed files with 827 additions and 1 deletions
+11
View File
@@ -0,0 +1,11 @@
# کپی کنید به e2e/.env (در git ثبت نمی‌شود) — اینجا رمز ننویسید
E2E_USER=
E2E_PASSWORD=
# اگر کاربر عضو چند سازمان است، نام سازمان
E2E_ORG=
# پیش‌فرض: سرور لوکال (npm run dev:web که خودش به سرور اصلی وصل است)
E2E_BASE_URL=http://127.0.0.1:5173
# اختیاری: مشتری دارای حساب ته‌حساب برای تست اسناد
E2E_PERSON_ID=
# اختیاری: توکن یک فرم عمومی لید
E2E_FORM_TOKEN=
+36
View File
@@ -0,0 +1,36 @@
import { test as setup, expect } from '@playwright/test';
import { mkdirSync } from 'node:fs';
const STATE = 'e2e/.auth/user.json';
setup('ورود به پنل', async ({ page }) => {
const user = process.env.E2E_USER;
const password = process.env.E2E_PASSWORD;
if (!user || !password) {
throw new Error('E2E_USER و E2E_PASSWORD را در e2e/.env تنظیم کنید (نمونه: e2e/.env.example).');
}
await page.goto('/login');
await page.locator('#identifier').fill(user);
await page.locator('#password').fill(password);
await page.getByRole('button', { name: 'ورود', exact: true }).click();
const orgCards = page.locator('.org-card');
await expect(async () => {
const onOrgPicker = await orgCards.first().isVisible();
const leftLogin = !page.url().includes('/login');
expect(onOrgPicker || leftLogin).toBe(true);
}).toPass({ timeout: 30_000 });
if (await orgCards.first().isVisible()) {
const orgName = process.env.E2E_ORG;
const card = orgName ? orgCards.filter({ hasText: orgName }).first() : orgCards.first();
await card.click();
}
await expect(page).not.toHaveURL(/\/login/, { timeout: 30_000 });
await expect(page.locator('main.content')).toBeVisible();
mkdirSync('e2e/.auth', { recursive: true });
await page.context().storageState({ path: STATE });
});
+27
View File
@@ -0,0 +1,27 @@
import { test as base, expect, type Page } from '@playwright/test';
type Problems = { list: string[] };
/** هر خطای جاوااسکریپت یا پاسخ 5xx از API در طول تست، تست را رد می‌کند */
export const test = base.extend<{ problems: Problems }>({
problems: [
async ({ page }, use) => {
const problems: Problems = { list: [] };
page.on('pageerror', (err) => problems.list.push(`JS error: ${err.message}`));
page.on('response', (res) => {
if (res.url().includes('/api/') && res.status() >= 500) {
problems.list.push(`API ${res.status()}: ${res.request().method()} ${res.url()}`);
}
});
await use(problems);
expect(problems.list, 'خطا در صفحه').toEqual([]);
},
{ auto: true },
],
});
export { expect };
export async function waitForIdle(page: Page) {
await page.waitForLoadState('networkidle', { timeout: 20_000 }).catch(() => undefined);
}
+72
View File
@@ -0,0 +1,72 @@
import { test, expect, waitForIdle } from './fixtures';
/** فقط خواندنی: هیچ رکوردی ساخته، ویرایش یا حذف نمی‌شود؛ اجرا روی سرور اصلی بی‌خطر است */
const PAGES: Array<[string, string]> = [
['/', 'داشبورد'],
['/contacts', 'مشتریان'],
['/leads', 'لیدها'],
['/pipeline', 'فروش'],
['/activities', 'کارها'],
['/activities/calendar', 'تقویم کارها'],
['/routines', 'کارهای روتین'],
['/conversations', 'گفتگو'],
['/notes', 'یادداشت‌ها'],
['/tickets', 'پشتیبانی'],
['/loyalty', 'باشگاه مشتریان'],
['/reports', 'گزارش‌ها'],
['/invoices', 'فاکتورها'],
['/quotes', 'پیش‌فاکتورها'],
['/cheques', 'چک‌ها'],
['/projects', 'پروژه‌ها'],
['/profile', 'پروفایل'],
['/settings/team', 'تنظیمات تیم'],
['/settings/lead-forms', 'فرم‌های لید'],
['/settings/message-automations', 'اتوماسیون پیام'],
['/settings/tahesab', 'تنظیمات ته‌حساب'],
];
for (const [path, title] of PAGES) {
test(`صفحهٔ ${title} بدون خطا باز می‌شود (${path})`, async ({ page, problems }) => {
await page.goto(path);
await expect(page).not.toHaveURL(/\/login/);
await expect(page.locator('main.content')).toBeVisible();
await waitForIdle(page);
expect(problems.list).toEqual([]);
});
}
test('جستجوی سراسری باز می‌شود و نتیجه می‌دهد', async ({ page }) => {
await page.goto('/');
const search = page.getByPlaceholder(/جستجو در مشتریان/);
await expect(search).toBeVisible();
await search.fill('a');
await waitForIdle(page);
});
test('دارایی‌ها و اسناد ته‌حساب مشتری', async ({ page }) => {
const personId = process.env.E2E_PERSON_ID;
test.skip(!personId, 'E2E_PERSON_ID تنظیم نشده');
await page.goto(`/contacts/people/${personId}?tab=assets`);
await expect(page.getByText('مانده حساب')).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: 'جزئیات حساب', exact: true }).click();
await expect(page.locator('.invoice, .asnad-grid .sanad-card').first().or(page.getByText('در این بازه سندی یافت نشد'))).toBeVisible({ timeout: 60_000 });
const invoice = page.locator('.invoice').first();
if (await invoice.isVisible()) {
await expect(invoice.locator('.inv-head')).toContainText('سند شماره');
await expect(invoice.locator('.inv-table tbody tr').first()).toBeVisible();
}
});
test('فرم عمومی لید بدون لاگین باز می‌شود', async ({ browser }) => {
const token = process.env.E2E_FORM_TOKEN;
test.skip(!token, 'E2E_FORM_TOKEN تنظیم نشده');
const ctx = await browser.newContext({ storageState: undefined });
const page = await ctx.newPage();
const errors: string[] = [];
page.on('pageerror', (e) => errors.push(e.message));
await page.goto(`/f/${token}`);
await expect(page.locator('form')).toBeVisible();
await expect(page.getByRole('button', { name: /ارسال|ثبت/ })).toBeVisible();
expect(errors).toEqual([]);
await ctx.close();
});