-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathdata-table-selection-mode.test.tsx
More file actions
150 lines (131 loc) · 5.24 KB
/
Copy pathdata-table-selection-mode.test.tsx
File metadata and controls
150 lines (131 loc) · 5.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Selection mode ↔ spec vocabulary parity + behavior (#2941).
*
* `SelectionConfigSchema.type` (`ui/view.zod.ts`) publishes three names:
* `none | single | multiple`. The table used to treat `selectable` as a bare
* truthy, so a view authored with `selection.type: 'single'` rendered the full
* multi-select UX — per-row checkboxes that accumulate AND a select-all
* header. The output looked right and wasn't: `single` was never
* distinguished from `multiple`.
*
* Contract under test:
* - parity: the table's declared mode set equals the spec enum, both ways;
* - `single` offers no select-all and replaces the selection on each pick;
* - `multiple` (and legacy `true`) keeps the accumulate + select-all UX.
*/
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { SelectionConfigSchema } from '@objectstack/spec/ui';
import { renderComponent } from './test-utils';
import { SUPPORTED_SELECTION_MODES } from '../renderers/complex/data-table';
beforeAll(async () => {
await import('../renderers');
}, 30000);
/** The spec's selection vocabulary, read through the `.default()` wrapper. */
function specSelectionModes(): string[] {
const typeSchema = (SelectionConfigSchema as unknown as { shape?: Record<string, unknown> })
.shape?.type as { def?: { innerType?: { options?: readonly string[] } } } | undefined;
const options = typeSchema?.def?.innerType?.options;
return Array.isArray(options) ? [...options] : [];
}
const baseSchema = {
type: 'data-table' as const,
columns: [{ header: 'Name', accessorKey: 'name' }],
data: [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
{ id: '3', name: 'Carol' },
],
pagination: false,
searchable: false,
};
describe('data-table selection mode covers the spec selection vocabulary', () => {
const specNames = specSelectionModes();
it('reads a non-empty enum from the spec', () => {
expect(specNames, 'could not read SelectionConfigSchema.shape.type options from the spec').not.toEqual([]);
});
it('implements every selection mode the spec accepts', () => {
const unimplemented = specNames.filter((name) => !SUPPORTED_SELECTION_MODES.has(name));
expect(
unimplemented,
'these pass schema validation but render an undistinguished selection UX — implement them in data-table',
).toEqual([]);
});
it('does not accept selection modes the spec rejects', () => {
const extra = [...SUPPORTED_SELECTION_MODES].filter((name) => !specNames.includes(name));
expect(
extra,
'these are renderer-local dialect — promote them into @objectstack/spec instead',
).toEqual([]);
});
});
describe('data-table selection behavior per mode', () => {
it("'single' renders no select-all header and replaces the selection on each pick", () => {
const onSelectionChange = vi.fn();
const { getAllByRole } = renderComponent({
...baseSchema,
selectable: 'single',
onSelectionChange,
} as any);
// 3 rows → exactly 3 checkboxes; a select-all header would make it 4.
const checkboxes = getAllByRole('checkbox');
expect(checkboxes).toHaveLength(3);
fireEvent.click(checkboxes[0]);
expect(onSelectionChange).toHaveBeenLastCalledWith([expect.objectContaining({ name: 'Alice' })]);
// Picking Bob must REPLACE Alice, never accumulate to two rows.
fireEvent.click(checkboxes[1]);
expect(onSelectionChange).toHaveBeenLastCalledWith([expect.objectContaining({ name: 'Bob' })]);
});
it("'single' allows deselecting the picked row", () => {
const onSelectionChange = vi.fn();
const { getAllByRole } = renderComponent({
...baseSchema,
selectable: 'single',
onSelectionChange,
} as any);
const checkboxes = getAllByRole('checkbox');
fireEvent.click(checkboxes[0]);
fireEvent.click(checkboxes[0]);
expect(onSelectionChange).toHaveBeenLastCalledWith([]);
});
it("'multiple' keeps the select-all header and accumulates picks", () => {
const onSelectionChange = vi.fn();
const { getAllByRole } = renderComponent({
...baseSchema,
selectable: 'multiple',
onSelectionChange,
} as any);
// 3 rows + the select-all header.
const checkboxes = getAllByRole('checkbox');
expect(checkboxes).toHaveLength(4);
// First checkbox is the header select-all; rows follow.
fireEvent.click(checkboxes[1]);
fireEvent.click(checkboxes[2]);
expect(onSelectionChange).toHaveBeenLastCalledWith([
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Bob' }),
]);
});
it('legacy `selectable: true` still means multi-select', () => {
const { getAllByRole } = renderComponent({
...baseSchema,
selectable: true,
} as any);
expect(getAllByRole('checkbox')).toHaveLength(4);
});
it("'none' renders no selection column", () => {
const { queryAllByRole } = renderComponent({
...baseSchema,
selectable: 'none',
} as any);
expect(queryAllByRole('checkbox')).toHaveLength(0);
});
});