Unit test own validation function in Angular - javascript

I'm new to testing and starting out writing a own validation method for my Angular app. But I cant figure it out how to test it with Karma. What is the right way to set a value and test it against my validation method?
I get following error
Property 'valid' does not exist on type 'void'.
Please be patient with me but I don't grasp why I get this error.
The form
this.carForm = this.fb.group({
car: this.fb.array([
this.fb.group({
id: car.id,
properties: this.fb.group({
color: 'blue',
vinNumber: ['', ValidationService.isValidVinNumber]
})
})
]),
registrationDate: '2011-11-12',
howManyOwners: car.previousOwners,
})
});
this.carForm.valueChanges.subscribe(value => {
});
The validation method
static isValidVinNumber(formControl: FormControl) {
if (formControl && formControl.value) {
if (formControl.value.length !== 14) {
return false;
}
} else {
return null;
}
}
My test case not working
beforeEach(() => {
fixture = TestBed.createComponent(CarComponent);
component = fixture.componentInstance;
}
........
it('should be possible to post empty vin number', () => {
const iccNumber = component.carForm.get(['car', 0, 'properties', 'vinNumber']);
expect(iccNumber.valid).toBeTruthy();
});

Related

Flutter problem with display searching form firebase. Too many positional arguments

Hello I have a problem with my code.
I want to display a list of a users from firebase, but im stuck at the end.
the line of code shuld look like this:
userName: searchSnapshopt.documents[index].data["name"]);
but after the update, there are no documents anymore and there is "docs" and it breaks everything down for me
here is my code
class _SearchScrState extends State<SearchScr> {
DatabaseMethods databaseMethods = new DatabaseMethods();
TextEditingController searchTextEditingController =
new TextEditingController();
QuerySnapshot searchSnapshopt;
initiateSearch() {
databaseMethods
.getUserByUsername(searchTextEditingController.text)
.then((val) {
setState(() {
searchSnapshopt = val;
});
});
}
Widget searchList() {
return searchSnapshopt != null
? ListView.builder(
itemCount: searchSnapshopt.docChanges.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return SearchTile(
userName: searchSnapshopt.docs[0].data("name"));
})
: Container();
}
Ok i have a solution, just change this
userName: searchSnapshopt.docs[index].data("name")
to this:
userName: searchSnapshopt.docs[index].get("name")
and now works
Try this:
class _SearchScrState extends State<SearchScr> {
DatabaseMethods databaseMethods = new DatabaseMethods();
TextEditingController searchTextEditingController =
new TextEditingController();
QuerySnapshot searchSnapshopt;
initiateSearch() {
databaseMethods
.getUserByUsername(searchTextEditingController.text)
.then((val) {
setState(() {
searchSnapshopt = val;
});
});
}
Widget searchList() {
return searchSnapshopt != null
? ListView.builder(
itemCount: searchSnapshopt.docChanges.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return SearchTile(
userName: searchSnapshopt.docs[index].get("name"));
})
: Container();
}

Having problem in accessing values from state when traversing an array

i'm trying to traverse an Array and perform some operation. But cann't access the values as i want to.
inputObj.map(el => {
const msg = this.state.validation[el.id].message;
const msg2 = this.state.validation['name'].message;
})
Here, el.id can be name, dob or address. When i use this.state.validation[el.id].message;, it shows TypeError: Cannot read property 'message' of undefined. But if i hardcode it like this.state.validation['name'].message;, it works fine. when comparing both el.id and 'name', they have same datatype and same value. So, why having problem when using el.id instead of hardcoding it.
NB: i'm using reactjs.
Edit:
this.state:
this.state = {
super(props);
this.validator = new FormValidator([
{
field: 'name',
method: 'isEmpty',
validWhen: false,
message: 'Name is required'
},
...
]);
orderForm: {
name: {
elementType: 'input',
elementConfig: ''
},
...
}
validation: this.validator.setValid() // it will be the updated upon submitting form on submitHandler by calling validate() from FormValidator
}
inputObj:
const inputObj= [];
for(let key in this.state.orderForm){
inputObj.push({
id : key,
config: this.state.orderForm[key]
});
}
FormValidator
import validator from 'validator';
class FormValidator {
constructor(rules){
this.rules = rules;
}
setValid(){
const validation = {};
this.rules.map(rule => (
validation[rule.field] = {isValid: true, message: ''}
));
return {isValid: true, ...validation};
}
validate(form){
let validation = this.setValid();
this.rules.forEach( rule => {
if (validation[rule.field].isValid){
const field = form[rule.field].toString();
const args = rule.args || [];
const validationMethod = typeof rule.method === 'string' ?
validator[rule.method] : rule.method;
if (validationMethod(field, ...args, form) !== rule.validWhen){
validation[rule.field] = {isValid: false, message: rule.message};
validation.isValid = false;
}
}
});
return validation;
}
}
export default FormValidator;
You can check if this.state.validation[el.id] is defined before getting message key.
Like that you can't get fatal error.
inputObj.map(el => {
this.state.validation[el.id] && (
const msg = this.state.validation[el.id].message
);
})

