ezfw-core 1.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.
Files changed (154) hide show
  1. package/components/EzBaseComponent.ts +648 -0
  2. package/components/EzComponent.ts +89 -0
  3. package/components/EzInput.module.scss +183 -0
  4. package/components/EzInput.ts +104 -0
  5. package/components/EzLabel.ts +22 -0
  6. package/components/EzOutlet.ts +181 -0
  7. package/components/HtmlWrapper.ts +305 -0
  8. package/components/avatar/EzAvatar.module.scss +200 -0
  9. package/components/avatar/EzAvatar.ts +130 -0
  10. package/components/badge/EzBadge.module.scss +202 -0
  11. package/components/badge/EzBadge.ts +77 -0
  12. package/components/button/EzButton.module.scss +402 -0
  13. package/components/button/EzButton.ts +175 -0
  14. package/components/button/EzButtonGroup.ts +48 -0
  15. package/components/card/EzCard.module.scss +71 -0
  16. package/components/card/EzCard.ts +120 -0
  17. package/components/chart/EzBarChart.ts +47 -0
  18. package/components/chart/EzChart.module.scss +14 -0
  19. package/components/chart/EzChart.ts +279 -0
  20. package/components/chart/EzDoughnutChart.ts +47 -0
  21. package/components/chart/EzLineChart.ts +53 -0
  22. package/components/checkbox/EzCheckbox.module.scss +145 -0
  23. package/components/checkbox/EzCheckbox.ts +115 -0
  24. package/components/dataview/EzDataView.module.scss +115 -0
  25. package/components/dataview/EzDataView.ts +355 -0
  26. package/components/dataview/modes/EzDataViewCards.ts +322 -0
  27. package/components/dataview/modes/EzDataViewGrid.ts +76 -0
  28. package/components/datepicker/EzDatePicker.module.scss +348 -0
  29. package/components/datepicker/EzDatePicker.ts +519 -0
  30. package/components/dialog/EzDialog.module.scss +180 -0
  31. package/components/dropdown/EzDropdown.module.scss +107 -0
  32. package/components/dropdown/EzDropdown.ts +235 -0
  33. package/components/feed/EzActivityFeed.module.scss +90 -0
  34. package/components/feed/EzActivityFeed.ts +78 -0
  35. package/components/form/EzForm.ts +364 -0
  36. package/components/form/EzValidators.test.js +421 -0
  37. package/components/form/EzValidators.ts +202 -0
  38. package/components/grid/EzGrid.scss +88 -0
  39. package/components/grid/EzGrid.ts +1085 -0
  40. package/components/grid/EzGridContainer.ts +104 -0
  41. package/components/grid/body/EzGridBody.scss +283 -0
  42. package/components/grid/body/EzGridBody.ts +549 -0
  43. package/components/grid/body/EzGridCell.ts +211 -0
  44. package/components/grid/body/EzGridRow.ts +196 -0
  45. package/components/grid/filter/EzGridFilters.scss +78 -0
  46. package/components/grid/filter/EzGridFilters.ts +285 -0
  47. package/components/grid/footer/EzGridFooter.scss +136 -0
  48. package/components/grid/footer/EzGridFooter.ts +448 -0
  49. package/components/grid/header/EzGridHeader.scss +199 -0
  50. package/components/grid/header/EzGridHeader.ts +430 -0
  51. package/components/grid/query/EzGridQuery.ts +81 -0
  52. package/components/grid/state/EzGridColumns.ts +155 -0
  53. package/components/grid/state/EzGridController.ts +470 -0
  54. package/components/grid/state/EzGridLifecycle.ts +136 -0
  55. package/components/grid/state/EzGridNormalizers.test.js +273 -0
  56. package/components/grid/state/EzGridNormalizers.ts +162 -0
  57. package/components/grid/state/EzGridParts.ts +233 -0
  58. package/components/grid/state/EzGridPersistence.ts +140 -0
  59. package/components/grid/state/EzGridRemote.test.js +573 -0
  60. package/components/grid/state/EzGridRemote.ts +335 -0
  61. package/components/grid/state/EzGridSelection.ts +231 -0
  62. package/components/grid/state/EzGridSort.ts +286 -0
  63. package/components/grid/title/EzGridActionBar.ts +98 -0
  64. package/components/grid/title/EzGridTitle.ts +114 -0
  65. package/components/grid/title/EzGridTitleBar.scss +65 -0
  66. package/components/grid/title/EzGridTitleBar.ts +87 -0
  67. package/components/grid/types.ts +607 -0
  68. package/components/panel/EzPanel.module.scss +133 -0
  69. package/components/panel/EzPanel.ts +147 -0
  70. package/components/radio/EzRadio.module.scss +190 -0
  71. package/components/radio/EzRadio.ts +149 -0
  72. package/components/select/EzSelect.module.scss +153 -0
  73. package/components/select/EzSelect.ts +238 -0
  74. package/components/skeleton/EzSkeleton.module.scss +95 -0
  75. package/components/skeleton/EzSkeleton.ts +70 -0
  76. package/components/store/EzStore.ts +344 -0
  77. package/components/switch/EzSwitch.module.scss +164 -0
  78. package/components/switch/EzSwitch.ts +117 -0
  79. package/components/tabs/EzTabPanel.module.scss +181 -0
  80. package/components/tabs/EzTabPanel.ts +402 -0
  81. package/components/textarea/EzTextarea.module.scss +131 -0
  82. package/components/textarea/EzTextarea.ts +161 -0
  83. package/components/timepicker/EzTimePicker.module.scss +282 -0
  84. package/components/timepicker/EzTimePicker.ts +540 -0
  85. package/components/toast/EzToast.module.scss +291 -0
  86. package/components/tooltip/EzTooltip.module.scss +124 -0
  87. package/components/tooltip/EzTooltip.ts +153 -0
  88. package/core/EzComponentTypes.ts +693 -0
  89. package/core/EzError.ts +63 -0
  90. package/core/EzModel.ts +268 -0
  91. package/core/EzTypes.ts +328 -0
  92. package/core/eventBus.ts +284 -0
  93. package/core/ez.ts +617 -0
  94. package/core/loader.ts +725 -0
  95. package/core/renderer.ts +1010 -0
  96. package/core/router.ts +490 -0
  97. package/core/services.ts +124 -0
  98. package/core/state.ts +142 -0
  99. package/core/utils.ts +81 -0
  100. package/package.json +51 -0
  101. package/services/RouteUI.js +17 -0
  102. package/services/crypto.js +64 -0
  103. package/services/dialog.js +222 -0
  104. package/services/fetchApi.js +63 -0
  105. package/services/firebase.js +30 -0
  106. package/services/toast.js +214 -0
  107. package/template/doc/EzDocs.js +15 -0
  108. package/template/doc/EzDocs.module.scss +627 -0
  109. package/template/doc/EzDocsController.js +164 -0
  110. package/template/doc/data/activityfeed/EzActivityFeedDoc.js +42 -0
  111. package/template/doc/data/avatar/EzAvatarDoc.js +71 -0
  112. package/template/doc/data/badge/EzBadgeDoc.js +92 -0
  113. package/template/doc/data/button/EzButtonDoc.js +77 -0
  114. package/template/doc/data/buttongroup/EzButtonGroupDoc.js +102 -0
  115. package/template/doc/data/card/EzCardDoc.js +39 -0
  116. package/template/doc/data/chart/EzChartDoc.js +60 -0
  117. package/template/doc/data/checkbox/EzCheckboxDoc.js +67 -0
  118. package/template/doc/data/component/EzComponentDoc.js +34 -0
  119. package/template/doc/data/cssmodules/CSSModulesDoc.js +70 -0
  120. package/template/doc/data/datepicker/EzDatePickerDoc.js +126 -0
  121. package/template/doc/data/dialog/EzDialogDoc.js +217 -0
  122. package/template/doc/data/dropdown/EzDropdownDoc.js +178 -0
  123. package/template/doc/data/form/EzFormDoc.js +90 -0
  124. package/template/doc/data/grid/EzGridDoc.js +99 -0
  125. package/template/doc/data/input/EzInputDoc.js +92 -0
  126. package/template/doc/data/label/EzLabelDoc.js +40 -0
  127. package/template/doc/data/model/EzModelDoc.js +53 -0
  128. package/template/doc/data/outlet/EzOutletDoc.js +63 -0
  129. package/template/doc/data/panel/EzPanelDoc.js +214 -0
  130. package/template/doc/data/radio/EzRadioDoc.js +174 -0
  131. package/template/doc/data/router/EzRouterDoc.js +75 -0
  132. package/template/doc/data/select/EzSelectDoc.js +37 -0
  133. package/template/doc/data/skeleton/EzSkeletonDoc.js +149 -0
  134. package/template/doc/data/switch/EzSwitchDoc.js +82 -0
  135. package/template/doc/data/tabpanel/EzTabPanelDoc.js +44 -0
  136. package/template/doc/data/textarea/EzTextareaDoc.js +131 -0
  137. package/template/doc/data/timepicker/EzTimePickerDoc.js +107 -0
  138. package/template/doc/data/tooltip/EzTooltipDoc.js +193 -0
  139. package/template/doc/data/validators/EzValidatorsDoc.js +37 -0
  140. package/template/doc/sidebar/EzDocsSidebar.js +32 -0
  141. package/template/doc/sidebar/category/EzDocsCategory.js +33 -0
  142. package/template/doc/sidebar/item/EzDocsComponentItem.js +24 -0
  143. package/template/doc/viewer/EzDocsViewer.js +18 -0
  144. package/template/doc/viewer/codepanel/EzDocsCodePanel.js +51 -0
  145. package/template/doc/viewer/content/EzDocsContent.js +315 -0
  146. package/template/doc/viewer/header/EzDocsViewerHeader.js +46 -0
  147. package/template/doc/viewer/showcase/EzDocsShowcase.js +59 -0
  148. package/template/doc/viewer/showcase/EzDocsShowcaseSection.js +25 -0
  149. package/template/doc/viewer/showcase/EzDocsVariantItem.js +29 -0
  150. package/template/doc/welcome/EzDocsWelcome.js +48 -0
  151. package/themes/ez-theme.scss +179 -0
  152. package/themes/nature-fresh.scss +169 -0
  153. package/types/global.d.ts +21 -0
  154. package/utils/cssModules.js +81 -0
