import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import { renderWithProviders } from '@/test/utils'; vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); vi.mock('@/lib/api', () => ({ api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() }, ApiError: class extends Error {}, })); import { api } from '@/lib/api'; import BlogReviewPage from '@/pages/BlogReviewPage'; const get = api.get as ReturnType; /** ردیف صف — همان شکل `toListArray()`: بدون body و faq و sources. */ const ROW = { uuid: 'b-1', title: 'لیزر موهای زائد چطور کار می‌کند؟', slug: 'laser', summary: 'خلاصهٔ کوتاه', image_url: null, tags: ['پوست'], status: 'draft', review_status: 'pending_review', topic_slug: 'skin/laser', created_at: 1_700_000_000, }; /** پاسخ اندپوینت جزئیات — double-nested، همان قرارداد فرم ویرایش مقاله. */ const DETAIL = { success: true, data: { data: { ...ROW, body: '

متن کامل مقاله اینجاست.

', reading_time: 7, primary_keyword: 'لیزر موهای زائد', faq: [{ q: 'چند جلسه لازم است؟', a: 'معمولاً شش تا هشت جلسه.' }], sources: [{ url: 'https://example.org/a', title: 'راهنمای پوست' }], }, }, }; function mockApi(detail: unknown = DETAIL) { get.mockImplementation((url: string) => { if (url.includes('/review-queue')) { return Promise.resolve({ success: true, data: [ROW], meta: { totalRecords: 1, totalPages: 1, currentPage: 1 }, errors: [], }); } return Promise.resolve(detail); }); } describe('BlogReviewPage — مودال بازبینی', () => { beforeEach(() => { get.mockReset(); mockApi(); }); /** * صف با `toListArray()` ساخته می‌شود و `body` ندارد؛ مودال باید متن کامل را از * اندپوینت جزئیات بگیرد، وگرنه بازبین یک قاب خالی می‌بیند. */ it('با کلیک روی بازبینی، متن کامل مقاله را می‌گیرد و نشان می‌دهد', async () => { renderWithProviders(, { route: '/admin/blog-review' }); fireEvent.click(await screen.findByRole('button', { name: 'بازبینی' })); await waitFor(() => expect(screen.getByText('متن کامل مقاله اینجاست.')).toBeInTheDocument()); expect(get).toHaveBeenCalledWith('/api/v1/admin/blog/b-1'); }); it('پرسش‌های متداول و منابع و برچسب‌ها را هم نشان می‌دهد', async () => { renderWithProviders(, { route: '/admin/blog-review' }); fireEvent.click(await screen.findByRole('button', { name: 'بازبینی' })); await waitFor(() => expect(screen.getByText('پرسش‌های متداول')).toBeInTheDocument()); expect(screen.getByText('چند جلسه لازم است؟')).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'راهنمای پوست' })).toHaveAttribute('href', 'https://example.org/a'); expect(screen.getByText('پوست')).toBeInTheDocument(); }); /** بازبین باید بداند چه چیزی را تأیید می‌کند: زمان مطالعه و کلیدواژه و موضوع. */ it('شناسنامهٔ مقاله را بالای متن می‌آورد', async () => { renderWithProviders(, { route: '/admin/blog-review' }); fireEvent.click(await screen.findByRole('button', { name: 'بازبینی' })); await waitFor(() => expect(screen.getByText('۷ دقیقه مطالعه')).toBeInTheDocument()); expect(screen.getByText('کلیدواژه: لیزر موهای زائد')).toBeInTheDocument(); }); /** تا نیامدنِ متن، دکمه‌های تصمیم نباید روی یک قاب خالی بنشینند. */ it('پیش از رسیدن متن، حالت بارگذاری نشان می‌دهد', async () => { get.mockImplementation((url: string) => { if (url.includes('/review-queue')) { return Promise.resolve({ success: true, data: [ROW], meta: { totalRecords: 1, totalPages: 1, currentPage: 1 }, errors: [], }); } return new Promise(() => {}); // هرگز resolve نمی‌شود }); renderWithProviders(, { route: '/admin/blog-review' }); fireEvent.click(await screen.findByRole('button', { name: 'بازبینی' })); expect(await screen.findByText('در حال بارگذاری مقاله...')).toBeInTheDocument(); }); });