From 345a6cf9028672b3cbbda95f6f2b39cda14dc2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 15 May 2026 08:58:11 +0300 Subject: [PATCH] =?UTF-8?q?refactor(kanban):=20C4=20review=20fixes=20?= =?UTF-8?q?=E2=80=94=20array-revert=20test=20coverage=20+=20JSDoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/resources/js/views/KanbanView.vue | 7 +++++-- app/tests/Frontend/KanbanView.spec.ts | 20 ++++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/resources/js/views/KanbanView.vue b/app/resources/js/views/KanbanView.vue index 6a3d1022..cf0e93f6 100644 --- a/app/resources/js/views/KanbanView.vue +++ b/app/resources/js/views/KanbanView.vue @@ -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). */ diff --git a/app/tests/Frontend/KanbanView.spec.ts b/app/tests/Frontend/KanbanView.spec.ts index 3c2706c1..f67a6a90 100644 --- a/app/tests/Frontend/KanbanView.spec.ts +++ b/app/tests/Frontend/KanbanView.spec.ts @@ -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 () => {