@object-ui/plugin-kanban 3.1.5 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +24 -0
  3. package/dist/{KanbanEnhanced-CvxO2soF.js → KanbanEnhanced-Do9ZB1Mh.js} +36 -33
  4. package/dist/{KanbanImpl-ii52_k8g.js → KanbanImpl-BdocXM5T.js} +2 -2
  5. package/dist/{chevron-down-DpXJN6OX.js → chevron-down-C0JUlGjk.js} +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +35 -26
  8. package/dist/index.umd.cjs +4 -4
  9. package/dist/packages/plugin-kanban/src/CardTemplates.d.ts.map +1 -0
  10. package/dist/packages/plugin-kanban/src/InlineQuickAdd.d.ts.map +1 -0
  11. package/dist/packages/plugin-kanban/src/KanbanEnhanced.d.ts.map +1 -0
  12. package/dist/packages/plugin-kanban/src/KanbanImpl.d.ts.map +1 -0
  13. package/dist/packages/plugin-kanban/src/ObjectKanban.EdgeCases.stories.d.ts.map +1 -0
  14. package/dist/packages/plugin-kanban/src/ObjectKanban.d.ts.map +1 -0
  15. package/dist/packages/plugin-kanban/src/ObjectKanban.stories.d.ts.map +1 -0
  16. package/dist/packages/plugin-kanban/src/index.d.ts.map +1 -0
  17. package/dist/packages/plugin-kanban/src/types.d.ts.map +1 -0
  18. package/dist/packages/plugin-kanban/src/useColumnWidths.d.ts.map +1 -0
  19. package/dist/packages/plugin-kanban/src/useCrossSwimlaneMove.d.ts.map +1 -0
  20. package/dist/packages/plugin-kanban/src/useQuickAddReorder.d.ts.map +1 -0
  21. package/dist/{plus-CAtTu4zt.js → plus-CHsXVJSY.js} +39 -36
  22. package/dist/{sortable.esm-DzUCoMzQ.js → sortable.esm-LJG1TjKd.js} +4 -4
  23. package/package.json +35 -12
  24. package/.turbo/turbo-build.log +0 -32
  25. package/dist/src/CardTemplates.d.ts.map +0 -1
  26. package/dist/src/InlineQuickAdd.d.ts.map +0 -1
  27. package/dist/src/KanbanEnhanced.d.ts.map +0 -1
  28. package/dist/src/KanbanImpl.d.ts.map +0 -1
  29. package/dist/src/ObjectKanban.EdgeCases.stories.d.ts.map +0 -1
  30. package/dist/src/ObjectKanban.d.ts.map +0 -1
  31. package/dist/src/ObjectKanban.stories.d.ts.map +0 -1
  32. package/dist/src/index.d.ts.map +0 -1
  33. package/dist/src/types.d.ts.map +0 -1
  34. package/dist/src/useColumnWidths.d.ts.map +0 -1
  35. package/dist/src/useCrossSwimlaneMove.d.ts.map +0 -1
  36. package/dist/src/useQuickAddReorder.d.ts.map +0 -1
  37. package/src/CardTemplates.tsx +0 -123
  38. package/src/InlineQuickAdd.tsx +0 -189
  39. package/src/KanbanEnhanced.tsx +0 -525
  40. package/src/KanbanImpl.tsx +0 -597
  41. package/src/ObjectKanban.EdgeCases.stories.tsx +0 -168
  42. package/src/ObjectKanban.msw.test.tsx +0 -91
  43. package/src/ObjectKanban.stories.tsx +0 -152
  44. package/src/ObjectKanban.tsx +0 -262
  45. package/src/__tests__/KanbanEnhanced.test.tsx +0 -260
  46. package/src/__tests__/KanbanGrouping.test.tsx +0 -164
  47. package/src/__tests__/KanbanSwimlanes.test.tsx +0 -194
  48. package/src/__tests__/ObjectKanbanTitle.test.tsx +0 -93
  49. package/src/__tests__/SwimlanePersistence.test.tsx +0 -159
  50. package/src/__tests__/accessibility.test.tsx +0 -296
  51. package/src/__tests__/dnd-undo-integration.test.tsx +0 -525
  52. package/src/__tests__/performance-benchmark.test.tsx +0 -306
  53. package/src/__tests__/phase13-features.test.tsx +0 -387
  54. package/src/__tests__/view-states.test.tsx +0 -403
  55. package/src/index.test.ts +0 -112
  56. package/src/index.tsx +0 -327
  57. package/src/registration.test.tsx +0 -26
  58. package/src/types.ts +0 -185
  59. package/src/useColumnWidths.ts +0 -125
  60. package/src/useCrossSwimlaneMove.ts +0 -116
  61. package/src/useQuickAddReorder.ts +0 -107
  62. package/tsconfig.json +0 -19
  63. package/vite.config.ts +0 -61
  64. package/vitest.config.ts +0 -12
  65. package/vitest.setup.ts +0 -1
  66. /package/dist/{src → packages/plugin-kanban/src}/CardTemplates.d.ts +0 -0
  67. /package/dist/{src → packages/plugin-kanban/src}/InlineQuickAdd.d.ts +0 -0
  68. /package/dist/{src → packages/plugin-kanban/src}/KanbanEnhanced.d.ts +0 -0
  69. /package/dist/{src → packages/plugin-kanban/src}/KanbanImpl.d.ts +0 -0
  70. /package/dist/{src → packages/plugin-kanban/src}/ObjectKanban.EdgeCases.stories.d.ts +0 -0
  71. /package/dist/{src → packages/plugin-kanban/src}/ObjectKanban.d.ts +0 -0
  72. /package/dist/{src → packages/plugin-kanban/src}/ObjectKanban.stories.d.ts +0 -0
  73. /package/dist/{src → packages/plugin-kanban/src}/index.d.ts +0 -0
  74. /package/dist/{src → packages/plugin-kanban/src}/types.d.ts +0 -0
  75. /package/dist/{src → packages/plugin-kanban/src}/useColumnWidths.d.ts +0 -0
  76. /package/dist/{src → packages/plugin-kanban/src}/useCrossSwimlaneMove.d.ts +0 -0
  77. /package/dist/{src → packages/plugin-kanban/src}/useQuickAddReorder.d.ts +0 -0
