dc64226ec3
Широкая таблица (7 колонок) не влезала в узкую колонку планшета рядом с rail-меню — правые колонки «Комментарий»/«Поставлен» обрезались на 600–1279px. ResponsiveTable теперь показывает карточки на телефоне И планшете, таблицу — только на десктопе (>=1280). Тесты табличного слоя зафиксированы на десктоп-ширину. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
203 lines
7.7 KiB
TypeScript
203 lines
7.7 KiB
TypeScript
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);
|
||
});
|
||
});
|