66204a045c
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>
147 lines
6.4 KiB
TypeScript
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('Этот клиент уже закреплён за вами.');
|
|
});
|
|
});
|