Using tests to rebuild the validate function is what we’re up to today.
Stepping back briefly to do thing properly
After the exciting wizardry of entirely removing the input handlers, today I take care of tests that should have been done yesterday.
Yesterday I was using existing tests for the input handlers to confirm that the validate function was behaving appropriately. But really, I should have created tests to define what the validate function needs to do.
Today I’ll do just that by recreating the validate function, using tests to define what it needs to do.
Check for empty value
The first thing that we want the validate function to do is to check for an empty value.
The this keyword in the input handler is not a jQuery object, and it refers to a form-group, so it helps if I start with a form-group reference.
And as the validate function uses inputStatus, we don’t need to check those details as they have already been checked elsewhere. We just need to check if the error message contains “is empty”. And when the value is not empty, to check if it doesn’t contain “is empty”.
describe("validate", function () {
const emailGroup = $("#login .form-group").has("[name='E-mail']").get(0);
const $emailError = $(emailGroup).find(".error");
it("is empty", function () {
emailGroup.value = "";
validate(emailGroup);
expect($emailError.html()).to.equal("E-mail is empty");
});
});
We can tell the validate function to use checkEmpty,
function validate(inputGroup) {
const value = $(inputGroup).find(".input-check").val().trim();
checkEmpty(inputGroup, value);
}
and we are told exactly what to do to get things working.
Use test errors to create the checkEmpty() function
The test tells us: ReferenceError: checkEmpty is not defined at validate
so we create a function.
function validate(inputGroup) {
function checkEmpty() {
}
//...
}
Then the test says: AssertionError: expected 'Your email' to equal 'E-mail is empty'
so we use inputStatus to achieve that. We also need to use inputGroup to target the warning at the right place.
function checkEmpty(inputGroup) {
inputStatus.warning(inputGroup, "E-mail is empty");
}
And the test passes successfully.
Check for not empty
There’s no point in only checking for one type of thing. After all, a broken lightbulb is off all of the time, not only when the switch is turned off. We also want a test to check for what happens when the email isn’t empty. We should not find “is empty” in the error message.
it("isn't empty", function () {
emailGroup.value = "test.value@example.com";
validate(emailGroup);
expect($emailError.html()).to.contain("is Ok");
});
The test now says: AssertionError: expected 'E-mail is empty' to not include 'is empty'
telling us that the “isn’t empty” test shouldn’t see the empty warning. We need to check if the email is or isn’t empty, which means getting the value.
function checkEmpty(inputGroup, value) {
if (value === "") {
inputStatus.warning(inputGroup, "E-mail is empty");
}
}
The next test message is: AssertionError: expected 'E-mail is empty' to not include 'is empty'
so we need a way to say that everything is okay.
We could put the ok message in the checkEmpty function, but then we would also need to duplicate that in all other validation functions. That’s not a good idea.
Instead of doing that, we can use a separate function called showValid().
function checkEmpty(inputGroup, value) {
if (value === "") {
inputStatus.warning(inputGroup, "E-mail is empty");
return;
}
showValid(inputGroup);
}
function showValid(inputGroup) {
inputStatus.ok(inputGroup, "E-mail is Ok: Your data has been entered correctly");
}
It’s important to note that the test is not checking if “test value” results in ok. Other validations will clobber that kind of test.
Instead of of testing for empty or ok, safety of a sort is found by using the true dichotomy of is empty, or not empty. That way the test continues to be valid regardless of other checks that the code makes.
Check for fake text
The next test is for when we have fake text.
it("is fake text", function () {
input.value = "aaabbb";
validate(emailGroup);
expect($emailError.html()).to.equal("E-mail is Fake text: Please remove repetition");
});
it("isn't fake text", function () {
input.value = "test.value@example.com";
validate(emailGroup);
expect($emailError.html()).to.contain("is Ok");
});
We can add checkFake to our isValid code,
checkEmpty(inputGroup, value);
checkFake(inputGroup, value);
and the checkFake function to put in the validate function is easy to create:
function checkFake(inputGroup, value) {
const fakeReg = /(.)\1{2,}/;
if (fakeReg.test(value)) {
inputStatus.warning(inputGroup, "E-mail is Fake text: Please remove repetition");
return;
}
showValid(inputGroup);
return true;
}
The email test now fails, because the checkFake is clobbering the checkEmpty function.
We need to find out if checkEmpty was successful or not. Returning true and false from the function is how we do that.
function checkEmpty(inputGroup, value) {
if (value === "") {
inputStatus.warning(inputGroup, "E-mail is empty");
return false;
}
showValid(inputGroup);
return true;
}
//...
const notEmpty = checkEmpty(inputGroup, value);
if (notEmpty) {
checkFake(inputGroup, value);
}
Tidy up name and value
Now that the tests all pass, is a good time to refactor the code and make structural improvements.
I’ve been noticing that the value are being passed to several functions. Those functions can easily get the information from inputGroup, but I don’t want each function to use the $(inputGroup).find(".input-check").val().trim();
code to get the value. Instead, I’ll move the value code into a separate function instead.
function validate(inputGroup) {
function getValue(inputGroup) {
return $(inputGroup).find(".input-check").val().trim();
}
//...
// const value = return $(inputGroup).find(".input-check").val().trim();
const value = getValue(inputGroup);
}```
We can now use getValue(inputGroup) in the functions, to help simplify the function parameters.
// function checkEmpty(inputGroup, value) {
function checkEmpty(inputGroup) {
// if (value === "") {
if (getValue(inputGroup) === "") {
inputStatus.warning(inputGroup, "E-mail is empty");
and after removing value function parameters from all functions, we can simplify the end of the code too.
// const value = getValue(inputGroup);
// const notEmpty = checkEmpty(inputGroup, value);
const notEmpty = checkEmpty(inputGroup);
if (notEmpty) {
// checkFake(inputGroup, value);
checkFake(inputGroup);
}
The single parameter functions are also easier to manipulate as an array, but we’ll get to that later on.
There is a problem with the above code though. When the value isn’t empty the inputStatus.ok() runs from the checkEmpty function, and then the code goes on to checkFake.
I don’t want inputStatus.ok() to run until after all of the checking functions have been attempted.
I would prefer that it inputStatus.ok() doesn’t occur until after all of the validations have succeeded.
For the notFake variable, we can check if notEmpty is true before using checkFake. That way we can check if both notEmpty and notFake are true before showing valid.
const notEmpty = checkEmpty(inputGroup);
const notFake = notEmpty && checkFake(inputGroup);
if (notEmpty && notFake) {
showValid(inputGroup);
}
We can now remove that showValid from the checkEmpty and checkFake functions, and we have successfully extracted it from out of there.
We now don’t need the separate notEmpty and notFake variables. We can just use isValid, and work through with that instead.
// const notEmpty = checkEmpty(inputGroup);
// const notFake = notEmpty && checkFake(inputGroup);
const isValid = checkEmpty(inputGroup) && checkFake(inputGroup);
if (isValid) {
showValid(inputGroup);
}
That way, we can keep on adding more && sections to isValid for the other checks, and everything keeps on working fine.
Check is email
The test for checking if the value is an invalid or a valid email is similar to the other tests:
it("is invalid email", function () {
input.value = "test.value";
validate(emailGroup);
expect($emailError.html()).to.equal("E-mail is Incorrect: Please enter it correctly");
});
it("is valid email", function () {
input.value = "test.value@example.com";
validate(emailGroup);
expect($emailError.html()).to.not.contain("is Incorrect");
});
We can add checkEmailReg to the isValid line of code:
// const isValid = checkEmpty(inputGroup) && checkFake(inputGroup);
const isValid = checkEmpty(inputGroup) && checkFake(inputGroup) && checkEmailReg(inputGroup);
We can use the same structure of the checkEmpty and checkFake functions, for when creating the checkEmailReg function.
function checkEmailReg(inputGroup) {
const emailReg = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/;
const value = getValue(inputGroup);
if (!emailReg.test(value)) {
inputStatus.warning(inputGroup, "E-mail is Incorrect: Please enter it correctly");
console.log("isn't email");
return false;
}
return true;
}
That works, but improvement is needed.
Refactoring the isValid checks
The isValid line now has three functions chained together with && symbols. That is not a pattern that can successfully grow.
We really need to put those function names into an array, and loop through them getting the result instead.
const emailTypes = [checkEmpty, checkFake, checkEmailReg];
But how are we going to efficiently work through them? We don’t want all of them to be checked when it’s not needed, as that can lead to multiple messages interfering with each other. We want an array technique that stops as soon as it gets a false result.
Looking at the list of array methods
In that article we find the Array.every method which says:
The every method executes the provided callback function once for each element present in the array until it finds the one where callback returns a falsy value. If such an element is found, the every method immediately returns false.
We can replace the isValid code now, using the every method.
// const isValid = checkEmpty(inputGroup) && checkFake(inputGroup) && checkEmailReg(inputGroup);
const isValid = emailTypes.every(function (check) {
return check(inputGroup);
});
In my previous post I used Array.some which stops when it gets a truthy value, whereas Array.every in this post stops when it gets a falsy value. It can be handy to know that Array.some and Array.every are the opposite of each other.
That’s the email code all dealt with. Now it’s on to passwords.
Test for empty password value
The first password test checks for an empty value.
To help keep the tests organised, I’ve put the email tests into their own describe section, so we can have a separate password tests section in there too.
describe("validate", function () {
describe("email", function () {
//...
});
describe("password", function () {
});
});
The password test is as follows:
describe("password", function () {
const passwordGroup = $("#login .form-group").has("[name='Password']").get(0);
const input = $(passwordGroup).find("input").get(0);
const $passwordError = $(passwordGroup).find(".error");
it("is empty", function () {
input.value = "";
validate(passwordGroup);
expect($passwordError.html()).to.equal("Password is empty");
});
it("isn't empty", function () {
input.value = "Password123";
validate(passwordGroup);
expect($passwordError.html()).to.not.contain("is empty");
});
});
There is a problem though - we are told by the test: AssertionError: expected 'E-mail is empty' to equal 'Password is empty'
Use field name to differentiate what gets checked
The email test is interfering with the password test, which is bad news. We need the email validation to restrict itself only to email fields.
As each input field has a reliably consistant name with it, we can use that name to differentiate between the different types of input fields, and move the emailTypes check into an if statment.
function getName(inputGroup) {
return $(inputGroup).find(".input-check").attr("name");
}
//...
const emailTypes = [checkEmpty, checkFake, checkEmailReg];
let isValid = false;
if (getName(inputGroup) === "E-mail") {
isValid = emailTypes.every(function (check) {
return check(inputGroup);
});
}
I’m not happy about emailTypes configuration being low down in the function either. Things tend to be better when configuration is kept near the top of the function, where we can easily find it. I’ll move that emailTypes up to the top of the validate function.
function validate(inputGroup) {
const emailTypes = [checkEmpty, checkFake, checkEmailReg];
//...
let isValid = false;
The password test now fails for the correct reason: AssertionError: expected 'Your password' to equal 'Password is empty'
Adding the password check
We can go ahead to add some password code to the validate function. Up where we have that emailTypes array, I’ll add a passwordTypes array too.
const emailTypes = [checkEmpty, checkFake, checkEmailReg];
const passwordTypes = [checkEmpty];
and we can see if the input field is a password one, before checking that field.
let isValid = false;
if (getName(inputGroup) === "E-mail") {
//...
}
if (getName(inputGroup) === "Password") {
isValid = passwordTypes.every(function (check) {
return check(inputGroup);
});
}
We are given a test error of: AssertionError: expected 'E-mail is empty' to equal 'Password is empty'
so we need to update the checkEmpty function so that it uses the correct name.
Use the name variable in the check functions
In the checkEmpty function we can use getName to get the appropriate name to use on the warning message:
function checkEmpty(inputGroup) {
if (getValue(inputGroup) === "") {
inputStatus.warning(inputGroup, getName(inputGroup) + " is empty");
and the tests all successfully pass.
This is also a good time to update all of the other check functions to use getName(inputGroup) instead.
Improve the code structure
We will eventually have more than just email and passwords to check. There’s a huge set of requirements in the registration form that will eventually be taking place too.
All of the code in the if email and if password sections is nearly identical, all but for the emailTypes and passwordTypes variables. We can associate the names of “E-mail” and “Password” with emailTypes and passwordTypes by putting them into a validationTypes object. That way, we can get the types to use, and replace emailTypes and passwordTypes with just types, making the contents of the if statements exactly the same.
// const emailTypes = [checkEmpty, checkFake, checkEmailReg];
// const passwordTypes = [checkEmpty];
const validationTypes = {
"E-mail": [checkEmpty, checkFake, checkEmailReg],
"Password": [checkEmpty]
};
//...
const types = validationTypes[getName(inputGroup)];
let isValid = false;
if (getName(inputGroup) === "E-mail") {
// isValid = emailTypes.every(function (check) {
isValid = types.every(function (check) {
return check(inputGroup);
});
}
if (getName(inputGroup) === "Password") {
// isValid = passwordTypes.every(function (check) {
isValid = types.every(function (check) {
return check(inputGroup);
});
}
Now that the if statements have exactly the same contents, we can remove the if statements as they’re no longer needed.
const types = validationTypes[getName(inputGroup)];
// if (getName(inputGroup) === "E-mail") {
// isValid = types.every(function (check) {
// return check(inputGroup);
// });
// }
// if (getName(inputGroup) === "Password") {
// isValid = types.every(function (check) {
// return check(inputGroup);
// });
// }
const isValid = types.every(function (check) {
return check(inputGroup);
});
The code that gets the types and uses them, is now easily organised away into a validateByTypes function.
function validateByTypes(inputGroup) {
const name = getName(inputGroup);
const types = validationTypes[name];
return types.every(function (check) {
return check(inputGroup);
});
}
let isValid = validateByTypes(inputGroup);
Check password for fake text
The next password test for fake text is as follows:
it("is fake text", function () {
input.value = "aaabbb";
validate(passwordGroup);
expect($passwordError.html()).to.equal("Password is Fake text: Please remove repetition");
});
it("isn't fake text", function () {
input.value = "Password123";
validate(passwordGroup);
expect($passwordError.html()).to.not.contain("Fake text");
});
After making improvements to the validate code, it’s really easy to make those tests pass.
const validationTypes = {
"E-mail": [checkEmpty, checkFake, checkEmailReg],
// "Password": [checkEmpty]
"Password": [checkEmpty, checkFake]
};
Check for short password
The tests for a short password can follow the same structure that we’ve developed:
it("is short password", function () {
input.value = "ab";
validate(passwordGroup);
expect($passwordError.html()).to.equal("Password is Incorrect: Please enter at least 6 characters");
});
it("isn't a short password", function () {
input.value = "Password123";
validate(passwordGroup);
expect($passwordError.html()).to.not.contain("enter at least");
});
We add a reference to a checkPasswordShort function to the validator by updating the password rules:
// "Password": [checkEmpty, checkFake]
"Password": [checkEmpty, checkFake, checkPasswordShort]
The checkPasswordShort function to make the test pass is as easy as using the regular expression for a short password.
function checkPasswordShort(inputGroup) {
const pswReglow = /^([a-zA-Z0-9]{0,5})$/;
const value = getValue(inputGroup);
if (pswReglow.test(value)) {
inputStatus.warning(inputGroup, getName(inputGroup) + " is Incorrect: Please enter at least 6 characters");
return false;
}
return true;
}
Check for long password
The final test for a long password are almost anticlimatic, as they are similar to the test for a short password:
it("is long password", function () {
input.value = "abcdefghijklmnopqrstuvwxyz";
validate(passwordGroup);
expect($passwordError.html()).to.equal("Password is Incorrect: Please enter no more than 12 characters");
});
it("isn't long password", function () {
input.value = "Password123";
validate(passwordGroup);
expect($passwordError.html()).to.not.contain("12 characters");
});
We add a checkPasswordLong reference to the password list,
// "Password": [checkEmpty, checkFake, checkPasswordShort]
"Password": [checkEmpty, checkFake, checkPasswordShort, checkPasswordLong]
and code to check for a long password uses the high text length regular expression.
function checkPasswordLong(inputGroup) {
const pswReghigh = /^([a-zA-Z0-9]{13,})$/;
const value = getValue(inputGroup);
if (pswReghigh.test(value)) {
inputStatus.warning(inputGroup, getName(inputGroup) + " is Incorrect: Please enter no more than 12 characters");
return false;
}
return true;
}
Summary
We have used tests to recreate the validate function, improving the structure as we come across problems, while ensuring that those tests properly support the code.
The code as it stands today is found at v0.0.20 in releases
Next time we clean up some issues with the existing tests, that are no longer needed.