PM_Console/frontend/e2e/command-center.spec.js

393 خطوط
23 KiB
JavaScript

import { expect, test } from '@playwright/test';
test('login password can be shown and hidden', async ({ page }) => {
await page.goto('/login');
const password = page.locator('#login-password');
await password.fill('secret-password');
await expect(password).toHaveAttribute('type', 'password');
await page.getByRole('button', { name: 'نمایش رمز عبور' }).click();
await expect(password).toHaveAttribute('type', 'text');
await page.getByRole('button', { name: 'مخفی کردن رمز عبور' }).click();
await expect(password).toHaveAttribute('type', 'password');
});
test('dashboard command surfaces and settings render without runtime errors', async ({ page }, testInfo) => {
const runtimeErrors = [];
page.on('pageerror', (error) => runtimeErrors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') runtimeErrors.push(message.text());
});
const user = { id: 1, name: 'مدیر تست رابط', email: 'ui@example.test', permissions: ['reports.view', 'settings.view', 'notifications.view'] };
await page.addInitScript(({ testUser }) => {
localStorage.setItem('token', 'visual-smoke-token');
localStorage.setItem('user', JSON.stringify(testUser));
}, { testUser: user });
await page.route('**/api/**', async (route) => {
const url = new URL(route.request().url());
const path = url.pathname;
if (path === '/api/user') return route.fulfill({ json: { success: true, data: user } });
if (path === '/api/dashboard/monitoring') return route.fulfill({ json: { success: true, data: {
generated_at: new Date().toISOString(),
summary: { active_projects: 4, projects_at_risk: 1, open_tasks: 18, overdue_tasks: 3, blocked_tasks: 2, active_sprints: 2, meetings_today: 3, overdue_action_items: 1, open_blockers: 2 },
projects: [{ id: 1, title: 'پروژه نمونه رابط', status: 'in_progress', health: 'at_risk', progress: 62, total_tasks: 14, completed_tasks: 8, overdue_tasks: 2, blocked_tasks: 1 }],
sprints: [{ id: 1, title: 'Sprint رابط', project: 'پروژه نمونه رابط', progress: 58, expected_progress: 65, remaining_days: 5, health: 'on_track', blocked_tasks: 0 }],
workload: [{ id: 1, name: 'کاربر نمونه', open_tasks: 7, overdue_tasks: 1, load: 'high' }],
attention: [],
upcoming_meetings: [],
trends: [{ month: '2026-06', created: 12, completed: 9 }, { month: '2026-07', created: 14, completed: 13 }],
} } });
if (path === '/api/reports/project-status') return route.fulfill({ json: { success: true, data: [
{ status: 'in_progress', label: 'in_progress', count: 3, value: 3 },
{ status: 'cancelled', label: 'cancelled', count: 1, value: 1 },
] } });
if (path === '/api/reports/team-performance') return route.fulfill({ json: { success: true, data: [
{ name: 'عضو نمونه تیم', completed: 8, in_progress: 3, delayed: 1 },
] } });
if (path === '/api/calendar/events') return route.fulfill({ json: { success: true, data: [] } });
if (path === '/api/notifications') return route.fulfill({ json: { success: true, data: [], meta: { unread_count: 0, archived_count: 0 } } });
if (path === '/api/settings') return route.fulfill({ json: { success: true, data: [
{ id: 1, key: 'working_days', value: ['saturday', 'sunday'], group: 'calendar', type: 'list' },
{ id: 2, key: 'default_meeting_reminder_minutes', value: 30, group: 'meeting', type: 'number' },
] } });
if (path === '/api/branding') return route.fulfill({ json: { success: true, data: { company_name: 'شرکت آزمون', logo_path: null, logo_url: null } } });
return route.fulfill({ json: { success: true, data: [] } });
});
await page.goto('/');
await expect(page.getByText('مانیتورینگ عملیاتی')).toBeVisible();
await expect(page.getByRole('cell', { name: '۱۴۰۵ خرداد', exact: true })).toBeVisible();
await expect(page.getByText('2026-06')).toHaveCount(0);
await expect(page.locator('img.sidebar-brand-mark')).toBeVisible();
await page.getByRole('button', { name: 'جمع کردن منوی اصلی' }).click();
await expect(page.locator('.main-content')).toHaveClass(/sidebar-collapsed/);
await page.getByRole('button', { name: 'باز کردن منوی اصلی' }).click();
await expect(page.locator('.main-content')).not.toHaveClass(/sidebar-collapsed/);
await page.getByRole('button', { name: 'تقویم رویدادها' }).click();
await expect(page.getByRole('region', { name: 'تقویم رویدادها' })).toBeVisible();
await expect(page.getByText('برنامه روز')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.locator('.calendar-popover')).toBeHidden();
await page.getByRole('button', { name: 'اعلان‌ها' }).click();
await expect(page.getByLabel('مرکز اعلان‌ها')).toBeVisible();
await expect(page.getByText(/برای آرشیو به راست/)).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.locator('.notification-center')).toBeHidden();
await page.getByRole('button', { name: 'باز کردن پروفایل کاربری' }).click();
await expect(page.getByRole('dialog', { name: 'پروفایل کاربری' })).toBeVisible();
await expect(page.getByRole('button', { name: 'ذخیره هویت' })).toBeVisible();
await page.getByRole('tab', { name: 'اطلاعات کاری' }).click();
await expect(page.getByRole('button', { name: 'ذخیره اطلاعات کاری' })).toBeVisible();
const desktopModalMetrics = await page.getByRole('dialog', { name: 'پروفایل کاربری' }).evaluate((element) => {
const body = element.querySelector('.modal-body');
const styles = getComputedStyle(element);
return {
overflow: styles.overflowY,
bodyOverflow: getComputedStyle(body).overflowY,
radius: [styles.borderTopLeftRadius, styles.borderTopRightRadius, styles.borderBottomRightRadius, styles.borderBottomLeftRadius],
};
});
expect(desktopModalMetrics.overflow).toBe('hidden');
expect(desktopModalMetrics.bodyOverflow).toBe('hidden');
expect(desktopModalMetrics.radius.every((value) => parseFloat(value) > 0)).toBe(true);
await page.getByRole('button', { name: 'بستن', exact: true }).click();
await expect(page.getByRole('dialog', { name: 'پروفایل کاربری' })).toBeHidden();
await page.goto('/settings');
await expect(page.getByRole('heading', { name: 'مرکز تنظیمات' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'شخصی‌سازی' })).toBeVisible();
await expect(page.getByRole('button', { name: 'تنظیم جدید' })).toBeVisible();
await expect(page.getByRole('button', { name: 'ساده' })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'پیشرفته' })).toHaveCount(0);
await page.getByRole('button', { name: /تقویم و زمان کاری/ }).click();
await expect(page.getByText('شنبه', { exact: true })).toBeVisible();
await page.getByText('جزئیات فنی', { exact: true }).first().click();
await expect(page.locator('.settings-technical-details').first().locator('input[readonly]')).toBeVisible();
await page.screenshot({ path: testInfo.outputPath('settings-command-center.png'), fullPage: true });
await page.goto('/reports');
await expect(page.getByRole('heading', { name: 'جزئیات' })).toBeVisible();
await expect(page.getByText('در حال انجام', { exact: true }).last()).toBeVisible();
await expect(page.getByText('لغوشده', { exact: true }).last()).toBeVisible();
await expect(page.getByText('in_progress', { exact: true })).toHaveCount(0);
await page.getByRole('button', { name: 'عملکرد تیم' }).click();
const teamNameTick = page.locator('.team-performance-chart text').filter({ hasText: 'عضو نمونه تیم' });
await expect(teamNameTick).toBeVisible();
const tickAndGrid = await page.locator('.team-performance-chart').evaluate((chart) => {
const tick = [...chart.querySelectorAll('text')].find((node) => node.textContent.includes('عضو نمونه تیم'));
const gridLine = chart.querySelector('.recharts-cartesian-grid line');
return { tickRight: tick.getBoundingClientRect().right, gridLeft: gridLine.getBoundingClientRect().left };
});
expect(tickAndGrid.tickRight).toBeLessThanOrEqual(tickAndGrid.gridLeft + 1);
await page.setViewportSize({ width: 390, height: 844 });
await page.evaluate(() => localStorage.setItem('preferredAppMode', 'desktop'));
await page.goto('/');
await expect(page.getByText('مانیتورینگ عملیاتی')).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
await page.getByRole('button', { name: 'باز کردن منوی اصلی' }).click();
await expect(page.locator('.app-sidebar.mobile-open')).toBeVisible();
await expect(page.locator('.app-sidebar.mobile-open').getByText('پروژه‌ها')).toBeVisible();
await page.locator('.app-sidebar.mobile-open').getByRole('button', { name: 'بستن منوی اصلی' }).click();
await expect(page.locator('.app-sidebar.mobile-open')).toHaveCount(0);
await page.getByRole('button', { name: 'باز کردن پروفایل کاربری' }).click();
await expect(page.getByRole('dialog', { name: 'پروفایل کاربری' })).toBeVisible();
expect(await page.getByRole('dialog', { name: 'پروفایل کاربری' }).evaluate((element) => element.getBoundingClientRect().height <= window.innerHeight)).toBe(true);
await page.getByRole('button', { name: 'بستن', exact: true }).click();
await page.screenshot({ path: testInfo.outputPath('mobile-dashboard.png'), fullPage: true });
expect(runtimeErrors).toEqual([]);
});
test('dark projects and task details keep readable centered surfaces', async ({ page }, testInfo) => {
const user = { id: 1, name: 'مدیر رابط', email: 'manager@example.test' };
const project = { id: 10, title: 'پروژه خوانا در تاریکی', description: 'شرح نمونه پروژه', status: 'in_progress', priority: 'high', progress: 45, project_manager: user, end_date: '2026-09-20' };
const task = { id: 20, title: 'تسک مودال مرکزی', description: 'شرح تسک برای بررسی مودال', project_id: project.id, project, assignee: user, assignee_id: user.id, priority: 'medium', status: 'todo', start_date: '2026-09-01', due_date: '2026-09-10', estimated_time: 4 };
await page.addInitScript(({ testUser }) => {
localStorage.setItem('token', 'visual-details-token');
localStorage.setItem('user', JSON.stringify(testUser));
localStorage.setItem('theme', 'dark');
}, { testUser: user });
await page.route('**/api/**', async (route) => {
const url = new URL(route.request().url());
if (url.pathname === '/api/user') return route.fulfill({ json: { success: true, data: user } });
if (url.pathname === '/api/projects' && route.request().method() === 'GET') return route.fulfill({ json: { success: true, data: [project], meta: { current_page: 1, last_page: 1 } } });
if (url.pathname === '/api/tasks' && route.request().method() === 'GET') return route.fulfill({ json: { success: true, data: [task], meta: { current_page: 1, last_page: 1 } } });
if (url.pathname === '/api/users') return route.fulfill({ json: { success: true, data: [user] } });
if (url.pathname === '/api/departments') return route.fulfill({ json: { success: true, data: [], flat: [] } });
if (url.pathname === `/api/tasks/${task.id}/checklists`) return route.fulfill({ json: { success: true, data: [] } });
if (url.pathname === '/api/comments') return route.fulfill({ json: { success: true, data: [] } });
if (url.pathname === '/api/notifications') return route.fulfill({ json: { success: true, data: [], meta: { unread_count: 0 } } });
return route.fulfill({ json: { success: true, data: [] } });
});
await page.goto('/projects');
const projectTitle = page.getByRole('link', { name: project.title });
await expect(projectTitle).toBeVisible();
expect(await projectTitle.evaluate((element) => getComputedStyle(element).color)).toBe('rgb(255, 255, 255)');
await page.goto('/tasks');
await page.getByRole('cell', { name: task.title, exact: true }).click();
const dialog = page.getByRole('dialog', { name: task.title });
await expect(dialog).toBeVisible();
const metrics = await dialog.evaluate((element) => {
const rect = element.getBoundingClientRect();
const styles = getComputedStyle(element);
return {
centerOffsetX: Math.abs((rect.left + rect.width / 2) - window.innerWidth / 2),
centerOffsetY: Math.abs((rect.top + rect.height / 2) - window.innerHeight / 2),
radii: [styles.borderTopLeftRadius, styles.borderTopRightRadius, styles.borderBottomRightRadius, styles.borderBottomLeftRadius],
};
});
expect(metrics.centerOffsetX).toBeLessThan(3);
expect(metrics.centerOffsetY).toBeLessThan(3);
expect(metrics.radii.every((radius) => parseFloat(radius) >= 18)).toBe(true);
await page.screenshot({ path: testInfo.outputPath('dark-task-modal.png'), fullPage: true });
});
test('backlog conversion date pickers stay visible inside the modal flow', async ({ page }, testInfo) => {
const user = { id: 1, name: 'مدیر بک‌لاگ', email: 'backlog@example.test' };
const member = { id: 2, name: 'عضو پروژه', email: 'member@example.test', status: 'active' };
const project = { id: 10, title: 'پروژه تقویم', project_manager: user, members: [member] };
const backlog = {
id: 30,
title: 'بک‌لاگ قابل تبدیل',
description: 'بررسی نمایش تقویم در مودال',
type: 'feature',
priority: 'medium',
project_id: project.id,
project,
is_archived: false,
};
let conversionPayload;
await page.addInitScript(({ testUser }) => {
localStorage.setItem('token', 'backlog-date-token');
localStorage.setItem('user', JSON.stringify(testUser));
}, { testUser: user });
await page.route('**/api/**', async (route) => {
const url = new URL(route.request().url());
const method = route.request().method();
if (url.pathname === '/api/user') return route.fulfill({ json: { success: true, data: user } });
if (url.pathname === '/api/backlog-items' && method === 'GET') return route.fulfill({ json: { success: true, data: [backlog] } });
if (url.pathname === '/api/projects' && method === 'GET') return route.fulfill({ json: { success: true, data: [project] } });
if (url.pathname === '/api/users' && method === 'GET') return route.fulfill({ json: { success: true, data: [user, member] } });
if (url.pathname === `/api/projects/${project.id}` && method === 'GET') return route.fulfill({ json: { success: true, data: project } });
if (url.pathname === `/api/backlog-items/${backlog.id}/convert-to-task` && method === 'POST') {
conversionPayload = route.request().postDataJSON();
return route.fulfill({ json: { success: true, data: { id: 99 } } });
}
if (url.pathname === '/api/notifications') return route.fulfill({ json: { success: true, data: [], meta: { unread_count: 0 } } });
return route.fulfill({ json: { success: true, data: [] } });
});
const assertCalendarIsInViewport = async (calendar) => {
await expect(calendar).toBeVisible();
const metrics = await calendar.evaluate((element) => {
const rect = element.getBoundingClientRect();
const centerElement = document.elementFromPoint(rect.left + (rect.width / 2), rect.top + 12);
return {
top: rect.top,
left: rect.left,
right: rect.right,
bottom: rect.bottom,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
isTopLayerHit: Boolean(centerElement?.closest('.persian-date-popover')),
};
});
expect(metrics.top).toBeGreaterThanOrEqual(0);
expect(metrics.left).toBeGreaterThanOrEqual(0);
expect(metrics.right).toBeLessThanOrEqual(metrics.viewportWidth);
expect(metrics.bottom).toBeLessThanOrEqual(metrics.viewportHeight);
expect(metrics.isTopLayerHit).toBe(true);
};
await page.goto('/backlog');
await page.getByRole('button', { name: `تبدیل ${backlog.title} به تسک` }).click();
const conversionDialog = page.getByRole('dialog', { name: 'تبدیل بک‌لاگ به تسک' });
await expect(conversionDialog).toBeVisible();
await conversionDialog.locator('#task-assignee').selectOption(String(member.id));
const startDateTrigger = conversionDialog.locator('.form-group').filter({ hasText: 'تاریخ شروع' }).locator('.persian-date-input');
await startDateTrigger.click();
const startCalendar = page.getByRole('dialog', { name: 'انتخاب تاریخ شمسی' });
await assertCalendarIsInViewport(startCalendar);
await page.keyboard.press('Escape');
await expect(startCalendar).toBeHidden();
await expect(conversionDialog).toBeVisible();
await expect(startDateTrigger).toBeFocused();
await startDateTrigger.click();
await assertCalendarIsInViewport(startCalendar);
await startCalendar.locator('button[aria-current="date"]').click();
await expect(startDateTrigger).toHaveAttribute('aria-expanded', 'false');
await page.setViewportSize({ width: 390, height: 700 });
const dueDateTrigger = conversionDialog.locator('.form-group').filter({ hasText: 'تاریخ پایان' }).locator('.persian-date-input');
await dueDateTrigger.click();
const dueCalendar = page.getByRole('dialog', { name: 'انتخاب تاریخ شمسی' });
await assertCalendarIsInViewport(dueCalendar);
await page.screenshot({ path: testInfo.outputPath('mobile-backlog-date-picker.png'), fullPage: true });
await dueCalendar.locator('button[aria-current="date"]').click();
await conversionDialog.getByRole('button', { name: 'ایجاد و تخصیص تسک' }).click();
await expect(conversionDialog).toBeHidden();
expect(conversionPayload.assignee_id).toBe(String(member.id));
expect(conversionPayload.start_date).toBeTruthy();
expect(conversionPayload.due_date).toBe(conversionPayload.start_date);
});
test('kanban task cards show assignee photos and keep initials as fallback', async ({ page }) => {
const user = { id: 1, name: 'مدیر کانبان', email: 'kanban@example.test' };
const avatarDataUrl = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20"%3E%3Crect width="20" height="20" fill="%236366f1"/%3E%3C/svg%3E';
const tasks = [
{
id: 71,
title: 'تسک دارای عکس',
status: 'todo',
priority: 'medium',
assignee: { id: 2, name: 'علی رضایی', avatar: 'avatars/ali.webp', avatar_url: avatarDataUrl },
project: { id: 10, title: 'پروژه کانبان' },
},
{
id: 72,
title: 'تسک بدون عکس',
status: 'in_progress',
priority: 'low',
assignee: { id: 3, name: 'بهار احمدی', avatar: null, avatar_url: null },
project: { id: 10, title: 'پروژه کانبان' },
},
];
await page.addInitScript(({ testUser }) => {
localStorage.setItem('token', 'kanban-avatar-token');
localStorage.setItem('user', JSON.stringify(testUser));
}, { testUser: user });
await page.route('**/api/**', async (route) => {
const path = new URL(route.request().url()).pathname;
if (path === '/api/user') return route.fulfill({ json: { success: true, data: user } });
if (path === '/api/tasks') return route.fulfill({ json: { success: true, data: tasks } });
if (path === '/api/projects') return route.fulfill({ json: { success: true, data: [{ id: 10, title: 'پروژه کانبان' }] } });
if (path === '/api/users') return route.fulfill({ json: { success: true, data: tasks.map((task) => task.assignee) } });
if (path === '/api/branding') return route.fulfill({ json: { success: true, data: { company_name: 'مدیریت پروژه', logo_url: null } } });
if (path === '/api/notifications') return route.fulfill({ json: { success: true, data: [], meta: { unread_count: 0 } } });
return route.fulfill({ json: { success: true, data: [] } });
});
await page.goto('/kanban');
const photoCard = page.locator('.kanban-task-card[data-task-id="71"]');
const fallbackCard = page.locator('.kanban-task-card[data-task-id="72"]');
await expect(photoCard.getByRole('img', { name: 'عکس پروفایل علی رضایی' })).toBeVisible();
await expect(fallbackCard.locator('.kanban-assignee-avatar')).toHaveText('ب');
await expect(fallbackCard.locator('.kanban-assignee-avatar img')).toHaveCount(0);
});
test('project task can be transferred to an approved project member', async ({ page }) => {
const assignPermission = { id: 1, name: 'tasks.assign', display_name: 'تخصیص وظایف' };
const user = {
id: 1,
name: 'مدیر پروژه',
email: 'manager@example.test',
roles: [{ id: 1, name: 'project_manager', permissions: [assignPermission] }],
};
const approvedMember = { id: 2, name: 'عضو تأییدشده', email: 'member@example.test', job_title: 'توسعه‌دهنده', status: 'active' };
const project = {
id: 10,
title: 'پروژه انتقال تسک',
description: 'پروژه تست',
project_manager_id: user.id,
project_manager: user,
members: [approvedMember],
status: 'in_progress',
priority: 'high',
progress: 30,
};
const task = {
id: 50,
title: 'تسک قابل انتقال',
project_id: project.id,
assignee_id: user.id,
assignee: user,
priority: 'medium',
status: 'todo',
due_date: '2026-09-10',
};
let assignmentPayload = null;
await page.addInitScript(({ testUser }) => {
localStorage.setItem('token', 'task-assignment-token');
localStorage.setItem('user', JSON.stringify(testUser));
}, { testUser: user });
await page.route('**/api/**', async (route) => {
const url = new URL(route.request().url());
if (url.pathname === '/api/user') return route.fulfill({ json: { success: true, data: user } });
if (url.pathname === `/api/projects/${project.id}`) return route.fulfill({ json: { success: true, data: project } });
if (url.pathname === '/api/tasks' && route.request().method() === 'GET') return route.fulfill({ json: { success: true, data: [task] } });
if (url.pathname === `/api/tasks/${task.id}/assignee` && route.request().method() === 'PUT') {
assignmentPayload = route.request().postDataJSON();
return route.fulfill({ json: { success: true, data: { ...task, assignee_id: approvedMember.id, assignee: approvedMember } } });
}
return route.fulfill({ json: { success: true, data: [] } });
});
await page.goto(`/projects/${project.id}`);
await page.getByRole('button', { name: 'تسک‌ها', exact: true }).click();
const assigneeSelect = page.getByLabel('انتقال مسئول تسک تسک قابل انتقال');
await expect(assigneeSelect).toBeVisible();
await assigneeSelect.selectOption(String(approvedMember.id));
await expect.poll(() => assignmentPayload?.assignee_id).toBe(approvedMember.id);
await expect(assigneeSelect).toHaveValue(String(approvedMember.id));
await expect(page.getByText(/به عضو تأییدشده منتقل شد/)).toBeVisible();
});