Files
portal/app/tests/Frontend/SalesRequests.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

175 lines
6.9 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 SalesRequestsView from '../../resources/js/views/sales/SalesRequestsView.vue';
// ─── mock sales api ───────────────────────────────────────────────────────────
vi.mock('../../resources/js/api/sales', () => ({
listAttachmentQueue: vi.fn().mockResolvedValue({
pending: [
{
id: 1,
login_input: 'info@potolki-spb.ru',
status: 'pending',
status_label: 'На рассмотрении',
tenant_id: 601,
organization_name: 'Натяжные потолки СПб',
manager_name: 'Алексей Смирнов',
hint: 'свободен',
comment: null,
created_at: '2026-06-28T10:14:00Z',
decided_at: null,
},
{
id: 2,
login_input: 'info@dveri-prem.ru',
status: 'pending',
status_label: 'На рассмотрении',
tenant_id: 602,
organization_name: 'Двери Премиум',
manager_name: 'Мария Котова',
hint: 'уже за: А. Смирнов',
comment: null,
created_at: '2026-06-28T09:02:00Z',
decided_at: null,
},
{
id: 3,
login_input: 'info@unknown.ru',
status: 'pending',
status_label: 'На рассмотрении',
tenant_id: null,
organization_name: null,
manager_name: 'Игорь Лебедев',
hint: null,
comment: null,
created_at: '2026-06-28T08:00:00Z',
decided_at: null,
},
],
history: [
{
id: 90,
login_input: 'sale@kuhni-ekb.ru',
status: 'approved',
status_label: 'Одобрено',
tenant_id: 501,
organization_name: 'Кухни на заказ Екб',
manager_name: 'Алексей Смирнов',
comment: null,
created_at: '2026-06-27T16:10:00Z',
decided_at: '2026-06-27T16:12:00Z',
},
],
}),
decideAttachment: vi.fn().mockResolvedValue({
id: 1,
login_input: 'info@potolki-spb.ru',
status: 'approved',
status_label: 'Одобрено',
tenant_id: 601,
comment: null,
}),
extractSalesErrorMessage: vi.fn((_e: unknown, fallback = 'Произошла ошибка. Попробуйте позже.') => fallback),
}));
beforeEach(() => vi.clearAllMocks());
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeRouter() {
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/sales/requests', name: 'sales-requests', component: SalesRequestsView }],
});
return router.push('/sales/requests').then(() => router);
}
async function mountView() {
const router = await makeRouter();
const wrapper = mount(SalesRequestsView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
return wrapper;
}
describe('SalesRequestsView', () => {
it('заголовок + счётчик = число pending', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('Заявки на привязку');
expect(text).toContain('3 ждут решения');
const api = await import('../../resources/js/api/sales');
expect(api.listAttachmentQueue).toHaveBeenCalled();
});
it('очередь рендерит chip по hint (свободен / уже за / не найден)', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('Алексей Смирнов');
expect(text).toContain('info@potolki-spb.ru');
expect(text).toContain('НАЙДЕН · свободен');
expect(text).toContain('уже за: А. Смирнов');
expect(text).toContain('не найден');
});
it('история рендерит строки', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('Кухни на заказ Екб');
expect(text).toContain('Одобрено');
});
it('клик «Подтвердить» на свободном → decideAttachment(id, approve)', async () => {
const wrapper = await mountView();
const api = await import('../../resources/js/api/sales');
const btn = wrapper.find('[data-testid="approve-1"]');
expect(btn.exists()).toBe(true);
await btn.trigger('click');
await flushPromises();
expect(api.decideAttachment).toHaveBeenCalledWith(1, 'approve', undefined);
});
it('клик «Переназначить» на конфликте → decideAttachment(id, approve)', async () => {
const wrapper = await mountView();
const api = await import('../../resources/js/api/sales');
const btn = wrapper.find('[data-testid="approve-2"]');
expect(btn.exists()).toBe(true);
await btn.trigger('click');
await flushPromises();
expect(api.decideAttachment).toHaveBeenCalledWith(2, 'approve', undefined);
});
it('клик «Отклонить» → decideAttachment(id, reject)', async () => {
const wrapper = await mountView();
const api = await import('../../resources/js/api/sales');
const btn = wrapper.find('[data-testid="reject-1"]');
expect(btn.exists()).toBe(true);
await btn.trigger('click');
await flushPromises();
expect(api.decideAttachment).toHaveBeenCalledWith(1, 'reject', undefined);
});
it('после решения список перезагружается', async () => {
const wrapper = await mountView();
const api = await import('../../resources/js/api/sales');
(api.listAttachmentQueue as ReturnType<typeof vi.fn>).mockClear();
await wrapper.find('[data-testid="approve-1"]').trigger('click');
await flushPromises();
expect(api.listAttachmentQueue).toHaveBeenCalled();
});
});