angular-formly
Version:
AngularJS directive which takes JSON representing a form and renders to HTML
462 lines (420 loc) • 15.1 kB
JavaScript
const sinon = require('sinon');
const apiCheck = require('api-check');
module.exports = ngModule => {
describe('formly-field', function() {
let $compile, scope, formlyConfig;
let template = '<formly-form form="theForm" model="model" fields="fields"></formly-form>';
beforeEach(window.module(ngModule.name));
beforeEach(inject((_$compile_, $rootScope, _formlyConfig_) => {
$compile = _$compile_;
scope = $rootScope.$new();
formlyConfig = _formlyConfig_;
}));
describe('with template wrapper', function() {
beforeEach(() => {
formlyConfig.setWrapper([
{
types: 'text',
template: `
<div class="my-template-wrapper">
<label for="{{id}}">{{options.label}}</label>
<formly-transclude></formly-transclude>
</div>
`
},
{
types: 'other',
template: `
<div class="my-other-template-wrapper">
<formly-transclude></formly-transclude>
<div>
This is great for ng-messages
</div>
</div>
`
}
]);
formlyConfig.setType({
name: 'text', template: `<input name="{{id}}" ng-model="model[options.key]" />`
});
scope.model = {};
});
it('should take the entire wrapper, not just the contents of the wrapper', function() {
scope.fields = [
{
type: 'text',
key: 'text',
templateOptions: {
label: 'Text input'
}
}
];
const el = compileAndDigest();
expect(el[0].querySelector('.my-template-wrapper')).to.exist;
});
it('should wrap arrays of wrappers', () => {
scope.fields = [
{
type: 'text',
key: 'text',
wrapper: ['text', 'other'],
templateOptions: {
label: 'Text input'
}
}
];
const el = compileAndDigest();
var outerEl = el[0].querySelector('.my-other-template-wrapper');
expect(outerEl).to.exist;
expect(outerEl.querySelector('.my-template-wrapper')).to.exist;
});
it(`should allow for specifying null for the wrappers of a field`, () => {
scope.fields = [
{
type: 'text',
key: 'text',
wrapper: null,
templateOptions: {
label: 'Text input'
}
}
];
const el = compileAndDigest();
expect(el[0].querySelector('.my-template-wrapper')).to.not.exist;
});
});
describe('api check', () => {
let validateOptions;
beforeEach(() => {
validateOptions = sinon.spy();
formlyConfig.setType({
name: 'text', template: `<input name="{{id}}" ng-model="model[options.key]" />`,
validateOptions
});
scope.model = {};
});
it('should throw an error when a field has extra properties', () => {
scope.fields = [
{
type: 'text',
extraProp: 'whatever'
}
];
expect(() => compileAndDigest()).to.throw(/extra.*properties.*extraProp/);
});
it(`should invoke the validateOptions property of the type`, () => {
const field = {type: 'text'};
scope.fields = [field];
compileAndDigest();
expect(validateOptions).to.have.been.calledWith(field);
});
});
describe('default type options', () => {
beforeEach(() => {
scope.model = {};
formlyConfig.setType({
name: 'ipAddress', template: '<input name="{{id}}" ng-model="model[options.key]" />',
defaultOptions: {
data: {
usingDefaultOptions: true
},
validators: {
ipAddress: function(viewValue, modelValue) {
var value = modelValue || viewValue;
return /(\d{1,3}\.){3}\d{1,3}/.test(value);
}
}
}
});
formlyConfig.setType({
name: 'text', template: '<input name="{{id}}" ng-model="model[options.key]" />',
defaultOptions: {
data: {
hasPropertiesFromTextType: true
}
}
});
formlyConfig.setType({
name: 'phone',
defaultOptions: {
ngModelAttrs: {
'/^1[2-9]\\d{2}[2-9]\\d{6}$/': {
value: 'ng-pattern'
}
}
}
});
formlyConfig.setType({
name: 'required',
defaultOptions: {
ngModelAttrs: {
'/overwriting stuff is fun for tests/': {
value: 'ng-pattern'
},
required: {
bound: 'ng-required',
attribute: 'required'
},
myChange: {
expression: 'ng-change'
}
},
templateOptions: {
required: true
}
}
});
});
it('should default to the ipAddress type options', () => {
var field = {type: 'ipAddress'};
scope.fields = [field];
compileAndDigest();
expect(field.data.usingDefaultOptions).to.be.true;
expect(field.validators.ipAddress).to.be.a('function');
});
it('should be possible to specify defaultOptions-only types (non-template types)', () => {
const field = {
type: 'text', optionsTypes: ['phone', 'required'], templateOptions: {myChange: 'model.otherThing = true'}
};
scope.fields = [field];
const el = compileAndDigest();
const input = el.find('input');
expect(field.data.hasPropertiesFromTextType).to.be.true;
expect(input.attr('ng-pattern')).to.equal('/overwriting stuff is fun for tests/');
expect(input.attr('ng-change')).to.contain('myChange');
});
});
describe('templateManipulators', () => {
testTemplateManipulators(true);
testTemplateManipulators(false);
function testTemplateManipulators(isPre) {
describe(isPre ? 'preWrapper' : 'postWrapper', () => {
var manipulators;
var textTemplate = '<input class="text-template" name="{{id}}" ng-model="model[options.key]">';
beforeEach(() => {
manipulators = formlyConfig.templateManipulators[isPre ? 'preWrapper' : 'postWrapper'];
formlyConfig.setWrapper([
{
types: 'text',
template: '<div class="my-template-wrapper"><formly-transclude></formly-transclude></div>'
}
]);
formlyConfig.setType({
name: 'text', template: textTemplate
});
scope.model = {};
scope.fields = [
{type: 'text'}
];
});
var when = isPre ? 'before' : 'after';
it(`should call the manipulators when compiling a field ${when} the element is wrapped in wrappers`, () => {
var manipulatedTemplate;
manipulators.push((templateToManipulate, fieldOptions, scope) => {
if (isPre) {
expect(templateToManipulate).to.contain('text-template');
}
expect(fieldOptions).to.equal(scope.fields[0]);
expect(scope.options).to.equal(fieldOptions);
if (isPre) {
expect(templateToManipulate).to.not.contain('my-template-wrapper');
} else {
expect(templateToManipulate).to.contain('my-template-wrapper');
}
manipulatedTemplate = angular.element(templateToManipulate).addClass('manipulated');
return manipulatedTemplate;
});
manipulators.push((templateToManipulate, fieldOptions, scope) => {
if (isPre) {
expect(asHtml(manipulatedTemplate)).to.equal(templateToManipulate);
}
expect(fieldOptions).to.equal(scope.fields[0]);
expect(scope.options).to.equal(fieldOptions);
if (isPre) {
expect(templateToManipulate).to.not.contain('my-template-wrapper');
} else {
expect(templateToManipulate).to.contain('my-template-wrapper');
}
expect(templateToManipulate).to.contain('manipulated');
return angular.element(templateToManipulate).addClass('manipulated-twice');
});
var el = $compile(angular.element(template))(scope);
scope.$digest();
expect(el[0].querySelector('.manipulated')).to.exist;
expect(el[0].querySelector('.manipulated-twice')).to.exist;
function asHtml(el) {
return angular.element('<a></a>').append(el).html();
}
});
});
}
});
describe('type controllers and link functions', () => {
let controllerFn, linkFn;
beforeEach(() => {
controllerFn = function($scope) {
$scope.setInTypeController = true;
};
linkFn = function(scope, el, attrs) {
scope.setInTypeLink = true;
scope.el = el;
scope.attrs = attrs;
};
formlyConfig.setType({
name: 'text',
template: `<input name="{{id}}" ng-model="model[options.key]" />`,
controller: ['$scope', controllerFn],
link: linkFn
});
scope.model = {};
});
it('should run the controller function of a type', () => {
scope.fields = [
{type: 'text'}
];
const el = compileAndDigest();
var fieldEl = angular.element(el[0].querySelector('.formly-field'));
expect(fieldEl.isolateScope().setInTypeController).to.be.true;
});
it('should run the link function of a type', () => {
scope.fields = [
{type: 'text'}
];
const el = compileAndDigest();
var fieldEl = angular.element(el[0].querySelector('.formly-field'));
var fieldScope = fieldEl.isolateScope();
expect(fieldScope.setInTypeLink).to.be.true;
expect(fieldScope.el[0]).to.equal(fieldEl[0]);
});
it('should run the controller of the specific field', () => {
scope.fields = [
{template: 'sweet mercy', controller: ['$scope', controllerFn]}
];
const el = compileAndDigest();
var fieldEl = angular.element(el[0].querySelector('.formly-field'));
expect(fieldEl.isolateScope().setInTypeController).to.be.true;
});
it('should run the link function of a type', () => {
scope.fields = [
{template: 'sweet mercy', link: linkFn}
];
const el = compileAndDigest();
var fieldEl = angular.element(el[0].querySelector('.formly-field'));
var fieldScope = fieldEl.isolateScope();
expect(fieldScope.setInTypeLink).to.be.true;
expect(fieldScope.el[0]).to.equal(fieldEl[0]);
});
});
describe(`type apiCheck`, () => {
let type = 'input';
beforeEach(() => {
formlyConfig.setType({
name: type,
template: '<label>{{to.label}}</label><input class="{{to.className}}" ng-model="model[options.key]" />',
apiCheck: {
templateOptions: apiCheck.shape({
label: apiCheck.string,
className: apiCheck.string
})
},
apiCheckInstance: apiCheck({
output: {prefix: 'custom-api-check'}
}),
apiCheckOptions: {
prefix: type + ' type checker',
url: 'http://example.com/some-custom-url'
}
});
scope.model = {};
});
it(`should not warn if everything's fine`, () => {
scope.fields = [
{type, templateOptions: {label: 'string', className: 'string'}}
];
shouldNotWarn(compileAndDigest);
});
it(`should warn if everything's not fine`, () => {
scope.fields = [
{type, templateOptions: {label: 'string'}}
];
shouldWarn(
/custom-api-check.*?input type checker.*?className.*?some-custom-url/,
compileAndDigest
);
});
it(`should throw if the apiCheckFunction is set to "throw" and everything's not fine`, () => {
formlyConfig.getType(type).apiCheckFunction = 'throw';
scope.fields = [
{type, templateOptions: {label: 'string'}}
];
expect(compileAndDigest).to.throw(
/custom-api-check.*?input type checker.*?className.*?some-custom-url/
);
});
});
describe(`wrapper apiCheck`, () => {
const name = 'input';
const template = '<input ng-model="model[options.key]" />';
const wrapper = name;
beforeEach(() => {
formlyConfig.setWrapper({
name,
template:
'<div class="to.className"><label>{{to.label}}</label><formly-transclude></formly-transclude></div>',
apiCheck: {
templateOptions: apiCheck.shape({
label: apiCheck.string,
className: apiCheck.string
})
},
apiCheckInstance: apiCheck({
output: {prefix: 'custom-api-check'}
})
});
scope.model = {};
});
it(`should not warn if everything's fine`, () => {
scope.fields = [
{template, wrapper, templateOptions: {label: 'string', className: 'string'}}
];
shouldNotWarn(compileAndDigest);
});
it(`should warn if everything's not fine`, () => {
scope.fields = [
{template, wrapper, templateOptions: {label: 'string'}}
];
shouldWarn(/custom-api-check.*?formly-field.*?className/, compileAndDigest);
});
it(`should throw if the apiCheckFunction is set to "throw" and everything's not fine`, () => {
formlyConfig.getWrapper(name).apiCheckFunction = 'throw';
scope.fields = [
{template, wrapper, templateOptions: {label: 'string'}}
];
expect(compileAndDigest).to.throw(/custom-api-check.*?formly-field.*?className/);
});
});
function compileAndDigest() {
const el = $compile(template)(scope);
scope.$digest();
return el;
}
function shouldWarn(match, test) {
var originalWarn = console.warn;
var calledArgs;
console.warn = function() {
calledArgs = arguments;
};
test();
expect(calledArgs[0]).to.match(match);
console.warn = originalWarn;
}
function shouldNotWarn(test) {
var originalWarn = console.warn;
var callCount = 0;
console.warn = () => callCount++;
test();
expect(callCount).to.equal(0);
console.warn = originalWarn;
}
});
};