mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-27 08:39:59 +08:00
add validator tests
This commit is contained in:
parent
6bcee65a54
commit
27b5a3eb60
@ -1,72 +1,83 @@
|
||||
import { ApplicationModel } from '../../src/AppModel/AppModel';
|
||||
import {
|
||||
ComponentId,
|
||||
ComponentType,
|
||||
} from '../../src/AppModel/IAppModel';
|
||||
import {AppSchema} from './schema'
|
||||
import { ComponentId, ComponentType } from '../../src/AppModel/IAppModel';
|
||||
import { AppSchema, DuplicatedIdSchema } from './schema';
|
||||
|
||||
describe('AppModel test', () => {
|
||||
const appModel = new ApplicationModel(AppSchema.spec.components);
|
||||
it('init corectlly', () => {
|
||||
expect(appModel.allComponents.length).toBe(10)
|
||||
expect(appModel.topComponents.length).toBe(2)
|
||||
});
|
||||
describe('resolve tree', () => {
|
||||
it('resolve Tree corectlly', () => {
|
||||
expect(appModel.allComponents.length).toBe(10);
|
||||
expect(appModel.topComponents.length).toBe(2);
|
||||
});
|
||||
|
||||
it('components order', () => {
|
||||
expect(appModel.allComponents[0].id).toBe('hstack1')
|
||||
expect(appModel.allComponents[1].id).toBe('vstack1')
|
||||
expect(appModel.allComponents[2].id).toBe('text3')
|
||||
expect(appModel.allComponents[3].id).toBe('text4')
|
||||
expect(appModel.allComponents[4].id).toBe('hstack2')
|
||||
expect(appModel.allComponents[5].id).toBe('text1')
|
||||
expect(appModel.allComponents[6].id).toBe('text2')
|
||||
expect(appModel.allComponents[7].id).toBe('button1')
|
||||
expect(appModel.allComponents[8].id).toBe('moduleContainer1')
|
||||
expect(appModel.allComponents[9].id).toBe('apiFetch')
|
||||
it('components order', () => {
|
||||
expect(appModel.allComponents[0].id).toBe('hstack1');
|
||||
expect(appModel.allComponents[1].id).toBe('vstack1');
|
||||
expect(appModel.allComponents[2].id).toBe('text3');
|
||||
expect(appModel.allComponents[3].id).toBe('text4');
|
||||
expect(appModel.allComponents[4].id).toBe('hstack2');
|
||||
expect(appModel.allComponents[5].id).toBe('text1');
|
||||
expect(appModel.allComponents[6].id).toBe('text2');
|
||||
expect(appModel.allComponents[7].id).toBe('button1');
|
||||
expect(appModel.allComponents[8].id).toBe('moduleContainer1');
|
||||
expect(appModel.allComponents[9].id).toBe('apiFetch');
|
||||
});
|
||||
|
||||
it('detect duplicated dd', () => {
|
||||
try {
|
||||
new ApplicationModel(DuplicatedIdSchema);
|
||||
} catch (e: any) {
|
||||
expect(e.message).toBe('Duplicate component id: hstack1');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('to schema', () => {
|
||||
const schema = appModel.toSchema();
|
||||
expect(AppSchema.spec.components).toStrictEqual(schema)
|
||||
})
|
||||
expect(AppSchema.spec.components).toStrictEqual(schema);
|
||||
});
|
||||
|
||||
it('create component', () => {
|
||||
const newComponent = appModel.createComponent('core/v1/text' as ComponentType);
|
||||
expect(newComponent.id).toEqual('text10');
|
||||
expect(newComponent.type).toEqual('core/v1/text');
|
||||
expect(newComponent.appModel).toBe(appModel);
|
||||
|
||||
});
|
||||
|
||||
it('create component with id', () => {
|
||||
const newComponent = appModel.createComponent('core/v1/text' as ComponentType, 'text1000' as ComponentId);
|
||||
const newComponent = appModel.createComponent(
|
||||
'core/v1/text' as ComponentType,
|
||||
'text1000' as ComponentId
|
||||
);
|
||||
expect(newComponent.id).toEqual('text1000');
|
||||
})
|
||||
});
|
||||
|
||||
it('get component', () => {
|
||||
const c = appModel.getComponentById('text1' as ComponentId);
|
||||
expect(c?.id).toEqual('text1');
|
||||
});
|
||||
it ('get component doesnt exist', () => {
|
||||
it('get component doesnt exist', () => {
|
||||
const c = appModel.getComponentById('hello' as ComponentId);
|
||||
expect(c).toEqual(undefined);
|
||||
})
|
||||
});
|
||||
|
||||
it('append component to top level', () => {
|
||||
const newComponent = appModel.createComponent('core/v1/text' as ComponentType);
|
||||
appModel.appendChild(newComponent);
|
||||
expect(appModel.allComponents.length).toBe(11);
|
||||
expect(appModel.topComponents[appModel.topComponents.length - 1].id).toBe(newComponent.id);
|
||||
expect(appModel.topComponents[appModel.topComponents.length - 1].id).toBe(
|
||||
newComponent.id
|
||||
);
|
||||
expect(appModel.getComponentById(newComponent.id)).toBe(newComponent);
|
||||
expect(newComponent.appModel).toBe(appModel);
|
||||
|
||||
})
|
||||
});
|
||||
it('can append component from other appModel', () => {
|
||||
const appModel2 = new ApplicationModel(AppSchema.spec.components);
|
||||
const newComponent2 = appModel2.createComponent('core/v1/text' as ComponentType);
|
||||
expect(newComponent2.appModel).not.toBe(appModel);
|
||||
appModel.appendChild(newComponent2);
|
||||
expect(newComponent2.appModel).toBe(appModel);
|
||||
})
|
||||
});
|
||||
|
||||
describe('remove component', () => {
|
||||
const appModel = new ApplicationModel(AppSchema.spec.components);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Application } from '@sunmao-ui/core';
|
||||
import { Application, ApplicationComponent } from '@sunmao-ui/core';
|
||||
|
||||
export const AppSchema: Application = {
|
||||
kind: 'Application',
|
||||
@ -127,3 +127,24 @@ export const AppSchema: Application = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const DuplicatedIdSchema: ApplicationComponent[] = [
|
||||
{
|
||||
id: 'hstack1',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'hstack1',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'hstack3',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [],
|
||||
},
|
||||
];
|
||||
|
19
packages/editor/__tests__/validator/allComponents.spec.ts
Normal file
19
packages/editor/__tests__/validator/allComponents.spec.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { registry } from '../../src/setup';
|
||||
import { ComponentInvalidSchema } from './schema';
|
||||
import { SchemaValidator } from '../../src/validator';
|
||||
|
||||
const schemaValidator = new SchemaValidator(registry);
|
||||
|
||||
describe('Validate all components', () => {
|
||||
const result = schemaValidator.validate(ComponentInvalidSchema);
|
||||
describe('detect orphen components', () => {
|
||||
it('no parent', () => {
|
||||
expect(result[0].message).toBe(`Cannot find parent component: aParent.`);
|
||||
});
|
||||
it('no slot', () => {
|
||||
expect(result[1].message).toBe(
|
||||
`Parent component 'hstack1' does not have slot: aSlot.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
21
packages/editor/__tests__/validator/component.spec.ts
Normal file
21
packages/editor/__tests__/validator/component.spec.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { registry } from '../../src/setup';
|
||||
import { ComponentInvalidSchema,ComponentPropertyExpressionSchema } from './schema';
|
||||
import { SchemaValidator } from '../../src/validator';
|
||||
|
||||
const schemaValidator = new SchemaValidator(registry);
|
||||
|
||||
describe('Validate component', () => {
|
||||
describe('validate component properties', () => {
|
||||
const result = schemaValidator.validate(ComponentInvalidSchema);
|
||||
it('detect missing field', () => {
|
||||
expect(result[0].message).toBe(`must have required property 'format'`);
|
||||
});
|
||||
it('detect wrong type', () => {
|
||||
expect(result[1].message).toBe(`must be string`);
|
||||
});
|
||||
it('ignore expreesion', () => {
|
||||
const result = schemaValidator.validate(ComponentPropertyExpressionSchema);
|
||||
expect(result.length).toBe(0);
|
||||
})
|
||||
});
|
||||
});
|
227
packages/editor/__tests__/validator/schema.ts
Normal file
227
packages/editor/__tests__/validator/schema.ts
Normal file
@ -0,0 +1,227 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
|
||||
export const OrphenComponentSchema: ApplicationComponent[] = [
|
||||
{
|
||||
id: 'hstack1',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'text1',
|
||||
type: 'core/v1/text',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: 'aParent', slot: 'content' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'text2',
|
||||
type: 'core/v1/text',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: 'hstack1', slot: 'aSlot' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const ComponentInvalidSchema: ApplicationComponent[] = [
|
||||
{
|
||||
id: 'text1',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: 'hello',
|
||||
},
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'text2',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: false,
|
||||
format: 'md',
|
||||
},
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const ComponentPropertyExpressionSchema: ApplicationComponent[] = [
|
||||
{
|
||||
id: 'text1',
|
||||
type: 'chakra_ui/v1/list',
|
||||
properties: {
|
||||
listData: '{{data}}',
|
||||
template: '{{template}}',
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const TraitInvalidSchema: ApplicationComponent[] = [
|
||||
{
|
||||
id: 'text1',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: 'hello',
|
||||
format: 'md',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/state',
|
||||
properties: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'text2',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: 'hello',
|
||||
format: 'md',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/state',
|
||||
properties: { key: true, initialValue: 'hhh' },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const EventTraitSchema: ApplicationComponent[] = [
|
||||
{
|
||||
id: 'input1',
|
||||
type: 'chakra_ui/v1/input',
|
||||
properties: {
|
||||
variant: 'outline',
|
||||
placeholder: 'Please input value',
|
||||
size: 'md',
|
||||
isDisabled: false,
|
||||
isRequired: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'button1',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: {
|
||||
raw: 'hello',
|
||||
format: 'md',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/event',
|
||||
properties: {
|
||||
handlers: [
|
||||
{
|
||||
type: 'change',
|
||||
componentId: 'input1',
|
||||
method: {
|
||||
name: 'setInputValue',
|
||||
parameters: {
|
||||
value: '666',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'onClick',
|
||||
componentId: 'dialog1',
|
||||
method: {
|
||||
name: 'setInputValue',
|
||||
parameters: {
|
||||
value: '666',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'onClick',
|
||||
componentId: 'input1',
|
||||
method: {
|
||||
name: 'fetch',
|
||||
parameters: {
|
||||
value: '666',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'onClick',
|
||||
componentId: 'input1',
|
||||
method: {
|
||||
name: 'setInputValue',
|
||||
parameters: {
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const EventTraitTraitMethodSchema: ApplicationComponent[] = [
|
||||
{
|
||||
id: 'text1',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: 'hello',
|
||||
format: 'md',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/state',
|
||||
properties: { key: 'value', initialValue: 'hhh' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'button1',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: {
|
||||
raw: 'hello',
|
||||
format: 'md',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/event',
|
||||
properties: {
|
||||
handlers: [
|
||||
{
|
||||
type: 'onClick',
|
||||
componentId: 'text1',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: {
|
||||
key: 'value',
|
||||
value: '666',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
44
packages/editor/__tests__/validator/trait.spec.ts
Normal file
44
packages/editor/__tests__/validator/trait.spec.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { registry } from '../../src/setup';
|
||||
import {
|
||||
TraitInvalidSchema,
|
||||
EventTraitSchema,
|
||||
EventTraitTraitMethodSchema,
|
||||
} from './schema';
|
||||
import { SchemaValidator } from '../../src/validator';
|
||||
|
||||
const schemaValidator = new SchemaValidator(registry);
|
||||
|
||||
describe('Validate trait', () => {
|
||||
describe('validate trait properties', () => {
|
||||
const result = schemaValidator.validate(TraitInvalidSchema);
|
||||
it('detect missing field', () => {
|
||||
expect(result[0].message).toBe(`must have required property 'key'`);
|
||||
});
|
||||
it('detect wrong type', () => {
|
||||
expect(result[1].message).toBe(`must be string`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate event trait', () => {
|
||||
const result = schemaValidator.validate(EventTraitSchema);
|
||||
console.log('result', result);
|
||||
it('detect wrong event', () => {
|
||||
expect(result[0].message).toBe(`Component does not have event: change.`);
|
||||
});
|
||||
it('detect missing target', () => {
|
||||
expect(result[1].message).toBe(`Event target component is not exist: dialog1.`);
|
||||
});
|
||||
it('detect missing method', () => {
|
||||
expect(result[2].message).toBe(
|
||||
`Event target component does not have method: fetch.`
|
||||
);
|
||||
});
|
||||
it('detect wrong method parameters', () => {
|
||||
expect(result[3].message).toBe(`must be string`);
|
||||
});
|
||||
it('detect method on trait', () => {
|
||||
const result = schemaValidator.validate(EventTraitTraitMethodSchema);
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
@ -121,12 +121,6 @@ export class ApplicationModel implements IApplicationModel {
|
||||
private traverseTree(cb: (c: IComponentModel) => void) {
|
||||
function traverse(root: IComponentModel) {
|
||||
cb(root);
|
||||
if (root.id === 'hstack2') {
|
||||
console.log(
|
||||
'traver',
|
||||
root.children['content' as SlotName].map(c => c.id)
|
||||
);
|
||||
}
|
||||
for (const slot in root.children) {
|
||||
root.children[slot as SlotName].forEach(child => {
|
||||
traverse(child);
|
||||
|
@ -177,11 +177,6 @@ export class ComponentModel implements IComponentModel {
|
||||
slotChildren.splice(slotChildren.indexOf(child), 1);
|
||||
child._isDirty = true;
|
||||
this._isDirty = true;
|
||||
console.log(
|
||||
'after',
|
||||
this.id,
|
||||
this.allComponents.map(c => c.id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,7 +249,6 @@ export class ComponentModel implements IComponentModel {
|
||||
trait.properties[property].update(properties[property]);
|
||||
trait._isDirty = true;
|
||||
}
|
||||
console.log('new trait', trait);
|
||||
this._isDirty = true;
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,6 @@ export class AdjustComponentOrderLeafOperation extends BaseLeafOperation<AdjustC
|
||||
|
||||
switch (orientation) {
|
||||
case 'up':
|
||||
console.log('component.prevSilbling', component.prevSilbling)
|
||||
if (!component.prevSilbling) {
|
||||
console.warn('destination index out of bound');
|
||||
return prev;
|
||||
|
@ -18,7 +18,6 @@ export class RemoveComponentLeafOperation extends BaseLeafOperation<RemoveCompon
|
||||
this.context.componentId as ComponentId
|
||||
);
|
||||
this.beforeComponent = this.deletedComponent?.prevSilbling || undefined;
|
||||
console.log(this.beforeComponent)
|
||||
appModel.removeComponent(this.context.componentId as ComponentId);
|
||||
return appModel.toSchema();
|
||||
}
|
||||
@ -41,10 +40,9 @@ export class RemoveComponentLeafOperation extends BaseLeafOperation<RemoveCompon
|
||||
this.deletedComponent.parentSlot as SlotName
|
||||
);
|
||||
} else {
|
||||
appModel.updateSingleComponent(this.deletedComponent);
|
||||
appModel._registerComponent(this.deletedComponent);
|
||||
}
|
||||
this.deletedComponent.moveAfter(this.beforeComponent?.id || null);
|
||||
console.log(appModel)
|
||||
this.deletedComponent.moveAfter(this.beforeComponent || null);
|
||||
return appModel.toSchema();
|
||||
}
|
||||
}
|
||||
|
@ -70,18 +70,17 @@ export class SchemaValidator implements ISchemaValidator {
|
||||
if (r.length > 0) {
|
||||
this.result = this.result.concat(r);
|
||||
}
|
||||
|
||||
this.traitRules.forEach(rule => {
|
||||
component.traits.forEach(trait => {
|
||||
const r = rule.validate({
|
||||
trait,
|
||||
component,
|
||||
...baseContext,
|
||||
});
|
||||
if (r.length > 0) {
|
||||
this.result = this.result.concat(r);
|
||||
}
|
||||
});
|
||||
this.traitRules.forEach(rule => {
|
||||
component.traits.forEach(trait => {
|
||||
const r = rule.validate({
|
||||
trait,
|
||||
component,
|
||||
...baseContext,
|
||||
});
|
||||
if (r.length > 0) {
|
||||
this.result = this.result.concat(r);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -25,7 +25,6 @@ class TraitPropertyValidatorRule implements TraitValidatorRule {
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
const valid = validate(trait.rawProperties);
|
||||
if (!valid) {
|
||||
validate.errors!.forEach(error => {
|
||||
@ -108,11 +107,9 @@ class EventHandlerValidatorRule implements TraitValidatorRule {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
method.parameters &&
|
||||
!ajv.validate(method.parameters, parameters)
|
||||
) {
|
||||
ajv.errors!.forEach(error => {JSON
|
||||
if (method.parameters && !ajv.validate(method.parameters, parameters)) {
|
||||
ajv.errors!.forEach(error => {
|
||||
JSON;
|
||||
if (error.keyword === 'type') {
|
||||
const { instancePath } = error;
|
||||
const path = instancePath.split('/')[1];
|
||||
|
Loading…
Reference in New Issue
Block a user