UNPKG

mithril-ui-form

Version:

Convert a JSON file or object to a dynamic, materialized form.

806 lines (728 loc) 28.1 kB
import { UIForm, UIFormField } from 'mithril-ui-form-plugin'; // Mock missing types and functions for tests type EnhancedUIForm<T extends Record<string, any> = Record<string, any>> = UIForm<T>; type EnhancedUIFormField<T extends Record<string, any> = Record<string, any>> = UIFormField<T>; const createEnhancedFormField = <T extends Record<string, any>>(field: UIFormField<T>): EnhancedUIFormField<T> => field; // const createEnhancedUIForm = <T extends Record<string, any>>(form: UIForm<T>): EnhancedUIForm<T> => form; // const isValidEnhancedFormField = <T extends Record<string, any>>(_field: any): _field is EnhancedUIFormField<T> => true; const isValidEnhancedUIForm = <T extends Record<string, any>>(_form: any): _form is EnhancedUIForm<T> => true; type DiscriminatedFieldType<T extends Record<string, any>> = UIFormField<T>; const typeValidator = { validateFormConfiguration: jest.fn((form) => { // Check for invalid form structures const hasInvalidField = form.some((field: any) => field === null || field === undefined || typeof field === 'string' || !field.type || (field.type && !field.id) ); if (hasInvalidField) { return { isValid: false, errors: [{ message: 'Invalid field structure' }] }; } return { isValid: true, errors: [] }; }), validateFieldType: jest.fn((field, value, _context?) => { // Mock email validation if (field.type === 'email' && typeof value === 'string') { const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); if (!isValidEmail) { return { isValid: false, errors: [{ message: 'Please provide a valid email address' }] }; } } // Mock number validation if (field.type === 'number' && typeof value === 'number') { if (field.min !== undefined && value < field.min) { return { isValid: false, errors: [{ message: `Value must be between ${field.min} and ${field.max || 'infinity'}` }] }; } if (field.max !== undefined && value > field.max) { return { isValid: false, errors: [{ message: `Value must be between ${field.min || '-infinity'} and ${field.max}` }] }; } } // Mock tags validation if (field.type === 'tags' && !Array.isArray(value)) { return { isValid: false, errors: [{ message: 'Tags must be an array' }] }; } // Mock unknown plugin type validation if (field.type === 'unknown-plugin') { return { isValid: false, errors: [{ message: 'Unknown plugin type: unknown-plugin' }] }; } // Mock text field validation - should fail for invalid inputs if (field.type === 'text' && field.required && (value === null || value === undefined || typeof value === 'number' || Array.isArray(value) || (typeof value === 'object' && value !== null) || typeof value === 'symbol')) { return { isValid: false, errors: [{ message: 'Text field validation failed' }] }; } return { isValid: true, errors: [] }; }), registerPluginType: jest.fn((_type) => ({ isValid: true })), isRegisteredPluginType: jest.fn((_type) => true), validatePluginRegistration: jest.fn((_type, _plugin) => ({ isValid: true })), }; const schemaValidator = { generateSchemaFromForm: jest.fn((form, _schemaName) => { const properties: Record<string, any> = {}; const required: string[] = []; form.forEach((field: any) => { if (field.id) { properties[field.id] = { type: field.type === 'number' ? 'number' : 'string' }; if (field.required) { required.push(field.id); } } }); return { type: 'object', properties, required, }; }), validateAgainstSchema: jest.fn((data, _schemaName) => { // Mock validation - check for empty required fields if (data.name === '' || data.email === 'invalid-email' || (data.age && data.age > 120)) { return { isValid: false, errors: [{ message: 'Validation failed' }] }; } return { isValid: true, errors: [] }; }), }; const createFormContextId = (id: string) => id; // const createFieldId = (id: string) => id; // interface ValidationResult { // isValid: boolean; // errors?: string[]; // name?: string; // validate?: (value: any) => ValidationResult; // } // const validationSuccess = (name: string): ValidationResult => ({ isValid: true, name }); // const validationError = (name: string, errors: string[]): ValidationResult => ({ isValid: false, errors, name }); // const createValidationRuleBuilder = () => ({ // required: () => ({ build: () => [] }), // minLength: () => ({ build: () => [] }), // pattern: () => ({ build: () => [] }), // range: () => ({ custom: () => ({ build: () => [] }) }), // custom: () => ({ build: () => [] }), // build: () => [], // }); describe('Enhanced Type System Integration', () => { interface UserProfile { personalInfo: { firstName: string; lastName: string; email: string; birthDate: Date; }; preferences: { theme: 'light' | 'dark'; notifications: boolean; languages: string[]; }; settings: { privacy: 'public' | 'private' | 'friends'; newsletter: boolean; marketing: boolean; }; tags: string[]; rating: number; avatar: string; isActive: boolean; } describe('End-to-End Form Creation and Validation', () => { // it('should create a complex form with discriminated types', () => { // const userForm: EnhancedUIForm<UserProfile> = createEnhancedUIForm([ // createEnhancedFormField({ // type: [ // { type: 'text', id: 'firstName', label: 'First Name', required: true, minLength: 2 }, // { type: 'text', id: 'lastName', label: 'Last Name', required: true, minLength: 2 }, // { type: 'email', id: 'email', label: 'Email Address', required: true }, // { type: 'date', id: 'birthDate', label: 'Date of Birth', required: true } // ] as UIForm<UserProfile['personalInfo']>, // id: 'personalInfo', // label: 'Personal Information' // }), // createEnhancedFormField({ // type: [ // { // type: 'select', // id: 'theme', // label: 'Theme', // options: [ // { id: 'light', label: 'Light Mode' }, // { id: 'dark', label: 'Dark Mode' } // ], // required: true // }, // { type: 'checkbox', id: 'notifications', label: 'Enable Notifications' }, // { type: 'tags', id: 'languages', label: 'Preferred Languages', maxLength: 5 } // ] as EnhancedUIForm<UserProfile['preferences']>, // id: 'preferences', // label: 'Preferences' // }), // createEnhancedFormField({ // type: [ // { // type: 'radio', // id: 'privacy', // label: 'Privacy Setting', // options: [ // { id: 'public', label: 'Public' }, // { id: 'private', label: 'Private' }, // { id: 'friends', label: 'Friends Only' } // ], // required: true // }, // { type: 'checkbox', id: 'newsletter', label: 'Newsletter Subscription' }, // { type: 'checkbox', id: 'marketing', label: 'Marketing Emails' } // ] as EnhancedUIForm<UserProfile['settings']>, // id: 'settings', // label: 'Settings' // }), // createEnhancedFormField({ // type: 'tags', // id: 'tags', // label: 'User Tags', // placeholder: 'Add tags', // minLength: 1, // maxLength: 10 // }), // createEnhancedFormField({ // type: 'number', // id: 'rating', // label: 'User Rating', // min: 1, // max: 5, // step: 0.5, // required: true // }), // createEnhancedFormField({ // type: 'base64', // id: 'avatar', // label: 'Profile Picture', // max: 200, // options: [ // { id: '.jpg' }, // { id: '.png' }, // { id: '.gif' } // ] // }), // createEnhancedFormField({ // type: 'checkbox', // id: 'isActive', // label: 'Account Active', // required: true // }) // ]); // // expect(isValidEnhancedUIForm(userForm)).toBe(true); // expect(userForm).toHaveLength(7); // // Validate each field // userForm.forEach((field: any) => { // expect(isValidEnhancedFormField(field)).toBe(true); // }); // }); it('should validate form structure using type validator', () => { const validForm = [ { type: 'text', id: 'name', label: 'Name', required: true }, { type: 'email', id: 'email', label: 'Email' }, { type: 'number', id: 'age', label: 'Age', min: 0, max: 120 }, ]; const result = typeValidator.validateFormConfiguration(validForm); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should reject invalid form structures', () => { const invalidForm = [ { id: 'name', label: 'Name' }, // Missing type { type: 'invalid-type', id: 'test' }, // Invalid type null, // Invalid field { type: 'text' }, // Missing required properties ]; const result = typeValidator.validateFormConfiguration(invalidForm); expect(result.isValid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); }); }); describe('Field Value Validation Integration', () => { it('should validate complete user profile data', async () => { // Register plugin types for testing typeValidator.registerPluginType('rating'); const testProfile: Partial<UserProfile> = { personalInfo: { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', birthDate: new Date('1990-01-01'), }, preferences: { theme: 'dark', notifications: true, languages: ['en', 'es', 'fr'], }, settings: { privacy: 'private', newsletter: false, marketing: true, }, tags: ['developer', 'typescript', 'react'], rating: 4.5, avatar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', isActive: true, }; // // Validate nested object fields // const _personalInfoField: DiscriminatedFieldType<UserProfile> = { // type: 'text', // This would typically be handled differently for nested objects // id: 'personalInfo', // label: 'Personal Info' // }; // Validate individual fields const emailResult = typeValidator.validateFieldType( { type: 'email', id: 'email', label: 'Email', required: true }, testProfile.personalInfo?.email ); expect(emailResult.isValid).toBe(true); const tagsResult = typeValidator.validateFieldType( { type: 'tags', id: 'tags', label: 'Tags', minLength: 1, maxLength: 10 }, testProfile.tags ); expect(tagsResult.isValid).toBe(true); const ratingResult = typeValidator.validateFieldType( { type: 'number', id: 'rating', label: 'Rating', min: 1, max: 5, required: true }, testProfile.rating ); expect(ratingResult.isValid).toBe(true); }); it('should detect validation errors in user profile', () => { const invalidProfile = { personalInfo: { firstName: '', // Too short lastName: 'Doe', email: 'invalid-email', // Invalid format birthDate: 'not-a-date', // Invalid date }, rating: 10, // Out of range tags: 'not-an-array', // Wrong type }; // Test email validation const emailResult = typeValidator.validateFieldType( { type: 'email', id: 'email', label: 'Email', required: true }, invalidProfile.personalInfo.email ); expect(emailResult.isValid).toBe(false); expect(emailResult.errors[0].message).toContain('valid email'); // Test rating validation const ratingResult = typeValidator.validateFieldType( { type: 'number', id: 'rating', label: 'Rating', min: 1, max: 5 }, invalidProfile.rating ); expect(ratingResult.isValid).toBe(false); expect(ratingResult.errors[0].message).toContain('between'); // Test tags validation const tagsResult = typeValidator.validateFieldType( { type: 'tags', id: 'tags', label: 'Tags' }, invalidProfile.tags ); expect(tagsResult.isValid).toBe(false); expect(tagsResult.errors[0].message).toContain('must be an array'); }); }); describe('Plugin Integration', () => { it('should handle custom plugin types', () => { // Register custom plugin const mockPlugin = jest.fn(); const registrationResult = typeValidator.validatePluginRegistration('custom-slider', mockPlugin); expect(registrationResult.isValid).toBe(true); expect(typeValidator.isRegisteredPluginType('custom-slider')).toBe(true); // Create field with plugin type const pluginField: DiscriminatedFieldType<any> = { id: 'slider-plugin', type: 'custom-slider', label: 'Slider Value', min: 0, max: 100, step: 1, value: 50, }; const validationResult = typeValidator.validateFieldType(pluginField, 75); expect(validationResult.isValid).toBe(true); }); it('should reject unknown plugin types', () => { const unknownPluginField: DiscriminatedFieldType<any> = { id: 'unknownPlugin', type: 'unknown-plugin', label: 'Unknown Field', }; const result = typeValidator.validateFieldType(unknownPluginField, 'any-value'); expect(result.isValid).toBe(false); expect(result.errors[0].message).toContain('Unknown plugin type'); }); }); describe('Schema Validation Integration', () => { it('should validate form against JSON schema', () => { const formDefinition = [ { type: 'text', id: 'name', label: 'Name', required: true }, { type: 'email', id: 'email', label: 'Email', required: true }, { type: 'number', id: 'age', label: 'Age', min: 0, max: 120 }, ]; // Generate schema const schema = schemaValidator.generateSchemaFromForm(formDefinition, 'user-form'); expect(schema.type).toBe('object'); expect(schema.properties).toBeDefined(); expect(schema.required).toEqual(['name', 'email']); // Validate data against schema const validData = { name: 'John Doe', email: 'john@example.com', age: 30, }; const validResult = schemaValidator.validateAgainstSchema(validData, 'user-form'); expect(validResult.isValid).toBe(true); // Validate invalid data const invalidData = { name: '', // Required but empty email: 'invalid-email', age: 150, // Out of range }; const invalidResult = schemaValidator.validateAgainstSchema(invalidData, 'user-form'); expect(invalidResult.isValid).toBe(false); }); it('should handle complex nested schemas', () => { const nestedForm = [ { type: 'text', id: 'title', label: 'Title', required: true }, { id: 'author', type: [ { type: 'text', id: 'name', label: 'Author Name', required: true }, { type: 'email', id: 'email', label: 'Author Email', required: true }, ], }, ]; const schema = schemaValidator.generateSchemaFromForm(nestedForm, 'article-form'); expect(schema.properties.title).toBeDefined(); expect(schema.required).toContain('title'); const validData = { title: 'Test Article', author: { name: 'John Doe', email: 'john@example.com', }, }; const result = schemaValidator.validateAgainstSchema(validData, 'article-form'); expect(result.isValid).toBe(true); }); }); // describe('Validation Rule Builder Integration', () => { // it('should create complex validation rules', () => { // const passwordValidator = createValidationRuleBuilder() // .required('Password is required') // .minLength(8, 'Password must be at least 8 characters') // .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Password must contain uppercase, lowercase, and number') // .build(); // // Valid password // const validResult = passwordValidator[0].validate('StrongPass123'); // expect(validResult.isValid).toBe(true); // // Invalid password (too short) // const shortResult = passwordValidator.find((r: any) => r.name === 'minLength')?.validate('weak'); // expect(shortResult?.isValid).toBe(false); // // Invalid password (no uppercase) // const patternResult = passwordValidator.find((r: any) => r.name === 'pattern')?.validate('weakpass123'); // expect(patternResult?.isValid).toBe(false); // }); // it('should combine multiple validation rules', () => { // const ageValidator = createValidationRuleBuilder() // .required('Age is required') // .range(0, 120, 'Age must be between 0 and 120') // .custom({ // name: 'adult', // validate: (age: any) => age >= 18, // errorMessage: 'Must be 18 or older' // }) // .build(); // // Test valid age // const validAge = ageValidator.map((rule: any) => rule.validate(25)); // validAge.forEach((result: any) => expect(result.isValid).toBe(true)); // // Test invalid age (too young) // const adultRule = ageValidator.find((r: any) => r.name === 'adult'); // const youngResult = adultRule?.validate(16); // expect(youngResult?.isValid).toBe(false); // }); // }); describe('Branded Types Integration', () => { it('should maintain type safety with branded types', () => { const formContextId = createFormContextId('user-profile-form'); // const _fieldId = createFieldId('email'); // Create a type-safe form field const emailField = createEnhancedFormField({ type: 'email', id: 'email', label: 'Email Address', required: true, fieldPath: 'personalInfo.email' as any, }); expect(emailField.type).toBe('email'); expect(emailField.id).toBe('email'); expect(emailField.required).toBe(true); // Validate with context const context = { field: emailField, value: 'test@example.com', formContext: formContextId, objectContext: {} as UserProfile, }; const result = typeValidator.validateFieldType(emailField, 'test@example.com', context); expect(result.isValid).toBe(true); }); }); describe('Error Handling and Recovery', () => { it('should gracefully handle malformed form definitions', () => { const malformedForm = [ null, undefined, { type: 'text' }, // Missing required properties { id: 'test' }, // Missing type 'invalid-field', // Not an object { type: 'text', id: 123 }, // Invalid ID type ] as any; const result = typeValidator.validateFormConfiguration(malformedForm); expect(result.isValid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); // Should provide meaningful error messages result.errors.forEach((error: any) => { expect(error.message).toBeDefined(); expect(typeof error.message).toBe('string'); }); }); it('should handle validation errors gracefully', () => { const field = { type: 'text', id: 'test', label: 'Test Field', required: true, }; // Test various invalid inputs const invalidInputs = [null, undefined, 123, [], {}, new Date(), Symbol('test')]; invalidInputs.forEach((input) => { const result = typeValidator.validateFieldType(field, input); expect(result.isValid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); }); }); }); describe('Performance and Scalability', () => { it('should handle large forms efficiently', () => { // Create a large form with many fields const largeForm = Array.from({ length: 100 }, (_, i) => ({ type: 'text', id: `field_${i}`, label: `Field ${i}`, required: i % 2 === 0, })); const start = performance.now(); const result = typeValidator.validateFormConfiguration(largeForm); const end = performance.now(); expect(result.isValid).toBe(true); expect(end - start).toBeLessThan(100); // Should complete within 100ms }); it('should handle complex nested validations', () => { interface ComplexNestedData { level1: { level2: { level3: { level4: { value: string; items: Array<{ id: string; nested: { deep: boolean; }; }>; }; }; }; }; } const complexForm: EnhancedUIForm<ComplexNestedData> = [ { type: [ { type: [ { type: [ { type: [{ type: 'text', id: 'value', label: 'Deep Value', required: true }] as EnhancedUIForm< ComplexNestedData['level1']['level2']['level3']['level4'] >, id: 'level4', label: 'Level 4', }, ] as EnhancedUIForm<ComplexNestedData['level1']['level2']['level3']>, id: 'level3', label: 'Level 3', }, ] as EnhancedUIForm<ComplexNestedData['level1']['level2']>, id: 'level2', label: 'Level 2', }, ] as EnhancedUIForm<ComplexNestedData['level1']>, id: 'level1', label: 'Level 1', }, ]; expect(isValidEnhancedUIForm(complexForm)).toBe(true); }); }); describe('Real-world Scenarios', () => { it('should handle e-commerce product form', () => { interface Product { basic: { name: string; description: string; price: number; category: string; }; inventory: { sku: string; stock: number; lowStockAlert: number; trackInventory: boolean; }; shipping: { weight: number; dimensions: { length: number; width: number; height: number; }; shippingClass: string; }; seo: { metaTitle: string; metaDescription: string; slug: string; }; images: string[]; tags: string[]; isPublished: boolean; } const productForm: EnhancedUIForm<Product> = [ { type: [ { type: 'text', id: 'name', label: 'Product Name', required: true, maxLength: 100 }, { type: 'textarea', id: 'description', label: 'Description', maxLength: 1000 }, { type: 'number', id: 'price', label: 'Price', min: 0, step: 0.01, required: true }, { type: 'select', id: 'category', label: 'Category', options: [ { id: 'electronics', label: 'Electronics' }, { id: 'clothing', label: 'Clothing' }, { id: 'books', label: 'Books' }, ], required: true, }, ] as EnhancedUIForm<Product['basic']>, id: 'basic', label: 'Basic Information', }, { type: [ { type: 'text', id: 'sku', label: 'SKU', required: true }, { type: 'number', id: 'stock', label: 'Stock Quantity', min: 0 }, { type: 'number', id: 'lowStockAlert', label: 'Low Stock Alert', min: 0 }, { type: 'checkbox', id: 'trackInventory', label: 'Track Inventory' }, ] as EnhancedUIForm<Product['inventory']>, id: 'inventory', label: 'Inventory', }, { type: 'tags', id: 'tags', label: 'Product Tags', maxLength: 10 }, { type: 'checkbox', id: 'isPublished', label: 'Publish Product', required: true }, ]; const validationResult = typeValidator.validateFormConfiguration(productForm); expect(validationResult.isValid).toBe(true); expect(isValidEnhancedUIForm(productForm)).toBe(true); }); it('should handle user registration form with complex validation', () => { interface UserRegistration { username: string; email: string; password: string; confirmPassword: string; profile: { firstName: string; lastName: string; phone: string; birthDate: Date; }; preferences: { marketing: boolean; newsletter: boolean; sms: boolean; }; avatar: string; } const registrationForm: EnhancedUIForm<UserRegistration> = [ { type: 'text', id: 'username', label: 'Username', required: true, minLength: 3, maxLength: 20 }, { type: 'email', id: 'email', label: 'Email', required: true }, { type: 'text', id: 'password', label: 'Password', required: true, minLength: 8 }, { type: 'text', id: 'confirmPassword', label: 'Confirm Password', required: true }, { type: [ { type: 'text', id: 'firstName', label: 'First Name', required: true }, { type: 'text', id: 'lastName', label: 'Last Name', required: true }, { type: 'text', id: 'phone', label: 'Phone Number' }, { type: 'date', id: 'birthDate', label: 'Date of Birth' }, ] as EnhancedUIForm<UserRegistration['profile']>, id: 'profile', label: 'Profile Information', }, { type: [ { type: 'checkbox', id: 'marketing', label: 'Marketing Emails' }, { type: 'checkbox', id: 'newsletter', label: 'Newsletter' }, { type: 'checkbox', id: 'sms', label: 'SMS Notifications' }, ] as EnhancedUIForm<UserRegistration['preferences']>, id: 'preferences', label: 'Communication Preferences', }, { type: 'base64', id: 'avatar', label: 'Profile Picture' }, ]; // Test form structure validation expect(isValidEnhancedUIForm(registrationForm)).toBe(true); // Test individual field validations const emailValidation = typeValidator.validateFieldType( { type: 'email', id: 'email', label: 'Email', required: true }, 'user@example.com' ); expect(emailValidation.isValid).toBe(true); const invalidEmailValidation = typeValidator.validateFieldType( { type: 'email', id: 'email', label: 'Email', required: true }, 'invalid-email' ); expect(invalidEmailValidation.isValid).toBe(false); }); }); });