@object-ui/plugin-view 0.5.0 → 3.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,375 @@
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 { ObjectView } from '../ObjectView';
12
+ import type { ObjectViewSchema, DataSource } from '@object-ui/types';
13
+
14
+ // Mock @object-ui/react to avoid circular dependency issues
15
+ vi.mock('@object-ui/react', () => ({
16
+ SchemaRenderer: ({ schema }: any) => (
17
+ <div data-testid="schema-renderer" data-schema-type={schema?.type}>
18
+ {schema?.type}
19
+ </div>
20
+ ),
21
+ SchemaRendererContext: null,
22
+ }));
23
+
24
+ // Mock @object-ui/plugin-grid
25
+ vi.mock('@object-ui/plugin-grid', () => ({
26
+ ObjectGrid: ({ schema, onRowClick }: any) => (
27
+ <div data-testid="object-grid" data-object={schema?.objectName}>
28
+ <button data-testid="grid-row" onClick={() => onRowClick?.({ _id: '1', name: 'Test' })}>
29
+ Row 1
30
+ </button>
31
+ </div>
32
+ ),
33
+ }));
34
+
35
+ // Mock @object-ui/plugin-form
36
+ vi.mock('@object-ui/plugin-form', () => ({
37
+ ObjectForm: ({ schema }: any) => (
38
+ <div data-testid="object-form" data-mode={schema?.mode}>
39
+ Form ({schema?.mode})
40
+ </div>
41
+ ),
42
+ }));
43
+
44
+ const createMockDataSource = (overrides: Partial<DataSource> = {}): DataSource => ({
45
+ find: vi.fn().mockResolvedValue([]),
46
+ findOne: vi.fn().mockResolvedValue(null),
47
+ create: vi.fn().mockResolvedValue({}),
48
+ update: vi.fn().mockResolvedValue({}),
49
+ delete: vi.fn().mockResolvedValue({}),
50
+ getObjectSchema: vi.fn().mockResolvedValue({
51
+ label: 'Contacts',
52
+ fields: {
53
+ name: { label: 'Name', type: 'text' },
54
+ email: { label: 'Email', type: 'text' },
55
+ status: {
56
+ label: 'Status',
57
+ type: 'select',
58
+ options: [
59
+ { label: 'Active', value: 'active' },
60
+ { label: 'Inactive', value: 'inactive' },
61
+ ],
62
+ },
63
+ created_at: { label: 'Created', type: 'date' },
64
+ },
65
+ }),
66
+ ...overrides,
67
+ } as DataSource);
68
+
69
+ describe('ObjectView', () => {
70
+ let mockDataSource: DataSource;
71
+
72
+ beforeEach(() => {
73
+ vi.clearAllMocks();
74
+ mockDataSource = createMockDataSource();
75
+ });
76
+
77
+ // ============================
78
+ // Basic Rendering
79
+ // ============================
80
+ describe('Basic Rendering', () => {
81
+ it('should render with minimal schema', () => {
82
+ const schema: ObjectViewSchema = {
83
+ type: 'object-view',
84
+ objectName: 'contacts',
85
+ };
86
+
87
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
88
+
89
+ // Should render the grid by default
90
+ expect(screen.getByTestId('object-grid')).toBeDefined();
91
+ });
92
+
93
+ it('should render title and description', () => {
94
+ const schema: ObjectViewSchema = {
95
+ type: 'object-view',
96
+ objectName: 'contacts',
97
+ title: 'Contact List',
98
+ description: 'Manage your contacts',
99
+ };
100
+
101
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
102
+
103
+ expect(screen.getByText('Contact List')).toBeDefined();
104
+ expect(screen.getByText('Manage your contacts')).toBeDefined();
105
+ });
106
+
107
+ it('should not render search box (delegated to ListView toolbar)', () => {
108
+ const schema: ObjectViewSchema = {
109
+ type: 'object-view',
110
+ objectName: 'contacts',
111
+ };
112
+
113
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
114
+
115
+ expect(screen.queryByPlaceholderText(/search/i)).toBeNull();
116
+ });
117
+
118
+ it('should not render search box when showSearch is false', () => {
119
+ const schema: ObjectViewSchema = {
120
+ type: 'object-view',
121
+ objectName: 'contacts',
122
+ showSearch: false,
123
+ };
124
+
125
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
126
+
127
+ expect(screen.queryByPlaceholderText(/search/i)).toBeNull();
128
+ });
129
+
130
+ it('should render create button by default', () => {
131
+ const schema: ObjectViewSchema = {
132
+ type: 'object-view',
133
+ objectName: 'contacts',
134
+ };
135
+
136
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
137
+
138
+ expect(screen.getByText('Create')).toBeDefined();
139
+ });
140
+
141
+ it('should hide create button when showCreate is false', () => {
142
+ const schema: ObjectViewSchema = {
143
+ type: 'object-view',
144
+ objectName: 'contacts',
145
+ showCreate: false,
146
+ };
147
+
148
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
149
+
150
+ expect(screen.queryByText('Create')).toBeNull();
151
+ });
152
+ });
153
+
154
+ // ============================
155
+ // Named List Views
156
+ // ============================
157
+ describe('Named List Views', () => {
158
+ it('should render named view tabs when listViews has multiple entries', () => {
159
+ const schema: ObjectViewSchema = {
160
+ type: 'object-view',
161
+ objectName: 'contacts',
162
+ listViews: {
163
+ all: { label: 'All Contacts', type: 'grid' },
164
+ active: { label: 'Active', type: 'grid', filter: [['status', '=', 'active']] },
165
+ },
166
+ defaultListView: 'all',
167
+ };
168
+
169
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
170
+
171
+ expect(screen.getByText('All Contacts')).toBeDefined();
172
+ expect(screen.getByText('Active')).toBeDefined();
173
+ });
174
+
175
+ it('should not render tabs when only one named view exists', () => {
176
+ const schema: ObjectViewSchema = {
177
+ type: 'object-view',
178
+ objectName: 'contacts',
179
+ listViews: {
180
+ all: { label: 'All Contacts', type: 'grid' },
181
+ },
182
+ };
183
+
184
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
185
+
186
+ // Should not show tabs for a single view
187
+ expect(screen.queryByRole('tablist')).toBeNull();
188
+ });
189
+
190
+ it('should default to first named view when defaultListView is not set', () => {
191
+ const schema: ObjectViewSchema = {
192
+ type: 'object-view',
193
+ objectName: 'contacts',
194
+ listViews: {
195
+ all: { label: 'All Contacts', type: 'grid' },
196
+ active: { label: 'Active', type: 'grid' },
197
+ },
198
+ };
199
+
200
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
201
+
202
+ // The grid should be rendered (first view is grid type)
203
+ expect(screen.getByTestId('object-grid')).toBeDefined();
204
+ });
205
+ });
206
+
207
+ // ============================
208
+ // Default View Type
209
+ // ============================
210
+ describe('Default View Type', () => {
211
+ it('should render grid by default when no defaultViewType set', () => {
212
+ const schema: ObjectViewSchema = {
213
+ type: 'object-view',
214
+ objectName: 'contacts',
215
+ };
216
+
217
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
218
+
219
+ expect(screen.getByTestId('object-grid')).toBeDefined();
220
+ });
221
+ });
222
+
223
+ // ============================
224
+ // Navigation Config
225
+ // ============================
226
+ describe('Navigation Config', () => {
227
+ it('should not navigate when mode is none', () => {
228
+ const onRowClick = vi.fn();
229
+ const schema: ObjectViewSchema = {
230
+ type: 'object-view',
231
+ objectName: 'contacts',
232
+ navigation: { mode: 'none' },
233
+ };
234
+
235
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
236
+
237
+ // Click a grid row
238
+ fireEvent.click(screen.getByTestId('grid-row'));
239
+
240
+ // onRowClick should not be called (mode is none)
241
+ expect(onRowClick).not.toHaveBeenCalled();
242
+ });
243
+
244
+ it('should not navigate when preventNavigation is true', () => {
245
+ const onRowClick = vi.fn();
246
+ const schema: ObjectViewSchema = {
247
+ type: 'object-view',
248
+ objectName: 'contacts',
249
+ navigation: { mode: 'page', preventNavigation: true },
250
+ };
251
+
252
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
253
+
254
+ fireEvent.click(screen.getByTestId('grid-row'));
255
+
256
+ expect(onRowClick).not.toHaveBeenCalled();
257
+ });
258
+
259
+ it('should open in new window when mode is new_window', () => {
260
+ const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
261
+
262
+ const schema: ObjectViewSchema = {
263
+ type: 'object-view',
264
+ objectName: 'contacts',
265
+ navigation: { mode: 'new_window' },
266
+ };
267
+
268
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
269
+
270
+ fireEvent.click(screen.getByTestId('grid-row'));
271
+
272
+ expect(openSpy).toHaveBeenCalledWith('/contacts/1', '_blank');
273
+ openSpy.mockRestore();
274
+ });
275
+
276
+ it('should call onNavigate for page mode', () => {
277
+ const onNavigate = vi.fn();
278
+ const schema: ObjectViewSchema = {
279
+ type: 'object-view',
280
+ objectName: 'contacts',
281
+ navigation: { mode: 'page' },
282
+ onNavigate,
283
+ };
284
+
285
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
286
+
287
+ fireEvent.click(screen.getByTestId('grid-row'));
288
+
289
+ expect(onNavigate).toHaveBeenCalledWith('1', 'view');
290
+ });
291
+ });
292
+
293
+ // ============================
294
+ // CRUD Operations
295
+ // ============================
296
+ describe('CRUD Operations', () => {
297
+ it('should hide create button when operations.create is false', () => {
298
+ const schema: ObjectViewSchema = {
299
+ type: 'object-view',
300
+ objectName: 'contacts',
301
+ operations: { create: false, read: true, update: true, delete: true },
302
+ };
303
+
304
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
305
+
306
+ expect(screen.queryByText('Create')).toBeNull();
307
+ });
308
+ });
309
+
310
+ // ============================
311
+ // Data Source Integration
312
+ // ============================
313
+ describe('Data Source Integration', () => {
314
+ it('should fetch object schema on mount', async () => {
315
+ const schema: ObjectViewSchema = {
316
+ type: 'object-view',
317
+ objectName: 'contacts',
318
+ };
319
+
320
+ render(<ObjectView schema={schema} dataSource={mockDataSource} />);
321
+
322
+ await waitFor(() => {
323
+ expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
324
+ });
325
+ });
326
+ });
327
+
328
+ // ============================
329
+ // View Switcher (prop-based views)
330
+ // ============================
331
+ describe('View Switcher (prop-based)', () => {
332
+ it('should not render view switcher with single view prop', () => {
333
+ const schema: ObjectViewSchema = {
334
+ type: 'object-view',
335
+ objectName: 'contacts',
336
+ };
337
+
338
+ const views = [
339
+ { id: 'grid', label: 'Grid', type: 'grid' as const },
340
+ ];
341
+
342
+ render(
343
+ <ObjectView schema={schema} dataSource={mockDataSource} views={views} />,
344
+ );
345
+
346
+ // Only one view, no switcher needed
347
+ expect(screen.getByTestId('object-grid')).toBeDefined();
348
+ });
349
+ });
350
+
351
+ // ============================
352
+ // showViewSwitcher config
353
+ // ============================
354
+ describe('showViewSwitcher', () => {
355
+ it('should hide view switcher when showViewSwitcher is false', () => {
356
+ const schema: ObjectViewSchema = {
357
+ type: 'object-view',
358
+ objectName: 'contacts',
359
+ showViewSwitcher: false,
360
+ };
361
+
362
+ const views = [
363
+ { id: 'grid', label: 'Grid', type: 'grid' as const },
364
+ { id: 'kanban', label: 'Kanban', type: 'kanban' as const },
365
+ ];
366
+
367
+ render(
368
+ <ObjectView schema={schema} dataSource={mockDataSource} views={views} />,
369
+ );
370
+
371
+ // Switcher should be hidden
372
+ expect(screen.queryByText('Kanban')).toBeNull();
373
+ });
374
+ });
375
+ });