(Vue) Axios set data not working with 2 properties. I can't explain why

I have the weirdest bug I have ever encountered. I am using Axios and Vee-Validate in my Vue project and from my api I get an error. So withing axios I have a catch.
example:
this.$http.post('v1/auth/register', {
first_name: this.first_name,
last_name: this.last_name,
email: this.email,
phone: this.phone,
password:this.password
}).then((response) => {
this.registration_card = 2;
}).catch((error) => {
if(error.data.error.message === "email_already_exists") {
let input = this.$refs['email'].$children[0];
input.errors.add({ field: 'email', msg: 'email already is use'});
this.loading = false;
console.log(input.errors);
console.log(this.loading);
}
});
Now here comes the weird part. With this code:
let input = this.$refs['email'].$children[0];
input.errors.add({ field: 'email', msg: 'email already is use'});
this.loading = false;
the input.errors is still empty and error wil not be displayed. BUT when i do this:
let input = this.$refs['email'].$children[0];
input.errors.add({ field: 'email', msg: 'email already is use'});
// this.loading = false;
So this.loading will NOT get set, then the error will get set and displayed in my view.
But I want this.loading still be false because I want my loading icon not be displayed. Anyone have a explanation about this.
EDIT: More code
methods: {
register: function () {
let anyError = false;
this.$validate(this, ['first_name', 'last_name', 'phone', 'email', 'password'], function (value, last_item) {
this.loading = true;
if (value === false) {
anyError = true;
}
if (anyError || !last_item) {
return;
}
this.$http.post('v1/auth/register', {
first_name: this.first_name,
last_name: this.last_name,
email: this.email,
phone: this.phone,
password: this.password
}).then((response) => {
this.registration_card = 2;
}).catch((error) => {
if (error.data.error.message === "email_already_exists") {
let input = this.$refs['email'].$children[0];
input.errors.add({field: 'email', msg: 'email already is use'});
this.loadingTest = false;
console.log(input.errors);
console.log(this.loadingTest);
}
});
}.bind(this));
},
}
this.$validate does this:
export default function(scope, arrayOfValues, callback) {
let total = arrayOfValues.length - 1;
let last_item = false;
arrayOfValues.forEach(function(value, index) {
let input = scope.$refs[value].$children[0];
input.$validator.validate().then(value => callback(value, total === index, index));
});
}
I do this because i have custom input components
EDIT: this is where i am using loading:
<j-button label="Register" :loading="loading" #click.native="register"/>
And button coomponent is:
<template>
<button type="button">
<span v-if="!loading">{{label}}</span>
<loading v-if="loading"/>
</button>
</template>
<script>
import loading from 'vue-loading-spinner/src/components/Circle'
export default {
name: 'j-button',
props: [
'label',
'loading'
],
components: {
loading
}
}
</script>
EDIT: Even more code!!!!!
My j-input component
<template>
<div>
<label v-bind:class="{ 'active': (newValue.length > 0)}">{{label}}</label>
<input v-bind:class="{ 'error': (errors.has(name))}" type="text" :name="name" v-validate="rules" :placeholder="label" v-model="newValue" v-on:input="updateValue()" ref="input">
<span v-if="errors.has(name)">{{errors.first(name)}}</span>
</div>
</template>
<script>
export default {
name: 'j-text',
inject: ['$validator'],
props: [
'label',
'name',
'rules',
'value',
],
data() {
return {
newValue: ''
}
},
created() {
this.newValue = this.value;
this.updateValue();
},
methods: {
updateValue: function () {
this.$emit('input', this.newValue);
},
}
}
</script>
So i have found the issue and it is still very strange why. I will make another question for this. Its about my j-button component:
<template>
<button type="button">
<span v-if="!loading">{{label}}</span>
<loading v-if="loading"/>
</button>
</template>
<script>
import loading from 'vue-loading-spinner/src/components/Circle'
export default {
name: 'jellow-button',
props: [
'label',
'loading'
],
components: {
loading
}
}
</script>
To fix this weird issue I had to change this:
<loading v-if="loading"/>
To:
<loading v-show="loading"/>
If I changed this, then the error will be loaded and the button loading icon will be turned off doing this in my catch:
}).catch(error => {
if(error.data.error.message === "email_already_exists") {
let input = this.$refs['email'].$children[0];
input.errors.add({field: 'email', msg: 'email already in use'});
this.loading = false;
}
});
But again. If I do the v-if instead of the v-show in my button then the error will not be showing. Very strange. I will create another question and I hope I get a answer on that.
This is very simple. Only reference change refreshes Vue view.
When you do this:
new Vue({
data: ['property'],
method: {
change() {
this.property = "yes"; // will get refreshed
}
}
});
The view gets refreshed (changes are displayed). But when you change the object's field reference (not the object itself) or when you call a method on it, it won't get refreshed.
change() {
this.property.field = "yes"; // won't get refreshed
this.property.mtehod("yes"); // won't get refreshed
}
Only some certain methods (like array.push()) are tweaked by Vue.js to recognize that those methods get view refreshed. If you want to make it work you need to call this.$forceUpdate() or use Vue.set() to change vales.
So when you add your errors, the view won't get refreshed, only when you change your data property (loading) the view recognize that data value changed and refreshed your view.
Please read Reactivity in Depth, especially chapter "How Changes Are Tracked". Please see which ways of setting data are reactive and which aren't.

