ohayolibs
Version:
Ohayo is a set of essential modules for ohayojp.
820 lines (775 loc) • 26.3 kB
text/typescript
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ACLService, OhayoACLModule } from '@ohayo/acl';
import { createTestContext } from '@ohayo/testing';
import { OhayoI18NService, OhayoThemeModule, OHAYO_I18N_TOKEN, OhayoLocaleService, en_US } from '@ohayo/theme';
import { deepCopy } from '@ohayo/util';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzIconService } from 'ng-zorro-antd/icon';
import { of } from 'rxjs';
import { FormPropertyFactory } from '../src/model/form.property.factory';
import { OhayoFormModule } from '../src/module';
import { SFSchema } from '../src/schema/index';
import { WidgetRegistry } from '../src/widget.factory';
import { SCHEMA, SFPage, TestFormComponent } from './base.spec';
describe('form: component', () => {
let fixture: ComponentFixture<TestFormComponent>;
let dl: DebugElement;
let context: TestFormComponent;
let page: SFPage;
function genModule(options: { acl?: boolean; i18n?: boolean } = {}): void {
options = { acl: false, i18n: false, ...options };
const imports = [NoopAnimationsModule, OhayoFormModule.forRoot(), OhayoThemeModule.forRoot()];
if (options.i18n) {
imports.push(OhayoThemeModule.forRoot());
}
if (options.acl) {
imports.push(OhayoACLModule.forRoot());
}
TestBed.configureTestingModule({
imports,
declarations: [TestFormComponent, TestModeComponent],
});
}
function createComp(): void {
fixture.detectChanges();
page = new SFPage(context.comp);
page.prop(dl, context, fixture);
}
describe('', () => {
beforeEach(() => {
genModule();
({ fixture, dl, context } = createTestContext(TestFormComponent));
createComp();
});
describe('[default]', () => {
it('should throw error when parent is not object or array', () => {
expect(() => {
// tslint:disable-next-line: no-string-literal
const factory = context.comp['formPropertyFactory'] as FormPropertyFactory;
factory.createProperty({}, {}, {}, { type: 'invalid', path: 'a' } as any, 'a');
}).toThrowError(`Instanciation of a FormProperty with an unknown parent type: invalid`);
});
it('should throw error when type is invalid', () => {
expect(() => {
context.schema = {
properties: {
a: {
type: 'aa' as any,
},
},
};
fixture.detectChanges();
}).toThrowError(`Undefined type aa`);
});
it('should throw error when is invalid schema', () => {
expect(() => {
context.schema = null;
fixture.detectChanges();
}).toThrowError('Invalid Schema');
});
it(`Don't support string with root ui property`, () => {
expect(() => {
context.schema = {
ui: 'string',
properties: {},
};
fixture.detectChanges();
}).toThrowError(`Don't support string with root ui property`);
});
it('should be used default widget when is invalid schema type', () => {
spyOn(console, 'warn');
expect(console.warn).not.toHaveBeenCalled();
context.schema = {
type: 'string',
properties: {},
ui: {
widget: 'asdf',
},
};
fixture.detectChanges();
expect(console.warn).toHaveBeenCalled();
});
it('should be console debug informations', () => {
spyOn(console, 'warn');
expect(console.warn).not.toHaveBeenCalled();
context.schema = {
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name', 'age'],
ui: {
debug: true,
},
};
fixture.detectChanges();
page.setValue('/name', 'a');
expect(console.warn).toHaveBeenCalled();
});
it('should be console debug informations when ajv throw error', () => {
spyOn(console, 'warn');
expect(console.warn).not.toHaveBeenCalled();
context.schema = {
properties: {
time: {
type: 'string',
ui: { widget: 'date', mode: 'range' },
title: 'Date',
format: 'yyyy-MM-dd HH:mm:ss',
},
},
ui: {
debug: true,
},
};
fixture.detectChanges();
expect(console.warn).toHaveBeenCalled();
});
it('should be ingore required when element is hidden', () => {
const s: SFSchema = {
properties: {
name: {
type: 'string',
ui: { hidden: true },
},
},
required: ['name'],
};
page.newSchema(s);
expect(context.comp._schema.required!.indexOf('name') === -1).toBe(true);
});
it('should be ingore trigger formChange event when call refreshSchema method', () => {
expect(context.formChange).not.toHaveBeenCalled();
page.newSchema({ properties: { name: { type: 'string' } } });
expect(context.formChange).not.toHaveBeenCalled();
});
it('should be hava values when call refreshSchema method after', () => {
page.newSchema({ properties: { name: { type: 'string', default: 'a' } } });
expect(context.formChange).not.toHaveBeenCalled();
expect(context.comp.value.name).toBe('a');
});
});
describe('[button]', () => {
it('should be has a primary button when default value', () => {
page.checkCount('.sf-btns', 1).checkCount('.ant-btn-primary', 1);
});
it('should be null', () => {
context.button = null;
fixture.detectChanges();
page.checkCount('.sf-btns', 1).checkCount('button', 0);
});
it('should be undefined', () => {
context.button = undefined;
fixture.detectChanges();
page.checkCount('.sf-btns', 1).checkCount('button', 0);
});
it('should be none', () => {
context.button = 'none';
fixture.detectChanges();
page.checkCount('.sf-btns', 0);
});
it('should be icon', () => {
context.button = {
submit_icon: {
type: 'search',
},
reset_icon: {
type: 'file',
},
};
fixture.detectChanges();
page.checkCount('[type="submit"] .anticon', 1);
page.checkCount('[type="button"] .anticon', 1);
});
describe('when layout is horizontal', () => {
it('should be has a fix 100px width', () => {
page
.newSchema({
properties: {
name: {
type: 'string',
ui: {
spanLabelFixed: 100,
},
},
},
})
.checkStyle('.sf-btns .ant-form-item-control', 'margin-left', '100px');
});
it('should be specified grid', () => {
const span = 11;
context.button = {
render: {
grid: { span },
},
};
fixture.detectChanges();
page.checkCls('.sf-btns .ant-form-item-control', `ant-col-${span}`);
});
it('should be fixed label', () => {
const spanLabelFixed = 56;
context.button = {
render: {
spanLabelFixed,
},
};
fixture.detectChanges();
page.checkStyle('.sf-btns .ant-form-item-control', 'margin-left', `${spanLabelFixed}px`);
});
});
describe('#size', () => {
it('with small', () => {
context.button = { render: { size: 'small' } };
fixture.detectChanges();
page.checkCount('.ant-btn-sm', 2);
});
it('with large', () => {
context.button = { render: { size: 'large' } };
fixture.detectChanges();
page.checkCount('.ant-btn-lg', 2);
});
});
it('should be update button text when i18n changed', () => {
page.checkElText('.ant-btn-primary', '提交');
const i18n = TestBed.inject<OhayoLocaleService>(OhayoLocaleService) as OhayoLocaleService;
i18n.setLocale(en_US);
fixture.detectChanges();
page.checkElText('.ant-btn-primary', 'Submit');
});
});
describe('properites', () => {
describe('#validate', () => {
it('should be validate when submitted and not liveValidate', () => {
page.submit(false);
expect((page.getEl('.ant-btn-primary') as HTMLButtonElement).disabled).toBe(true);
context.liveValidate = false;
fixture.detectChanges();
page.submit(false).setValue('/name', 'cipchk').setValue('/pwd', '1111').submit(true);
});
});
describe('#submit', () => {
it('should be submit when is valid', () => {
page.setValue('/name', 'cipchk').setValue('/pwd', '1111').isValid();
});
it('should not be submit when is invalid', () => {
page.setValue('/name', 'cipchk').isValid(false);
});
});
describe('#reset', () => {
it('should be set default value', () => {
const schema = deepCopy(SCHEMA.user) as SFSchema;
schema.properties!.name.default = 'cipchk';
page.newSchema(schema).reset().checkValue('/name', 'cipchk');
});
});
describe('#layout', () => {
['horizontal', 'vertical', 'inline'].forEach(type => {
it(`with ${type}`, () => {
context.layout = type;
fixture.detectChanges();
page.checkCls('form', `ant-form-${type}`);
});
});
describe(`when with horizontal`, () => {
it('shoule be fixed label width', () => {
page
.newSchema({
properties: {
name: {
type: 'string',
ui: {
spanLabelFixed: 100,
},
},
},
})
.checkStyle('.ant-form-item-label', 'flex', '0 0 100px')
.checkStyle('.ant-form-item-control', 'maxWidth', 'calc(100% - 100px)');
});
it('should inherit parent node', () => {
page
.newSchema({
properties: {
address: {
type: 'object',
ui: { spanLabelFixed: 98 },
properties: {
city: { type: 'string' },
},
},
},
})
.checkStyle('.ant-form-item-label', 'flex', '0 0 98px')
.checkStyle('.ant-form-item-control', 'maxWidth', 'calc(100% - 98px)');
});
});
});
describe('#autocomplete', () => {
[null, 'on', 'off'].forEach((type: any) => {
it(`with [${type}]`, () => {
context.autocomplete = type;
fixture.detectChanges();
page.checkAttr('form', 'autocomplete', type, !!type);
});
});
});
describe('#firstVisual', () => {
it('with false', () => {
context.firstVisual = false;
fixture.detectChanges();
page.checkCount('.ant-form-item-explain', 0);
});
it('with true', () => {
context.firstVisual = true;
fixture.detectChanges();
page.checkCount('.ant-form-item-explain', 2);
});
});
describe('#onlyVisual', () => {
it('with false', () => {
context.onlyVisual = false;
fixture.detectChanges();
page.checkCount('.sf__no-error', 0);
page.checkCount('.ant-form-item-explain', 2);
});
it('with true', () => {
context.onlyVisual = true;
fixture.detectChanges();
page.checkCount('.sf__no-error', 1);
page.checkCount('.ant-form-item-explain', 0);
});
});
it('#disabled', () => {
const CLS: { [key: string]: string | Array<NzSafeAny> } = {
input: '.ant-input[disabled]',
number: '.ant-input-number-disabled',
switch: '.ant-switch-disabled',
};
page.newSchema({
properties: {
ipt: { type: 'string' },
number: { type: 'number' },
switch: { type: 'boolean' },
select: { type: 'string', enum: ['A'] },
},
});
context.disabled = false;
fixture.detectChanges();
Object.keys(CLS).forEach(key => {
if (Array.isArray(CLS[key])) {
page.checkCount(CLS[key][0][0], CLS[key][0][1]);
} else {
page.checkCount(CLS[key] as string, 0);
}
});
context.disabled = true;
fixture.detectChanges();
Object.keys(CLS).forEach(key => {
if (Array.isArray(CLS[key])) {
page.checkCount(CLS[key][1][0], CLS[key][1][1]);
} else {
page.checkCount(CLS[key] as string, 1);
}
});
});
it('#loading', () => {
context.loading = false;
fixture.detectChanges();
const CLS = {
loading: '[data-type="submit"].ant-btn-loading',
disabled: '[data-type="reset"][disabled]',
};
page.checkCount(CLS.loading, 0);
page.checkCount(CLS.disabled, 0);
context.loading = true;
fixture.detectChanges();
page.checkCount(CLS.loading, 1);
page.checkCount(CLS.disabled, 1);
});
it('#noColon', () => {
context.noColon = true;
fixture.detectChanges();
const CLS = `.sf__no-colon`;
page.checkCount(CLS, 1);
context.noColon = false;
fixture.detectChanges();
page.checkCount(CLS, 0);
});
describe('#cleanValue', () => {
it('with true', () => {
context.cleanValue = true;
fixture.detectChanges();
page.newSchema(
{
properties: {
name: { type: 'string' },
arr: { type: 'array', items: { type: 'object', properties: { x: { type: 'string' } } } },
},
},
{},
{ name: 'a', age: 10, arr: [{ x: 1, y: 2 }] },
);
expect(context.comp.value.age == null).toBe(true);
expect(context.comp.value.arr[0].y == null).toBe(true);
});
it('with false', () => {
context.cleanValue = false;
fixture.detectChanges();
page.newSchema(
{
properties: {
name: { type: 'string' },
arr: { type: 'array', items: { type: 'object', properties: { x: { type: 'string' } } } },
},
},
{},
{ name: 'a', age: 10, arr: [{ x: 1, y: 2 }] },
);
expect(context.comp.value.age).toBe(10);
expect(context.comp.value.arr[0].y).toBe(2);
});
});
it('#formChange', () => {
page.setValue('/name', 'cipchk');
expect(context.formChange).toHaveBeenCalled();
});
it('#formValueChange', () => {
page.setValue('/name', 'cipchk');
expect(context.formValueChange).toHaveBeenCalled();
});
it('#formSubmit', () => {
page.setValue('/name', 'cipchk').setValue('/pwd', 'asdf').submit();
expect(context.formSubmit).toHaveBeenCalled();
});
it('#formReset', () => {
page.setValue('/name', 'cipchk').setValue('/pwd', 'asdf').reset();
expect(context.formReset).toHaveBeenCalled();
});
it('#formError', () => {
page.setValue('/name', 'cipchk').setValue('/name', '');
expect(context.formError).toHaveBeenCalled();
});
});
describe('[widgets]', () => {
it('#size', () => {
page
.newSchema({
properties: { name: { type: 'string', ui: { size: 'large' } } },
})
.checkCls('input', 'ant-input-lg');
});
it('#disabled', fakeAsync(() => {
const el = page.newSchema({ properties: { name: { type: 'string', readOnly: true } } }).getEl('input') as HTMLInputElement;
tick();
expect(el.disabled).toBe(true);
expect(el.classList).toContain('ant-input-disabled');
}));
it('should be custom class', () => {
page
.newSchema({
properties: { name: { type: 'string', ui: { class: 'test-cls' } } },
})
.checkCls('sf-string', 'test-cls');
});
it('should get all registered widgets', () => {
const wr = TestBed.inject(WidgetRegistry) as WidgetRegistry;
expect(Object.keys(wr.widgets).length).toBeGreaterThan(0);
});
});
describe('public methods', () => {
it('#getProperty', () => {
expect(context.comp.getProperty('/name')).not.toBeNull();
});
it('#getValue', () => {
const name = 'asdf';
page.newSchema({ properties: { name: { type: 'string' } } }, null!, { name });
expect(context.comp.getValue('/name')).toBe(name);
});
it('#setValue', () => {
const name = 'new-asdf';
context.comp.setValue('/name', name);
expect(context.comp.value.name).toBe(name);
});
it('#setValue, shoule be throw error when invlaid path', () => {
expect(() => {
context.comp.setValue('/invalid-path', name);
}).toThrow();
});
});
describe('[Custom Validator]', () => {
it('with function and shoule be success when return a empty errors', () => {
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
validator: jasmine.createSpy().and.returnValue([{ keyword: 'required', message: 'a' }]),
},
},
},
};
page.newSchema(s);
expect(page.getProperty('/a').valid).toBe(false);
});
it('with function', () => {
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
validator: jasmine.createSpy().and.returnValue([]),
},
},
},
};
page.newSchema(s);
expect(page.getProperty('/a').valid).toBe(true);
});
it('with observable', () => {
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
validator: jasmine.createSpy().and.returnValue(of([{ keyword: 'required', message: 'a' }])),
},
},
},
};
page.newSchema(s);
expect(page.getProperty('/a').valid).toBe(false);
});
it('shoule be throw error when non-include a message property', () => {
expect(() => {
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
validator: jasmine.createSpy().and.returnValue([{ keyword: 'required' }]),
},
},
},
};
page.newSchema(s);
}).toThrowError();
});
it('shoule be support custom params in message', () => {
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
validator: () => [
{
keyword: 'a',
message: 'a-{id}-{invalid}',
params: {
id: 10,
},
},
],
},
},
},
};
page.newSchema(s);
expect(page.getProperty('/a').errors![0].message).toBe(`a-10-`);
});
});
describe('[Custom Show Errors]', () => {
it('shoule be re-error message via error property', () => {
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
errors: {
required: 'REQUEST',
},
},
},
},
required: ['a'],
};
page.newSchema(s);
expect(page.getProperty('/a').errors![0].message).toBe('REQUEST');
});
it('shoule be re-error message via error property and type is function', () => {
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
errors: {
required: jasmine.createSpy().and.returnValue('A'),
},
},
},
},
required: ['a'],
};
page.newSchema(s);
expect(page.getProperty('/a').errors![0].message).toBe('A');
expect((s.properties!.a.ui as any).errors.required).toHaveBeenCalled();
});
it('should be i18n', () => {
const iconSrv = TestBed.inject(NzIconService);
spyOn(iconSrv, 'getRenderedContent').and.returnValue(of());
const s: SFSchema = {
properties: {
a: {
type: 'string',
},
arr: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
},
required: ['name'],
},
},
},
required: ['a'],
};
page.newSchema(s, undefined, { a: '', arr: [{ name: '' }] });
expect(page.getProperty('/a').errors![0].message).toBe(context.comp.locale.error.required);
const i18n = TestBed.inject(OhayoLocaleService);
i18n.setLocale(en_US);
fixture.detectChanges();
expect(page.getProperty('/a').errors![0].message).toBe(context.comp.locale.error.required);
expect(page.getProperty('/arr').errors![0].message).toBe(context.comp.locale.error.required);
});
it('should be display required * when showRequired is true', () => {
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
showRequired: true,
},
},
},
};
page.newSchema(s).checkCount('.ant-form-item-required', 1);
});
});
});
describe('#mode', () => {
beforeEach(() => {
genModule();
({ fixture, dl, context } = createTestContext(TestModeComponent));
});
it('should be auto 搜索 in submit', () => {
context.mode = 'search';
createComp();
expect(page.getEl('.ant-btn-primary').textContent).toContain('搜索');
});
it('should be auto 保存 in submit', () => {
context.mode = 'edit';
createComp();
expect(page.getEl('.ant-btn-primary').textContent).toContain('保存');
});
it('should be custom text of search', () => {
context.mode = 'search';
context.button = {
search: 'SEARCH',
};
createComp();
expect(page.getEl('.ant-btn-primary').textContent).toContain('SEARCH');
});
it('should be custom text of edit', () => {
context.mode = 'edit';
context.button = {
edit: 'SAVE',
};
createComp();
expect(page.getEl('.ant-btn-primary').textContent).toContain('SAVE');
});
});
describe('ACL', () => {
beforeEach(() => genModule({ acl: true }));
it('should working', fakeAsync(() => {
({ fixture, dl, context } = createTestContext(TestFormComponent));
createComp();
const acl = TestBed.inject<ACLService>(ACLService);
acl.setFull(false);
acl.setRole(['admin']);
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
acl: 'admin',
},
},
},
required: ['a'],
};
page.newSchema(s);
page.checkUI('/a', 'hidden', false);
acl.setRole(['user']);
tick();
fixture.detectChanges();
page.checkUI('/a', 'hidden', true);
}));
});
describe('I18N', () => {
beforeEach(() => genModule({ i18n: true }));
it('should working', fakeAsync(() => {
({ fixture, dl, context } = createTestContext(TestFormComponent));
createComp();
const i18n = TestBed.inject(OHAYO_I18N_TOKEN) as OhayoI18NService;
let lang = 'en';
spyOn(i18n, 'fanyi').and.callFake(((key: string) => {
if (key === 'null') return null;
return lang === 'en' ? key : `zh-${key}`;
}) as any);
const s: SFSchema = {
properties: {
a: {
type: 'string',
title: 'title',
description: 'title',
ui: {
i18n: 'i18n',
descriptionI18n: 'descriptionI18n',
optionalHelp: {
i18n: 'ohi18n',
},
},
},
b: {
type: 'string',
title: 'a',
ui: {
i18n: 'null',
},
},
},
};
page.newSchema(s);
page
.checkSchema('/a', 'title', 'i18n')
.checkSchema('/b', 'title', 'null')
.checkSchema('/a', 'description', 'descriptionI18n')
.checkUI('/a', 'optionalHelp.text', 'ohi18n');
lang = 'zh';
i18n.use(lang);
page
.checkSchema('/a', 'title', 'zh-i18n')
.checkSchema('/a', 'description', 'zh-descriptionI18n')
.checkUI('/a', 'optionalHelp.text', 'zh-ohi18n');
}));
});
});
({
template: ` <sf [layout]="layout" #comp [schema]="schema" [ui]="ui" [button]="button" [mode]="mode" [loading]="loading"></sf> `,
})
class TestModeComponent extends TestFormComponent { }