@object-ui/plugin-kanban 3.3.0 → 3.3.2
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.
- package/CHANGELOG.md +19 -0
- package/README.md +24 -0
- package/dist/{KanbanEnhanced-TdUe0kQH.js → KanbanEnhanced-Do9ZB1Mh.js} +35 -32
- package/dist/{KanbanImpl-BtlPa7GE.js → KanbanImpl-BdocXM5T.js} +1 -1
- package/dist/{chevron-down-B6UH8BbF.js → chevron-down-C0JUlGjk.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/index.umd.cjs +2 -2
- package/dist/{plus-BTqoaaEC.js → plus-CHsXVJSY.js} +1 -1
- package/package.json +34 -11
- package/.turbo/turbo-build.log +0 -32
- package/src/CardTemplates.tsx +0 -123
- package/src/InlineQuickAdd.tsx +0 -189
- package/src/KanbanEnhanced.tsx +0 -525
- package/src/KanbanImpl.tsx +0 -597
- package/src/ObjectKanban.EdgeCases.stories.tsx +0 -168
- package/src/ObjectKanban.msw.test.tsx +0 -95
- package/src/ObjectKanban.stories.tsx +0 -152
- package/src/ObjectKanban.tsx +0 -276
- package/src/__tests__/KanbanEnhanced.test.tsx +0 -260
- package/src/__tests__/KanbanGrouping.test.tsx +0 -164
- package/src/__tests__/KanbanSwimlanes.test.tsx +0 -194
- package/src/__tests__/ObjectKanbanTitle.test.tsx +0 -93
- package/src/__tests__/SwimlanePersistence.test.tsx +0 -159
- package/src/__tests__/accessibility.test.tsx +0 -296
- package/src/__tests__/dnd-undo-integration.test.tsx +0 -525
- package/src/__tests__/performance-benchmark.test.tsx +0 -306
- package/src/__tests__/phase13-features.test.tsx +0 -387
- package/src/__tests__/view-states.test.tsx +0 -403
- package/src/index.test.ts +0 -112
- package/src/index.tsx +0 -327
- package/src/registration.test.tsx +0 -26
- package/src/types.ts +0 -185
- package/src/useColumnWidths.ts +0 -125
- package/src/useCrossSwimlaneMove.ts +0 -116
- package/src/useQuickAddReorder.ts +0 -107
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -62
- package/vitest.config.ts +0 -12
- package/vitest.setup.ts +0 -1
|
@@ -1,93 +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 } from 'vitest';
|
|
10
|
-
import { renderHook } from '@testing-library/react';
|
|
11
|
-
import { useMemo } from 'react';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Tests for the title resolution fallback chain in ObjectKanban.
|
|
15
|
-
*
|
|
16
|
-
* The effectiveData logic tries fields in this order:
|
|
17
|
-
* 1. Explicit cardTitle / titleField from schema
|
|
18
|
-
* 2. objectDef.titleFormat (e.g. "{subject}")
|
|
19
|
-
* 3. objectDef.NAME_FIELD_KEY
|
|
20
|
-
* 4. Fallback chain: name → title → subject → label → display_name
|
|
21
|
-
* 5. 'Untitled'
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
// Extract the title resolution logic from ObjectKanban to test it in isolation
|
|
25
|
-
const TITLE_FALLBACK_FIELDS = ['name', 'title', 'subject', 'label', 'display_name'];
|
|
26
|
-
|
|
27
|
-
function resolveTitle(item: Record<string, any>, titleField?: string): string {
|
|
28
|
-
let resolvedTitle = titleField ? item[titleField] : undefined;
|
|
29
|
-
|
|
30
|
-
if (!resolvedTitle) {
|
|
31
|
-
for (const field of TITLE_FALLBACK_FIELDS) {
|
|
32
|
-
if (item[field]) {
|
|
33
|
-
resolvedTitle = item[field];
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return resolvedTitle || 'Untitled';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe('ObjectKanban title resolution', () => {
|
|
43
|
-
it('uses explicit titleField when value exists', () => {
|
|
44
|
-
const item = { id: '1', custom_title: 'My Custom Title', name: 'Fallback Name' };
|
|
45
|
-
expect(resolveTitle(item, 'custom_title')).toBe('My Custom Title');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('falls back to common fields when titleField value is empty', () => {
|
|
49
|
-
const item = { id: '1', custom_title: '', name: 'Name Field' };
|
|
50
|
-
expect(resolveTitle(item, 'custom_title')).toBe('Name Field');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('resolves name field first in fallback chain', () => {
|
|
54
|
-
const item = { id: '1', name: 'Name Value', title: 'Title Value', subject: 'Subject Value' };
|
|
55
|
-
expect(resolveTitle(item)).toBe('Name Value');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('resolves title field second in fallback chain', () => {
|
|
59
|
-
const item = { id: '1', title: 'Title Value', subject: 'Subject Value' };
|
|
60
|
-
expect(resolveTitle(item)).toBe('Title Value');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('resolves subject field third in fallback chain', () => {
|
|
64
|
-
const item = { id: '1', subject: 'Subject Value', label: 'Label Value' };
|
|
65
|
-
expect(resolveTitle(item)).toBe('Subject Value');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('resolves label field fourth in fallback chain', () => {
|
|
69
|
-
const item = { id: '1', label: 'Label Value', display_name: 'Display Name' };
|
|
70
|
-
expect(resolveTitle(item)).toBe('Label Value');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('resolves display_name field fifth in fallback chain', () => {
|
|
74
|
-
const item = { id: '1', display_name: 'Display Name' };
|
|
75
|
-
expect(resolveTitle(item)).toBe('Display Name');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('falls back to Untitled when no common fields exist', () => {
|
|
79
|
-
const item = { id: '1', status: 'open', priority: 'high' };
|
|
80
|
-
expect(resolveTitle(item)).toBe('Untitled');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('skips falsy field values in fallback chain', () => {
|
|
84
|
-
const item = { id: '1', name: '', title: null, subject: 'Bug Report' };
|
|
85
|
-
expect(resolveTitle(item)).toBe('Bug Report');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('handles todo_task objects with subject field', () => {
|
|
89
|
-
// This is the exact scenario from the bug report
|
|
90
|
-
const todoTask = { id: '1', status: 'in_progress', subject: 'Fix login bug', priority: 'high' };
|
|
91
|
-
expect(resolveTitle(todoTask)).toBe('Fix login bug');
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1,159 +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, fireEvent } from '@testing-library/react';
|
|
11
|
-
import React from 'react';
|
|
12
|
-
|
|
13
|
-
// Mock @dnd-kit/core and utilities
|
|
14
|
-
vi.mock('@dnd-kit/core', () => ({
|
|
15
|
-
DndContext: ({ children }: any) => <div data-testid="dnd-context">{children}</div>,
|
|
16
|
-
DragOverlay: ({ children }: any) => <div data-testid="drag-overlay">{children}</div>,
|
|
17
|
-
PointerSensor: vi.fn(),
|
|
18
|
-
TouchSensor: vi.fn(),
|
|
19
|
-
useSensor: vi.fn(),
|
|
20
|
-
useSensors: () => [],
|
|
21
|
-
closestCorners: vi.fn(),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
vi.mock('@dnd-kit/sortable', () => ({
|
|
25
|
-
SortableContext: ({ children }: any) => <div data-testid="sortable-context">{children}</div>,
|
|
26
|
-
useSortable: () => ({
|
|
27
|
-
attributes: {},
|
|
28
|
-
listeners: {},
|
|
29
|
-
setNodeRef: vi.fn(),
|
|
30
|
-
transform: null,
|
|
31
|
-
transition: null,
|
|
32
|
-
isDragging: false,
|
|
33
|
-
}),
|
|
34
|
-
arrayMove: (array: any[], from: number, to: number) => {
|
|
35
|
-
const newArray = [...array];
|
|
36
|
-
newArray.splice(to, 0, newArray.splice(from, 1)[0]);
|
|
37
|
-
return newArray;
|
|
38
|
-
},
|
|
39
|
-
verticalListSortingStrategy: vi.fn(),
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
vi.mock('@dnd-kit/utilities', () => ({
|
|
43
|
-
CSS: {
|
|
44
|
-
Transform: {
|
|
45
|
-
toString: () => '',
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
vi.mock('@object-ui/components', () => ({
|
|
51
|
-
Badge: ({ children, ...props }: any) => <span {...props}>{children}</span>,
|
|
52
|
-
Card: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
53
|
-
CardHeader: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
54
|
-
CardTitle: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
55
|
-
CardDescription: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
56
|
-
CardContent: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
57
|
-
ScrollArea: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
58
|
-
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
59
|
-
Input: (props: any) => <input {...props} />,
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
|
-
vi.mock('@object-ui/react', () => ({
|
|
63
|
-
useHasDndProvider: () => false,
|
|
64
|
-
useDnd: () => ({
|
|
65
|
-
startDrag: vi.fn(),
|
|
66
|
-
endDrag: vi.fn(),
|
|
67
|
-
}),
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
vi.mock('lucide-react', () => ({
|
|
71
|
-
Plus: () => <span>+</span>,
|
|
72
|
-
}));
|
|
73
|
-
|
|
74
|
-
import KanbanBoard from '../KanbanImpl';
|
|
75
|
-
|
|
76
|
-
// Mock localStorage
|
|
77
|
-
const localStorageMock = (() => {
|
|
78
|
-
let store: Record<string, string> = {};
|
|
79
|
-
return {
|
|
80
|
-
getItem: vi.fn((key: string) => store[key] ?? null),
|
|
81
|
-
setItem: vi.fn((key: string, value: string) => { store[key] = value; }),
|
|
82
|
-
clear: () => { store = {}; },
|
|
83
|
-
removeItem: vi.fn((key: string) => { delete store[key]; }),
|
|
84
|
-
};
|
|
85
|
-
})();
|
|
86
|
-
|
|
87
|
-
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
|
88
|
-
|
|
89
|
-
const mockColumns = [
|
|
90
|
-
{
|
|
91
|
-
id: 'todo',
|
|
92
|
-
title: 'To Do',
|
|
93
|
-
cards: [
|
|
94
|
-
{ id: 'c1', title: 'Task 1', category: 'Frontend' },
|
|
95
|
-
{ id: 'c2', title: 'Task 2', category: 'Backend' },
|
|
96
|
-
],
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: 'done',
|
|
100
|
-
title: 'Done',
|
|
101
|
-
cards: [
|
|
102
|
-
{ id: 'c3', title: 'Task 3', category: 'Frontend' },
|
|
103
|
-
],
|
|
104
|
-
},
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
describe('KanbanBoard swimlane persistence', () => {
|
|
108
|
-
beforeEach(() => {
|
|
109
|
-
localStorageMock.clear();
|
|
110
|
-
localStorageMock.getItem.mockClear();
|
|
111
|
-
localStorageMock.setItem.mockClear();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('reads collapsed lanes from localStorage on mount when swimlaneField is set', () => {
|
|
115
|
-
localStorageMock.setItem(
|
|
116
|
-
'objectui:kanban-collapsed:category',
|
|
117
|
-
JSON.stringify(['Frontend']),
|
|
118
|
-
);
|
|
119
|
-
localStorageMock.getItem.mockClear();
|
|
120
|
-
|
|
121
|
-
render(<KanbanBoard columns={mockColumns} swimlaneField="category" />);
|
|
122
|
-
|
|
123
|
-
expect(localStorageMock.getItem).toHaveBeenCalledWith(
|
|
124
|
-
'objectui:kanban-collapsed:category',
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('writes collapsed state to localStorage when a lane is toggled', () => {
|
|
129
|
-
render(<KanbanBoard columns={mockColumns} swimlaneField="category" />);
|
|
130
|
-
|
|
131
|
-
// Find a swimlane collapse button and click it
|
|
132
|
-
const collapseButtons = screen.getAllByRole('button').filter(
|
|
133
|
-
btn => btn.getAttribute('aria-label')?.includes('collapse') ||
|
|
134
|
-
btn.getAttribute('aria-label')?.includes('Toggle') ||
|
|
135
|
-
btn.textContent?.includes('▸') ||
|
|
136
|
-
btn.textContent?.includes('▾'),
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
if (collapseButtons.length > 0) {
|
|
140
|
-
fireEvent.click(collapseButtons[0]);
|
|
141
|
-
expect(localStorageMock.setItem).toHaveBeenCalled();
|
|
142
|
-
const lastCall = localStorageMock.setItem.mock.calls.at(-1);
|
|
143
|
-
expect(lastCall?.[0]).toBe('objectui:kanban-collapsed:category');
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('does not access localStorage when swimlaneField is not set', () => {
|
|
148
|
-
localStorageMock.getItem.mockClear();
|
|
149
|
-
localStorageMock.setItem.mockClear();
|
|
150
|
-
|
|
151
|
-
render(<KanbanBoard columns={mockColumns} />);
|
|
152
|
-
|
|
153
|
-
// No localStorage reads for collapsed state key
|
|
154
|
-
const collapsedCalls = localStorageMock.getItem.mock.calls.filter(
|
|
155
|
-
([key]: [string]) => key.startsWith('objectui:kanban-collapsed:'),
|
|
156
|
-
);
|
|
157
|
-
expect(collapsedCalls).toHaveLength(0);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
@@ -1,296 +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
|
-
/**
|
|
10
|
-
* Screen reader experience tests for KanbanEnhanced.
|
|
11
|
-
*
|
|
12
|
-
* Tests ARIA attributes, roles, landmarks, keyboard navigation,
|
|
13
|
-
* and screen reader announcements for the kanban board plugin.
|
|
14
|
-
* Part of P2.3 Accessibility & Inclusive Design roadmap.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
18
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
19
|
-
import '@testing-library/jest-dom';
|
|
20
|
-
import React from 'react';
|
|
21
|
-
import { KanbanEnhanced, type KanbanColumn } from '../KanbanEnhanced';
|
|
22
|
-
|
|
23
|
-
// Mock @tanstack/react-virtual
|
|
24
|
-
vi.mock('@tanstack/react-virtual', () => ({
|
|
25
|
-
useVirtualizer: () => ({
|
|
26
|
-
getTotalSize: () => 1000,
|
|
27
|
-
getVirtualItems: () => [],
|
|
28
|
-
measureElement: vi.fn(),
|
|
29
|
-
}),
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
// Mock @dnd-kit/core and utilities
|
|
33
|
-
vi.mock('@dnd-kit/core', () => ({
|
|
34
|
-
DndContext: ({ children }: any) => <div data-testid="dnd-context">{children}</div>,
|
|
35
|
-
DragOverlay: ({ children }: any) => <div data-testid="drag-overlay">{children}</div>,
|
|
36
|
-
PointerSensor: vi.fn(),
|
|
37
|
-
TouchSensor: vi.fn(),
|
|
38
|
-
useSensor: vi.fn(),
|
|
39
|
-
useSensors: () => [],
|
|
40
|
-
closestCorners: vi.fn(),
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
vi.mock('@dnd-kit/sortable', () => ({
|
|
44
|
-
SortableContext: ({ children }: any) => <div data-testid="sortable-context">{children}</div>,
|
|
45
|
-
useSortable: () => ({
|
|
46
|
-
attributes: {},
|
|
47
|
-
listeners: {},
|
|
48
|
-
setNodeRef: vi.fn(),
|
|
49
|
-
transform: null,
|
|
50
|
-
transition: null,
|
|
51
|
-
isDragging: false,
|
|
52
|
-
}),
|
|
53
|
-
arrayMove: (array: any[], from: number, to: number) => {
|
|
54
|
-
const newArray = [...array];
|
|
55
|
-
newArray.splice(to, 0, newArray.splice(from, 1)[0]);
|
|
56
|
-
return newArray;
|
|
57
|
-
},
|
|
58
|
-
verticalListSortingStrategy: vi.fn(),
|
|
59
|
-
}));
|
|
60
|
-
|
|
61
|
-
vi.mock('@dnd-kit/utilities', () => ({
|
|
62
|
-
CSS: {
|
|
63
|
-
Transform: {
|
|
64
|
-
toString: () => '',
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
}));
|
|
68
|
-
|
|
69
|
-
const mockColumns: KanbanColumn[] = [
|
|
70
|
-
{
|
|
71
|
-
id: 'todo',
|
|
72
|
-
title: 'To Do',
|
|
73
|
-
cards: [
|
|
74
|
-
{
|
|
75
|
-
id: 'card-1',
|
|
76
|
-
title: 'Design Landing Page',
|
|
77
|
-
description: 'Create wireframes and mockups',
|
|
78
|
-
badges: [{ label: 'High', variant: 'destructive' as const }],
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: 'card-2',
|
|
82
|
-
title: 'Write Documentation',
|
|
83
|
-
description: 'API reference and guides',
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: 'in-progress',
|
|
89
|
-
title: 'In Progress',
|
|
90
|
-
limit: 3,
|
|
91
|
-
cards: [
|
|
92
|
-
{
|
|
93
|
-
id: 'card-3',
|
|
94
|
-
title: 'Build Components',
|
|
95
|
-
description: 'Implement the design system',
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
id: 'done',
|
|
101
|
-
title: 'Done',
|
|
102
|
-
cards: [],
|
|
103
|
-
},
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
describe('KanbanEnhanced: Screen Reader & Accessibility', () => {
|
|
107
|
-
describe('board structure and landmarks', () => {
|
|
108
|
-
it('renders all column titles visible for screen readers', () => {
|
|
109
|
-
render(<KanbanEnhanced columns={mockColumns} />);
|
|
110
|
-
|
|
111
|
-
expect(screen.getByText('To Do')).toBeInTheDocument();
|
|
112
|
-
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
|
113
|
-
expect(screen.getByText('Done')).toBeInTheDocument();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('column titles use heading elements for hierarchy', () => {
|
|
117
|
-
render(<KanbanEnhanced columns={mockColumns} />);
|
|
118
|
-
|
|
119
|
-
const todoTitle = screen.getByText('To Do');
|
|
120
|
-
expect(todoTitle.tagName).toBe('H3');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('renders within a dnd-context container', () => {
|
|
124
|
-
const { container } = render(<KanbanEnhanced columns={mockColumns} />);
|
|
125
|
-
|
|
126
|
-
const dndContext = container.querySelector('[data-testid="dnd-context"]');
|
|
127
|
-
expect(dndContext).toBeInTheDocument();
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('card content accessibility', () => {
|
|
132
|
-
it('card titles are visible to screen readers', () => {
|
|
133
|
-
render(<KanbanEnhanced columns={mockColumns} />);
|
|
134
|
-
|
|
135
|
-
expect(screen.getByText('Design Landing Page')).toBeInTheDocument();
|
|
136
|
-
expect(screen.getByText('Write Documentation')).toBeInTheDocument();
|
|
137
|
-
expect(screen.getByText('Build Components')).toBeInTheDocument();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('card descriptions are accessible', () => {
|
|
141
|
-
render(<KanbanEnhanced columns={mockColumns} />);
|
|
142
|
-
|
|
143
|
-
expect(screen.getByText('Create wireframes and mockups')).toBeInTheDocument();
|
|
144
|
-
expect(screen.getByText('API reference and guides')).toBeInTheDocument();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('card badges convey semantic information', () => {
|
|
148
|
-
render(<KanbanEnhanced columns={mockColumns} />);
|
|
149
|
-
|
|
150
|
-
const badge = screen.getByText('High');
|
|
151
|
-
expect(badge).toBeInTheDocument();
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe('column count and limit indicators', () => {
|
|
156
|
-
it('displays card count per column for progress tracking', () => {
|
|
157
|
-
render(<KanbanEnhanced columns={mockColumns} />);
|
|
158
|
-
|
|
159
|
-
// "In Progress" has limit = 3, cards = 1 → shows "1 / 3"
|
|
160
|
-
expect(screen.getByText('1 / 3')).toBeInTheDocument();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('shows warning for columns near capacity', () => {
|
|
164
|
-
const nearLimitColumns: KanbanColumn[] = [
|
|
165
|
-
{
|
|
166
|
-
id: 'limited',
|
|
167
|
-
title: 'Limited Column',
|
|
168
|
-
limit: 5,
|
|
169
|
-
cards: Array(4)
|
|
170
|
-
.fill(null)
|
|
171
|
-
.map((_, i) => ({
|
|
172
|
-
id: `card-${i}`,
|
|
173
|
-
title: `Task ${i}`,
|
|
174
|
-
})),
|
|
175
|
-
},
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
const { container } = render(<KanbanEnhanced columns={nearLimitColumns} />);
|
|
179
|
-
|
|
180
|
-
// Near-limit indicator uses yellow color
|
|
181
|
-
expect(container.querySelector('.text-yellow-500')).toBeTruthy();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('shows error indicator for columns over limit', () => {
|
|
185
|
-
const overLimitColumns: KanbanColumn[] = [
|
|
186
|
-
{
|
|
187
|
-
id: 'full',
|
|
188
|
-
title: 'Full Column',
|
|
189
|
-
limit: 2,
|
|
190
|
-
cards: [
|
|
191
|
-
{ id: 'card-1', title: 'Task 1' },
|
|
192
|
-
{ id: 'card-2', title: 'Task 2' },
|
|
193
|
-
],
|
|
194
|
-
},
|
|
195
|
-
];
|
|
196
|
-
|
|
197
|
-
const { container } = render(<KanbanEnhanced columns={overLimitColumns} />);
|
|
198
|
-
|
|
199
|
-
// Over-limit shows destructive badge "Full"
|
|
200
|
-
const fullBadge = container.querySelector('[class*="destructive"]');
|
|
201
|
-
expect(fullBadge).toBeTruthy();
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe('collapse/expand behavior', () => {
|
|
206
|
-
it('columns have toggle buttons for collapse/expand', () => {
|
|
207
|
-
render(<KanbanEnhanced columns={mockColumns} />);
|
|
208
|
-
|
|
209
|
-
const buttons = screen.getAllByRole('button');
|
|
210
|
-
// Each column has at least one toggle button
|
|
211
|
-
expect(buttons.length).toBeGreaterThanOrEqual(3);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('collapsed column shows title in vertical orientation', () => {
|
|
215
|
-
const collapsedColumns: KanbanColumn[] = [
|
|
216
|
-
{
|
|
217
|
-
id: 'collapsed',
|
|
218
|
-
title: 'Collapsed Column',
|
|
219
|
-
collapsed: true,
|
|
220
|
-
cards: [{ id: 'card-1', title: 'Task 1' }],
|
|
221
|
-
},
|
|
222
|
-
];
|
|
223
|
-
|
|
224
|
-
render(<KanbanEnhanced columns={collapsedColumns} />);
|
|
225
|
-
|
|
226
|
-
// Collapsed column still shows title (vertically)
|
|
227
|
-
expect(screen.getByText('Collapsed Column')).toBeInTheDocument();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('toggle button click changes column state', () => {
|
|
231
|
-
render(<KanbanEnhanced columns={mockColumns} />);
|
|
232
|
-
|
|
233
|
-
// Find the toggle buttons (ghost variant, small)
|
|
234
|
-
const buttons = screen.getAllByRole('button');
|
|
235
|
-
const toggleButton = buttons[0];
|
|
236
|
-
|
|
237
|
-
// Click to collapse
|
|
238
|
-
fireEvent.click(toggleButton);
|
|
239
|
-
|
|
240
|
-
// The component should still render (no crash)
|
|
241
|
-
expect(toggleButton).toBeInTheDocument();
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe('drag and drop accessibility', () => {
|
|
246
|
-
it('drag overlay container exists for visual feedback', () => {
|
|
247
|
-
const { container } = render(<KanbanEnhanced columns={mockColumns} />);
|
|
248
|
-
|
|
249
|
-
const overlay = container.querySelector('[data-testid="drag-overlay"]');
|
|
250
|
-
expect(overlay).toBeTruthy();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('sortable context wraps card list for DnD', () => {
|
|
254
|
-
const { container } = render(<KanbanEnhanced columns={mockColumns} />);
|
|
255
|
-
|
|
256
|
-
const sortableContexts = container.querySelectorAll('[data-testid="sortable-context"]');
|
|
257
|
-
// Each non-collapsed column has a sortable context
|
|
258
|
-
expect(sortableContexts.length).toBe(3);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('empty state handling', () => {
|
|
263
|
-
it('handles empty columns array without errors', () => {
|
|
264
|
-
const { container } = render(<KanbanEnhanced columns={[]} />);
|
|
265
|
-
expect(container).toBeTruthy();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('renders columns with no cards correctly', () => {
|
|
269
|
-
const emptyColumns: KanbanColumn[] = [
|
|
270
|
-
{ id: 'empty', title: 'Empty Column', cards: [] },
|
|
271
|
-
];
|
|
272
|
-
|
|
273
|
-
render(<KanbanEnhanced columns={emptyColumns} />);
|
|
274
|
-
|
|
275
|
-
expect(screen.getByText('Empty Column')).toBeInTheDocument();
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
describe('visual hierarchy and semantics', () => {
|
|
280
|
-
it('card uses Card component with proper structure', () => {
|
|
281
|
-
render(<KanbanEnhanced columns={mockColumns} />);
|
|
282
|
-
|
|
283
|
-
// Card titles use CardTitle which renders with proper styling
|
|
284
|
-
const cardTitle = screen.getByText('Design Landing Page');
|
|
285
|
-
expect(cardTitle).toBeInTheDocument();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('custom className is applied to root container', () => {
|
|
289
|
-
const { container } = render(
|
|
290
|
-
<KanbanEnhanced columns={mockColumns} className="custom-board" />
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
expect(container.querySelector('.custom-board')).toBeTruthy();
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
});
|