import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; // Мокаем api/admin до import'а компонента — иначе реальный axios вызывает // сетевые запросы в jsdom-runtime. vi.mock('../../resources/js/api/admin', () => ({ impersonationInit: vi.fn(), impersonationVerify: vi.fn(), impersonationEnd: vi.fn(), })); // extractValidationErrors / extractErrorMessage используют axios.isAxiosError, // который в jsdom возвращает false для plain Error → мокаем helper'ы тоже // (паттерн из auth-store.spec.ts). vi.mock('../../resources/js/api/client', () => ({ extractValidationErrors: vi.fn(() => null), extractErrorMessage: vi.fn((_e, fallback?: string) => fallback ?? 'err'), apiClient: {}, ensureCsrfCookie: vi.fn(), })); import * as adminApi from '../../resources/js/api/admin'; import ImpersonationDialog from '../../resources/js/components/admin/ImpersonationDialog.vue'; import { MOCK_TENANTS } from '../../resources/js/composables/mockTenants'; /** * v-dialog требует layout-injection (v-app/v-layout) — в Vitest без auto-import * это невозможно. Stub'им VDialog как passthrough div, чтобы slot рендерился. */ const factory = (props: { modelValue: boolean; tenant: (typeof MOCK_TENANTS)[number] | null }) => mount(ImpersonationDialog, { props: { ...props, requestedBy: 1 }, global: { plugins: [createVuetify()], stubs: { VDialog: { template: '
', props: ['modelValue'], }, VTooltip: { template: '
', }, }, }, }); const sampleTenant = MOCK_TENANTS[0]; // Окна Москва ООО describe('ImpersonationDialog.vue', () => { beforeEach(() => { vi.clearAllMocks(); }); it('не рендерит content когда modelValue=false', () => { const wrapper = factory({ modelValue: false, tenant: sampleTenant }); expect(wrapper.find('.dialog-stub').exists()).toBe(false); }); it('step 1 — заголовок «Войти как клиент» + tenant info + textarea reason', () => { const wrapper = factory({ modelValue: true, tenant: sampleTenant }); const text = wrapper.text(); expect(text).toContain('Войти как клиент'); expect(text).toContain(sampleTenant.name); expect(text).toContain(sampleTenant.code); expect(text).toContain('ИНН ' + sampleTenant.inn); expect(wrapper.find('[data-testid="reason-input"]').exists()).toBe(true); // submit-init disabled при пустом reason expect(wrapper.find('[data-testid="submit-init-btn"]').exists()).toBe(true); }); it('reason < 30 chars — submit disabled, отображается remaining counter', async () => { const wrapper = factory({ modelValue: true, tenant: sampleTenant }); const textarea = wrapper.find('[data-testid="reason-input"] textarea'); await textarea.setValue('коротко'); await wrapper.vm.$nextTick(); // Hint показывает сколько осталось до 30 chars expect(wrapper.text()).toMatch(/Ещё\s+\d+\s+символов/); }); it('успешный init → step verify → показывает email + dev-code banner', async () => { vi.mocked(adminApi.impersonationInit).mockResolvedValue({ token_id: 42, expires_at: '2026-05-09T12:00:00Z', sent_to_email: 'admin@okna-moscow.ru', _dev_plain_code: '123456', }); const wrapper = factory({ modelValue: true, tenant: sampleTenant }); const textarea = wrapper.find('[data-testid="reason-input"] textarea'); await textarea.setValue('Тикет SUP-12453: клиент сообщил, что в карточке сделки не сохраняется коммент.'); await wrapper.find('[data-testid="submit-init-btn"]').trigger('click'); await flushPromises(); expect(adminApi.impersonationInit).toHaveBeenCalledWith({ tenant_id: sampleTenant.id, requested_by: 1, reason: expect.stringContaining('Тикет SUP-12453'), }); // step 2 → email + dev-code visible const text = wrapper.text(); expect(text).toContain('Код отправлен на email клиента'); expect(text).toContain('admin@okna-moscow.ru'); expect(wrapper.find('[data-testid="dev-code-banner"]').exists()).toBe(true); expect(wrapper.find('[data-testid="dev-code-banner"]').text()).toContain('123456'); expect(wrapper.find('[data-testid="code-input"]').exists()).toBe(true); }); it('verify success → step active → кнопка «Завершить сессию»', async () => { vi.mocked(adminApi.impersonationInit).mockResolvedValue({ token_id: 42, expires_at: '2026-05-09T12:00:00Z', sent_to_email: 'admin@okna-moscow.ru', _dev_plain_code: '111222', }); vi.mocked(adminApi.impersonationVerify).mockResolvedValue({ token_id: 42, tenant_id: sampleTenant.id, used_at: '2026-05-09T11:30:00Z', message: 'OK', }); const wrapper = factory({ modelValue: true, tenant: sampleTenant }); // Step 1 → 2 await wrapper.find('[data-testid="reason-input"] textarea').setValue('A'.repeat(35)); await wrapper.find('[data-testid="submit-init-btn"]').trigger('click'); await flushPromises(); // Step 2 → 3 await wrapper.find('[data-testid="code-input"] input').setValue('111222'); await wrapper.find('[data-testid="submit-verify-btn"]').trigger('click'); await flushPromises(); expect(adminApi.impersonationVerify).toHaveBeenCalledWith({ token_id: 42, code: '111222' }); expect(wrapper.text()).toContain('Impersonation активен'); expect(wrapper.find('[data-testid="submit-end-btn"]').exists()).toBe(true); }); it('verify с не-6-цифрами не вызывает API + показывает ошибку', async () => { vi.mocked(adminApi.impersonationInit).mockResolvedValue({ token_id: 42, expires_at: '2026-05-09T12:00:00Z', sent_to_email: 'admin@example.ru', _dev_plain_code: '999999', }); const wrapper = factory({ modelValue: true, tenant: sampleTenant }); await wrapper.find('[data-testid="reason-input"] textarea').setValue('A'.repeat(35)); await wrapper.find('[data-testid="submit-init-btn"]').trigger('click'); await flushPromises(); await wrapper.find('[data-testid="code-input"] input').setValue('12345'); // 5 цифр await wrapper.find('[data-testid="submit-verify-btn"]').trigger('click'); await flushPromises(); expect(adminApi.impersonationVerify).not.toHaveBeenCalled(); expect(wrapper.text()).toContain('Введите 6 цифр'); }); it('end success → step done → кнопка «Закрыть»', async () => { vi.mocked(adminApi.impersonationInit).mockResolvedValue({ token_id: 42, expires_at: '2026-05-09T12:00:00Z', sent_to_email: 'admin@example.ru', _dev_plain_code: '777777', }); vi.mocked(adminApi.impersonationVerify).mockResolvedValue({ token_id: 42, tenant_id: sampleTenant.id, used_at: '2026-05-09T11:30:00Z', message: 'OK', }); vi.mocked(adminApi.impersonationEnd).mockResolvedValue({ token_id: 42, session_ended_at: '2026-05-09T12:00:00Z', message: 'Завершён', }); const wrapper = factory({ modelValue: true, tenant: sampleTenant }); await wrapper.find('[data-testid="reason-input"] textarea').setValue('A'.repeat(35)); await wrapper.find('[data-testid="submit-init-btn"]').trigger('click'); await flushPromises(); await wrapper.find('[data-testid="code-input"] input').setValue('777777'); await wrapper.find('[data-testid="submit-verify-btn"]').trigger('click'); await flushPromises(); await wrapper.find('[data-testid="submit-end-btn"]').trigger('click'); await flushPromises(); expect(adminApi.impersonationEnd).toHaveBeenCalledWith(42); expect(wrapper.text()).toContain('Сессия impersonation завершена'); expect(wrapper.find('[data-testid="close-btn"]').exists()).toBe(true); }); it('Отмена на step reason эмитит update:modelValue=false', async () => { const wrapper = factory({ modelValue: true, tenant: sampleTenant }); await wrapper.find('[data-testid="cancel-btn"]').trigger('click'); const events = wrapper.emitted('update:modelValue'); expect(events).toBeDefined(); expect(events?.[0]).toEqual([false]); }); });