Angular 4 - Custom validator with dynamic parameter value

I have written a custom validator that checks if a date is above a certain minimum date.
the code looks like this:
export function validateMinDate(min: Date): ValidatorFn {
return (c: AbstractControl) => {
if (c == null || c.value == null)
return null;
let isValid = c.value >= min;
if (isValid) {
return null;
} else {
return {
validateMinDate: {
valid: false
}
};
}
};
}
I initate my form like this
this.definitionForm = this.fb.group({
"from": [this.details.From, Validators.required],
"to": [this.details.To, [Validators.required, validateMinDate(this.details.From)]]
});
I can see that the validator is being applied, but when I console.log() my min value in the validator I can see that it equal null.
this.details.From starts at null when I initiate the form, so I assume the parameter is not dynamic and just takes the value when the form is being set?
How can I make sure the min date is being updated when a users picks a from date, and thus changes the value of this.details.From?
#Nicolas Validator takes value only once it does not look for it changes. So we can change parameters value dynamically by assigning new validator on value changes. In your case you can do in this way:
onChanges(){
var self=this;
this.definitionForm.get('from').valueChanges.subscribe(val => {
this.from=val;
this.definitionForm.controls['to'].
setValidators(Validators.compose([Validators.required,
TimeValidators.isTimeAfter(this.from)]));
})}
Here i created a separate custom validator for comparing the time. You can either use this or modify yours
import { FormControl, Validators,ValidatorFn, AbstractControl} from '#angular/forms';
export class TimeValidators extends Validators{
static isTimeBefore(timeStr : string): ValidatorFn{
return(c: AbstractControl): {[key:string]: boolean} | null => {
if(c.value!==undefined && (isNaN(c.value)) || c.value > timeStr || c.value== timeStr){
return {
'isTimeBefore':true
}
}
return null;
}
}
static isTimeAfter(timeStr : string): ValidatorFn{
return(c: AbstractControl): {[key:string]: boolean} | null => {
if(c.value!==undefined && (isNaN(c.value)) && (c.value < timeStr || c.value == timeStr)){
return {
'isTimeAfter':true
}
}
return null;
}
}
}
Call onChanges() function after you initialize your definitionForm FormGroup.
You can modify your custom validator to take function as parameter like
export function validateMinDate(min: DateFunc): ValidatorFn {
return (c: AbstractControl) => {
if (c == null || c.value == null)
return null;
let isValid = c.value >= min();
if (isValid) {
return null;
} else {
return {
validateMinDate: {
valid: false
}
};
}
};
and initiate the form like this
this.definitionForm = this.fb.group({
...
"to": [this.details.To, [Validators.required, validateMinDate(() => this.details.From)]]
});
the DateFunc is just a type that you can create like
export interface DateFunc{
(): Date
}
and expect this.details.From to return value of type Date
How I see it, would be to apply the validator on the form group, or if you have a large form, I suggest you create a nested group for from and to and apply the validator on that, since otherwise this custom validator would be fired whenever any changes happen to form. So it would mean to update the validator and formgroup to such:
this.definitionForm = this.fb.group({
"from": [this.details.From, Validators.required],
"to": [this.details.To, [Validators.required]]
}, {validator: validateMinDate()});
export function validateMinDate(): ValidatorFn {
return (c: AbstractControl) => {
if(c) {
let isValid = c.get('to').value >= c.get('from').value;
if (isValid) {
return null;
} else {
return {validateMinDate: true};
}
}
};
}
Of course there are other options as well, such as listening for change event and then do the check of the dates, if not valid, use setErrors on form.
As an alternative to the given answers, at least in Angular 6 you can pass the component as ValidatorFn argument, so you can use its properties at runtime to validate your form control.
Here is a working example:
Component declarations:
#Component({
templateUrl: './create-application.component.html'
})
export class CreateApplicationComponent implements OnInit, OnDestroy {
viewMode: ViewMode;
application: Application;
form: FormGroup;
enterprisesData: any[] = [];
Form:
this.form = this.formBuilder.group({
name: new FormControl(
{
value: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT) ? this.application.name : '',
disabled: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT)
},
{
validators: [ Validators.required, Validators.minLength(3), Validators.maxLength(80) ],
updateOn: 'blur'
}
),
area: new FormControl(
{
value: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT) ? this.application.area : '',
disabled: (this.viewMode === ViewMode.CONSULT)
},
{
validators: [ Validators.required ]
}
),
country: new FormControl(
{
value: '',
disabled: (this.viewMode === ViewMode.CONSULT)
},
{
validators: [ applicationCountryEnterprisesValidator(this) ]
}
)
});
ValidatorFn:
export function applicationCountryEnterprisesValidator(component: CreateApplicationComponent): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
return (component.enterprisesData && component.enterprisesData.length > 0) ? null : { noEnterprisesSelected: true };
};
}

