Files
portal/app/tests/Frontend/DealsTable.spec.ts
T
Дмитрий dc64226ec3 fix(portal): карточки вместо таблицы на планшете-портрете (Сделки/Списания)
Широкая таблица (7 колонок) не влезала в узкую колонку планшета рядом с
rail-меню — правые колонки «Комментарий»/«Поставлен» обрезались на 600–1279px.
ResponsiveTable теперь показывает карточки на телефоне И планшете, таблицу —
только на десктопе (>=1280). Тесты табличного слоя зафиксированы на десктоп-ширину.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:57:07 +03:00

203 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import DealsTable from '../../resources/js/components/deals/DealsTable.vue';
import type { MockDeal } from '../../resources/js/composables/mockDeals';
// ResponsiveTable показывает таблицу только на десктопе (>=1280); эти тесты
// проверяют табличный слой — фиксируем десктоп-ширину.
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1440 });
const vuetify = createVuetify();
const sampleDeals: MockDeal[] = [
{
id: 1,
name: '+7 (916) 100-00-01',
phone: '+7 (916) 100-00-01',
statusSlug: 'new',
project: 'Окна',
manager: { initials: 'AD', name: 'Admin' },
cost: 0,
receivedMinutesAgo: 5,
signalType: 'call',
city: 'Москва',
comment: 'звонил',
receivedAt: '2026-05-15T09:00:00+00:00',
},
{
id: 2,
name: '+7 (916) 100-00-02',
phone: '+7 (916) 100-00-02',
statusSlug: 'new',
project: 'Двери',
manager: { initials: 'AD', name: 'Admin' },
cost: 0,
receivedMinutesAgo: 30,
signalType: 'site',
city: null,
comment: null,
receivedAt: '2026-05-14T09:00:00+00:00',
},
];
describe('DealsTable', () => {
it('рендерит колонки реестра лидов', () => {
const w = mount(DealsTable, {
props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
global: { plugins: [vuetify] },
});
const headers = w.findAll('thead th').map((h) => h.text());
['Телефон', 'Источник', 'Город', 'Статус', 'Комментарий', 'Поставлен'].forEach((label) => {
expect(headers.some((h) => h.includes(label))).toBe(true);
});
});
it('город без значения рендерится как «—»', () => {
const w = mount(DealsTable, {
props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
global: { plugins: [vuetify] },
});
expect(w.text()).toContain('—');
});
it('select-all чекбокс имеет aria-label', () => {
const w = mount(DealsTable, {
props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
global: { plugins: [vuetify] },
});
expect(w.find('th .v-selection-control input[type="checkbox"][aria-label="Выбрать все сделки"]').exists()).toBe(
true,
);
});
it('клик по строке эмитит row-click с deal', async () => {
const w = mount(DealsTable, {
props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
global: { plugins: [vuetify] },
});
await w.find('tbody tr').trigger('click');
expect(w.emitted('row-click')?.[0]?.[0]).toMatchObject({ id: 1 });
});
// Автоподбор (2026-07-08): колонки «Конкурент» + «Источник».
const RouterLinkStub = {
name: 'RouterLink',
props: ['to'],
template: '<a class="rl"><slot /></a>',
};
function mountTable(deals: MockDeal[]) {
return mount(DealsTable, {
props: { deals, selectedIds: [], statusBySlug: new Map() },
global: { plugins: [vuetify], stubs: { RouterLink: RouterLinkStub } },
});
}
it('рендерит колонки «Конкурент» и «Источник»', () => {
const w = mountTable(sampleDeals);
const headers = w.findAll('thead th').map((h) => h.text());
['Конкурент', 'Источник'].forEach((label) => {
expect(headers.some((h) => h.includes(label))).toBe(true);
});
});
it('рисует ссылку на конкурента и на источник', () => {
const deal: MockDeal = {
id: 10,
name: '79000000001',
phone: '79000000001',
statusSlug: 'new',
project: 'X',
manager: { initials: '—', name: '—' },
cost: 0,
receivedMinutesAgo: 1,
competitorId: 3,
competitorName: 'AutoMoney',
sourceIdentifier: '79135396546',
sourceSignalType: 'call',
sourceProjectId: 5,
sourceIsActive: true,
sourceExtraCount: 0,
};
const w = mountTable([deal]);
expect(w.text()).toContain('AutoMoney');
expect(w.text()).toContain('79135396546');
expect(w.text()).toContain('Звонки');
const links = w.findAllComponents(RouterLinkStub);
expect(links.length).toBe(2);
// Конкурент → autopodbor?competitor=3
expect(links[0].props('to')).toMatchObject({ name: 'autopodbor', query: { competitor: '3' } });
// Источник → autopodbor?competitor=3&project=5
expect(links[1].props('to')).toMatchObject({ name: 'autopodbor', query: { competitor: '3', project: '5' } });
});
it('«Конкурент» — только бренд до первой запятой, без описания', () => {
const deal: MockDeal = {
id: 12,
name: '79000000002',
phone: '79000000002',
statusSlug: 'new',
project: 'X',
manager: { initials: '—', name: '—' },
cost: 0,
receivedMinutesAgo: 1,
competitorId: 4,
competitorName: 'AutoMoney, микрокредитная компания',
sourceIdentifier: '79135396546',
sourceSignalType: 'call',
sourceProjectId: 5,
sourceIsActive: true,
sourceExtraCount: 0,
};
const w = mountTable([deal]);
expect(w.text()).toContain('AutoMoney');
expect(w.text()).not.toContain('микрокредитная');
});
it('показывает « · +N» при нескольких источниках проекта', () => {
const deal: MockDeal = {
id: 11,
name: 'x',
phone: 'x',
statusSlug: 'new',
project: 'X',
manager: { initials: '—', name: '—' },
cost: 0,
receivedMinutesAgo: 1,
competitorId: 3,
competitorName: 'Сфера займа24',
sourceIdentifier: '79029826282',
sourceSignalType: 'call',
sourceProjectId: 5,
sourceIsActive: true,
sourceExtraCount: 2,
};
const w = mountTable([deal]);
expect(w.text()).toContain('+2');
});
it('без конкурента — «Конкурент» = прочерк, «Источник» = название проекта без ссылок', () => {
const deal: MockDeal = {
id: 12,
name: 'y',
phone: '79000000002',
statusSlug: 'new',
project: 'Обычный проект',
manager: { initials: '—', name: '—' },
cost: 0,
receivedMinutesAgo: 1,
competitorId: null,
competitorName: null,
sourceIdentifier: null,
sourceSignalType: null,
sourceProjectId: null,
sourceIsActive: null,
sourceExtraCount: 0,
};
const w = mountTable([deal]);
expect(w.text()).toContain('Обычный проект');
expect(w.findAllComponents(RouterLinkStub).length).toBe(0);
});
});