@@ -1,306 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- *
8
- * Performance benchmark tests for KanbanBoard (KanbanImpl).
9
- * Part of P2.4 Performance at Scale roadmap.
10
- */
11
-
12
- import { describe, it, expect, vi } from 'vitest';
13
- import { render } from '@testing-library/react';
14
- import '@testing-library/jest-dom';
15
- import React from 'react';
16
- import type { KanbanColumn, KanbanCard, KanbanBoardProps } from '../KanbanImpl';
17
-
18
- // Mock @dnd-kit/core
19
- vi.mock('@dnd-kit/core', () => ({
20
- DndContext: ({ children }: any) => <div data-testid="dnd-context">{children}</div>,
21
- DragOverlay: ({ children }: any) => <div data-testid="drag-overlay">{children}</div>,
22
- PointerSensor: vi.fn(),
23
- TouchSensor: vi.fn(),
24
- useSensor: vi.fn(),
25
- useSensors: () => [],
26
- closestCorners: vi.fn(),
27
- }));
28
-
29
- // Mock @dnd-kit/sortable
30
- vi.mock('@dnd-kit/sortable', () => ({
31
- SortableContext: ({ children }: any) => <div data-testid="sortable-context">{children}</div>,
32
- useSortable: () => ({
33
- attributes: {},
34
- listeners: {},
35
- setNodeRef: vi.fn(),
36
- transform: null,
37
- transition: null,
38
- isDragging: false,
39
- }),
40
- arrayMove: (array: any[], from: number, to: number) => {
41
- const newArray = [...array];
42
- newArray.splice(to, 0, newArray.splice(from, 1)[0]);
43
- return newArray;
44
- },
45
- verticalListSortingStrategy: vi.fn(),
46
- }));
47
-
48
- // Mock @dnd-kit/utilities
49
- vi.mock('@dnd-kit/utilities', () => ({
50
- CSS: {
51
- Transform: {
52
- toString: () => '',
53
- },
54
- },
55
- }));
56
-
57
- // Mock @object-ui/components
58
- vi.mock('@object-ui/components', () => ({
59
- Badge: ({ children, ...props }: any) => <span data-testid="badge" {...props}>{children}</span>,
60
- Card: ({ children, ...props }: any) => <div data-testid="card" {...props}>{children}</div>,
61
- CardHeader: ({ children, ...props }: any) => <div {...props}>{children}</div>,
62
- CardTitle: ({ children, ...props }: any) => <div {...props}>{children}</div>,
63
- CardDescription: ({ children, ...props }: any) => <div {...props}>{children}</div>,
64
- CardContent: ({ children, ...props }: any) => <div {...props}>{children}</div>,
65
- ScrollArea: ({ children, ...props }: any) => <div {...props}>{children}</div>,
66
- }));
67
-
68
- // Mock @object-ui/react
69
- vi.mock('@object-ui/react', () => ({
70
- useHasDndProvider: () => false,
71
- useDnd: () => ({
72
- startDrag: vi.fn(),
73
- endDrag: vi.fn(),
74
- }),
75
- }));
76
-
77
- // --- Data generators ---
78
-
79
- function generateCards(count: number): KanbanCard[] {
80
- const cards: KanbanCard[] = [];
81
- for (let i = 0; i < count; i++) {
82
- cards.push({
83
- id: `card-${i}`,
84
- title: `Task ${i}`,
85
- description: `Description for task ${i}`,
86
- badges: i % 3 === 0
87
- ? [{ label: 'High', variant: 'destructive' as const }]
88
- : undefined,
89
- });
90
- }
91
- return cards;
92
- }
93
-
94
- function generateColumns(
95
- columnCount: number,
96
- totalCards: number,
97
- ): KanbanColumn[] {
98
- const cardsPerColumn = Math.ceil(totalCards / columnCount);
99
- const columns: KanbanColumn[] = [];
100
- let cardIndex = 0;
101
- for (let c = 0; c < columnCount; c++) {
102
- const columnCards: KanbanCard[] = [];
103
- const count = Math.min(cardsPerColumn, totalCards - cardIndex);
104
- for (let i = 0; i < count; i++) {
105
- columnCards.push({
106
- id: `col${c}-card-${cardIndex}`,
107
- title: `Task ${cardIndex}`,
108
- description: `Description for task ${cardIndex}`,
109
- badges: cardIndex % 3 === 0
110
- ? [{ label: 'High', variant: 'destructive' as const }]
111
- : undefined,
112
- });
113
- cardIndex++;
114
- }
115
- columns.push({
116
- id: `col-${c}`,
117
- title: `Column ${c}`,
118
- cards: columnCards,
119
- });
120
- }
121
- return columns;
122
- }
123
-
124
- let KanbanBoard: React.ComponentType<KanbanBoardProps>;
125
-
126
- async function setupMocksAndImport() {
127
- vi.resetModules();
128
-
129
- vi.doMock('@dnd-kit/core', () => ({
130
- DndContext: ({ children }: any) => React.createElement('div', { 'data-testid': 'dnd-context' }, children),
131
- DragOverlay: ({ children }: any) => React.createElement('div', { 'data-testid': 'drag-overlay' }, children),
132
- PointerSensor: vi.fn(),
133
- TouchSensor: vi.fn(),
134
- useSensor: vi.fn(),
135
- useSensors: () => [],
136
- closestCorners: vi.fn(),
137
- }));
138
-
139
- vi.doMock('@dnd-kit/sortable', () => ({
140
- SortableContext: ({ children }: any) => React.createElement('div', { 'data-testid': 'sortable-context' }, children),
141
- useSortable: () => ({
142
- attributes: {},
143
- listeners: {},
144
- setNodeRef: vi.fn(),
145
- transform: null,
146
- transition: null,
147
- isDragging: false,
148
- }),
149
- arrayMove: (array: any[], from: number, to: number) => {
150
- const newArray = [...array];
151
- newArray.splice(to, 0, newArray.splice(from, 1)[0]);
152
- return newArray;
153
- },
154
- verticalListSortingStrategy: vi.fn(),
155
- }));
156
-
157
- vi.doMock('@dnd-kit/utilities', () => ({
158
- CSS: { Transform: { toString: () => '' } },
159
- }));
160
-
161
- vi.doMock('@object-ui/components', () => ({
162
- Badge: ({ children, ...props }: any) => React.createElement('span', { 'data-testid': 'badge', ...props }, children),
163
- Card: ({ children, ...props }: any) => React.createElement('div', { 'data-testid': 'card', ...props }, children),
164
- CardHeader: ({ children, ...props }: any) => React.createElement('div', props, children),
165
- CardTitle: ({ children, ...props }: any) => React.createElement('div', props, children),
166
- CardDescription: ({ children, ...props }: any) => React.createElement('div', props, children),
167
- CardContent: ({ children, ...props }: any) => React.createElement('div', props, children),
168
- ScrollArea: ({ children, ...props }: any) => React.createElement('div', props, children),
169
- }));
170
-
171
- vi.doMock('@object-ui/react', () => ({
172
- useHasDndProvider: () => false,
173
- useDnd: () => ({ startDrag: vi.fn(), endDrag: vi.fn() }),
174
- }));
175
-
176
- const mod = await import('../KanbanImpl');
177
- KanbanBoard = mod.default;
178
- }
179
-
180
- // =========================================================================
181
- // Performance Benchmarks
182
- // =========================================================================
183
-
184
- describe('KanbanBoard (KanbanImpl): performance benchmarks', () => {
185
- beforeEach(async () => {
186
- await setupMocksAndImport();
187
- });
188
-
189
- it('renders 100 cards spread across 5 columns under 500ms', () => {
190
- const columns = generateColumns(5, 100);
191
-
192
- const start = performance.now();
193
- const { container } = render(<KanbanBoard columns={columns} />);
194
- const elapsed = performance.now() - start;
195
-
196
- expect(container).toBeTruthy();
197
- expect(elapsed).toBeLessThan(500);
198
- });
199
-
200
- it('renders 200 cards spread across 5 columns under 1,000ms', () => {
201
- const columns = generateColumns(5, 200);
202
-
203
- const start = performance.now();
204
- const { container } = render(<KanbanBoard columns={columns} />);
205
- const elapsed = performance.now() - start;
206
-
207
- expect(container).toBeTruthy();
208
- expect(elapsed).toBeLessThan(1_000);
209
- });
210
-
211
- it('renders 500 cards spread across 5 columns under 2,000ms', () => {
212
- const columns = generateColumns(5, 500);
213
-
214
- const start = performance.now();
215
- const { container } = render(<KanbanBoard columns={columns} />);
216
- const elapsed = performance.now() - start;
217
-
218
- expect(container).toBeTruthy();
219
- expect(elapsed).toBeLessThan(2_000);
220
- });
221
-
222
- it('renders with 10+ columns without degradation', () => {
223
- const columns = generateColumns(12, 120);
224
-
225
- const start = performance.now();
226
- const { container } = render(<KanbanBoard columns={columns} />);
227
- const elapsed = performance.now() - start;
228
-
229
- expect(container).toBeTruthy();
230
- expect(elapsed).toBeLessThan(2_000);
231
- });
232
-
233
- it('renders empty board with 10+ columns quickly', () => {
234
- const columns = generateColumns(12, 0);
235
-
236
- const start = performance.now();
237
- const { container } = render(<KanbanBoard columns={columns} />);
238
- const elapsed = performance.now() - start;
239
-
240
- expect(container).toBeTruthy();
241
- expect(elapsed).toBeLessThan(500);
242
- });
243
-
244
- it('data generation for 1,000 cards is fast (< 100ms)', () => {
245
- const start = performance.now();
246
- const cards = generateCards(1_000);
247
- const elapsed = performance.now() - start;
248
-
249
- expect(cards).toHaveLength(1_000);
250
- expect(elapsed).toBeLessThan(100);
251
- });
252
- });
253
-
254
- // =========================================================================
255
- // Scaling characteristics
256
- // =========================================================================
257
-
258
- describe('KanbanBoard (KanbanImpl): scaling characteristics', () => {
259
- beforeEach(async () => {
260
- await setupMocksAndImport();
261
- });
262
-
263
- it('renders all column titles for 10+ column board', () => {
264
- const columns = generateColumns(12, 24);
265
- render(<KanbanBoard columns={columns} />);
266
-
267
- for (let i = 0; i < 12; i++) {
268
- expect(document.body.textContent).toContain(`Column ${i}`);
269
- }
270
- });
271
-
272
- it('renders cards with badges without significant overhead', () => {
273
- const columns: KanbanColumn[] = [
274
- {
275
- id: 'badges-col',
276
- title: 'With Badges',
277
- cards: Array.from({ length: 100 }, (_, i) => ({
278
- id: `badge-card-${i}`,
279
- title: `Task ${i}`,
280
- badges: [
281
- { label: 'Priority', variant: 'destructive' as const },
282
- { label: 'Sprint 1', variant: 'secondary' as const },
283
- ],
284
- })),
285
- },
286
- ];
287
-
288
- const start = performance.now();
289
- const { container } = render(<KanbanBoard columns={columns} />);
290
- const elapsed = performance.now() - start;
291
-
292
- expect(container).toBeTruthy();
293
- expect(elapsed).toBeLessThan(2_000);
294
- });
295
-
296
- it('renders 500 cards across 10 columns under 2,000ms', () => {
297
- const columns = generateColumns(10, 500);
298
-
299
- const start = performance.now();
300
- const { container } = render(<KanbanBoard columns={columns} />);
301
- const elapsed = performance.now() - start;
302
-
303
- expect(container).toBeTruthy();
304
- expect(elapsed).toBeLessThan(2_000);
305
- });
306
- });
@@ -1,387 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, vi, beforeEach } from 'vitest';
10
- import { render, screen, waitFor, fireEvent } from '@testing-library/react';
11
- import { renderHook, act } from '@testing-library/react';
12
- import { InlineQuickAdd } from '../InlineQuickAdd';
13
- import { CardTemplates } from '../CardTemplates';
14
- import { useColumnWidths } from '../useColumnWidths';
15
- import { useCrossSwimlaneMove } from '../useCrossSwimlaneMove';
16
- import { useQuickAddReorder } from '../useQuickAddReorder';
17
- import type { InlineFieldDefinition, CardTemplate, KanbanCard, KanbanColumn } from '../types';
18
-
19
- // ---------------------------------------------------------------------------
20
- // localStorage mock
21
- // ---------------------------------------------------------------------------
22
- const localStorageMock = (() => {
23
- let store: Record<string, string> = {};
24
- return {
25
- getItem: vi.fn((key: string) => store[key] ?? null),
26
- setItem: vi.fn((key: string, value: string) => { store[key] = value; }),
27
- removeItem: vi.fn((key: string) => { delete store[key]; }),
28
- clear: vi.fn(() => { store = {}; }),
29
- };
30
- })();
31
-
32
- Object.defineProperty(window, 'localStorage', { value: localStorageMock });
33
-
34
- // ---------------------------------------------------------------------------
35
- // Helpers
36
- // ---------------------------------------------------------------------------
37
- const textField: InlineFieldDefinition = { name: 'title', label: 'Title', type: 'text' };
38
- const numberField: InlineFieldDefinition = { name: 'points', label: 'Points', type: 'number' };
39
- const selectField: InlineFieldDefinition = {
40
- name: 'priority',
41
- label: 'Priority',
42
- type: 'select',
43
- options: [
44
- { label: 'Low', value: 'low' },
45
- { label: 'High', value: 'high' },
46
- ],
47
- };
48
-
49
- const sampleTemplates: CardTemplate[] = [
50
- { id: 't1', name: 'Bug Report', values: { title: 'Bug: ', priority: 'high' } },
51
- { id: 't2', name: 'Feature', values: { title: 'Feature: ' } },
52
- ];
53
-
54
- const makeCards = (ids: string[]): KanbanCard[] =>
55
- ids.map(id => ({ id, title: `Card ${id}` }));
56
-
57
- const makeColumns = (ids: string[]): KanbanColumn[] =>
58
- ids.map(id => ({ id, title: `Col ${id}`, cards: [] }));
59
-
60
- // ---------------------------------------------------------------------------
61
- // InlineQuickAdd
62
- // ---------------------------------------------------------------------------
63
- describe('InlineQuickAdd', () => {
64
- const onSubmit = vi.fn();
65
- const onCancel = vi.fn();
66
-
67
- beforeEach(() => {
68
- vi.clearAllMocks();
69
- });
70
-
71
- it('renders form fields based on field definitions', () => {
72
- render(
73
- <InlineQuickAdd
74
- columnId="col1"
75
- fields={[textField, numberField, selectField]}
76
- onSubmit={onSubmit}
77
- onCancel={onCancel}
78
- />,
79
- );
80
- expect(screen.getByLabelText('Title')).toBeDefined();
81
- expect(screen.getByLabelText('Points')).toBeDefined();
82
- expect(screen.getByLabelText('Priority')).toBeDefined();
83
- });
84
-
85
- it('auto-focuses first field', async () => {
86
- render(
87
- <InlineQuickAdd
88
- columnId="col1"
89
- fields={[textField]}
90
- onSubmit={onSubmit}
91
- onCancel={onCancel}
92
- />,
93
- );
94
- await waitFor(() => {
95
- expect(document.activeElement).toBe(screen.getByLabelText('Title'));
96
- });
97
- });
98
-
99
- it('submits on Enter', async () => {
100
- render(
101
- <InlineQuickAdd
102
- columnId="col1"
103
- fields={[textField]}
104
- onSubmit={onSubmit}
105
- onCancel={onCancel}
106
- />,
107
- );
108
- const input = screen.getByLabelText('Title');
109
- fireEvent.change(input, { target: { value: 'Hello' } });
110
- fireEvent.keyDown(input, { key: 'Enter' });
111
- expect(onSubmit).toHaveBeenCalledWith('col1', { title: 'Hello' });
112
- });
113
-
114
- it('cancels on Escape', () => {
115
- render(
116
- <InlineQuickAdd
117
- columnId="col1"
118
- fields={[textField]}
119
- onSubmit={onSubmit}
120
- onCancel={onCancel}
121
- />,
122
- );
123
- fireEvent.keyDown(screen.getByLabelText('Title'), { key: 'Escape' });
124
- expect(onCancel).toHaveBeenCalled();
125
- });
126
-
127
- it('applies default values (from template)', () => {
128
- render(
129
- <InlineQuickAdd
130
- columnId="col1"
131
- fields={[textField, numberField]}
132
- onSubmit={onSubmit}
133
- onCancel={onCancel}
134
- defaultValues={{ title: 'Bug: ', points: 5 }}
135
- />,
136
- );
137
- expect((screen.getByLabelText('Title') as HTMLInputElement).value).toBe('Bug: ');
138
- expect((screen.getByLabelText('Points') as HTMLInputElement).value).toBe('5');
139
- });
140
-
141
- it('calls onSubmit with field values via Save button', () => {
142
- render(
143
- <InlineQuickAdd
144
- columnId="col1"
145
- fields={[textField]}
146
- onSubmit={onSubmit}
147
- onCancel={onCancel}
148
- defaultValues={{ title: 'task' }}
149
- />,
150
- );
151
- fireEvent.click(screen.getByRole('button', { name: /save/i }));
152
- expect(onSubmit).toHaveBeenCalledWith('col1', { title: 'task' });
153
- });
154
-
155
- it('calls onCancel via Cancel button', () => {
156
- render(
157
- <InlineQuickAdd
158
- columnId="col1"
159
- fields={[textField]}
160
- onSubmit={onSubmit}
161
- onCancel={onCancel}
162
- />,
163
- );
164
- fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
165
- expect(onCancel).toHaveBeenCalled();
166
- });
167
- });
168
-
169
- // ---------------------------------------------------------------------------
170
- // CardTemplates
171
- // ---------------------------------------------------------------------------
172
- describe('CardTemplates', () => {
173
- const onSelect = vi.fn();
174
-
175
- beforeEach(() => {
176
- vi.clearAllMocks();
177
- });
178
-
179
- it('renders template dropdown trigger', () => {
180
- render(<CardTemplates templates={sampleTemplates} onSelect={onSelect} columnId="col1" />);
181
- expect(screen.getByRole('button', { name: /add card to col1/i })).toBeDefined();
182
- });
183
-
184
- it('shows template options in dropdown', () => {
185
- render(<CardTemplates templates={sampleTemplates} onSelect={onSelect} columnId="col1" />);
186
- fireEvent.click(screen.getByRole('button', { name: /add card to col1/i }));
187
- expect(screen.getByRole('listbox', { name: /card templates/i })).toBeDefined();
188
- expect(screen.getByText('Bug Report')).toBeDefined();
189
- expect(screen.getByText('Feature')).toBeDefined();
190
- });
191
-
192
- it('calls onSelect with template when clicked', () => {
193
- render(<CardTemplates templates={sampleTemplates} onSelect={onSelect} columnId="col1" />);
194
- fireEvent.click(screen.getByRole('button', { name: /add card to col1/i }));
195
- fireEvent.click(screen.getByText('Bug Report'));
196
- expect(onSelect).toHaveBeenCalledWith(sampleTemplates[0]);
197
- });
198
-
199
- it('shows Custom option and calls onSelect(null)', () => {
200
- render(<CardTemplates templates={sampleTemplates} onSelect={onSelect} columnId="col1" />);
201
- fireEvent.click(screen.getByRole('button', { name: /add card to col1/i }));
202
- expect(screen.getByText('Custom')).toBeDefined();
203
- fireEvent.click(screen.getByText('Custom'));
204
- expect(onSelect).toHaveBeenCalledWith(null);
205
- });
206
- });
207
-
208
- // ---------------------------------------------------------------------------
209
- // useColumnWidths
210
- // ---------------------------------------------------------------------------
211
- describe('useColumnWidths', () => {
212
- beforeEach(() => {
213
- vi.clearAllMocks();
214
- localStorageMock.clear();
215
- });
216
-
217
- it('returns default width for all columns', () => {
218
- const columns = makeColumns(['a', 'b']);
219
- const { result } = renderHook(() => useColumnWidths({ columns, defaultWidth: 300 }));
220
- expect(result.current.getColumnWidth('a')).toBe(300);
221
- expect(result.current.getColumnWidth('b')).toBe(300);
222
- });
223
-
224
- it('applies per-column overrides', () => {
225
- const columns = makeColumns(['a', 'b']);
226
- const { result } = renderHook(() => useColumnWidths({ columns, defaultWidth: 300 }));
227
- act(() => { result.current.setColumnWidth('a', 400); });
228
- expect(result.current.getColumnWidth('a')).toBe(400);
229
- expect(result.current.getColumnWidth('b')).toBe(300);
230
- });
231
-
232
- it('clamps to minWidth and maxWidth', () => {
233
- const columns = makeColumns(['a']);
234
- const { result } = renderHook(() =>
235
- useColumnWidths({ columns, defaultWidth: 300, minWidth: 200, maxWidth: 500 }),
236
- );
237
- act(() => { result.current.setColumnWidth('a', 100); });
238
- expect(result.current.getColumnWidth('a')).toBe(200);
239
- act(() => { result.current.setColumnWidth('a', 900); });
240
- expect(result.current.getColumnWidth('a')).toBe(500);
241
- });
242
-
243
- it('persists to localStorage', () => {
244
- const columns = makeColumns(['a']);
245
- const { result } = renderHook(() =>
246
- useColumnWidths({ columns, storageKey: 'board1' }),
247
- );
248
- act(() => { result.current.setColumnWidth('a', 350); });
249
- expect(localStorageMock.setItem).toHaveBeenCalled();
250
- const stored = JSON.parse(
251
- localStorageMock.setItem.mock.calls.at(-1)![1] as string,
252
- );
253
- expect(stored.a).toBe(350);
254
- });
255
-
256
- it('resetWidths restores defaults', () => {
257
- const columns = makeColumns(['a']);
258
- const { result } = renderHook(() =>
259
- useColumnWidths({ columns, defaultWidth: 320, storageKey: 'board2' }),
260
- );
261
- act(() => { result.current.setColumnWidth('a', 400); });
262
- expect(result.current.getColumnWidth('a')).toBe(400);
263
- act(() => { result.current.resetWidths(); });
264
- expect(result.current.getColumnWidth('a')).toBe(320);
265
- expect(localStorageMock.removeItem).toHaveBeenCalled();
266
- });
267
- });
268
-
269
- // ---------------------------------------------------------------------------
270
- // useCrossSwimlaneMove
271
- // ---------------------------------------------------------------------------
272
- describe('useCrossSwimlaneMove', () => {
273
- const swimlanes = [
274
- { id: 'team-a', title: 'Team A' },
275
- { id: 'team-b', title: 'Team B', acceptFrom: ['team-a'] },
276
- { id: 'team-c', title: 'Team C' },
277
- ];
278
-
279
- beforeEach(() => {
280
- vi.clearAllMocks();
281
- });
282
-
283
- it('returns initial state (not dragging)', () => {
284
- const { result } = renderHook(() =>
285
- useCrossSwimlaneMove({ swimlanes }),
286
- );
287
- expect(result.current.isDraggingAcrossSwimlanes).toBe(false);
288
- });
289
-
290
- it('handleCrossSwimlaneMove calls onCardMove', () => {
291
- const onCardMove = vi.fn();
292
- const { result } = renderHook(() =>
293
- useCrossSwimlaneMove({ swimlanes, onCardMove }),
294
- );
295
- let allowed: boolean;
296
- act(() => {
297
- allowed = result.current.handleCrossSwimlaneMove('card1', 'team-a', 'team-c', 'col1');
298
- });
299
- expect(allowed!).toBe(true);
300
- expect(onCardMove).toHaveBeenCalledWith({
301
- cardId: 'card1',
302
- fromSwimlane: 'team-a',
303
- toSwimlane: 'team-c',
304
- columnId: 'col1',
305
- });
306
- });
307
-
308
- it('respects acceptFrom constraints', () => {
309
- const onCardMove = vi.fn();
310
- const { result } = renderHook(() =>
311
- useCrossSwimlaneMove({ swimlanes, onCardMove }),
312
- );
313
-
314
- // team-b only accepts from team-a
315
- let allowed: boolean;
316
- act(() => {
317
- allowed = result.current.handleCrossSwimlaneMove('card1', 'team-c', 'team-b', 'col1');
318
- });
319
- expect(allowed!).toBe(false);
320
- expect(onCardMove).not.toHaveBeenCalled();
321
-
322
- // team-a → team-b is allowed
323
- act(() => {
324
- allowed = result.current.handleCrossSwimlaneMove('card1', 'team-a', 'team-b', 'col1');
325
- });
326
- expect(allowed!).toBe(true);
327
- expect(onCardMove).toHaveBeenCalled();
328
- });
329
-
330
- it('isDraggingAcrossSwimlanes state tracks movement', () => {
331
- const { result } = renderHook(() =>
332
- useCrossSwimlaneMove({ swimlanes }),
333
- );
334
- expect(result.current.isDraggingAcrossSwimlanes).toBe(false);
335
- act(() => { result.current.startCrossSwimlaneDrag('team-a'); });
336
- expect(result.current.isDraggingAcrossSwimlanes).toBe(true);
337
- act(() => { result.current.endCrossSwimlaneDrag(); });
338
- expect(result.current.isDraggingAcrossSwimlanes).toBe(false);
339
- });
340
- });
341
-
342
- // ---------------------------------------------------------------------------
343
- // useQuickAddReorder
344
- // ---------------------------------------------------------------------------
345
- describe('useQuickAddReorder', () => {
346
- beforeEach(() => {
347
- vi.clearAllMocks();
348
- });
349
-
350
- it('initializes with provided cards', () => {
351
- const cards = makeCards(['1', '2', '3']);
352
- const { result } = renderHook(() => useQuickAddReorder({ cards }));
353
- expect(result.current.reorderedCards.map(c => c.id)).toEqual(['1', '2', '3']);
354
- });
355
-
356
- it('reorders cards correctly', () => {
357
- const cards = makeCards(['1', '2', '3']);
358
- const { result } = renderHook(() => useQuickAddReorder({ cards }));
359
- // Must be in drag state so the sync guard doesn't reset
360
- act(() => { result.current.startDrag(); });
361
- act(() => { result.current.onReorder(0, 2); });
362
- expect(result.current.reorderedCards.map(c => c.id)).toEqual(['2', '3', '1']);
363
- });
364
-
365
- it('returns isDragging state', () => {
366
- const cards = makeCards(['1']);
367
- const { result } = renderHook(() => useQuickAddReorder({ cards }));
368
- expect(result.current.isDragging).toBe(false);
369
- act(() => { result.current.startDrag(); });
370
- expect(result.current.isDragging).toBe(true);
371
- act(() => { result.current.endDrag(); });
372
- expect(result.current.isDragging).toBe(false);
373
- });
374
-
375
- it('syncs with external card changes', () => {
376
- const initial = makeCards(['1', '2']);
377
- const { result, rerender } = renderHook(
378
- ({ cards }) => useQuickAddReorder({ cards }),
379
- { initialProps: { cards: initial } },
380
- );
381
- expect(result.current.reorderedCards.map(c => c.id)).toEqual(['1', '2']);
382
-
383
- const updated = makeCards(['1', '2', '3']);
384
- rerender({ cards: updated });
385
- expect(result.current.reorderedCards.map(c => c.id)).toEqual(['1', '2', '3']);
386
- });
387
- });