<!DOCTYPE html>
<html lang="en">
<head>
<title>Angular Material Demo - Registration Form</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<!-- Angular Material style sheet -->
<link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css">
<!-- Angular Material Libraries -->
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script>
<script src="script.js"></script>
</head>
<body ng-app="myApp" ng-controller="mainController" ng-cloak>
<!-- Page Content -->
<div class="wrapper" >
<div id="regContainer" layout="column">
<md-toolbar class="md-toolbar-tools">
<h2>Registration Form</h2>
<span flex></span>
<md-button href="test.html">Go Unit Test</md-button>
</md-toolbar>
<md-content layout-padding>
<form name="regForm" role="form">
<md-input-container class="md-icon-float md-block">
<label>User ID</label>
<input required my-uid type="text" name="uid" ng-model="user.id">
<div ng-messages="regForm.uid.$error">
<div ng-message="required">This is required.</div>
<div ng-message="uid">The user ID must 9 digit number.And the remainder of sum of first 8 digits should equal to the 9th digit.</div>
</div>
</md-input-container>
<md-input-container class="md-icon-float md-block">
<label>User Name: </label>
<input required md-maxlength="35" type="text" name="username" ng-model="user.name">
<div ng-messages="regForm.username.$error">
<div ng-message="required">This is required.</div>
<div ng-message="md-maxlength">The user name must be less than 35 characters long.</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label>Email</label>
<input required type="email" name="useremail" ng-model="user.email"
minlength="10" maxlength="100" ng-pattern="/^.+@.+\..+$/" />
<div ng-messages="regForm.useremail.$error" role="alert">
<div ng-message-exp="['required', 'minlength', 'maxlength', 'pattern']">
Your email must be between 10 and 100 characters long and look like an e-mail address.
</div>
</div>
</md-input-container>
<md-input-container class="md-icon-float md-block">
<label>Password: </label>
<input required minlength="8" my-password type="password" name="password" ng-model="user.password">
<div ng-messages="regForm.password.$error">
<div ng-message="required">This is required.</div>
<div ng-message="minlength">The password must be more than 8 characters long.</div>
<div ng-message="pass">The password must contain at least one letter and one digit.</div>
</div>
</md-input-container>
<md-input-container class="md-icon-float md-block">
<label>Verify Password: </label>
<input required type="password" name="verifyPassword" ng-model="user.verifyPassword"
compare-to="user.password"/>
<div ng-messages="regForm.verifyPassword.$error">
<div ng-message="required">This is required.</div>
<div ng-message="compareTo">Must match the previous entry</div>
</div>
</md-input-container>
<md-button type="submit" ng-disabled="regForm.$invalid">Submit</md-button>
</form>
</md-content>
</div>
</div>
<!-- /Page Content -->
</body>
</html>
// Code goes here
angular.module('myApp', ['ngMaterial','ngMessages'])
.directive('myUid', function(){
return {
require: 'ngModel',
link: function(scope, element, attr, mCtrl){
mCtrl.$parsers.push(function(value){
if(value.length == 9){
var aryValue = value.split("");
var sum = 0;
for(var i=0;i<8;i++){
if (!Number.isInteger(parseInt(aryValue[i]))){
mCtrl.$setValidity('uid', false);
return value;
}
sum += parseInt(aryValue[i]);
}
if (!Number.isInteger(parseInt(aryValue[8]))){
mCtrl.$setValidity('uid', false);
return value;
}
if (aryValue[8] != (sum%10)){
mCtrl.$setValidity('uid', false);
return value;
}
mCtrl.$setValidity('uid', true);
}else {
mCtrl.$setValidity('uid', false);
}
return value;
});
}
};
})
.directive('myPassword', function(){
return {
require: 'ngModel',
link: function(scope, element, attr, mCtrl){
mCtrl.$parsers.push(function(value){
var aryValue = value.split("");
var isChar = false, isDigit = false;
aryValue.forEach(function(element, index, array){
if (Number.isInteger(parseInt(element))){
isDigit = true;
}else{
isChar = true;
}
})
mCtrl.$setValidity('pass', isDigit && isChar);
return value;
});
}
};
})
.directive('compareTo', function(){
return {
require: "ngModel",
scope: {
otherModelValue: "=compareTo"
},
link: function(scope, element, attributes, ngModel){
ngModel.$validators.compareTo = function(modelValue) {
return modelValue == scope.otherModelValue;
};
scope.$watch("otherModelValue", function() {
ngModel.$validate();
});
}
};
})
.controller('mainController',function($scope){
$scope.user = {
id : "",
name:"",
email:"",
password:"",
verifyPassword:""
};
})
;
/* Styles go here */
# Registration Form based on Angular Material with Unit Test
A registration form with validation and show error message with ng-message.
It needs some custome validations which done by custome directives.
These custome directives are covered by unit test
## Requirement:
### User ID
User ID is a 9 digits id. It need a custom validation: sum first 8 digits, % 10, = the 9th digit.
### User name
User name should be no more than 35 characters
### Email
Normal email validation applied
### Password
- Minimun 8 digits
- At least 1 character and 1 number
### Verify Password
equal to password of course.
### Mandatory
All field need to be mandatory
### Submit
Only avalible when all validation passed.
> This originally from an interview on 2016/09/05.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>AngularJS test</title>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css" data-semver="2.4.1" data-require="jasmine" />
<link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script>
<script src="//code.angularjs.org/1.5.5/angular-mocks.js"></script>
<script src="script.js"></script>
<script src="test.js"></script>
<!-- bootstraps Jasmine -->
</head>
<body >
<div id="testContainer" layout="column">
<md-content layout-padding>
<md-toolbar class="md-hue-2">
<div class="md-toolbar-tools">
<a href="index.html">Go Back</a>
</div>
</md-toolbar>
</md-content>
</div>
</body>
</html>
/*
Reference:
[To test a custom validation angular directive](http://stackoverflow.com/questions/15219717/to-test-a-custom-validation-angular-directive)
*/
describe('Testing form validation directives', function() {
var $compile;
var $rootScope;
var form;
// Load the module, which contains the directive
beforeEach(module('myApp'));
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject(function(_$compile_, _$rootScope_) {
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
// create the element use the directive
var element = angular.element(
"<form name='myForm'>" +
" <input my-uid name='uid' ng-model='user.id'>" +
" <input my-password name='password' ng-model='user.password'>" +
" <input compare-to='user.password' name='verifyPassword' ng-model='user.verifyPassword'>" +
"</from>"
);
// Compile the element
$compile(element)($rootScope);
// Get the form as the $valid is refered by formName.elementName.$valid
form = $rootScope.myForm;
}));
describe("myUid", function() {
it('should not pass with length >9', function() {
// setting values
form.uid.$setViewValue('11111111');
// fire all the watches, so the scope expression {{1 + 1}} will be evaluated
$rootScope.$digest();
// Checking / assert
expect(form.uid.$valid).toBe(false);
});
it('should not pass with length <9', function() {
// setting values
form.uid.$setViewValue('1111111181');
// fire all the watches, so the scope expression {{1 + 1}} will be evaluated
$rootScope.$digest();
// Checking / assert
expect(form.uid.$valid).toBe(false);
});
it('should not pass with characters involved such as 111111118a', function(){
// setting values
form.uid.$setViewValue('111111118a');
// fire all the watches, so the scope expression {{1 + 1}} will be evaluated
$rootScope.$digest();
// Checking / assert
expect(form.uid.$valid).toBe(false);
});
it('should not pass with incorrect pattern such 111111117', function(){
// setting values
form.uid.$setViewValue('111111118a');
// fire all the watches, so the scope expression {{1 + 1}} will be evaluated
$rootScope.$digest();
// Checking / assert
expect(form.uid.$valid).toBe(false);
});
it('should pass with 111111129', function() {
// setting values
form.uid.$setViewValue('111111129');
// fire all the watches, so the scope expression {{1 + 1}} will be evaluated
$rootScope.$digest();
// Checking / assert
expect(form.uid.$valid).toBe(true);
});
});
describe("myPassword",function(){
it("should not pass with all digital", function(){
form.password.$setViewValue('11111111');
$rootScope.$digest();
expect(form.password.$valid).toBe(false);
});
it("should not pass with all characters", function(){
form.password.$setViewValue('asdabdsadf');
$rootScope.$digest();
expect(form.password.$valid).toBe(false);
});
it("should pass with digital and characters", function(){
form.password.$setViewValue('a1dabdsadf');
$rootScope.$digest();
expect(form.password.$valid).toBe(true);
});
});
describe("compareTo",function(){
it("should not pass when password not equal verifyPassword",function(){
form.password.$setViewValue("newPassword");
form.verifyPassword.$setViewValue("newpassword");
$rootScope.$digest();
expect(form.verifyPassword.$valid).toBe(false);
});
it("should pass when password equal verifyPassword",function(){
form.password.$setViewValue("newPassword");
form.verifyPassword.$setViewValue("newPassword");
$rootScope.$digest();
expect(form.verifyPassword.$valid).toBe(true);
});
});
});
(function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 250;
/**
Create the `HTMLReporter`, which Jasmine calls to provide results of each spec and each suite. The Reporter is responsible for presenting results to the user.
*/
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
/**
Delegate filtering of specs to the reporter. Allows for clicking on single suites or specs in the results to only run a subset of the suite.
*/
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
/**
Run all of the tests when the page finishes loading - and make sure to run any previous `onload` handler
### Test Results
Scroll down to see the results of all of these specs.
*/
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
//document.querySelector('.version').innerHTML = jasmineEnv.versionString();
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
}
})();