add validator tests

This commit is contained in:
Bowen Tan 2021-12-27 17:32:55 +08:00
parent 6bcee65a54
commit 27b5a3eb60
12 changed files with 390 additions and 66 deletions

View File

@ -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);

View File

@ -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: [],
},
];

View 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.`
);
});
});
});

View 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);
})
});
});

View 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',
},
},
},
],
},
},
],
},
];

View 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);
});
});
});

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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);
}
});
});
});

View File

@ -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];