Angular2 forms : validator with interrelated fields

Given a form where one can enter either a city name or its latitude and longitude.
The form would validate if city name is filled OR if both latitude AND longitude are filled. Latitude and longitude, if filled, must be numbers.
I could create a FormGroup with those three fields and do one custom validators...
function fatValidator(group: FormGroup) {
// if cityName is present : is valid
// else if lat and lng are numbers : is valid
// else : is not valid
}
builder.group({
cityName: [''],
lat: [''],
lng: ['']
},
{
validators: fatValidator
});
...but I would like to take advantage of validators composition (e.g testing latitude and longitude to be valid numbers at the fields level in one validator and test the interrelation at the group level in another validator).
I have tested several options but I am stuck with the fact that a group is valid if all its fields are valid. The following construction seems not to be the proper way to approach the problem :
function isNumber(control: FormControl) { ... }
function areAllFilled(group: FormGroup) { ... }
function oneIsFilledAtLeast(group: FormGroup) { ... }
builder.group({
cityName: [''],
localisation: builder.group({
lat: ['', Validators.compose([Validators.minLength(1), isNumber])],
lng: ['', Validators.compose([Validators.minLength(1), isNumber])]
},
{
validators: areAllFilled
})
},
{
validators: oneIsFilledAtLeast
});
How would you do that with Angular2 Is it even possible ?
EDIT
Here is an example of how the fatValidator could be implemented. As you can see it is not reusable and harder to test than composed validators :
function fatValidator (group: FormGroup) {
const coordinatesValidatorFunc = Validators.compose([
Validators.required,
CustomValidators.isNumber
]);
const cityNameControl = group.controls.cityName;
const latControl = group.controls.lat;
const lngControl = group.controls.lng;
const cityNameValidationResult = Validators.required(cityNameControl);
const latValidationResult = coordinatesValidatorFunc(latControl);
const lngValidationResult = coordinatesValidatorFunc(lngControl);
const isCityNameValid = !cityNameValidationResult;
const isLatValid = !latValidationResult;
const isLngValid = !lngValidationResult;
if (isCityNameValid) {
return null;
}
if (isLatValid && isLngValid) {
return null;
}
if (!isCityNameValid && !isLatValid && !isLngValid) {
return { cityNameOrCoordinatesRequired: true, latAndLngMustBeNumbers: true };
}
return Object.assign({},
{ cityName: cityNameValidationResult },
{ lat: latValidationResult },
{ lng: lngValidationResult }
);
}
Using the final release or new of Angular, I have written a reusable method to add a Conditional Required- or other Validation -to a given set of Controls.
export class CustomValidators {
static controlsHaveValueCheck(controlKeys: Array<string>, formGroup: FormGroup): Array<boolean> {
return controlKeys.map((item) => {
// reset any errors already set (ON ALL GIVEN KEYS).
formGroup.controls[item].setErrors(null);
// Checks for empty string and empty array.
let hasValue = (formGroup.controls[item].value instanceof Array) ? formGroup.controls[item].value.length > 0 :
!(formGroup.controls[item].value === "");
return (hasValue) ? false : true;
});
}
static conditionalAnyRequired(controlKeys: Array<string>): ValidatorFn {
return (control: FormControl): {[key: string]: any} => {
let formGroup = control.root;
if (formGroup instanceof FormGroup) {
// Only check if all FormControls are siblings(& present on the nearest FormGroup)
if (controlKeys.every((item) => {
return formGroup.contains(item);
})) {
let result = CustomValidators.controlsHaveValueCheck(controlKeys, formGroup);
// If any item is valid return null, if all are invalid return required error.
return (result.some((item) => {
return item === false;
})) ? null : {required: true};
}
}
return null;
}
}
}
This can be used in your code like this:
this.form = new FormGroup({
'cityName': new FormControl('',
CustomValidators.conditionalAnyRequired(['cityName', 'lat', 'lng'])),
'lat': new FormControl('',
Validators.compose([Validators.minLength(1),
CustomValidators.conditionalAnyRequired(['cityName', 'lat', 'lng']))),
'lng': new FormControl('',
Validators.compose([Validators.minLength(1),
CustomValidators.conditionalAnyRequired(['cityName', 'lat', 'lng'])))
})
This would make any of 'city', 'lat' or 'lng' required.
Additionally, if you wanted either 'city' or 'lat' and 'lng' to be required you can include an additional validator such as this:
static conditionalOnRequired(conditionalControlKey: string, controlKeys: Array<string>): ValidatorFn {
return (control: FormControl): {[key: string]: any} => {
let formGroup = control.root;
if (formGroup instanceof FormGroup) {
if (controlKeys.every((item) => {
return formGroup.contains(item);
}) && formGroup.contains(conditionalControlKey)) {
let firstControlHasValue = (formGroup.controls[conditionalControlKey].value instanceof Array) ? formGroup.controls[conditionalControlKey].value.length > 0 :
!(formGroup.controls[conditionalControlKey].value === ""),
result = CustomValidators.controlsHaveValueCheck(controlKeys, formGroup);
formGroup.controls[conditionalControlKey].setErrors(null); // Also reset the conditional Control...
if (firstControlHasValue && formGroup.controls[conditionalControlKey].value !== false) {// also checks for false (for unchecked checkbox value)...
return (result.every((invalid) => {
return invalid === false;
})) ? null : {required: true};
}
}
}
return null;
}
}
This method will make a set of form controls 'required' based on the value of the conditionalControlKey, i.e. if conditionalControlKey has a value all other controls in controlKeys Array are not required, otherwise the all are required.
I hope this isn't too convoluted for anyone to follow- I am sure these code snippets can be improved, but I feel they aptly demonstrate one way of going about this.

Categories