Skip to content

Commit 4326c5a

Browse files
Copilothuangyiirene
andcommitted
Add comprehensive tests for Field and Action factory helpers
- Add tests for Field.phone() helper - Add tests for Field.select() backward compatibility (old and new API) - Add tests for Field.multiselect() backward compatibility - Add tests for Action.create() with default value handling - Add tests for Dashboard.create() with default value handling - All 279 tests passing Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
1 parent b072842 commit 4326c5a

3 files changed

Lines changed: 236 additions & 30 deletions

File tree

packages/spec/src/data/field.test.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
FieldSchema,
44
FieldType,
55
SelectOptionSchema,
6-
type Field,
6+
Field,
77
type SelectOption
88
} from './field.zod';
99

@@ -304,3 +304,122 @@ describe('FieldSchema', () => {
304304
});
305305
});
306306
});
307+
308+
describe('Field Factory Helpers', () => {
309+
describe('Basic Field Types', () => {
310+
it('should create phone field', () => {
311+
const phoneField = Field.phone({ label: 'Mobile Phone', required: true });
312+
313+
expect(phoneField.type).toBe('phone');
314+
expect(phoneField.label).toBe('Mobile Phone');
315+
expect(phoneField.required).toBe(true);
316+
});
317+
318+
it('should create text field', () => {
319+
const textField = Field.text({ label: 'Name', maxLength: 100 });
320+
321+
expect(textField.type).toBe('text');
322+
expect(textField.label).toBe('Name');
323+
expect(textField.maxLength).toBe(100);
324+
});
325+
326+
it('should create email field', () => {
327+
const emailField = Field.email({ label: 'Email Address' });
328+
329+
expect(emailField.type).toBe('email');
330+
expect(emailField.label).toBe('Email Address');
331+
});
332+
});
333+
334+
describe('Select Field Factory', () => {
335+
it('should create select field with string array (old API)', () => {
336+
const selectField = Field.select(['High', 'Medium', 'Low'], { label: 'Priority' });
337+
338+
expect(selectField.type).toBe('select');
339+
expect(selectField.label).toBe('Priority');
340+
expect(selectField.options).toHaveLength(3);
341+
expect(selectField.options[0]).toEqual({ label: 'High', value: 'High' });
342+
});
343+
344+
it('should create select field with SelectOption array in config (new API)', () => {
345+
const selectField = Field.select({
346+
label: 'Priority',
347+
options: [
348+
{ label: 'High Priority', value: 'high', color: '#FF0000' },
349+
{ label: 'Low Priority', value: 'low', color: '#00FF00' },
350+
],
351+
});
352+
353+
expect(selectField.type).toBe('select');
354+
expect(selectField.label).toBe('Priority');
355+
expect(selectField.options).toHaveLength(2);
356+
expect(selectField.options[0].color).toBe('#FF0000');
357+
expect(selectField.options[1].value).toBe('low');
358+
});
359+
360+
it('should create select field with mixed string/object array (new API)', () => {
361+
const selectField = Field.select({
362+
label: 'Status',
363+
options: [
364+
{ label: 'Active', value: 'active', color: '#00AA00' },
365+
'Inactive',
366+
'Pending',
367+
],
368+
});
369+
370+
expect(selectField.type).toBe('select');
371+
expect(selectField.options).toHaveLength(3);
372+
expect(selectField.options[0]).toEqual({ label: 'Active', value: 'active', color: '#00AA00' });
373+
expect(selectField.options[1]).toEqual({ label: 'Inactive', value: 'Inactive' });
374+
expect(selectField.options[2]).toEqual({ label: 'Pending', value: 'Pending' });
375+
});
376+
});
377+
378+
describe('Multiselect Field Factory', () => {
379+
it('should create multiselect field with string array (old API)', () => {
380+
const multiselectField = Field.multiselect(['Tag1', 'Tag2', 'Tag3'], { label: 'Tags' });
381+
382+
expect(multiselectField.type).toBe('multiselect');
383+
expect(multiselectField.label).toBe('Tags');
384+
expect(multiselectField.options).toHaveLength(3);
385+
});
386+
387+
it('should create multiselect field with SelectOption array (new API)', () => {
388+
const multiselectField = Field.multiselect({
389+
label: 'Categories',
390+
options: [
391+
{ label: 'Technology', value: 'tech' },
392+
{ label: 'Business', value: 'biz' },
393+
],
394+
});
395+
396+
expect(multiselectField.type).toBe('multiselect');
397+
expect(multiselectField.options).toHaveLength(2);
398+
expect(multiselectField.options[0].value).toBe('tech');
399+
});
400+
});
401+
402+
describe('Lookup and Master-Detail Fields', () => {
403+
it('should create lookup field', () => {
404+
const lookupField = Field.lookup('account', {
405+
label: 'Account',
406+
referenceFilters: ['status = "active"'],
407+
});
408+
409+
expect(lookupField.type).toBe('lookup');
410+
expect(lookupField.reference).toBe('account');
411+
expect(lookupField.label).toBe('Account');
412+
});
413+
414+
it('should create master_detail field', () => {
415+
const masterDetailField = Field.master_detail('parent_object', {
416+
label: 'Parent',
417+
deleteBehavior: 'cascade',
418+
});
419+
420+
expect(masterDetailField.type).toBe('master_detail');
421+
expect(masterDetailField.reference).toBe('parent_object');
422+
expect(masterDetailField.deleteBehavior).toBe('cascade');
423+
});
424+
});
425+
});

