diff --git a/.jscs.json b/.jscs.json index 5d5a43647..ca5b5f7db 100644 --- a/.jscs.json +++ b/.jscs.json @@ -1,4 +1,5 @@ { "preset": "google", - "maximumLineLength": 100 + "maximumLineLength": 100, + "disallowMultipleLineBreaks": false } diff --git a/docs/index.md b/docs/index.md index a682f1507..d8bd41873 100644 --- a/docs/index.md +++ b/docs/index.md @@ -260,6 +260,7 @@ The context variables available to you are: | error | The error code | | title | Title of the field | | value | The model value | +| viewValue | The view value (probably the one you want) | | form | form definition object for this field | | schema | schema for this field | diff --git a/examples/bootstrap-example.html b/examples/bootstrap-example.html index e5b2991b3..83eb0784a 100644 --- a/examples/bootstrap-example.html +++ b/examples/bootstrap-example.html @@ -273,11 +273,12 @@

Schema

}).error(function() { $scope.loadedData = 'dummy'; $scope.error = 'Failed to load gist.'; + $scope.selectedTest = $scope.tests[0]; }); + } else { + $scope.selectedTest = $scope.tests[0]; } - $scope.selectedTest = $scope.tests[0]; - $scope.$watch('selectedTest',function(val){ if (val) { $http.get(val.data).then(function(res) {setNewData(res.data);}); diff --git a/gulp/tasks/protractor.js b/gulp/tasks/protractor.js new file mode 100644 index 000000000..9a3bb1864 --- /dev/null +++ b/gulp/tasks/protractor.js @@ -0,0 +1,38 @@ +var gulp = require('gulp'); + +// The protractor task +var protractor = require('gulp-protractor'); + +// Start a standalone server +var webdriver_standalone = protractor.webdriver_standalone; + +// Download and update the selenium driver +var webdriver_update = protractor.webdriver_update; + +// Downloads the selenium webdriver +gulp.task('webdriver-update', webdriver_update); + +// Start the standalone selenium server +// NOTE: This is not needed if you reference the +// seleniumServerJar in your protractor.conf.js +gulp.task('webdriver-standalone', webdriver_standalone); + + +// Setting up the test task +gulp.task('protractor', ['webdriver-update'], function(cb) { + gulp.src(['test/protractor/specs/**/*.js']).pipe(protractor.protractor({ + configFile: 'test/protractor/conf.js', + })).on('error', function(e) { + console.log(e); + }).on('end', cb); +}); + +['validation-messages', 'custom-validation'].forEach(function(name) { + gulp.task('protractor:' + name, ['webdriver-update'], function(cb) { + gulp.src(['test/protractor/specs/' + name + '.js']).pipe(protractor.protractor({ + configFile: 'test/protractor/conf.js', + })).on('error', function(e) { + console.log(e); + }).on('end', cb); + }); +}); diff --git a/package.json b/package.json index d8c92e6d5..d4df929fb 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "gulp-concat": "^2.2.0", "gulp-jscs": "^1.1.0", "gulp-minify-html": "^0.1.1", + "gulp-protractor": "^1.0.0", "gulp-rename": "^1.2.0", "gulp-uglify": "^0.2.1", "karma": "^0.12.0", @@ -49,6 +50,7 @@ "karma-phantomjs-launcher": "^0.1.4", "mocha": "^1.18.0", "mocha-lcov-reporter": "0.0.1", + "protractor": "^2.0.0", "sinon": "^1.9.0", "sinon-chai": "^2.5.0", "streamqueue": "0.0.5" diff --git a/src/directives/message.js b/src/directives/message.js index 572bf36bf..b14518187 100644 --- a/src/directives/message.js +++ b/src/directives/message.js @@ -27,24 +27,13 @@ angular.module('schemaForm').directive('sfMessage', // We only show one error. // TODO: Make that optional - // tv4- errors take precedence var error = errors[0]; - if (errors.length > 1) { - - error = errors.reduce(function(prev, value) { - if (prev && prev.indexOf('tv4-') === 0) { - return prev; - } - return value; - }); - console.log('reduced',errors, error) - - } if (error) { element.html(sfErrorMessage.interpolate( error, scope.ngModel.$modelValue, + scope.ngModel.$viewValue, scope.form, scope.options && scope.options.validationMessage )); diff --git a/src/services/decorators.js b/src/services/decorators.js index bd012b12f..7ca55a882 100644 --- a/src/services/decorators.js +++ b/src/services/decorators.js @@ -159,6 +159,7 @@ angular.module('schemaForm').provider('schemaFormDecorators', return sfErrorMessage.interpolate( (schemaError && schemaError.code + '') || 'default', (scope.ngModel && scope.ngModel.$modelValue) || '', + (scope.ngModel && scope.ngModel.$viewValue) || '', scope.form, scope.options && scope.options.validationMessage ); diff --git a/src/services/errors.js b/src/services/errors.js index d00070e96..14038e09f 100644 --- a/src/services/errors.js +++ b/src/services/errors.js @@ -14,25 +14,25 @@ angular.module('schemaForm').provider('sfErrorMessage', function() { 12: 'Data is valid against more than one schema from "oneOf"', 13: 'Data matches schema from "not"', // Numeric errors - 100: 'Value {{value}} is not a multiple of {{schema.multipleOf}}', - 101: 'Value {{value}} is less than minimum {{schema.minimum}}', - 102: 'Value {{value}} is equal to exclusive minimum {{schema.minimum}}', - 103: 'Value {{value}} is greater than maximum {{schema.maximum}}', - 104: 'Value {{value}} is equal to exclusive maximum {{schema.maximum}}', - 105: 'Value {{value}} is not a valid number', + 100: 'Value is not a multiple of {{schema.divisibleBy}}', + 101: '{{viewValue}} is less than the allowed minimum of {{schema.minimum}}', + 102: '{{viewValue}} is equal to the exclusive minimum {{schema.minimum}}', + 103: '{{viewValue}} is greater than the allowed maximum of {{schema.maximum}}', + 104: '{{viewValue}} is equal to the exclusive maximum {{schema.maximum}}', + 105: 'Value is not a valid number', // String errors - 200: 'String is too short ({{value.length}} chars), minimum {{schema.minimum}}', - 201: 'String is too long ({{value.length}} chars), maximum {{schema.maximum}}', + 200: 'String is too short ({{viewValue.length}} chars), minimum {{schema.minLength}}', + 201: 'String is too long ({{viewValue.length}} chars), maximum {{schema.maxLength}}', 202: 'String does not match pattern: {{schema.pattern}}', // Object errors - 300: 'Too few properties defined, minimum {{schema.minimum}}', - 301: 'Too many properties defined, maximum {{schema.maximum}}', + 300: 'Too few properties defined, minimum {{schema.minProperties}}', + 301: 'Too many properties defined, maximum {{schema.maxProperties}}', 302: 'Required', 303: 'Additional properties not allowed', 304: 'Dependency failed - key must exist', // Array errors - 400: 'Array is too short ({{value.length}}), minimum {{schema.minimum}}', - 401: 'Array is too long ({{value.length}}), maximum {{schema.maximum}}', + 400: 'Array is too short ({{value.length}}), minimum {{schema.maxItems}}', + 401: 'Array is too long ({{value.length}}), maximum {{schema.minItems}}', 402: 'Array items are not unique', 403: 'Additional items not allowed', // Format errors @@ -44,6 +44,15 @@ angular.module('schemaForm').provider('sfErrorMessage', function() { 1000: 'Unknown property (not in schema)' }; + // In some cases we get hit with an angular validation error + defaultMessages.number = defaultMessages[105]; + defaultMessages.required = defaultMessages[302]; + defaultMessages.min = defaultMessages[101]; + defaultMessages.max = defaultMessages[103]; + defaultMessages.maxlength = defaultMessages[201]; + defaultMessages.minlength = defaultMessages[200]; + defaultMessages.pattern = defaultMessages[202]; + this.setDefaultMessages = function(messages) { defaultMessages = messages; }; @@ -68,12 +77,14 @@ angular.module('schemaForm').provider('sfErrorMessage', function() { * @param {string} error the error code, i.e. tv4-xxx for tv4 errors, otherwise it's whats on * ngModel.$error for custom errors. * @param {Any} value the actual model value. + * @param {Any} viewValue the viewValue * @param {Object} form a form definition object for this field * @param {Object} global the global validation messages object (even though its called global * its actually just shared in one instance of sf-schema) * @return {string} The error message. */ - service.interpolate = function(error, value, form, global) { + service.interpolate = function(error, value, viewValue, form, global) { + console.log(error, value, viewValue) global = global || {}; var validationMessage = form.validationMessage || {}; @@ -99,6 +110,7 @@ angular.module('schemaForm').provider('sfErrorMessage', function() { var context = { error: error, value: value, + viewValue: viewValue, form: form, schema: form.schema, title: form.title || (form.schema && form.schema.title) diff --git a/test/protractor/conf.js b/test/protractor/conf.js index ff83a3634..8f51a93ee 100644 --- a/test/protractor/conf.js +++ b/test/protractor/conf.js @@ -1,4 +1,3 @@ exports.config = { - seleniumAddress: 'http://localhost:4444/wd/hub', - specs: ['custom-validation.js'] + seleniumAddress: 'http://localhost:4444/wd/hub' } diff --git a/test/protractor/custom-validation.js b/test/protractor/specs/custom-validation.js similarity index 100% rename from test/protractor/custom-validation.js rename to test/protractor/specs/custom-validation.js diff --git a/test/protractor/specs/validation-messages.js b/test/protractor/specs/validation-messages.js new file mode 100644 index 000000000..323275649 --- /dev/null +++ b/test/protractor/specs/validation-messages.js @@ -0,0 +1,66 @@ +/* global browser, it, describe, element, by */ + +describe('Schema Form validation messages', function() { + + describe('#string', function() { + var URL = 'http://localhost:8080/examples/bootstrap-example.html#/86fb7505a8ab6a43bc70'; + + it('should not complain if it gets a normal string', function() { + browser.get(URL); + var input = element.all(by.css('form[name=ngform] input')).first(); + input.sendKeys('string'); + + expect(input.getAttribute('value')).toEqual('string'); + expect(input.evaluate('ngModel.$valid')).toEqual(true); + + }); + + + var validationMessageTestBuider = function(nr, value, validationMessage) { + it('should say "' + validationMessage + '" when input is ' + value, function() { + browser.get(URL); + var input = element.all(by.css('form[name=ngform] input')).get(nr); + input.sendKeys(value); + + var message = element.all(by.css('form[name=ngform] div[sf-message]')).get(nr); + expect(input.evaluate('ngModel.$valid')).toEqual(false); + expect(message.getText()).toEqual(validationMessage); + + }); + }; + + var stringTests = { + 's': 'String is too short (1 chars), minimum 3', + 'tooo long string': 'String is too long (11 chars), maximum 10', + 'foo 66': 'String does not match pattern: ^[a-zA-Z ]+$' + }; + + Object.keys(stringTests).forEach(function(value) { + validationMessageTestBuider(0, value, stringTests[value]); + }); + + + var integerTests = { + '3': '3 is less than the allowed minimum of 6', + '66': '66 is greater than the allowed maximum of 50', + '11': 'Value is not a multiple of 3', + 'aaa': 'Value is not a valid number' + }; + + Object.keys(integerTests).forEach(function(value) { + validationMessageTestBuider(1, value, integerTests[value]); + }); + + + it('should say "Required" when fields are required', function() { + browser.get(URL); + element.all(by.css('form[name=ngform]')).submit(); + var input = element.all(by.css('form[name=ngform] input')).get(1); + + var message = element.all(by.css('form[name=ngform] div[sf-message]')).get(1); + expect(input.evaluate('ngModel.$valid')).toEqual(false); + expect(message.getText()).toEqual('Required'); + + }); + }); +}); diff --git a/test/services/messages-test.js b/test/services/messages-test.js index a1a5202e6..57a91a97a 100644 --- a/test/services/messages-test.js +++ b/test/services/messages-test.js @@ -10,6 +10,7 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value {schema: {title: 'Foo'}}, //form {'default': 'Oh noes!'} ); @@ -25,6 +26,7 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value {validationMessage: {'default': 'Oh yes!'}, schema: {title: 'Foo'}}, //form {'default': 'Oh noes!'} ); @@ -40,6 +42,7 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value {schema: {title: 'Foo'}}, //form {'default': 'Oh noes!', 'foobar-error': 'Aw chucks!'} ); @@ -54,6 +57,7 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value {schema: {title: 'Foo'}, validationMessage: {'foobar-error': 'Noooooo!'}}, //form {'default': 'Oh noes!', 'foobar-error': 'Aw chucks!'} ); @@ -68,6 +72,7 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value { schema: {title: 'Foo'}, validationMessage: { @@ -87,6 +92,7 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value { schema: {title: 'Foo'}, validationMessage: { @@ -101,6 +107,7 @@ describe('schemaFormServices', function() { result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value { title: 'Bar', schema: {title: 'Foo'}, @@ -121,6 +128,7 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value { schema: {title: 'Foo'}, validationMessage: 'Huh?' @@ -140,6 +148,7 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value { schema: {title: 'Foo'}, validationMessage: { @@ -174,6 +183,7 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'foobar-error', //error 'foobar', //value + 'foobar', //view value { schema: {title: 'Foo'}, validationMessage: msgFn @@ -201,7 +211,8 @@ describe('schemaFormServices', function() { var result = sfErrorMessage.interpolate( 'tv4-302', //error - 'foobar', //value + 'foobar', //value + 'foobar', //view value { schema: {title: 'Foo'}, validationMessage: {302: 'tv4 error!'}