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(); });