@object-ui/plugin-view 3.3.0 → 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.
- package/CHANGELOG.md +12 -0
- package/README.md +21 -1
- package/dist/index.js +2679 -2936
- package/dist/index.umd.cjs +1 -5
- package/dist/packages/plugin-view/src/config/view-config-schema.d.ts +20 -0
- package/dist/packages/plugin-view/src/config/view-config-utils.d.ts +58 -0
- package/dist/packages/plugin-view/src/index.d.ts +4 -0
- package/package.json +41 -9
- package/.turbo/turbo-build.log +0 -39
- package/src/FilterUI.tsx +0 -350
- package/src/ObjectView.tsx +0 -1133
- package/src/SharedViewLink.tsx +0 -199
- package/src/SortUI.tsx +0 -210
- package/src/ViewSwitcher.tsx +0 -379
- package/src/ViewTabBar.tsx +0 -656
- package/src/__tests__/FilterUI.test.tsx +0 -641
- package/src/__tests__/ObjectView.test.tsx +0 -705
- package/src/__tests__/SharedViewLinkPassword.test.tsx +0 -172
- package/src/__tests__/SortUI.test.tsx +0 -380
- package/src/__tests__/ViewTabBar.test.tsx +0 -710
- package/src/__tests__/config-sync-integration.test.tsx +0 -588
- package/src/__tests__/toolbar-consistency.test.tsx +0 -755
- package/src/index.tsx +0 -197
- package/tsconfig.json +0 -8
- package/vite.config.ts +0 -48
- package/vitest.config.ts +0 -12
- package/vitest.setup.ts +0 -1
|
@@ -1,172 +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, waitFor } from '@testing-library/react';
|
|
11
|
-
import userEvent from '@testing-library/user-event';
|
|
12
|
-
import React from 'react';
|
|
13
|
-
|
|
14
|
-
vi.mock('@object-ui/components', () => ({
|
|
15
|
-
cn: (...args: any[]) => args.filter(Boolean).join(' '),
|
|
16
|
-
Button: ({ children, onClick, ...props }: any) => (
|
|
17
|
-
<button onClick={onClick} {...props}>{children}</button>
|
|
18
|
-
),
|
|
19
|
-
Badge: ({ children, ...props }: any) => <span {...props}>{children}</span>,
|
|
20
|
-
Input: React.forwardRef(({ ...props }: any, ref: any) => <input ref={ref} {...props} />),
|
|
21
|
-
Popover: ({ children }: any) => <div>{children}</div>,
|
|
22
|
-
PopoverContent: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
23
|
-
PopoverTrigger: ({ children, asChild }: any) => <>{children}</>,
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
vi.mock('lucide-react', () => ({
|
|
27
|
-
Share2: () => <span>ShareIcon</span>,
|
|
28
|
-
Copy: () => <span>CopyIcon</span>,
|
|
29
|
-
Check: () => <span>CheckIcon</span>,
|
|
30
|
-
Lock: () => <span>LockIcon</span>,
|
|
31
|
-
Calendar: () => <span>CalendarIcon</span>,
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
import { SharedViewLink } from '../SharedViewLink';
|
|
35
|
-
|
|
36
|
-
describe('SharedViewLink - Password & Expiration', () => {
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
vi.restoreAllMocks();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('renders the Share button', () => {
|
|
42
|
-
render(<SharedViewLink objectName="tasks" />);
|
|
43
|
-
expect(screen.getByText('Share')).toBeInTheDocument();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('renders password input field in the popover', () => {
|
|
47
|
-
render(<SharedViewLink objectName="tasks" />);
|
|
48
|
-
const passwordInput = screen.getByPlaceholderText('Enter password...');
|
|
49
|
-
expect(passwordInput).toBeInTheDocument();
|
|
50
|
-
expect(passwordInput).toHaveAttribute('type', 'password');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('renders expiration dropdown with options', () => {
|
|
54
|
-
render(<SharedViewLink objectName="tasks" />);
|
|
55
|
-
|
|
56
|
-
const select = screen.getByRole('combobox');
|
|
57
|
-
expect(select).toBeInTheDocument();
|
|
58
|
-
|
|
59
|
-
// Verify options are present
|
|
60
|
-
const options = screen.getAllByRole('option');
|
|
61
|
-
const optionTexts = options.map(o => o.textContent);
|
|
62
|
-
expect(optionTexts).toContain('Never');
|
|
63
|
-
expect(optionTexts).toContain('1 day');
|
|
64
|
-
expect(optionTexts).toContain('7 days');
|
|
65
|
-
expect(optionTexts).toContain('30 days');
|
|
66
|
-
expect(optionTexts).toContain('90 days');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('shows "Password protected" badge after generating link with a password', async () => {
|
|
70
|
-
render(<SharedViewLink objectName="tasks" baseUrl="https://example.com" />);
|
|
71
|
-
|
|
72
|
-
// Type a password
|
|
73
|
-
const passwordInput = screen.getByPlaceholderText('Enter password...');
|
|
74
|
-
fireEvent.change(passwordInput, { target: { value: 'secret123' } });
|
|
75
|
-
|
|
76
|
-
// Click Generate Link
|
|
77
|
-
const generateBtn = screen.getByText('Generate Link');
|
|
78
|
-
fireEvent.click(generateBtn);
|
|
79
|
-
|
|
80
|
-
// After link is generated, "Password protected" badge should appear
|
|
81
|
-
await waitFor(() => {
|
|
82
|
-
expect(screen.getByText('Password protected')).toBeInTheDocument();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('shows expiration badge after generating link with expiration set', async () => {
|
|
87
|
-
render(<SharedViewLink objectName="tasks" baseUrl="https://example.com" />);
|
|
88
|
-
|
|
89
|
-
// Select 7 days expiration
|
|
90
|
-
const select = screen.getByRole('combobox');
|
|
91
|
-
fireEvent.change(select, { target: { value: '7' } });
|
|
92
|
-
|
|
93
|
-
// Click Generate Link
|
|
94
|
-
const generateBtn = screen.getByText('Generate Link');
|
|
95
|
-
fireEvent.click(generateBtn);
|
|
96
|
-
|
|
97
|
-
// After link is generated, expiration badge should appear
|
|
98
|
-
await waitFor(() => {
|
|
99
|
-
expect(screen.getByText(/Expires in 7 days/)).toBeInTheDocument();
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('shows singular "day" for 1 day expiration', async () => {
|
|
104
|
-
render(<SharedViewLink objectName="tasks" baseUrl="https://example.com" />);
|
|
105
|
-
|
|
106
|
-
const select = screen.getByRole('combobox');
|
|
107
|
-
fireEvent.change(select, { target: { value: '1' } });
|
|
108
|
-
|
|
109
|
-
const generateBtn = screen.getByText('Generate Link');
|
|
110
|
-
fireEvent.click(generateBtn);
|
|
111
|
-
|
|
112
|
-
await waitFor(() => {
|
|
113
|
-
expect(screen.getByText(/Expires in 1 day$/)).toBeInTheDocument();
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('calls onShare callback with password and expiresAt options', async () => {
|
|
118
|
-
const onShare = vi.fn();
|
|
119
|
-
render(
|
|
120
|
-
<SharedViewLink
|
|
121
|
-
objectName="tasks"
|
|
122
|
-
baseUrl="https://example.com"
|
|
123
|
-
onShare={onShare}
|
|
124
|
-
/>
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// Set password
|
|
128
|
-
const passwordInput = screen.getByPlaceholderText('Enter password...');
|
|
129
|
-
fireEvent.change(passwordInput, { target: { value: 'mypass' } });
|
|
130
|
-
|
|
131
|
-
// Set expiration to 30 days
|
|
132
|
-
const select = screen.getByRole('combobox');
|
|
133
|
-
fireEvent.change(select, { target: { value: '30' } });
|
|
134
|
-
|
|
135
|
-
// Generate
|
|
136
|
-
const generateBtn = screen.getByText('Generate Link');
|
|
137
|
-
fireEvent.click(generateBtn);
|
|
138
|
-
|
|
139
|
-
expect(onShare).toHaveBeenCalledTimes(1);
|
|
140
|
-
const [url, options] = onShare.mock.calls[0];
|
|
141
|
-
|
|
142
|
-
// URL should contain share path
|
|
143
|
-
expect(url).toContain('/share/tasks/');
|
|
144
|
-
expect(url).toContain('token=');
|
|
145
|
-
|
|
146
|
-
// Options should include password and expiresAt
|
|
147
|
-
expect(options.password).toBe('mypass');
|
|
148
|
-
expect(options.expiresAt).toBeDefined();
|
|
149
|
-
// expiresAt should be a valid ISO date string roughly 30 days from now
|
|
150
|
-
const expiresAt = new Date(options.expiresAt);
|
|
151
|
-
expect(expiresAt.getTime()).toBeGreaterThan(Date.now());
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('calls onShare without password and expiresAt when neither is set', () => {
|
|
155
|
-
const onShare = vi.fn();
|
|
156
|
-
render(
|
|
157
|
-
<SharedViewLink
|
|
158
|
-
objectName="tasks"
|
|
159
|
-
baseUrl="https://example.com"
|
|
160
|
-
onShare={onShare}
|
|
161
|
-
/>
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
const generateBtn = screen.getByText('Generate Link');
|
|
165
|
-
fireEvent.click(generateBtn);
|
|
166
|
-
|
|
167
|
-
expect(onShare).toHaveBeenCalledTimes(1);
|
|
168
|
-
const [, options] = onShare.mock.calls[0];
|
|
169
|
-
expect(options.password).toBeUndefined();
|
|
170
|
-
expect(options.expiresAt).toBeUndefined();
|
|
171
|
-
});
|
|
172
|
-
});
|
|
@@ -1,380 +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 { render, screen, fireEvent } from '@testing-library/react';
|
|
11
|
-
import { SortUI } from '../SortUI';
|
|
12
|
-
import type { SortUISchema } from '@object-ui/types';
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Mock @object-ui/components – provide lightweight stand-ins for Shadcn
|
|
16
|
-
// primitives so tests render without a full component tree.
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
vi.mock('@object-ui/components', () => {
|
|
19
|
-
const cn = (...args: any[]) => args.filter(Boolean).join(' ');
|
|
20
|
-
|
|
21
|
-
const Button = ({ children, onClick, variant, ...rest }: any) => (
|
|
22
|
-
<button onClick={onClick} data-variant={variant} {...rest}>
|
|
23
|
-
{children}
|
|
24
|
-
</button>
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
const Select = ({ children, value, onValueChange }: any) => (
|
|
28
|
-
<div data-testid="select-root" data-value={value}>
|
|
29
|
-
{typeof children === 'function'
|
|
30
|
-
? children({ value, onValueChange })
|
|
31
|
-
: children}
|
|
32
|
-
</div>
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const SelectTrigger = ({ children, className }: any) => (
|
|
36
|
-
<button data-testid="select-trigger" className={className}>
|
|
37
|
-
{children}
|
|
38
|
-
</button>
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const SelectValue = ({ placeholder }: any) => (
|
|
42
|
-
<span data-testid="select-value">{placeholder}</span>
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const SelectContent = ({ children }: any) => (
|
|
46
|
-
<div data-testid="select-content">{children}</div>
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const SelectItem = ({ children, value }: any) => (
|
|
50
|
-
<div data-testid="select-item" data-value={value}>
|
|
51
|
-
{children}
|
|
52
|
-
</div>
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const SortBuilder = ({ fields, value, onChange }: any) => (
|
|
56
|
-
<div data-testid="sort-builder" data-fields={JSON.stringify(fields)} data-value={JSON.stringify(value)}>
|
|
57
|
-
<button
|
|
58
|
-
data-testid="sort-builder-change"
|
|
59
|
-
onClick={() =>
|
|
60
|
-
onChange?.([
|
|
61
|
-
{ id: 'date-desc', field: 'date', order: 'desc' },
|
|
62
|
-
])
|
|
63
|
-
}
|
|
64
|
-
>
|
|
65
|
-
Change Sort
|
|
66
|
-
</button>
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
return { cn, Button, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SortBuilder };
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// Helpers
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
const baseFields: SortUISchema['fields'] = [
|
|
77
|
-
{ field: 'name', label: 'Name' },
|
|
78
|
-
{ field: 'date', label: 'Date' },
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
const makeSchema = (overrides: Partial<SortUISchema> = {}): SortUISchema => ({
|
|
82
|
-
type: 'sort-ui',
|
|
83
|
-
fields: baseFields,
|
|
84
|
-
...overrides,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Tests
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
describe('SortUI', () => {
|
|
91
|
-
// -------------------------------------------------------------------------
|
|
92
|
-
// 1. Renders with default (buttons) variant
|
|
93
|
-
// -------------------------------------------------------------------------
|
|
94
|
-
describe('buttons variant', () => {
|
|
95
|
-
it('renders sort buttons for each field', () => {
|
|
96
|
-
render(<SortUI schema={makeSchema({ variant: 'buttons' })} />);
|
|
97
|
-
|
|
98
|
-
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
99
|
-
expect(screen.getByText('Date')).toBeInTheDocument();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('renders all fields as outline buttons when no sort is active', () => {
|
|
103
|
-
render(<SortUI schema={makeSchema({ variant: 'buttons' })} />);
|
|
104
|
-
|
|
105
|
-
const buttons = screen.getAllByRole('button');
|
|
106
|
-
expect(buttons).toHaveLength(2);
|
|
107
|
-
buttons.forEach(btn => {
|
|
108
|
-
expect(btn).toHaveAttribute('data-variant', 'outline');
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('highlights active sort field with secondary variant', () => {
|
|
113
|
-
render(
|
|
114
|
-
<SortUI
|
|
115
|
-
schema={makeSchema({
|
|
116
|
-
variant: 'buttons',
|
|
117
|
-
sort: [{ field: 'name', direction: 'asc' }],
|
|
118
|
-
})}
|
|
119
|
-
/>,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const nameBtn = screen.getByText('Name').closest('button')!;
|
|
123
|
-
expect(nameBtn).toHaveAttribute('data-variant', 'secondary');
|
|
124
|
-
|
|
125
|
-
const dateBtn = screen.getByText('Date').closest('button')!;
|
|
126
|
-
expect(dateBtn).toHaveAttribute('data-variant', 'outline');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('cycles through asc → desc → removed on repeated clicks', () => {
|
|
130
|
-
const onChange = vi.fn();
|
|
131
|
-
render(
|
|
132
|
-
<SortUI schema={makeSchema({ variant: 'buttons' })} onChange={onChange} />,
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const nameBtn = screen.getByText('Name').closest('button')!;
|
|
136
|
-
|
|
137
|
-
// First click: activate asc
|
|
138
|
-
fireEvent.click(nameBtn);
|
|
139
|
-
expect(onChange).toHaveBeenCalledWith([{ field: 'name', direction: 'asc' }]);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// -------------------------------------------------------------------------
|
|
144
|
-
// 2. Renders with dropdown variant
|
|
145
|
-
// -------------------------------------------------------------------------
|
|
146
|
-
describe('dropdown variant', () => {
|
|
147
|
-
it('renders select elements for field and direction', () => {
|
|
148
|
-
render(<SortUI schema={makeSchema({ variant: 'dropdown' })} />);
|
|
149
|
-
|
|
150
|
-
const selectRoots = screen.getAllByTestId('select-root');
|
|
151
|
-
expect(selectRoots.length).toBe(2);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('renders field options inside select', () => {
|
|
155
|
-
render(<SortUI schema={makeSchema({ variant: 'dropdown' })} />);
|
|
156
|
-
|
|
157
|
-
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
158
|
-
expect(screen.getByText('Date')).toBeInTheDocument();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('renders direction options (Ascending / Descending)', () => {
|
|
162
|
-
render(<SortUI schema={makeSchema({ variant: 'dropdown' })} />);
|
|
163
|
-
|
|
164
|
-
expect(screen.getByText('Ascending')).toBeInTheDocument();
|
|
165
|
-
expect(screen.getByText('Descending')).toBeInTheDocument();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('defaults to dropdown when variant is omitted', () => {
|
|
169
|
-
render(<SortUI schema={makeSchema()} />);
|
|
170
|
-
|
|
171
|
-
// dropdown renders select-root elements, not buttons
|
|
172
|
-
const selectRoots = screen.getAllByTestId('select-root');
|
|
173
|
-
expect(selectRoots.length).toBe(2);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// -------------------------------------------------------------------------
|
|
178
|
-
// 3. Renders with builder variant (multiple = true)
|
|
179
|
-
// -------------------------------------------------------------------------
|
|
180
|
-
describe('builder variant (multiple)', () => {
|
|
181
|
-
it('renders SortBuilder when multiple is true', () => {
|
|
182
|
-
render(
|
|
183
|
-
<SortUI
|
|
184
|
-
schema={makeSchema({ multiple: true })}
|
|
185
|
-
/>,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
expect(screen.getByTestId('sort-builder')).toBeInTheDocument();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('passes fields and value to SortBuilder', () => {
|
|
192
|
-
render(
|
|
193
|
-
<SortUI
|
|
194
|
-
schema={makeSchema({
|
|
195
|
-
multiple: true,
|
|
196
|
-
sort: [{ field: 'name', direction: 'asc' }],
|
|
197
|
-
})}
|
|
198
|
-
/>,
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const builder = screen.getByTestId('sort-builder');
|
|
202
|
-
const fields = JSON.parse(builder.getAttribute('data-fields')!);
|
|
203
|
-
expect(fields).toEqual([
|
|
204
|
-
{ value: 'name', label: 'Name' },
|
|
205
|
-
{ value: 'date', label: 'Date' },
|
|
206
|
-
]);
|
|
207
|
-
|
|
208
|
-
const value = JSON.parse(builder.getAttribute('data-value')!);
|
|
209
|
-
expect(value).toEqual([
|
|
210
|
-
{ id: 'name-asc', field: 'name', order: 'asc' },
|
|
211
|
-
]);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('calls onChange when SortBuilder triggers a change', () => {
|
|
215
|
-
const onChange = vi.fn();
|
|
216
|
-
render(
|
|
217
|
-
<SortUI
|
|
218
|
-
schema={makeSchema({ multiple: true })}
|
|
219
|
-
onChange={onChange}
|
|
220
|
-
/>,
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
fireEvent.click(screen.getByTestId('sort-builder-change'));
|
|
224
|
-
expect(onChange).toHaveBeenCalledWith([{ field: 'date', direction: 'desc' }]);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// -------------------------------------------------------------------------
|
|
229
|
-
// 4. Initial sort configuration from schema
|
|
230
|
-
// -------------------------------------------------------------------------
|
|
231
|
-
describe('initial sort from schema', () => {
|
|
232
|
-
it('initialises state from schema.sort in buttons variant', () => {
|
|
233
|
-
render(
|
|
234
|
-
<SortUI
|
|
235
|
-
schema={makeSchema({
|
|
236
|
-
variant: 'buttons',
|
|
237
|
-
sort: [{ field: 'date', direction: 'desc' }],
|
|
238
|
-
})}
|
|
239
|
-
/>,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const dateBtn = screen.getByText('Date').closest('button')!;
|
|
243
|
-
expect(dateBtn).toHaveAttribute('data-variant', 'secondary');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('renders without error when schema.sort is undefined', () => {
|
|
247
|
-
render(<SortUI schema={makeSchema({ variant: 'buttons' })} />);
|
|
248
|
-
|
|
249
|
-
const buttons = screen.getAllByRole('button');
|
|
250
|
-
expect(buttons).toHaveLength(2);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('renders without error when schema.sort is empty', () => {
|
|
254
|
-
render(
|
|
255
|
-
<SortUI schema={makeSchema({ variant: 'buttons', sort: [] })} />,
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
const buttons = screen.getAllByRole('button');
|
|
259
|
-
buttons.forEach(btn => {
|
|
260
|
-
expect(btn).toHaveAttribute('data-variant', 'outline');
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// -------------------------------------------------------------------------
|
|
266
|
-
// 5. onChange callback
|
|
267
|
-
// -------------------------------------------------------------------------
|
|
268
|
-
describe('onChange callback', () => {
|
|
269
|
-
it('fires onChange when a button sort is toggled', () => {
|
|
270
|
-
const onChange = vi.fn();
|
|
271
|
-
render(
|
|
272
|
-
<SortUI
|
|
273
|
-
schema={makeSchema({ variant: 'buttons' })}
|
|
274
|
-
onChange={onChange}
|
|
275
|
-
/>,
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
fireEvent.click(screen.getByText('Name').closest('button')!);
|
|
279
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
280
|
-
expect(onChange).toHaveBeenCalledWith([{ field: 'name', direction: 'asc' }]);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('dispatches custom window event when schema.onChange is set', () => {
|
|
284
|
-
const spy = vi.fn();
|
|
285
|
-
window.addEventListener('sort:changed', spy);
|
|
286
|
-
|
|
287
|
-
render(
|
|
288
|
-
<SortUI
|
|
289
|
-
schema={makeSchema({ variant: 'buttons', onChange: 'sort:changed' })}
|
|
290
|
-
/>,
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
fireEvent.click(screen.getByText('Name').closest('button')!);
|
|
294
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
295
|
-
|
|
296
|
-
const detail = (spy.mock.calls[0][0] as CustomEvent).detail;
|
|
297
|
-
expect(detail).toEqual({ sort: [{ field: 'name', direction: 'asc' }] });
|
|
298
|
-
|
|
299
|
-
window.removeEventListener('sort:changed', spy);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('replaces active sort when multiple is false (buttons)', () => {
|
|
303
|
-
const onChange = vi.fn();
|
|
304
|
-
render(
|
|
305
|
-
<SortUI
|
|
306
|
-
schema={makeSchema({
|
|
307
|
-
variant: 'buttons',
|
|
308
|
-
sort: [{ field: 'name', direction: 'asc' }],
|
|
309
|
-
})}
|
|
310
|
-
onChange={onChange}
|
|
311
|
-
/>,
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
// Click a different field — should replace, not append
|
|
315
|
-
fireEvent.click(screen.getByText('Date').closest('button')!);
|
|
316
|
-
expect(onChange).toHaveBeenCalledWith([{ field: 'date', direction: 'asc' }]);
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// -------------------------------------------------------------------------
|
|
321
|
-
// 6. Helper functions (toSortEntries / toSortItems) – tested indirectly
|
|
322
|
-
// -------------------------------------------------------------------------
|
|
323
|
-
describe('helper functions (toSortEntries / toSortItems)', () => {
|
|
324
|
-
it('toSortEntries: maps schema.sort to internal state shown via button variant', () => {
|
|
325
|
-
render(
|
|
326
|
-
<SortUI
|
|
327
|
-
schema={makeSchema({
|
|
328
|
-
variant: 'buttons',
|
|
329
|
-
sort: [
|
|
330
|
-
{ field: 'name', direction: 'asc' },
|
|
331
|
-
{ field: 'date', direction: 'desc' },
|
|
332
|
-
],
|
|
333
|
-
multiple: true,
|
|
334
|
-
})}
|
|
335
|
-
/>,
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
// Both fields should be highlighted since both are in the sort config
|
|
339
|
-
const nameBtn = screen.getByText('Name').closest('button')!;
|
|
340
|
-
const dateBtn = screen.getByText('Date').closest('button')!;
|
|
341
|
-
expect(nameBtn).toHaveAttribute('data-variant', 'secondary');
|
|
342
|
-
expect(dateBtn).toHaveAttribute('data-variant', 'secondary');
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('toSortItems: maps sort entries to SortBuilder items', () => {
|
|
346
|
-
render(
|
|
347
|
-
<SortUI
|
|
348
|
-
schema={makeSchema({
|
|
349
|
-
multiple: true,
|
|
350
|
-
sort: [
|
|
351
|
-
{ field: 'name', direction: 'asc' },
|
|
352
|
-
{ field: 'date', direction: 'desc' },
|
|
353
|
-
],
|
|
354
|
-
})}
|
|
355
|
-
/>,
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
const builder = screen.getByTestId('sort-builder');
|
|
359
|
-
const value = JSON.parse(builder.getAttribute('data-value')!);
|
|
360
|
-
expect(value).toEqual([
|
|
361
|
-
{ id: 'name-asc', field: 'name', order: 'asc' },
|
|
362
|
-
{ id: 'date-desc', field: 'date', order: 'desc' },
|
|
363
|
-
]);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('toSortEntries: returns empty array when sort is undefined', () => {
|
|
367
|
-
render(
|
|
368
|
-
<SortUI
|
|
369
|
-
schema={makeSchema({ variant: 'buttons' })}
|
|
370
|
-
/>,
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
// No button should have secondary variant
|
|
374
|
-
const buttons = screen.getAllByRole('button');
|
|
375
|
-
buttons.forEach(btn => {
|
|
376
|
-
expect(btn).toHaveAttribute('data-variant', 'outline');
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
});
|