packages/spec/src/ui/action.test.ts

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { ActionSchema, ActionParamSchema, type Action } from './action.zod';
2+
import { ActionSchema, ActionParamSchema, Action, type Action as ActionType } from './action.zod';
33

44
describe('ActionParamSchema', () => {
55
it('should accept minimal action parameter', () => {
@@ -43,7 +43,7 @@ describe('ActionParamSchema', () => {
4343
describe('ActionSchema', () => {
4444
describe('Basic Action Properties', () => {
4545
it('should accept minimal action', () => {
46-
const action: Action = {
46+
const action: ActionType = {
4747
name: 'approve',
4848
label: 'Approve',
4949
};
@@ -66,7 +66,7 @@ describe('ActionSchema', () => {
6666
});
6767

6868
it('should accept action with icon', () => {
69-
const action: Action = {
69+
const action: ActionType = {
7070
name: 'delete_record',
7171
label: 'Delete',
7272
icon: 'trash-2',
@@ -81,7 +81,7 @@ describe('ActionSchema', () => {
8181
const types = ['script', 'url', 'modal', 'flow', 'api'] as const;
8282

8383
types.forEach(type => {
84-
const action: Action = {
84+
const action: ActionType = {
8585
name: 'test_action',
8686
label: 'Test',
8787
type,
@@ -112,7 +112,7 @@ describe('ActionSchema', () => {
112112
'global_nav',
113113
] as const;
114114

115-
const action: Action = {
115+
const action: ActionType = {
116116
name: 'multi_location',
117117
label: 'Multi Location',
118118
locations,
@@ -122,7 +122,7 @@ describe('ActionSchema', () => {
122122
});
123123

124124
it('should accept single location', () => {
125-
const action: Action = {
125+
const action: ActionType = {
126126
name: 'toolbar_action',
127127
label: 'Toolbar Action',
128128
locations: ['list_toolbar'],
@@ -134,7 +134,7 @@ describe('ActionSchema', () => {
134134

135135
describe('Action Targets', () => {
136136
it('should accept URL action with target', () => {
137-
const action: Action = {
137+
const action: ActionType = {
138138
name: 'open_external',
139139
label: 'Open External',
140140
type: 'url',
@@ -145,7 +145,7 @@ describe('ActionSchema', () => {
145145
});
146146

147147
it('should accept flow action with target', () => {
148-
const action: Action = {
148+
const action: ActionType = {
149149
name: 'run_approval_flow',
150150
label: 'Run Approval',
151151
type: 'flow',
@@ -156,7 +156,7 @@ describe('ActionSchema', () => {
156156
});
157157

158158
it('should accept API action with target', () => {
159-
const action: Action = {
159+
const action: ActionType = {
160160
name: 'call_api',
161161
label: 'Call API',
162162
type: 'api',
@@ -169,7 +169,7 @@ describe('ActionSchema', () => {
169169

170170
describe('Action Parameters', () => {
171171
it('should accept action with parameters', () => {
172-
const action: Action = {
172+
const action: ActionType = {
173173
name: 'transfer_ownership',
174174
label: 'Transfer Ownership',
175175
type: 'script',
@@ -193,7 +193,7 @@ describe('ActionSchema', () => {
193193
});
194194

195195
it('should accept action with select parameter', () => {
196-
const action: Action = {
196+
const action: ActionType = {
197197
name: 'change_status',
198198
label: 'Change Status',
199199
params: [
@@ -216,7 +216,7 @@ describe('ActionSchema', () => {
216216

217217
describe('UX Behavior', () => {
218218
it('should accept action with confirmation', () => {
219-
const action: Action = {
219+
const action: ActionType = {
220220
name: 'delete_all',
221221
label: 'Delete All',
222222
confirmText: 'Are you sure you want to delete all records? This cannot be undone.',
@@ -226,7 +226,7 @@ describe('ActionSchema', () => {
226226
});
227227

228228
it('should accept action with success message', () => {
229-
const action: Action = {
229+
const action: ActionType = {
230230
name: 'send_notification',
231231
label: 'Send Notification',
232232
successMessage: 'Notification sent successfully!',
@@ -236,7 +236,7 @@ describe('ActionSchema', () => {
236236
});
237237

238238
it('should accept action that refreshes view', () => {
239-
const action: Action = {
239+
const action: ActionType = {
240240
name: 'update_status',
241241
label: 'Update Status',
242242
refreshAfter: true,
@@ -246,7 +246,7 @@ describe('ActionSchema', () => {
246246
});
247247

248248
it('should accept action with all UX properties', () => {
249-
const action: Action = {
249+
const action: ActionType = {
250250
name: 'complete_task',
251251
label: 'Complete Task',
252252
confirmText: 'Mark this task as complete?',
@@ -260,7 +260,7 @@ describe('ActionSchema', () => {
260260

261261
describe('Visibility Control', () => {
262262
it('should accept action with visibility formula', () => {
263-
const action: Action = {
263+
const action: ActionType = {
264264
name: 'approve',
265265
label: 'Approve',
266266
visible: 'status == "pending" && user.can_approve',
@@ -272,7 +272,7 @@ describe('ActionSchema', () => {
272272

273273
describe('Real-World Action Examples', () => {
274274
it('should accept approve opportunity action', () => {
275-
const approveAction: Action = {
275+
const approveAction: ActionType = {
276276
name: 'approve_opportunity',
277277
label: 'Approve',
278278
icon: 'check-circle',
@@ -289,7 +289,7 @@ describe('ActionSchema', () => {
289289
});
290290

291291
it('should accept transfer case action with parameters', () => {
292-
const transferAction: Action = {
292+
const transferAction: ActionType = {
293293
name: 'transfer_case',
294294
label: 'Transfer Case',
295295
icon: 'arrow-right',
@@ -323,7 +323,7 @@ describe('ActionSchema', () => {
323323
});
324324

325325
it('should accept send email action', () => {
326-
const emailAction: Action = {
326+
const emailAction: ActionType = {
327327
name: 'send_quote',
328328
label: 'Send Quote',
329329
icon: 'mail',
@@ -355,7 +355,7 @@ describe('ActionSchema', () => {
355355
});
356356

357357
it('should accept export to Excel action', () => {
358-
const exportAction: Action = {
358+
const exportAction: ActionType = {
359359
name: 'export_excel',
360360
label: 'Export to Excel',
361361
icon: 'file-spreadsheet',
@@ -369,7 +369,7 @@ describe('ActionSchema', () => {
369369
});
370370

371371
it('should accept delete action with confirmation', () => {
372-
const deleteAction: Action = {
372+
const deleteAction: ActionType = {
373373
name: 'delete_record',
374374
label: 'Delete',
375375
icon: 'trash-2',
@@ -386,7 +386,7 @@ describe('ActionSchema', () => {
386386
});
387387

388388
it('should accept clone record action', () => {
389-
const cloneAction: Action = {
389+
const cloneAction: ActionType = {
390390
name: 'clone_record',
391391
label: 'Clone',
392392
icon: 'copy',
@@ -409,7 +409,7 @@ describe('ActionSchema', () => {
409409
});
410410

411411
it('should accept open external link action', () => {
412-
const linkAction: Action = {
412+
const linkAction: ActionType = {
413413
name: 'view_on_map',
414414
label: 'View on Map',
415415
icon: 'map-pin',
@@ -423,3 +423,50 @@ describe('ActionSchema', () => {
423423
});
424424
});
425425
});
426+
427+
describe('Action Factory', () => {
428+
it('should create action with default values via factory', () => {
429+
const action = Action.create({
430+
name: 'test_action',
431+
label: 'Test Action',
432+
});
433+
434+
expect(action.name).toBe('test_action');
435+
expect(action.label).toBe('Test Action');
436+
expect(action.type).toBe('script');
437+
expect(action.refreshAfter).toBe(false);
438+
});
439+
440+
it('should create action without refreshAfter property (uses default)', () => {
441+
const action = Action.create({
442+
name: 'send_email',
443+
label: 'Send Email',
444+
type: 'flow',
445+
target: 'email_flow',
446+
});
447+
448+
expect(action.refreshAfter).toBe(false);
449+
});
450+
451+
it('should create action with explicit refreshAfter', () => {
452+
const action = Action.create({
453+
name: 'update_record',
454+
label: 'Update',
455+
refreshAfter: true,
456+
});
457+
458+
expect(action.refreshAfter).toBe(true);
459+
});
460+
461+
it('should validate snake_case name in factory', () => {
462+
expect(() => Action.create({
463+
name: 'invalidName',
464+
label: 'Invalid',
465+
})).toThrow();
466+
467+
expect(() => Action.create({
468+
name: 'valid_name',
469+
label: 'Valid',
470+
})).not.toThrow();
471+
});
472+
});

0 commit comments

Comments
 (0)