import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { createPinia, setActivePinia } from 'pinia'; import { createVuetify } from 'vuetify'; // Мокаем API-слой reminders, чтобы реальный Pinia-store работал поверх spy'ев. const createReminderMock = vi.fn(); const updateReminderMock = vi.fn(); vi.mock('../../resources/js/api/reminders', () => ({ listReminders: vi.fn(), createReminder: (...args: unknown[]) => createReminderMock(...args), updateReminder: (...args: unknown[]) => updateReminderMock(...args), completeReminder: vi.fn(), deleteReminder: vi.fn(), })); vi.mock('../../resources/js/api/client', () => ({ apiClient: {}, ensureCsrfCookie: vi.fn(), extractValidationErrors: vi.fn(() => null), extractErrorMessage: vi.fn(() => 'Произошла ошибка.'), extractRateLimitRetry: vi.fn(() => null), })); import ReminderDialog from '../../resources/js/components/reminders/ReminderDialog.vue'; import type { ApiReminder } from '../../resources/js/api/reminders'; const mockReminder = (overrides: Partial = {}): ApiReminder => ({ id: 42, deal_id: 7, text: 'Перезвонить клиенту', remind_at: '2026-06-01T10:30:00.000Z', completed_at: null, is_sent: false, sent_at: null, created_at: '2026-05-01T09:00:00.000Z', created_by: 1, assignee_id: null, creator_name: 'Иван Петров', ...overrides, }); // VDialog в JSDOM teleport'ится — стаб делает рендеримым inline. const factory = (props: { modelValue: boolean; dealId?: number | null; reminder?: ApiReminder | null; }) => mount(ReminderDialog, { props, global: { plugins: [createVuetify()], stubs: { VDialog: { template: '
', props: ['modelValue'], }, }, }, }); beforeEach(() => { setActivePinia(createPinia()); vi.clearAllMocks(); }); describe('ReminderDialog.vue', () => { describe('rendering', () => { it('не рендерит content при modelValue=false', () => { const wrapper = factory({ modelValue: false, dealId: 7 }); expect(wrapper.find('.dialog-stub').exists()).toBe(false); }); it('рендерит title "Новое напоминание" в create режиме', async () => { const wrapper = factory({ modelValue: true, dealId: 7, reminder: null }); await flushPromises(); expect(wrapper.text()).toContain('Новое напоминание'); expect(wrapper.text()).not.toContain('Редактировать напоминание'); }); it('рендерит title "Редактировать напоминание" в edit режиме', async () => { const wrapper = factory({ modelValue: true, reminder: mockReminder() }); await flushPromises(); expect(wrapper.text()).toContain('Редактировать напоминание'); }); it('кнопка submit подписана "Создать" в create режиме', async () => { const wrapper = factory({ modelValue: true, dealId: 7 }); await flushPromises(); const submitBtn = wrapper.find('[data-testid="reminder-submit"]'); expect(submitBtn.exists()).toBe(true); expect(submitBtn.text()).toContain('Создать'); }); it('кнопка submit подписана "Сохранить" в edit режиме', async () => { const wrapper = factory({ modelValue: true, reminder: mockReminder() }); await flushPromises(); const submitBtn = wrapper.find('[data-testid="reminder-submit"]'); expect(submitBtn.text()).toContain('Сохранить'); }); it('рендерит обязательные поля textarea + datetime-input', async () => { const wrapper = factory({ modelValue: true, dealId: 7 }); await flushPromises(); expect(wrapper.find('[data-testid="reminder-text"]').exists()).toBe(true); expect(wrapper.find('[data-testid="reminder-at"]').exists()).toBe(true); }); }); describe('watch(dialogOpen) — populate / reset', () => { it('open + edit → text и remindAt берутся из props.reminder', async () => { const reminder = mockReminder({ text: 'Тестовое описание', remind_at: '2026-06-15T14:25:00.000Z', }); const wrapper = factory({ modelValue: true, reminder }); await flushPromises(); const vm = wrapper.vm as unknown as { text: string; remindAt: string }; expect(vm.text).toBe('Тестовое описание'); // remind_at был ISO; превратился в YYYY-MM-DDThh:mm local-format. expect(vm.remindAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/); }); it('open + create → text сброшен, remindAt = default (1 час от now)', async () => { const wrapper = factory({ modelValue: true, dealId: 7, reminder: null }); await flushPromises(); const vm = wrapper.vm as unknown as { text: string; remindAt: string }; expect(vm.text).toBe(''); expect(vm.remindAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/); // Should be ~1 час впереди (allow tolerance ±5 минут). const parsed = new Date(vm.remindAt); const now = Date.now(); const diffMin = (parsed.getTime() - now) / 60_000; expect(diffMin).toBeGreaterThan(55); expect(diffMin).toBeLessThan(65); }); it('перезакрытие сбрасывает error и populates заново', async () => { const wrapper = factory({ modelValue: false, dealId: 7 }); // Open via v-model. await wrapper.setProps({ modelValue: true }); await flushPromises(); const vm = wrapper.vm as unknown as { text: string; remindAt: string; error: string | null }; expect(vm.error).toBeNull(); expect(vm.text).toBe(''); expect(vm.remindAt).not.toBe(''); }); it('reminder с remind_at=null → default используется', async () => { const wrapper = factory({ modelValue: true, reminder: mockReminder({ remind_at: null, text: null }), }); await flushPromises(); const vm = wrapper.vm as unknown as { text: string; remindAt: string }; expect(vm.text).toBe(''); expect(vm.remindAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/); }); }); describe('submit — create mode', () => { it('успешный create → store.create вызывается + emit saved + закрытие', async () => { const created = mockReminder({ id: 100 }); createReminderMock.mockResolvedValue(created); const wrapper = factory({ modelValue: true, dealId: 7, reminder: null }); const vm = wrapper.vm as unknown as { text: string; remindAt: string }; vm.text = ' Перезвонить '; vm.remindAt = '2026-06-01T12:30'; await flushPromises(); await wrapper.find('[data-testid="reminder-submit"]').trigger('click'); await flushPromises(); expect(createReminderMock).toHaveBeenCalledTimes(1); const payload = createReminderMock.mock.calls[0]![0] as { deal_id: number; text: string | null; remind_at: string; }; expect(payload.deal_id).toBe(7); expect(payload.text).toBe('Перезвонить'); // trimmed expect(payload.remind_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); const saved = wrapper.emitted('saved'); expect(saved).toBeDefined(); expect((saved![0] as unknown[])[0]).toEqual(created); // dialogOpen → false через update:modelValue. const closeEmits = wrapper.emitted('update:modelValue'); expect(closeEmits).toBeDefined(); expect(closeEmits!.some((e) => e[0] === false)).toBe(true); }); it('text пустой после trim → передаёт null', async () => { createReminderMock.mockResolvedValue(mockReminder({ id: 101 })); const wrapper = factory({ modelValue: true, dealId: 7, reminder: null }); const vm = wrapper.vm as unknown as { text: string; remindAt: string }; vm.text = ' '; vm.remindAt = '2026-06-01T12:30'; await flushPromises(); await wrapper.find('[data-testid="reminder-submit"]').trigger('click'); await flushPromises(); const payload = createReminderMock.mock.calls[0]![0] as { text: string | null }; expect(payload.text).toBeNull(); }); it('create без dealId → error "Не указан deal_id"', async () => { const wrapper = factory({ modelValue: true, dealId: null, reminder: null }); const vm = wrapper.vm as unknown as { text: string; remindAt: string }; vm.remindAt = '2026-06-01T12:30'; await flushPromises(); await wrapper.find('[data-testid="reminder-submit"]').trigger('click'); await flushPromises(); expect(createReminderMock).not.toHaveBeenCalled(); expect(wrapper.text()).toContain('Не указан deal_id для создания.'); expect(wrapper.emitted('saved')).toBeUndefined(); }); it('create API rejected → store returns null → error message', async () => { createReminderMock.mockRejectedValue(new Error('Network down')); const wrapper = factory({ modelValue: true, dealId: 7, reminder: null }); const vm = wrapper.vm as unknown as { text: string; remindAt: string }; vm.remindAt = '2026-06-01T12:30'; await flushPromises(); await wrapper.find('[data-testid="reminder-submit"]').trigger('click'); await flushPromises(); expect(wrapper.text()).toContain('Не удалось сохранить. Попробуйте позже.'); expect(wrapper.emitted('saved')).toBeUndefined(); }); }); describe('submit — edit mode', () => { it('успешный update → store.update вызывается с reminder.id + payload + emit saved', async () => { const original = mockReminder({ id: 55, text: 'Старое описание' }); const updated = mockReminder({ id: 55, text: 'Новое описание' }); updateReminderMock.mockResolvedValue(updated); const wrapper = factory({ modelValue: true, reminder: original }); await flushPromises(); const vm = wrapper.vm as unknown as { text: string; remindAt: string }; vm.text = 'Новое описание'; vm.remindAt = '2026-07-10T09:00'; await flushPromises(); await wrapper.find('[data-testid="reminder-submit"]').trigger('click'); await flushPromises(); expect(updateReminderMock).toHaveBeenCalledTimes(1); const [id, payload] = updateReminderMock.mock.calls[0] as [ number, { text: string | null; remind_at: string }, ]; expect(id).toBe(55); expect(payload.text).toBe('Новое описание'); expect(payload.remind_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); expect(createReminderMock).not.toHaveBeenCalled(); const saved = wrapper.emitted('saved'); expect(saved).toBeDefined(); expect((saved![0] as unknown[])[0]).toEqual(updated); }); it('update API rejected → error + не emit saved', async () => { updateReminderMock.mockRejectedValue(new Error('500')); const wrapper = factory({ modelValue: true, reminder: mockReminder() }); await flushPromises(); await wrapper.find('[data-testid="reminder-submit"]').trigger('click'); await flushPromises(); expect(wrapper.text()).toContain('Не удалось сохранить. Попробуйте позже.'); expect(wrapper.emitted('saved')).toBeUndefined(); }); }); describe('submit — общие пути', () => { it('пустой remindAt → error "Укажите дату и время"', async () => { const wrapper = factory({ modelValue: true, dealId: 7, reminder: null }); const vm = wrapper.vm as unknown as { remindAt: string }; vm.remindAt = ''; await flushPromises(); await wrapper.find('[data-testid="reminder-submit"]').trigger('click'); await flushPromises(); expect(createReminderMock).not.toHaveBeenCalled(); expect(updateReminderMock).not.toHaveBeenCalled(); expect(wrapper.text()).toContain('Укажите дату и время напоминания.'); }); it('submit вызывается также через клик по кнопке (v-form @submit интегрирован)', async () => { createReminderMock.mockResolvedValue(mockReminder({ id: 200 })); const wrapper = factory({ modelValue: true, dealId: 7, reminder: null }); const vm = wrapper.vm as unknown as { remindAt: string }; vm.remindAt = '2026-06-01T12:30'; await flushPromises(); await wrapper.find('[data-testid="reminder-submit"]').trigger('click'); await flushPromises(); expect(createReminderMock).toHaveBeenCalled(); }); }); describe('cancel', () => { it('cancel закрывает dialog (emit update:modelValue=false)', async () => { const wrapper = factory({ modelValue: true, dealId: 7 }); await flushPromises(); // Cancel btn — vanilla v-btn, ищем по тексту "Отмена". const buttons = wrapper.findAll('button'); const cancelBtn = buttons.find((b) => b.text().includes('Отмена')); expect(cancelBtn).toBeDefined(); await cancelBtn!.trigger('click'); await flushPromises(); const closeEmits = wrapper.emitted('update:modelValue'); expect(closeEmits).toBeDefined(); expect(closeEmits!.some((e) => e[0] === false)).toBe(true); // Не должен звать API. expect(createReminderMock).not.toHaveBeenCalled(); expect(updateReminderMock).not.toHaveBeenCalled(); }); }); });