e31ea5354a
Заменяет static-снапшот LEAD_STATUSES в коде на live-данные из БД. Custom slug'и (добавленные после deployment'а) теперь видны UI без rebuild'а. Backend: - LeadStatus model (PK=slug string, incrementing=false, timestamps=null). - LeadStatusController::index — GET /api/lead-statuses, ORDER BY sort_order, slug. Таблица глобальная (не tenant-aware), auth не требуется на MVP. Pest +5 (LeadStatusesIndexTest): - 200 + не пустой / все 14 системных slug'ов из seed / все нужные поля / sort_order ASC / кастомный slug после INSERT появляется в endpoint'е. Frontend: - api/leadStatuses.ts::listLeadStatuses — GET helper. - stores/leadStatuses.ts::useLeadStatusesStore — Pinia setup-store: statuses default = LEAD_STATUSES snapshot (UI работает без fetch'а), load(force=false) идемпотентен, bySlug computed Map, findBySlug helper. На fail — snapshot остаётся, fetchError=true. - DealsView/KanbanView/DealDetailDrawer переехали со static-импорта LEAD_STATUSES на store. KanbanView использует safe-access dealsByStatus[slug] || [] (защита от custom slug'а из API без seeded column). load() в onMounted у обоих view'ов. Vitest +7 (leadStatusesStore.spec.ts): - initial snapshot / findBySlug existing & null / load success replace + loaded / load reject — fetchError + snapshot fallback / load идемпотентен / load(force=true) refetch. - 2 spec'а DealDetailDrawer получили setActivePinia(createPinia()) в beforeEach (без этого Pinia store-injection в jsdom падает). PHPStan baseline регенерирован. Регресс: - Lint+type-check+format passed. - Vitest 280/280 за 19.44 сек (+7 от 273). - Vite build 1.17 сек. - Pint + PHPStan passed. - Pest 210/210 за 24.59 сек (+5 от 205, 840 assertions). Реестр v1.63→v1.64 / CLAUDE.md v1.54→v1.55. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { createPinia, setActivePinia } from 'pinia';
|
|
import { useLeadStatusesStore } from '../../resources/js/stores/leadStatuses';
|
|
import { LEAD_STATUSES } from '../../resources/js/composables/leadStatuses';
|
|
|
|
vi.mock('../../resources/js/api/leadStatuses', () => ({
|
|
listLeadStatuses: vi.fn(),
|
|
}));
|
|
|
|
const api = await import('../../resources/js/api/leadStatuses');
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
setActivePinia(createPinia());
|
|
});
|
|
|
|
describe('useLeadStatusesStore', () => {
|
|
it('initial state — snapshot из LEAD_STATUSES', () => {
|
|
const store = useLeadStatusesStore();
|
|
expect(store.statuses).toHaveLength(LEAD_STATUSES.length);
|
|
expect(store.loaded).toBe(false);
|
|
expect(store.fetchError).toBe(false);
|
|
});
|
|
|
|
it('findBySlug возвращает статус из snapshot до load', () => {
|
|
const store = useLeadStatusesStore();
|
|
const found = store.findBySlug('paid');
|
|
expect(found).not.toBeNull();
|
|
expect(found!.nameRu).toBe('Оплачено');
|
|
});
|
|
|
|
it('findBySlug возвращает null для неизвестного slug', () => {
|
|
const store = useLeadStatusesStore();
|
|
expect(store.findBySlug('not_a_real_slug')).toBeNull();
|
|
});
|
|
|
|
it('load() success — replace на API-данные + loaded=true', async () => {
|
|
vi.mocked(api.listLeadStatuses).mockResolvedValueOnce([
|
|
{
|
|
slug: 'custom',
|
|
name_ru: 'Кастомный',
|
|
is_system: false,
|
|
sort_order: 100,
|
|
color_hex: '#ABCDEF',
|
|
description: null,
|
|
},
|
|
]);
|
|
|
|
const store = useLeadStatusesStore();
|
|
await store.load();
|
|
|
|
expect(store.loaded).toBe(true);
|
|
expect(store.statuses).toHaveLength(1);
|
|
expect(store.statuses[0].slug).toBe('custom');
|
|
expect(store.statuses[0].nameRu).toBe('Кастомный');
|
|
expect(store.findBySlug('custom')!.colorHex).toBe('#ABCDEF');
|
|
});
|
|
|
|
it('load() reject — fetchError=true + snapshot остаётся', async () => {
|
|
vi.mocked(api.listLeadStatuses).mockRejectedValueOnce(new Error('500'));
|
|
|
|
const store = useLeadStatusesStore();
|
|
await store.load();
|
|
|
|
expect(store.fetchError).toBe(true);
|
|
expect(store.loaded).toBe(false);
|
|
expect(store.statuses).toHaveLength(LEAD_STATUSES.length);
|
|
});
|
|
|
|
it('load() идемпотентен — повторный вызов не делает второй request', async () => {
|
|
vi.mocked(api.listLeadStatuses).mockResolvedValue([
|
|
{
|
|
slug: 'x',
|
|
name_ru: 'X',
|
|
is_system: false,
|
|
sort_order: 1,
|
|
color_hex: '#000000',
|
|
description: null,
|
|
},
|
|
]);
|
|
|
|
const store = useLeadStatusesStore();
|
|
await store.load();
|
|
await store.load();
|
|
|
|
expect(api.listLeadStatuses).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('load(force=true) делает повторный fetch', async () => {
|
|
vi.mocked(api.listLeadStatuses).mockResolvedValue([
|
|
{
|
|
slug: 'x',
|
|
name_ru: 'X',
|
|
is_system: false,
|
|
sort_order: 1,
|
|
color_hex: '#000000',
|
|
description: null,
|
|
},
|
|
]);
|
|
|
|
const store = useLeadStatusesStore();
|
|
await store.load();
|
|
await store.load(true);
|
|
|
|
expect(api.listLeadStatuses).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|