mithril-ui-form
Version:
Convert a JSON file or object to a dynamic, materialized form.
806 lines (728 loc) • 28.1 kB
text/typescript
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);
});
});
});