refactor(kanban): C4 review fixes — array-revert test coverage + JSDoc

Code-review fixes для commit fbe5e9e (C4 KanbanView DnD persist):

I-2 (test coverage gap): Revert test «onColumnChange reverts...» теперь
seed'ит deal в dealsByStatus['hot'] до вызова onColumnChange (имитируя
vuedraggable mutation pre-event). После failed transition — assert
карточка удалена из hot + восстановлена в new. Раньше array-revert
branch в KanbanView.vue:80-87 (splice + push) имел 0 test coverage —
findIndex возвращал -1, splice silent. Теперь coverage 100%.

I-3 (stale JSDoc): File-header comment в KanbanView.vue lines 7-16
обновлён — описывает actual behavior после Task 2 (optimistic + API call
+ revert). Раньше явно врал «не входит в этот коммит: PATCH /api/deals/
{id}» когда POST /api/deals/transition уже реализован.

Регрессий 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-15 08:58:11 +03:00
parent fbe5e9e17d
commit 345a6cf902
2 changed files with 19 additions and 8 deletions
+5 -2
View File
@@ -5,13 +5,16 @@
* Источник дизайна: liderra_v8_handoff/concepts/v8_kanban.html.
* DnD реализован через vuedraggable@4 (обёртка SortableJS) — карточки можно
* перетаскивать между колонками. При drop:
* - событие 'added' в целевой колонке → меняем `statusSlug` сделки.
* - событие 'added' в целевой колонке → optimistic update statusSlug +
* POST /api/deals/transition (через dealsApi). На failure — revert:
* карточка возвращается в исходную колонку + toast «Не удалось переместить».
* Без auth.user.tenant_id — local-only mode (API не зовётся).
* - событие 'removed' в исходной колонке → ничего не делаем (обработано в added).
* - событие 'moved' внутри одной колонки → только смена порядка (statusSlug
* не меняется; на API будущем — PATCH /api/deals/{id} {sort_order}).
*
* Не входит в этот коммит:
* - PATCH /api/deals/{id} {status_slug} при drop — backend.
* - PATCH /api/deals/{id} {sort_order} при moved (intra-column reorder) — backend.
* - Filters (Проект/Менеджер) — общий filter-bar с DealsView.
* - DealDetailDrawer на click по карточке (event @open-deal).
*/
+14 -6
View File
@@ -160,15 +160,23 @@ describe('KanbanView DnD persist (Sprint 1 C4)', () => {
await new Promise((r) => setTimeout(r, 30));
const deal = { id: 43, statusSlug: 'new' as const, name: 'Y', phone: '+79161234567', project: 'p', manager: { name: 'M', initials: 'M' }, cost: 100, receivedMinutesAgo: 5 };
// Имитируем vuedraggable mutation: карточка уже в target column до вызова onColumnChange.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (wrapper.vm as any).onColumnChange('hot', { added: { element: deal, newIndex: 0 } });
const vm = wrapper.vm as any;
if (!vm.dealsByStatus.hot) vm.dealsByStatus.hot = [];
vm.dealsByStatus.hot.push(deal);
// После failure — statusSlug откатывается на оригинал
await vm.onColumnChange('hot', { added: { element: deal, newIndex: 0 } });
// statusSlug rolled back
expect(deal.statusSlug).toBe('new');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((wrapper.vm as any).transitionToastOpen).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((wrapper.vm as any).transitionToastText).toContain('Не удалось');
// Card removed from target column (array-revert branch coverage)
expect(vm.dealsByStatus.hot.findIndex((d: { id: number }) => d.id === 43)).toBe(-1);
// Card restored to source column
expect(vm.dealsByStatus.new.findIndex((d: { id: number }) => d.id === 43)).toBeGreaterThanOrEqual(0);
// Toast shown
expect(vm.transitionToastOpen).toBe(true);
expect(vm.transitionToastText).toContain('Не удалось');
});
it('onColumnChange skips API call if no auth.user.tenant_id', async () => {