@object-ui/plugin-view 0.5.0 → 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.
@@ -0,0 +1,380 @@
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
+ });
@@ -0,0 +1,32 @@
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 } from 'vitest';
10
+ import { ObjectView, ViewSwitcher, FilterUI, SortUI } from '../index';
11
+
12
+ describe('Plugin View Registration', () => {
13
+ it('exports ObjectView component', () => {
14
+ expect(ObjectView).toBeDefined();
15
+ expect(typeof ObjectView).toBe('function');
16
+ });
17
+
18
+ it('exports ViewSwitcher component', () => {
19
+ expect(ViewSwitcher).toBeDefined();
20
+ expect(typeof ViewSwitcher).toBe('function');
21
+ });
22
+
23
+ it('exports FilterUI component', () => {
24
+ expect(FilterUI).toBeDefined();
25
+ expect(typeof FilterUI).toBe('function');
26
+ });
27
+
28
+ it('exports SortUI component', () => {
29
+ expect(SortUI).toBeDefined();
30
+ expect(typeof SortUI).toBe('function');
31
+ });
32
+ });
package/src/index.tsx CHANGED
@@ -6,24 +6,166 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
 
9
- import React from 'react';
9
+ import React, { useContext } from 'react';
10
10
  import { ComponentRegistry } from '@object-ui/core';
11
11
  import { ObjectView } from './ObjectView';
12
+ import { ViewSwitcher } from './ViewSwitcher';
13
+ import { FilterUI } from './FilterUI';
14
+ import { SortUI } from './SortUI';
12
15
 
13
- export { ObjectView };
16
+ export { ObjectView, ViewSwitcher, FilterUI, SortUI };
14
17
  export type { ObjectViewProps } from './ObjectView';
18
+ export type { ViewSwitcherProps } from './ViewSwitcher';
19
+ export type { FilterUIProps } from './FilterUI';
20
+ export type { SortUIProps } from './SortUI';
21
+
22
+ /**
23
+ * SchemaRendererContext is created by @object-ui/react.
24
+ * We import it dynamically to avoid a circular dependency.
25
+ * The context value provides { dataSource }.
26
+ * A fallback context is created so hooks are never called conditionally.
27
+ */
28
+ const FallbackContext = React.createContext<any>(null);
29
+ let SchemaRendererContext: React.Context<any> = FallbackContext;
30
+ try {
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const mod = require('@object-ui/react');
33
+ // The context is re-exported from @object-ui/react
34
+ if (mod.SchemaRendererContext) {
35
+ SchemaRendererContext = mod.SchemaRendererContext;
36
+ }
37
+ } catch {
38
+ // @object-ui/react not available — registry-based dataSource only
39
+ }
15
40
 
16
41
  // Register object-view component
17
42
  const ObjectViewRenderer: React.FC<{ schema: any }> = ({ schema }) => {
18
- return <ObjectView schema={schema} dataSource={null as any} />;
43
+ // Resolve dataSource from SchemaRendererProvider context
44
+ const ctx = useContext(SchemaRendererContext);
45
+ const dataSource = ctx?.dataSource ?? null;
46
+
47
+ return <ObjectView schema={schema} dataSource={dataSource} />;
19
48
  };
20
49
 
