Files
portal/app/tests/Frontend/SalesAttach.spec.ts
T
Дмитрий 66204a045c feat(sales): экраны привязки клиентов и заявок (фронт)
Task 2.3b портала отдела продаж — два Vue-экрана поверх готовых
эндпоинтов /api/sales/attachments:

- SalesAttachView (менеджер, #page-attach): форма заявки по логину
  (e-mail/ИНН), живой баннер результата (отправлено/не найден/ошибка),
  таблица «Мои заявки».
- SalesRequestsView (начальник, #page-requests): очередь на решение
  с проверкой «свободен / уже за: …», кнопки Подтвердить/Переназначить
  /Отклонить (переназначение = тот же approve на бэке), история, счётчик.
- api/sales.ts: submitAttachment / listMyAttachments / listAttachmentQueue
  / decideAttachment + типы.
- Роуты /sales/attach и /sales/requests переключены со stub на реальные экраны.

Vitest: SalesAttach 5 + SalesRequests 7 = 12 зелёных; полный фронт-набор
не сломан. ESLint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:22:49 +03:00

147 lines
6.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createPinia } from 'pinia';
import { createRouter, createMemoryHistory } from 'vue-router';
import SalesAttachView from '../../resources/js/views/sales/SalesAttachView.vue';
// ─── mock sales api ───────────────────────────────────────────────────────────
vi.mock('../../resources/js/api/sales', () => ({
submitAttachment: vi.fn(),
listMyAttachments: vi.fn().mockResolvedValue([
{
id: 1,
login_input: 'sale@kuhni-ekb.ru',
status: 'approved',
status_label: 'Одобрено',
tenant_id: 501,
organization_name: 'Кухни на заказ Екб',
comment: null,
created_at: '2026-06-27T16:02:00Z',
decided_at: '2026-06-27T16:10:00Z',
},
{
id: 2,
login_input: 'info@dveri-eco.ru',
status: 'rejected',
status_label: 'Отклонено',
tenant_id: 502,
organization_name: 'Двери Эконом',
comment: 'Клиент закреплён за другим менеджером',
created_at: '2026-06-26T11:48:00Z',
decided_at: '2026-06-26T12:00:00Z',
},
]),
extractSalesErrorMessage: vi.fn((_e: unknown, fallback = 'Произошла ошибка. Попробуйте позже.') => fallback),
}));
beforeEach(() => vi.clearAllMocks());
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeRouter() {
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/sales/attach', name: 'sales-attach', component: SalesAttachView }],
});
return router.push('/sales/attach').then(() => router);
}
async function mountView() {
const router = await makeRouter();
const wrapper = mount(SalesAttachView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
return wrapper;
}
describe('SalesAttachView', () => {
it('рендерит заголовок и подсказку', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('Привязать клиента');
expect(text).toContain('Один клиент — один менеджер');
});
it('таблица «Мои заявки» рендерит строки из listMyAttachments', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('sale@kuhni-ekb.ru');
expect(text).toContain('info@dveri-eco.ru');
expect(text).toContain('Одобрено');
expect(text).toContain('Отклонено');
expect(text).toContain('Клиент закреплён за другим менеджером');
const api = await import('../../resources/js/api/sales');
expect(api.listMyAttachments).toHaveBeenCalled();
});
it('отправка заявки со статусом pending → зелёный баннер + перезагрузка списка', async () => {
const api = await import('../../resources/js/api/sales');
(api.submitAttachment as ReturnType<typeof vi.fn>).mockResolvedValue({
id: 10,
login_input: 'new@company.ru',
status: 'pending',
status_label: 'На рассмотрении',
tenant_id: 777,
comment: null,
});
const wrapper = await mountView();
(api.listMyAttachments as ReturnType<typeof vi.fn>).mockClear();
await wrapper.find('[data-testid="attach-login"] input').setValue('new@company.ru');
await wrapper.find('[data-testid="attach-submit"]').trigger('click');
await flushPromises();
expect(api.submitAttachment).toHaveBeenCalledWith('new@company.ru');
const banner = wrapper.find('[data-testid="attach-banner"]');
expect(banner.exists()).toBe(true);
expect(banner.text()).toContain('Заявка отправлена начальнику');
// список перезагружается после успешной отправки
expect(api.listMyAttachments).toHaveBeenCalled();
});
it('отправка заявки со статусом not_found → жёлтый баннер', async () => {
const api = await import('../../resources/js/api/sales');
(api.submitAttachment as ReturnType<typeof vi.fn>).mockResolvedValue({
id: 11,
login_input: 'info@unknown.ru',
status: 'not_found',
status_label: 'Клиент не найден',
tenant_id: null,
comment: null,
});
const wrapper = await mountView();
await wrapper.find('[data-testid="attach-login"] input').setValue('info@unknown.ru');
await wrapper.find('[data-testid="attach-submit"]').trigger('click');
await flushPromises();
const banner = wrapper.find('[data-testid="attach-banner"]');
expect(banner.exists()).toBe(true);
expect(banner.text()).toContain('Клиент не найден');
});
it('ошибка 422 → красный баннер с текстом message', async () => {
const api = await import('../../resources/js/api/sales');
(api.submitAttachment as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('boom'));
(api.extractSalesErrorMessage as ReturnType<typeof vi.fn>).mockReturnValue(
'Этот клиент уже закреплён за вами.',
);
const wrapper = await mountView();
await wrapper.find('[data-testid="attach-login"] input').setValue('mine@company.ru');
await wrapper.find('[data-testid="attach-submit"]').trigger('click');
await flushPromises();
const banner = wrapper.find('[data-testid="attach-banner"]');
expect(banner.exists()).toBe(true);
expect(banner.text()).toContain('Этот клиент уже закреплён за вами.');
});
});