@object-ui/plugin-view 0.3.1 → 2.0.0
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/.turbo/turbo-build.log +199 -0
- package/CHANGELOG.md +16 -0
- package/README.md +58 -0
- package/dist/index.js +1178 -340
- package/dist/index.umd.cjs +2 -2
- package/dist/plugin-view/src/FilterUI.d.ts +16 -0
- package/dist/plugin-view/src/ObjectView.d.ts +85 -5
- package/dist/plugin-view/src/SortUI.d.ts +16 -0
- package/dist/plugin-view/src/ViewSwitcher.d.ts +16 -0
- package/dist/plugin-view/src/__tests__/FilterUI.test.d.ts +8 -0
- package/dist/plugin-view/src/__tests__/ObjectView.test.d.ts +8 -0
- package/dist/plugin-view/src/__tests__/SortUI.test.d.ts +8 -0
- package/dist/plugin-view/src/__tests__/registration.test.d.ts +8 -0
- package/dist/plugin-view/src/index.d.ts +7 -1
- package/package.json +9 -8
- package/src/FilterUI.tsx +317 -0
- package/src/ObjectView.tsx +668 -148
- package/src/SortUI.tsx +210 -0
- package/src/ViewSwitcher.tsx +311 -0
- package/src/__tests__/FilterUI.test.tsx +544 -0
- package/src/__tests__/ObjectView.test.tsx +375 -0
- package/src/__tests__/SortUI.test.tsx +380 -0
- package/src/__tests__/registration.test.tsx +32 -0
- package/src/index.tsx +172 -4
- package/vite.config.ts +3 -0
- package/vitest.config.ts +12 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,544 @@
|
|
|
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 { FilterUI } from '../FilterUI';
|
|
12
|
+
import type { FilterUISchema } 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', async () => {
|
|
19
|
+
const React = await import('react');
|
|
20
|
+
const cn = (...args: any[]) => args.filter(Boolean).join(' ');
|
|
21
|
+
|
|
22
|
+
const Button = ({ children, onClick, variant, size, type, ...rest }: any) => (
|
|
23
|
+
<button onClick={onClick} data-variant={variant} data-size={size} type={type} {...rest}>
|
|
24
|
+
{children}
|
|
25
|
+
</button>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const Input = ({ value, onChange, placeholder, type, ...rest }: any) => (
|
|
29
|
+
<input
|
|
30
|
+
value={value}
|
|
31
|
+
onChange={onChange}
|
|
32
|
+
placeholder={placeholder}
|
|
33
|
+
type={type}
|
|
34
|
+
data-testid={`input-${type || 'text'}`}
|
|
35
|
+
{...rest}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const Label = ({ children, className }: any) => (
|
|
40
|
+
<label className={className}>{children}</label>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const Checkbox = ({ checked, onCheckedChange }: any) => (
|
|
44
|
+
<input
|
|
45
|
+
type="checkbox"
|
|
46
|
+
data-testid="checkbox"
|
|
47
|
+
checked={checked}
|
|
48
|
+
onChange={(e: any) => onCheckedChange?.(e.target.checked)}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Share onValueChange from Select to SelectItem via React Context
|
|
53
|
+
const SelectCtx = React.createContext<((v: string) => void) | undefined>(undefined);
|
|
54
|
+
|
|
55
|
+
const Select = ({ children, value, onValueChange }: any) => {
|
|
56
|
+
return (
|
|
57
|
+
<SelectCtx.Provider value={onValueChange}>
|
|
58
|
+
<div data-testid="select-root" data-value={value}>
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
</SelectCtx.Provider>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const SelectTrigger = ({ children }: any) => (
|
|
66
|
+
<button data-testid="select-trigger">{children}</button>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const SelectValue = ({ placeholder }: any) => (
|
|
70
|
+
<span data-testid="select-value">{placeholder}</span>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const SelectContent = ({ children }: any) => (
|
|
74
|
+
<div data-testid="select-content">{children}</div>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const SelectItem = ({ children, value }: any) => {
|
|
78
|
+
const onValueChange = React.useContext(SelectCtx);
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
data-testid="select-item"
|
|
82
|
+
data-value={value}
|
|
83
|
+
role="option"
|
|
84
|
+
onClick={() => onValueChange?.(String(value))}
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const Popover = ({ children, open }: any) => (
|
|
92
|
+
<div data-testid="popover" data-open={open}>
|
|
93
|
+
{children}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const PopoverTrigger = ({ children }: any) => (
|
|
98
|
+
<div data-testid="popover-trigger">{children}</div>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const PopoverContent = ({ children }: any) => (
|
|
102
|
+
<div data-testid="popover-content">{children}</div>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const Drawer = ({ children, open }: any) => (
|
|
106
|
+
<div data-testid="drawer" data-open={open}>
|
|
107
|
+
{children}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const DrawerContent = ({ children }: any) => (
|
|
112
|
+
<div data-testid="drawer-content">{children}</div>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const DrawerHeader = ({ children }: any) => (
|
|
116
|
+
<div data-testid="drawer-header">{children}</div>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const DrawerTitle = ({ children }: any) => (
|
|
120
|
+
<h2 data-testid="drawer-title">{children}</h2>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const DrawerDescription = ({ children }: any) => (
|
|
124
|
+
<p data-testid="drawer-description">{children}</p>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
cn,
|
|
129
|
+
Button,
|
|
130
|
+
Input,
|
|
131
|
+
Label,
|
|
132
|
+
Checkbox,
|
|
133
|
+
Select,
|
|
134
|
+
SelectTrigger,
|
|
135
|
+
SelectValue,
|
|
136
|
+
SelectContent,
|
|
137
|
+
SelectItem,
|
|
138
|
+
Popover,
|
|
139
|
+
PopoverTrigger,
|
|
140
|
+
PopoverContent,
|
|
141
|
+
Drawer,
|
|
142
|
+
DrawerContent,
|
|
143
|
+
DrawerHeader,
|
|
144
|
+
DrawerTitle,
|
|
145
|
+
DrawerDescription,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Helpers
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
const baseFilters: FilterUISchema['filters'] = [
|
|
153
|
+
{
|
|
154
|
+
field: 'status',
|
|
155
|
+
label: 'Status',
|
|
156
|
+
type: 'select',
|
|
157
|
+
options: [
|
|
158
|
+
{ label: 'Active', value: 'active' },
|
|
159
|
+
{ label: 'Inactive', value: 'inactive' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{ field: 'name', label: 'Name', type: 'text' },
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const makeSchema = (overrides: Partial<FilterUISchema> = {}): FilterUISchema => ({
|
|
166
|
+
type: 'filter-ui',
|
|
167
|
+
filters: baseFilters,
|
|
168
|
+
...overrides,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Tests
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
describe('FilterUI', () => {
|
|
175
|
+
// -------------------------------------------------------------------------
|
|
176
|
+
// 1. Renders with inline layout
|
|
177
|
+
// -------------------------------------------------------------------------
|
|
178
|
+
describe('inline layout', () => {
|
|
179
|
+
it('renders the filter form inline by default', () => {
|
|
180
|
+
const { container } = render(<FilterUI schema={makeSchema()} />);
|
|
181
|
+
|
|
182
|
+
// Labels should be visible
|
|
183
|
+
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
184
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
185
|
+
|
|
186
|
+
// No popover or drawer elements
|
|
187
|
+
expect(screen.queryByTestId('popover')).not.toBeInTheDocument();
|
|
188
|
+
expect(screen.queryByTestId('drawer')).not.toBeInTheDocument();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('renders with explicit inline layout', () => {
|
|
192
|
+
render(<FilterUI schema={makeSchema({ layout: 'inline' })} />);
|
|
193
|
+
|
|
194
|
+
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
195
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
// 2. Renders filter fields
|
|
201
|
+
// -------------------------------------------------------------------------
|
|
202
|
+
describe('filter fields', () => {
|
|
203
|
+
it('renders a label for each filter', () => {
|
|
204
|
+
render(<FilterUI schema={makeSchema()} />);
|
|
205
|
+
|
|
206
|
+
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
207
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('falls back to field name when label is omitted', () => {
|
|
211
|
+
render(
|
|
212
|
+
<FilterUI
|
|
213
|
+
schema={makeSchema({
|
|
214
|
+
filters: [{ field: 'email', type: 'text' }],
|
|
215
|
+
})}
|
|
216
|
+
/>,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(screen.getByText('email')).toBeInTheDocument();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('renders select options for select-type filters', () => {
|
|
223
|
+
render(<FilterUI schema={makeSchema()} />);
|
|
224
|
+
|
|
225
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
226
|
+
expect(screen.getByText('Inactive')).toBeInTheDocument();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('renders a text input for text-type filters', () => {
|
|
230
|
+
render(
|
|
231
|
+
<FilterUI
|
|
232
|
+
schema={makeSchema({
|
|
233
|
+
filters: [{ field: 'search', label: 'Search', type: 'text' }],
|
|
234
|
+
})}
|
|
235
|
+
/>,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const input = screen.getByPlaceholderText('Filter by Search');
|
|
239
|
+
expect(input).toBeInTheDocument();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// -------------------------------------------------------------------------
|
|
244
|
+
// 3. Handles text filter values
|
|
245
|
+
// -------------------------------------------------------------------------
|
|
246
|
+
describe('text filter values', () => {
|
|
247
|
+
it('calls onChange when a text input is changed', () => {
|
|
248
|
+
const onChange = vi.fn();
|
|
249
|
+
render(
|
|
250
|
+
<FilterUI
|
|
251
|
+
schema={makeSchema({
|
|
252
|
+
filters: [{ field: 'name', label: 'Name', type: 'text' }],
|
|
253
|
+
})}
|
|
254
|
+
onChange={onChange}
|
|
255
|
+
/>,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const input = screen.getByPlaceholderText('Filter by Name');
|
|
259
|
+
fireEvent.change(input, { target: { value: 'Alice' } });
|
|
260
|
+
|
|
261
|
+
expect(onChange).toHaveBeenCalledWith({ name: 'Alice' });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('renders with pre-set values from schema', () => {
|
|
265
|
+
render(
|
|
266
|
+
<FilterUI
|
|
267
|
+
schema={makeSchema({
|
|
268
|
+
filters: [{ field: 'name', label: 'Name', type: 'text' }],
|
|
269
|
+
values: { name: 'Bob' },
|
|
270
|
+
})}
|
|
271
|
+
/>,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const input = screen.getByPlaceholderText('Filter by Name') as HTMLInputElement;
|
|
275
|
+
expect(input.value).toBe('Bob');
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// -------------------------------------------------------------------------
|
|
280
|
+
// 4. Handles select filter values
|
|
281
|
+
// -------------------------------------------------------------------------
|
|
282
|
+
describe('select filter values', () => {
|
|
283
|
+
it('calls onChange when a select value is changed', () => {
|
|
284
|
+
const onChange = vi.fn();
|
|
285
|
+
render(
|
|
286
|
+
<FilterUI
|
|
287
|
+
schema={makeSchema({
|
|
288
|
+
filters: [
|
|
289
|
+
{
|
|
290
|
+
field: 'status',
|
|
291
|
+
label: 'Status',
|
|
292
|
+
type: 'select',
|
|
293
|
+
options: [
|
|
294
|
+
{ label: 'Active', value: 'active' },
|
|
295
|
+
{ label: 'Inactive', value: 'inactive' },
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
})}
|
|
300
|
+
onChange={onChange}
|
|
301
|
+
/>,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const activeOption = screen.getByText('Active');
|
|
305
|
+
fireEvent.click(activeOption);
|
|
306
|
+
|
|
307
|
+
expect(onChange).toHaveBeenCalledWith({ status: 'active' });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('renders with pre-set select value from schema', () => {
|
|
311
|
+
render(
|
|
312
|
+
<FilterUI
|
|
313
|
+
schema={makeSchema({
|
|
314
|
+
values: { status: 'inactive' },
|
|
315
|
+
})}
|
|
316
|
+
/>,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const selectRoot = screen.getAllByTestId('select-root')[0];
|
|
320
|
+
expect(selectRoot).toHaveAttribute('data-value', 'inactive');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// -------------------------------------------------------------------------
|
|
325
|
+
// 5. Renders with popover layout
|
|
326
|
+
// -------------------------------------------------------------------------
|
|
327
|
+
describe('popover layout', () => {
|
|
328
|
+
it('renders a Filters button with popover wrapper', () => {
|
|
329
|
+
render(<FilterUI schema={makeSchema({ layout: 'popover' })} />);
|
|
330
|
+
|
|
331
|
+
expect(screen.getByTestId('popover')).toBeInTheDocument();
|
|
332
|
+
expect(screen.getByText('Filters')).toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('renders the filter form inside popover content', () => {
|
|
336
|
+
render(<FilterUI schema={makeSchema({ layout: 'popover' })} />);
|
|
337
|
+
|
|
338
|
+
const popoverContent = screen.getByTestId('popover-content');
|
|
339
|
+
expect(popoverContent).toBeInTheDocument();
|
|
340
|
+
|
|
341
|
+
// Filter labels should still be rendered within popover
|
|
342
|
+
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
343
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('shows active filter count badge when filters have values', () => {
|
|
347
|
+
render(
|
|
348
|
+
<FilterUI
|
|
349
|
+
schema={makeSchema({
|
|
350
|
+
layout: 'popover',
|
|
351
|
+
values: { status: 'active' },
|
|
352
|
+
})}
|
|
353
|
+
/>,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// The active count should be rendered
|
|
357
|
+
expect(screen.getByText('1')).toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// -------------------------------------------------------------------------
|
|
362
|
+
// 6. Renders with empty / no filters
|
|
363
|
+
// -------------------------------------------------------------------------
|
|
364
|
+
describe('empty / no filters', () => {
|
|
365
|
+
it('renders without error when filters array is empty', () => {
|
|
366
|
+
render(<FilterUI schema={makeSchema({ filters: [] })} />);
|
|
367
|
+
|
|
368
|
+
// Should not throw; no labels rendered
|
|
369
|
+
expect(screen.queryByText('Status')).not.toBeInTheDocument();
|
|
370
|
+
expect(screen.queryByText('Name')).not.toBeInTheDocument();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('renders without error when values are empty', () => {
|
|
374
|
+
render(<FilterUI schema={makeSchema({ values: {} })} />);
|
|
375
|
+
|
|
376
|
+
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
377
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('renders without error when values are undefined', () => {
|
|
381
|
+
render(<FilterUI schema={makeSchema()} />);
|
|
382
|
+
|
|
383
|
+
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// -------------------------------------------------------------------------
|
|
388
|
+
// 7. isEmptyValue edge cases (tested indirectly via active count badge)
|
|
389
|
+
// -------------------------------------------------------------------------
|
|
390
|
+
describe('isEmptyValue edge cases', () => {
|
|
391
|
+
it('treats null as empty — no active count badge', () => {
|
|
392
|
+
render(
|
|
393
|
+
<FilterUI
|
|
394
|
+
schema={makeSchema({
|
|
395
|
+
layout: 'popover',
|
|
396
|
+
values: { status: null },
|
|
397
|
+
})}
|
|
398
|
+
/>,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// No badge with count should appear
|
|
402
|
+
expect(screen.queryByText('1')).not.toBeInTheDocument();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('treats undefined as empty — no active count badge', () => {
|
|
406
|
+
render(
|
|
407
|
+
<FilterUI
|
|
408
|
+
schema={makeSchema({
|
|
409
|
+
layout: 'popover',
|
|
410
|
+
values: { status: undefined },
|
|
411
|
+
})}
|
|
412
|
+
/>,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
expect(screen.queryByText('1')).not.toBeInTheDocument();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('treats empty string as empty — no active count badge', () => {
|
|
419
|
+
render(
|
|
420
|
+
<FilterUI
|
|
421
|
+
schema={makeSchema({
|
|
422
|
+
layout: 'popover',
|
|
423
|
+
values: { status: '' },
|
|
424
|
+
})}
|
|
425
|
+
/>,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
expect(screen.queryByText('1')).not.toBeInTheDocument();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('treats empty array as empty — no active count badge', () => {
|
|
432
|
+
render(
|
|
433
|
+
<FilterUI
|
|
434
|
+
schema={makeSchema({
|
|
435
|
+
layout: 'popover',
|
|
436
|
+
values: { tags: [] },
|
|
437
|
+
})}
|
|
438
|
+
/>,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
expect(screen.queryByText('1')).not.toBeInTheDocument();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('counts non-empty values for the active count badge', () => {
|
|
445
|
+
render(
|
|
446
|
+
<FilterUI
|
|
447
|
+
schema={makeSchema({
|
|
448
|
+
layout: 'popover',
|
|
449
|
+
values: { status: 'active', name: 'Alice' },
|
|
450
|
+
})}
|
|
451
|
+
/>,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// -------------------------------------------------------------------------
|
|
459
|
+
// 8. onChange callback
|
|
460
|
+
// -------------------------------------------------------------------------
|
|
461
|
+
describe('onChange callback', () => {
|
|
462
|
+
it('fires onChange immediately when showApply is not set', () => {
|
|
463
|
+
const onChange = vi.fn();
|
|
464
|
+
render(
|
|
465
|
+
<FilterUI
|
|
466
|
+
schema={makeSchema({
|
|
467
|
+
filters: [{ field: 'name', label: 'Name', type: 'text' }],
|
|
468
|
+
})}
|
|
469
|
+
onChange={onChange}
|
|
470
|
+
/>,
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const input = screen.getByPlaceholderText('Filter by Name');
|
|
474
|
+
fireEvent.change(input, { target: { value: 'Test' } });
|
|
475
|
+
|
|
476
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
477
|
+
expect(onChange).toHaveBeenCalledWith({ name: 'Test' });
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('defers onChange until Apply is clicked when showApply is true', () => {
|
|
481
|
+
const onChange = vi.fn();
|
|
482
|
+
render(
|
|
483
|
+
<FilterUI
|
|
484
|
+
schema={makeSchema({
|
|
485
|
+
filters: [{ field: 'name', label: 'Name', type: 'text' }],
|
|
486
|
+
showApply: true,
|
|
487
|
+
})}
|
|
488
|
+
onChange={onChange}
|
|
489
|
+
/>,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const input = screen.getByPlaceholderText('Filter by Name');
|
|
493
|
+
fireEvent.change(input, { target: { value: 'Deferred' } });
|
|
494
|
+
|
|
495
|
+
// onChange should NOT have been called yet
|
|
496
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
497
|
+
|
|
498
|
+
// Click Apply
|
|
499
|
+
fireEvent.click(screen.getByText('Apply'));
|
|
500
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
501
|
+
expect(onChange).toHaveBeenCalledWith({ name: 'Deferred' });
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('clears all values when Clear is clicked', () => {
|
|
505
|
+
const onChange = vi.fn();
|
|
506
|
+
render(
|
|
507
|
+
<FilterUI
|
|
508
|
+
schema={makeSchema({
|
|
509
|
+
filters: [{ field: 'name', label: 'Name', type: 'text' }],
|
|
510
|
+
values: { name: 'Existing' },
|
|
511
|
+
showClear: true,
|
|
512
|
+
})}
|
|
513
|
+
onChange={onChange}
|
|
514
|
+
/>,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
fireEvent.click(screen.getByText('Clear'));
|
|
518
|
+
expect(onChange).toHaveBeenCalledWith({});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('dispatches custom window event when schema.onChange is set', () => {
|
|
522
|
+
const spy = vi.fn();
|
|
523
|
+
window.addEventListener('filter:changed', spy);
|
|
524
|
+
|
|
525
|
+
render(
|
|
526
|
+
<FilterUI
|
|
527
|
+
schema={makeSchema({
|
|
528
|
+
filters: [{ field: 'name', label: 'Name', type: 'text' }],
|
|
529
|
+
onChange: 'filter:changed',
|
|
530
|
+
})}
|
|
531
|
+
/>,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const input = screen.getByPlaceholderText('Filter by Name');
|
|
535
|
+
fireEvent.change(input, { target: { value: 'Event' } });
|
|
536
|
+
|
|
537
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
538
|
+
const detail = (spy.mock.calls[0][0] as CustomEvent).detail;
|
|
539
|
+
expect(detail).toEqual({ values: { name: 'Event' } });
|
|
540
|
+
|
|
541
|
+
window.removeEventListener('filter:changed', spy);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
});
|