@@ -0,0 +1,140 @@
1
+ // EzGrid/state/EzGridPersistence.ts
2
+
3
+ import { EzError } from '../../../core/EzError.js';
4
+ import type {
5
+ ColumnsSnapshot,
6
+ Sorter,
7
+ SelectionSnapshot,
8
+ GridStateSnapshot
9
+ } from '../types.js';
10
+
11
+ declare const ez: {
12
+ _gridStateRegistry: Record<string, GridRegistryEntry>;
13
+ };
14
+
15
+ export interface GridRegistryEntry {
16
+ _columnState?: ColumnsSnapshot | null;
17
+ _sortState?: Sorter[] | null;
18
+ _selectionState?: SelectionSnapshot | null;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ export interface EzGridPersistenceRef {
23
+ _context?: {
24
+ route?: string;
25
+ view?: string;
26
+ };
27
+ config: {
28
+ id?: string;
29
+ };
30
+ statefulPersist?: boolean;
31
+ }
32
+
33
+ // Re-export for backwards compatibility
34
+ export type { GridStateSnapshot } from '../types.js';
35
+
36
+ export class EzGridPersistence {
37
+ grid: EzGridPersistenceRef;
38
+
39
+ constructor(grid: EzGridPersistenceRef) {
40
+ this.grid = grid;
41
+ }
42
+
43
+ // ==========================================================
44
+ // State Key Management
45
+ // ==========================================================
46
+
47
+ getStateKey(): string {
48
+ const route = this.grid._context?.route ?? 'global';
49
+ const view = this.grid._context?.view ?? 'unknown';
50
+ const id = this.grid.config.id;
51
+
52
+ if (!id) {
53
+ throw new EzError({
54
+ code: 'EZ_GRID_STATE_001',
55
+ message: 'Stateful EzGrid requires a unique "id"'
56
+ });
57
+ }
58
+
59
+ return `${route}:${view}:${id}`;
60
+ }
61
+
62
+ getStorageKey(): string {
63
+ return `EZ_GRID_STATE::${this.getStateKey()}`;
64
+ }
65
+
66
+ // ==========================================================
67
+ // Storage Operations
68
+ // ==========================================================
69
+
70
+ load(): GridStateSnapshot | null {
71
+ if (!this.grid.statefulPersist) return null;
72
+
73
+ try {
74
+ const raw = localStorage.getItem(this.getStorageKey());
75
+ if (!raw) return null;
76
+ return JSON.parse(raw) as GridStateSnapshot;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ save(): void {
83
+ if (!this.grid.statefulPersist) return;
84
+
85
+ try {
86
+ const entry = ez._gridStateRegistry[this.getStateKey()];
87
+ if (!entry) return;
88
+
89
+ const snapshot: GridStateSnapshot = {
90
+ columnState: entry._columnState ?? null,
91
+ sortState: entry._sortState ?? null,
92
+ selectionState: entry._selectionState ?? null
93
+ };
94
+
95
+ localStorage.setItem(
96
+ this.getStorageKey(),
97
+ JSON.stringify(snapshot)
98
+ );
99
+ } catch {
100
+ // fail silently
101
+ }
102
+ }
103
+
104
+ // ==========================================================
105
+ // Registry Operations
106
+ // ==========================================================
107
+
108
+ getRegistryEntry(): GridRegistryEntry | undefined {
109
+ return ez._gridStateRegistry[this.getStateKey()];
110
+ }
111
+
112
+ setRegistryEntry(controller: GridRegistryEntry): void {
113
+ ez._gridStateRegistry[this.getStateKey()] = controller;
114
+ }
115
+
116
+ hasRegistryEntry(): boolean {
117
+ return !!ez._gridStateRegistry[this.getStateKey()];
118
+ }
119
+
120
+ updateColumnState(snapshot: ColumnsSnapshot | null): void {
121
+ const entry = this.getRegistryEntry();
122
+ if (entry) {
123
+ entry._columnState = snapshot;
124
+ }
125
+ }
126
+
127
+ updateSortState(snapshot: Sorter[] | null): void {
128
+ const entry = this.getRegistryEntry();
129
+ if (entry) {
130
+ entry._sortState = snapshot;
131
+ }
132
+ }
133
+
134
+ updateSelectionState(snapshot: SelectionSnapshot | null): void {
135
+ const entry = this.getRegistryEntry();
136
+ if (entry) {
137
+ entry._selectionState = snapshot;
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,573 @@
1
+ // ==========================================================
2
+ // EzGridRemote - Unit Tests
3
+ // ==========================================================
4
+ // Para correr: npm test EzGridRemote
5
+ // ==========================================================
6
+
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import { EzGridRemote } from './EzGridRemote.js';
9
+
10
+ // ==========================================================
11
+ // Mock Setup
12
+ // ==========================================================
13
+
14
+ // Mock del objeto global ez
15
+ const mockEz = {
16
+ _api: {
17
+ request: vi.fn()
18
+ },
19
+ getController: vi.fn()
20
+ };
21
+
22
+ // Instalar mock global antes de cada test
23
+ beforeEach(() => {
24
+ globalThis.ez = mockEz;
25
+ vi.clearAllMocks();
26
+ });
27
+
28
+ afterEach(() => {
29
+ delete globalThis.ez;
30
+ });
31
+
32
+ // Helper para crear mock de grid
33
+ function createMockGrid(options = {}) {
34
+ return {
35
+ controller: {
36
+ state: {
37
+ page: 1,
38
+ pageSize: 25
39
+ },
40
+ setLoading: vi.fn(),
41
+ setError: vi.fn(),
42
+ _lastSortSnapshot: null,
43
+ _lastFilterSnapshot: null
44
+ },
45
+ config: {
46
+ controller: options.controllerName || 'TestController'
47
+ }
48
+ };
49
+ }
50
+
51
+ // ==========================================================
52
+ // Constructor & Config
53
+ // ==========================================================
54
+
55
+ describe('EzGridRemote - Configuration', () => {
56
+
57
+ it('should normalize string api to object', () => {
58
+ const grid = createMockGrid();
59
+ const remote = new EzGridRemote(grid, {
60
+ api: 'v1/users'
61
+ });
62
+
63
+ expect(remote.config.api.read).toBe('v1/users');
64
+ expect(remote.config.api.create).toBe('v1/users');
65
+ expect(remote.config.api.update).toBe('v1/users');
66
+ expect(remote.config.api.delete).toBe('v1/users');
67
+ });
68
+
69
+ it('should preserve object api', () => {
70
+ const grid = createMockGrid();
71
+ const remote = new EzGridRemote(grid, {
72
+ api: {
73
+ read: 'v1/users',
74
+ create: 'v1/users/create'
75
+ }
76
+ });
77
+
78
+ expect(remote.config.api.read).toBe('v1/users');
79
+ expect(remote.config.api.create).toBe('v1/users/create');
80
+ });
81
+
82
+ it('should use default source paths', () => {
83
+ const grid = createMockGrid();
84
+ const remote = new EzGridRemote(grid, {
85
+ api: 'v1/users'
86
+ });
87
+
88
+ expect(remote.config.source.dataPath).toBe('data');
89
+ expect(remote.config.source.countPath).toBe('count');
90
+ });
91
+
92
+ it('should use custom source paths', () => {
93
+ const grid = createMockGrid();
94
+ const remote = new EzGridRemote(grid, {
95
+ api: 'v1/users',
96
+ source: {
97
+ dataPath: 'result.items',
98
+ countPath: 'result.total'
99
+ }
100
+ });
101
+
102
+ expect(remote.config.source.dataPath).toBe('result.items');
103
+ expect(remote.config.source.countPath).toBe('result.total');
104
+ });
105
+
106
+ it('should default filter mode to onInput', () => {
107
+ const grid = createMockGrid();
108
+ const remote = new EzGridRemote(grid, {
109
+ api: 'v1/users'
110
+ });
111
+
112
+ expect(remote.config.filter).toBe('onInput');
113
+ });
114
+
115
+ it('should preserve filter mode onEnter', () => {
116
+ const grid = createMockGrid();
117
+ const remote = new EzGridRemote(grid, {
118
+ api: 'v1/users',
119
+ filter: 'onEnter'
120
+ });
121
+
122
+ expect(remote.config.filter).toBe('onEnter');
123
+ });
124
+
125
+ });
126
+
127
+ // ==========================================================
128
+ // isEnabled
129
+ // ==========================================================
130
+
131
+ describe('EzGridRemote.isEnabled', () => {
132
+
133
+ it('should return true when read api is configured', () => {
134
+ const grid = createMockGrid();
135
+ const remote = new EzGridRemote(grid, {
136
+ api: 'v1/users'
137
+ });
138
+
139
+ expect(remote.isEnabled()).toBe(true);
140
+ });
141
+
142
+ it('should return false when no config', () => {
143
+ const grid = createMockGrid();
144
+ const remote = new EzGridRemote(grid, null);
145
+
146
+ expect(remote.isEnabled()).toBe(false);
147
+ });
148
+
149
+ it('should return false when no read api', () => {
150
+ const grid = createMockGrid();
151
+ const remote = new EzGridRemote(grid, {
152
+ api: {}
153
+ });
154
+
155
+ expect(remote.isEnabled()).toBe(false);
156
+ });
157
+
158
+ });
159
+
160
+ // ==========================================================
161
+ // URL Building
162
+ // ==========================================================
163
+
164
+ describe('EzGridRemote._buildReadUrl', () => {
165
+
166
+ it('should build basic URL with pagination', () => {
167
+ const grid = createMockGrid();
168
+ const remote = new EzGridRemote(grid, {
169
+ api: 'v1/users'
170
+ });
171
+
172
+ const url = remote._buildReadUrl({ page: 1, pageSize: 25 });
173
+
174
+ expect(url).toBe('v1/users?page=1&pageSize=25');
175
+ });
176
+
177
+ it('should include sort params', () => {
178
+ const grid = createMockGrid();
179
+ const remote = new EzGridRemote(grid, {
180
+ api: 'v1/users'
181
+ });
182
+
183
+ const url = remote._buildReadUrl({
184
+ page: 1,
185
+ pageSize: 25,
186
+ sort: [{ property: 'name', direction: 'ASC' }]
187
+ });
188
+
189
+ expect(url).toContain('sortBy=name');
190
+ expect(url).toContain('sortDir=ASC');
191
+ });
192
+
193
+ it('should include filters as JSON', () => {
194
+ const grid = createMockGrid();
195
+ const remote = new EzGridRemote(grid, {
196
+ api: 'v1/users'
197
+ });
198
+
199
+ const filters = [{ field: 'name', operator: 'LIKE', value: 'John' }];
200
+ const url = remote._buildReadUrl({
201
+ page: 1,
202
+ pageSize: 25,
203
+ filters
204
+ });
205
+
206
+ expect(url).toContain('filters=');
207
+ expect(url).toContain(encodeURIComponent(JSON.stringify(filters)));
208
+ });
209
+
210
+ });
211
+
212
+ // ==========================================================
213
+ // Response Parsing
214
+ // ==========================================================
215
+
216
+ describe('EzGridRemote._parseResponse', () => {
217
+
218
+ it('should parse data and count from default paths', () => {
219
+ const grid = createMockGrid();
220
+ const remote = new EzGridRemote(grid, {
221
+ api: 'v1/users'
222
+ });
223
+
224
+ const response = {
225
+ data: [{ id: 1 }, { id: 2 }],
226
+ count: 100
227
+ };
228
+
229
+ const result = remote._parseResponse(response);
230
+
231
+ expect(result.data).toEqual([{ id: 1 }, { id: 2 }]);
232
+ expect(result.total).toBe(100);
233
+ });
234
+
235
+ it('should parse data from nested path', () => {
236
+ const grid = createMockGrid();
237
+ const remote = new EzGridRemote(grid, {
238
+ api: 'v1/users',
239
+ source: {
240
+ dataPath: 'result.items',
241
+ countPath: 'result.total'
242
+ }
243
+ });
244
+
245
+ const response = {
246
+ result: {
247
+ items: [{ id: 1 }],
248
+ total: 50
249
+ }
250
+ };
251
+
252
+ const result = remote._parseResponse(response);
253
+
254
+ expect(result.data).toEqual([{ id: 1 }]);
255
+ expect(result.total).toBe(50);
256
+ });
257
+
258
+ it('should return empty array if data is not array', () => {
259
+ const grid = createMockGrid();
260
+ const remote = new EzGridRemote(grid, {
261
+ api: 'v1/users'
262
+ });
263
+
264
+ const result = remote._parseResponse({ data: null });
265
+
266
+ expect(result.data).toEqual([]);
267
+ });
268
+
269
+ it('should use data length if count not available', () => {
270
+ const grid = createMockGrid();
271
+ const remote = new EzGridRemote(grid, {
272
+ api: 'v1/users'
273
+ });
274
+
275
+ const result = remote._parseResponse({
276
+ data: [{ id: 1 }, { id: 2 }, { id: 3 }]
277
+ });
278
+
279
+ expect(result.total).toBe(3);
280
+ });
281
+
282
+ });
283
+
284
+ // ==========================================================
285
+ // Callback Resolution
286
+ // ==========================================================
287
+
288
+ describe('EzGridRemote._resolveCallback', () => {
289
+
290
+ it('should return function as-is', () => {
291
+ const grid = createMockGrid();
292
+ const remote = new EzGridRemote(grid, { api: 'v1/users' });
293
+
294
+ const fn = () => 'test';
295
+ const result = remote._resolveCallback(fn);
296
+
297
+ expect(result).toBe(fn);
298
+ });
299
+
300
+ it('should resolve ControllerName:functionName format', () => {
301
+ const mockController = {
302
+ handleData: vi.fn().mockReturnValue('handled')
303
+ };
304
+ mockEz.getController.mockReturnValue(mockController);
305
+
306
+ const grid = createMockGrid();
307
+ const remote = new EzGridRemote(grid, { api: 'v1/users' });
308
+
309
+ const result = remote._resolveCallback('UserController:handleData');
310
+
311
+ expect(mockEz.getController).toHaveBeenCalledWith('UserController');
312
+ expect(typeof result).toBe('function');
313
+ expect(result()).toBe('handled');
314
+ });
315
+
316
+ it('should resolve functionName using config.controller', () => {
317
+ const mockController = {
318
+ onLoad: vi.fn().mockReturnValue('loaded')
319
+ };
320
+ mockEz.getController.mockReturnValue(mockController);
321
+
322
+ const grid = createMockGrid({ controllerName: 'MyController' });
323
+ const remote = new EzGridRemote(grid, { api: 'v1/users' });
324
+
325
+ const result = remote._resolveCallback('onLoad');
326
+
327
+ expect(mockEz.getController).toHaveBeenCalledWith('MyController');
328
+ expect(typeof result).toBe('function');
329
+ });
330
+
331
+ it('should return null for invalid string reference', () => {
332
+ mockEz.getController.mockReturnValue(null);
333
+
334
+ const grid = createMockGrid();
335
+ const remote = new EzGridRemote(grid, { api: 'v1/users' });
336
+
337
+ const result = remote._resolveCallback('nonExistent');
338
+
339
+ expect(result).toBe(null);
340
+ });
341
+
342
+ it('should return null for non-function/non-string', () => {
343
+ const grid = createMockGrid();
344
+ const remote = new EzGridRemote(grid, { api: 'v1/users' });
345
+
346
+ expect(remote._resolveCallback(123)).toBe(null);
347
+ expect(remote._resolveCallback(null)).toBe(null);
348
+ expect(remote._resolveCallback({})).toBe(null);
349
+ });
350
+
351
+ });
352
+
353
+ // ==========================================================
354
+ // Load
355
+ // ==========================================================
356
+
357
+ describe('EzGridRemote.load', () => {
358
+
359
+ it('should call ez._api.request with correct URL', async () => {
360
+ mockEz._api.request.mockResolvedValue({
361
+ data: [{ id: 1 }],
362
+ count: 1
363
+ });
364
+
365
+ const grid = createMockGrid();
366
+ const remote = new EzGridRemote(grid, {
367
+ api: 'v1/users'
368
+ });
369
+
370
+ await remote.load();
371
+
372
+ expect(mockEz._api.request).toHaveBeenCalledWith(
373
+ expect.stringContaining('v1/users')
374
+ );
375
+ });
376
+
377
+ it('should return parsed data and total', async () => {
378
+ mockEz._api.request.mockResolvedValue({
379
+ data: [{ id: 1 }, { id: 2 }],
380
+ count: 100
381
+ });
382
+
383
+ const grid = createMockGrid();
384
+ const remote = new EzGridRemote(grid, {
385
+ api: 'v1/users'
386
+ });
387
+
388
+ const result = await remote.load();
389
+
390
+ expect(result.data).toHaveLength(2);
391
+ expect(result.total).toBe(100);
392
+ });
393
+
394
+ it('should set loading state', async () => {
395
+ mockEz._api.request.mockResolvedValue({ data: [], count: 0 });
396
+
397
+ const grid = createMockGrid();
398
+ const remote = new EzGridRemote(grid, {
399
+ api: 'v1/users'
400
+ });
401
+
402
+ await remote.load();
403
+
404
+ expect(grid.controller.setLoading).toHaveBeenCalledWith(true);
405
+ expect(grid.controller.setLoading).toHaveBeenCalledWith(false);
406
+ });
407
+
408
+ it('should call beforeLoad hook', async () => {
409
+ mockEz._api.request.mockResolvedValue({ data: [], count: 0 });
410
+
411
+ const beforeLoad = vi.fn();
412
+ const grid = createMockGrid();
413
+ const remote = new EzGridRemote(grid, {
414
+ api: 'v1/users',
415
+ listenTo: { beforeLoad }
416
+ });
417
+
418
+ await remote.load();
419
+
420
+ expect(beforeLoad).toHaveBeenCalledWith(
421
+ expect.objectContaining({ page: 1, pageSize: 25 }),
422
+ expect.objectContaining({ grid, remote })
423
+ );
424
+ });
425
+
426
+ it('should call afterLoad hook with result', async () => {
427
+ const responseData = { data: [{ id: 1 }], count: 1 };
428
+ mockEz._api.request.mockResolvedValue(responseData);
429
+
430
+ const afterLoad = vi.fn();
431
+ const grid = createMockGrid();
432
+ const remote = new EzGridRemote(grid, {
433
+ api: 'v1/users',
434
+ listenTo: { afterLoad }
435
+ });
436
+
437
+ await remote.load();
438
+
439
+ expect(afterLoad).toHaveBeenCalledWith(
440
+ expect.objectContaining({ data: [{ id: 1 }], total: 1 }),
441
+ expect.objectContaining({ grid, remote, response: responseData })
442
+ );
443
+ });
444
+
445
+ it('should call onError hook on failure', async () => {
446
+ const error = new Error('Network error');
447
+ mockEz._api.request.mockRejectedValue(error);
448
+
449
+ const onError = vi.fn();
450
+ const grid = createMockGrid();
451
+ const remote = new EzGridRemote(grid, {
452
+ api: 'v1/users',
453
+ listenTo: { onError }
454
+ });
455
+
456
+ await remote.load();
457
+
458
+ expect(onError).toHaveBeenCalledWith(
459
+ error,
460
+ expect.objectContaining({ grid, remote })
461
+ );
462
+ });
463
+
464
+ it('should return empty data on error', async () => {
465
+ mockEz._api.request.mockRejectedValue(new Error('Failed'));
466
+
467
+ const grid = createMockGrid();
468
+ const remote = new EzGridRemote(grid, {
469
+ api: 'v1/users'
470
+ });
471
+
472
+ const result = await remote.load();
473
+
474
+ expect(result.data).toEqual([]);
475
+ expect(result.total).toBe(0);
476
+ });
477
+
478
+ it('should prevent concurrent loads', async () => {
479
+ let resolveFirst;
480
+ mockEz._api.request.mockImplementationOnce(() =>
481
+ new Promise(resolve => { resolveFirst = resolve; })
482
+ );
483
+
484
+ const grid = createMockGrid();
485
+ const remote = new EzGridRemote(grid, {
486
+ api: 'v1/users'
487
+ });
488
+
489
+ // Start first load
490
+ const firstLoad = remote.load();
491
+
492
+ // Try second load while first is pending
493
+ const secondResult = await remote.load();
494
+
495
+ // Second should return empty immediately
496
+ expect(secondResult.data).toEqual([]);
497
+
498
+ // Complete first load
499
+ resolveFirst({ data: [{ id: 1 }], count: 1 });
500
+ await firstLoad;
501
+ });
502
+
503
+ });
504
+
505
+ // ==========================================================
506
+ // CRUD Operations
507
+ // ==========================================================
508
+
509
+ describe('EzGridRemote CRUD', () => {
510
+
511
+ describe('create', () => {
512
+ it('should POST to create endpoint', async () => {
513
+ mockEz._api.request.mockResolvedValue({ id: 1 });
514
+
515
+ const grid = createMockGrid();
516
+ const remote = new EzGridRemote(grid, {
517
+ api: { read: 'v1/users', create: 'v1/users' }
518
+ });
519
+
520
+ await remote.create({ name: 'John' });
521
+
522
+ expect(mockEz._api.request).toHaveBeenCalledWith('v1/users', {
523
+ method: 'POST',
524
+ body: { name: 'John' }
525
+ });
526
+ });
527
+
528
+ it('should throw if create not configured', async () => {
529
+ const grid = createMockGrid();
530
+ const remote = new EzGridRemote(grid, {
531
+ api: { read: 'v1/users' }
532
+ });
533
+
534
+ await expect(remote.create({})).rejects.toThrow('Create endpoint not configured');
535
+ });
536
+ });
537
+
538
+ describe('update', () => {
539
+ it('should PUT to update endpoint with id', async () => {
540
+ mockEz._api.request.mockResolvedValue({ id: 1 });
541
+
542
+ const grid = createMockGrid();
543
+ const remote = new EzGridRemote(grid, {
544
+ api: { read: 'v1/users', update: 'v1/users' }
545
+ });
546
+
547
+ await remote.update(1, { name: 'Jane' });
548
+
549
+ expect(mockEz._api.request).toHaveBeenCalledWith('v1/users/1', {
550
+ method: 'PUT',
551
+ body: { name: 'Jane' }
552
+ });
553
+ });
554
+ });
555
+
556
+ describe('delete', () => {
557
+ it('should DELETE to endpoint with id', async () => {
558
+ mockEz._api.request.mockResolvedValue({});
559
+
560
+ const grid = createMockGrid();
561
+ const remote = new EzGridRemote(grid, {
562
+ api: { read: 'v1/users', delete: 'v1/users' }
563
+ });
564
+
565
+ await remote.delete(1);
566
+
567
+ expect(mockEz._api.request).toHaveBeenCalledWith('v1/users/1', {
568
+ method: 'DELETE'
569
+ });
570
+ });
571
+ });
572
+
573
+ });