21
50
  ComponentRegistry.register('object-view', ObjectViewRenderer, {
22
- namespace: 'plugin-view'
51
+ namespace: 'plugin-view',
52
+ label: 'Object View',
53
+ category: 'view',
54
+ icon: 'LayoutDashboard',
55
+ inputs: [
56
+ { name: 'objectName', type: 'string', label: 'Object Name', required: true },
57
+ { name: 'title', type: 'string', label: 'Title' },
58
+ { name: 'description', type: 'string', label: 'Description' },
59
+ { name: 'layout', type: 'enum', label: 'Form Layout', enum: ['drawer', 'modal', 'page'] },
60
+ { name: 'defaultViewType', type: 'enum', label: 'Default View Type', enum: ['grid', 'kanban', 'gallery', 'calendar', 'timeline', 'gantt', 'map'] },
61
+ { name: 'defaultListView', type: 'string', label: 'Default Named View' },
62
+ { name: 'showSearch', type: 'boolean', label: 'Show Search' },
63
+ { name: 'showFilters', type: 'boolean', label: 'Show Filters' },
64
+ { name: 'showCreate', type: 'boolean', label: 'Show Create Button' },
65
+ { name: 'showRefresh', type: 'boolean', label: 'Show Refresh Button' },
66
+ { name: 'showViewSwitcher', type: 'boolean', label: 'Show View Switcher' },
67
+ { name: 'listViews', type: 'object', label: 'Named List Views' },
68
+ { name: 'navigation', type: 'object', label: 'Navigation Config' },
69
+ { name: 'searchableFields', type: 'array', label: 'Searchable Fields' },
70
+ { name: 'filterableFields', type: 'array', label: 'Filterable Fields' },
71
+ ],
72
+ defaultProps: {
73
+ layout: 'drawer',
74
+ defaultViewType: 'grid',
75
+ showSearch: true,
76
+ showFilters: true,
77
+ showCreate: true,
78
+ showRefresh: true,
79
+ showViewSwitcher: true,
80
+ },
81
+ });
82
+
83
+ // Register alias 'view' → same renderer
84
+ ComponentRegistry.register('view', ObjectViewRenderer, {
85
+ namespace: 'plugin-view',
86
+ label: 'View',
87
+ category: 'view',
88
+ });
89
+
90
+ ComponentRegistry.register('view-switcher', ViewSwitcher, {
91
+ namespace: 'view',
92
+ label: 'View Switcher',
93
+ category: 'view',
94
+ icon: 'LayoutGrid',
95
+ inputs: [
96
+ { name: 'views', type: 'array', label: 'Views', required: true },
97
+ { name: 'defaultView', type: 'string', label: 'Default View' },
98
+ { name: 'activeView', type: 'string', label: 'Active View' },
99
+ { name: 'variant', type: 'enum', label: 'Variant', enum: ['tabs', 'buttons', 'dropdown'] },
100
+ { name: 'position', type: 'enum', label: 'Position', enum: ['top', 'bottom', 'left', 'right'] },
101
+ { name: 'persistPreference', type: 'boolean', label: 'Persist Preference' },
102
+ { name: 'storageKey', type: 'string', label: 'Storage Key' },
103
+ { name: 'onViewChange', type: 'string', label: 'On View Change Event' },
104
+ ],
105
+ defaultProps: {
106
+ variant: 'tabs',
107
+ position: 'top',
108
+ defaultView: 'grid',
109
+ views: [
110
+ { type: 'grid', label: 'Grid', schema: { type: 'text', content: 'Grid view' } },
111
+ { type: 'list', label: 'List', schema: { type: 'text', content: 'List view' } },
112
+ ],
113
+ },
114
+ });
115
+
116
+ ComponentRegistry.register('filter-ui', FilterUI, {
117
+ namespace: 'view',
118
+ label: 'Filter UI',
119
+ category: 'view',
120
+ icon: 'SlidersHorizontal',
121
+ inputs: [
122
+ { name: 'filters', type: 'array', label: 'Filters', required: true },
123
+ { name: 'values', type: 'object', label: 'Values' },
124
+ { name: 'onChange', type: 'string', label: 'On Change Event' },
125
+ { name: 'showClear', type: 'boolean', label: 'Show Clear Button' },
126
+ { name: 'showApply', type: 'boolean', label: 'Show Apply Button' },
127
+ { name: 'layout', type: 'enum', label: 'Layout', enum: ['inline', 'popover', 'drawer'] },
128
+ ],
129
+ defaultProps: {
130
+ layout: 'inline',
131
+ showApply: false,
132
+ showClear: true,
133
+ filters: [
134
+ { field: 'name', label: 'Name', type: 'text', placeholder: 'Search name' },
135
+ { field: 'status', label: 'Status', type: 'select', options: [
136
+ { label: 'Open', value: 'open' },
137
+ { label: 'Closed', value: 'closed' },
138
+ ] },
139
+ { field: 'created_at', label: 'Created', type: 'date' },
140
+ ],
141
+ },
142
+ });
143
+
144
+ ComponentRegistry.register('sort-ui', SortUI, {
145
+ namespace: 'view',
146
+ label: 'Sort UI',
147
+ category: 'view',
148
+ icon: 'ArrowUpDown',
149
+ inputs: [
150
+ { name: 'fields', type: 'array', label: 'Fields', required: true },
151
+ { name: 'sort', type: 'array', label: 'Sort' },
152
+ { name: 'onChange', type: 'string', label: 'On Change Event' },
153
+ { name: 'multiple', type: 'boolean', label: 'Allow Multiple' },
154
+ { name: 'variant', type: 'enum', label: 'Variant', enum: ['dropdown', 'buttons'] },
155
+ ],
156
+ defaultProps: {
157
+ variant: 'dropdown',
158
+ multiple: true,
159
+ fields: [
160
+ { field: 'name', label: 'Name' },
161
+ { field: 'created_at', label: 'Created At' },
162
+ ],
163
+ sort: [{ field: 'name', direction: 'asc' }],
164
+ },
23
165
  });
24
166
 
25
167
  // Simple View Renderer (Container)
26
- const SimpleViewRenderer: React.FC<any> = ({ schema, className, children, ...props }) => {
168
+ const SimpleViewRenderer: React.FC<any> = ({ schema, className, children, dataSource, ...props }) => {
27
169
  // If columns prop is present, use grid layout
28
170
  const style = schema.props?.columns
29
171
  ? { display: 'grid', gridTemplateColumns: `repeat(${schema.props.columns}, 1fr)`, gap: '1rem' }
@@ -0,0 +1,12 @@
1
+ /// <reference types="vitest" />
2
+ import { defineConfig } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ test: {
8
+ environment: 'happy-dom',
9
+ globals: true,
10
+ setupFiles: ['./vitest.setup.ts'],
11
+ },
12
+ });
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';