Tuesday, November 17, 2020

Git Stash Message

I've often struggled with git stash because I was under the impression you couldn't tag, label, or otherwise identify what you were doing when you stashed something. I've still used it plenty, but it was always a bit of a struggle to figure out which stash was which down the road.

Yesterday I learned you actually can label the stashes, by using save.

git stash save [message]

Good times.

Monday, February 24, 2020

Array Intersection in JavaScript

As JavaScript  progresses we continue to see new features an enhancements that make it into a more robust language. As we apply other languages and frameworks on top of JavaScript we get even more functionality (duh, that's why we use those frameworks). One thing that doesn't exist quite yet out of the box in JavaScript is array intersection. What I mean is the ability to see when one array contains values from another array.

For example, let's say our application has role-based permissions that get populated as an array. That array might look like this:const roles = ['employee', 'manager']; We protect something in our app by applying restrictions to it, maybe like this:const restrictions = ['president', 'accountant']; What we need to know is does the user have the right role to lift the restriction. In other words, we want to know if the two arrays intersect.

Since there is no Array.intersect (yet) we have to come up with something. This is just what I use when I have two simple arrays (in this case two arrays of string values). We could add it to the Array prototype if we want, but I'm not going to show that here.

const roles = ['employee', 'manager'];
const restrictions = ['president', 'accountant'];
const matches = roles.filter(r => restrictions.includes(r));


Once this runs, matches contains all of the values that were in both arrays (so it's an empty array in this case). We can do evaluations on it like:
if (!!matches && !!matches.length) {
  // do some stuff
}

BONUS: We can do the same thing with complex objects by applying the map function to this process. So if our array of roles looked like this:const roles = [{name: 'employee'}, {name: 'manager'}]; we could do the intersect like this:const matches = roles.map(r => r.name).filter(role => restrictions.includes(role));

I got this one and a couple of other goodies from this post on Medium. Happy coding!

Tuesday, February 11, 2020

Masked Input Length Validation in Angular

We use angular2-text-mask to mask some of our inputs at work (so phone number fields show parentheses and a hyphen, for example). I discovered today that using the built-in Angular minLength validator doesn't work with the text mask because the masked value meets the length criteria. So I wrote my own validator based on the built-in one that accepts an optional array of characters to replace and then only returns true if the modified length (that is, the length of the string after removing masking characters) is long enough (or short enough, depending on which validator you use). You know what? Let me just show you.

   1:  import { Injectable } from '@angular/core';
   2:  import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
   3:  
   4:  @Injectable({
   5:    providedIn: 'root'
   6:  })
   7:  export class MaskedLengthValidator {
   8:    static minLength(minLength: number, replace?: Array<string>): ValidatorFn {
   9:      return (control: AbstractControl): ValidationErrors | null => {
  10:        if (this.isEmptyInputValue(control.value)) {
  11:          return null;
  12:        }
  13:  
  14:        let value = control.value;
  15:  
  16:        if (!!replace && !!replace.length) {
  17:          replace.forEach(c => {
  18:            var regEx = new RegExp(c, 'g');
  19:            value = value.replace(regEx, "");
  20:          });
  21:        } else {
  22:          value = control.value.replace(/_/g, "");
  23:        }
  24:  
  25:        const length: number = value ? value.length : 0;
  26:        return length < minLength ?
  27:          {'minlength': {'requiredLength': minLength, 'actualLength': length}} :
  28:          null;
  29:      };
  30:    }
  31:  
  32:    static maxLength(maxLength: number, replace?: Array<string>): ValidatorFn {
  33:      return (control: AbstractControl): ValidationErrors | null => {
  34:        let value = control.value;
  35:  
  36:        if (!!replace && !!replace.length) {
  37:          replace.forEach(c => {
  38:            var regEx = new RegExp(c, 'g');
  39:            value = value.replace(regEx, "");
  40:          });
  41:        } else {
  42:          value = !!value ? control.value.replace(/_/g, "") : value;
  43:        }
  44:  
  45:        const length: number = value ? value.length : 0;
  46:        return length > maxLength ?
  47:          {'maxLength': {'requiredLength': maxLength, 'actualLength': length}} :
  48:          null;
  49:      };
  50:    }
  51:  
  52:    static isEmptyInputValue(value: any): boolean {
  53:      // we don't check for string here so it also works with arrays
  54:      return value == null || value.length === 0;
  55:    }
  56:  }
  57:  

So that's the validator. The simple way to use it is like this:
   1:    ngOnInit() {
   2:      this.someForm = this.formBuilder.group({
   3:        phoneNumber: [null, [Validators.required, MaskedLengthValidator.minLength(10), MaskedLengthValidator.maxLength(10)]]
   4:      });
   5:    }

And if you have placeholders in your mask other than underscores you can use it like this:
   1:    ngOnInit() {
   2:      this.someForm = this.formBuilder.group({
   3:        phoneNumber: [null, [Validators.required, MaskedLengthValidator.minLength(10, ['-']), MaskedLengthValidator.maxLength(10, ['-'])]]
   4:      });
   5:    }

This is pretty straightforward once you think about it. Now you don't have to think about.

Wednesday, January 22, 2020

Debouncing Jasmine

I haven't been writing many unit tests lately, especially not in Jasmine for Angular. That led to a problem today that sucked up a few hours of my time and it really shouldn't have. I feel like at this point I've tested pretty much every "normal" thing I can so when I get stumped it really annoys me.

I have a typeahead control that uses debounceTime (from rxjs) to wait 250 milliseconds before firing off the request to the server. We do that to avoid banging against the server while the user is still typing something. In conjunction with debounceTime, we use switchMap to cancel the previous request and make sure the results we get back match what was actually searched for. When I tried to test this setup I was kept seeing that my spy had not been called and I could not figure out what was going on.

I finally Googled the right combination of terms and stumbled onto this answer on Stack Overflow. The gist of what's happening is that the debounceTime wasn't "passing" so the call to my service was never made. Here's the weird part, though: I added a bunch of logging and I could see that the service was receiving the call. That's why it took me so long to figure out what was going on. From my perspective it looked like the real service was being called instead of the spy I placed on the real service. That naturally sent me searching for known issues in Jasmine when the problem was with the design of my test.

OK, with all of that said, here's the component I was testing (or a close proximity of it anyway):
   1:  export class TypeaheadComponent implements OnInit {
   2:    public searchTermSubject = new Subject<string>();
   3:  
   4:    constructor(private searchService: SearchService) { }
   5:  
   6:    ngOnInit() {
   7:      this.searchTermSubject.pipe(
   8:        debounceTime(250)
   9:      )
  10:      .pipe(
  11:        switchMap(term => {
  12:          // check the term and do other stuff here before calling the server
  13:          return this.searchService.search(term);
  14:        })
  15:      ).subscribe((searchResults) => {
  16:        // do something with the results
  17:      });
  18:    }
  19:  
  20:    triggerSearch(term: string): void {
  21:      this.searchTermSubject.next(this.searchTerm);
  22:    }
  23:  }
  24:  

It looks pretty straightforward to me! Before I show the test that was failing, here's the overall test setup. I think it's important to include this so because I hate when I find an answer and something doesn't work quite right because whoever wrote it didn't show their import/using statements.
   1:  import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
   2:  import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 
   3:  
   4:  import { of } from 'rxjs';
   5:  
   6:  import { SearchService, MockSearchService } from '../providers'; 
   7:  
   8:  import { TypeaheadComponent } from './typeahead.component'; 
   9:  
  10:  describe('TypeaheadComponent', () =>
  11:    let component: TypeaheadComponent;
  12:    let fixture: ComponentFixture<TypeaheadComponent>;
  13:    let searchService: SearchService;
  14:  
  15:    beforeEach(async(() => {
  16:      TestBed.configureTestingModule({
  17:        imports: [FormsModule, ReactiveFormsModule],
  18:        declarations: [TypeaheadComponent],
  19:        providers: [
  20:          {
  21:            provide: SearchService,
  22:            useClass: MockSearchService
  23:          }
  24:        ]
  25:      }).compileComponents();
  26:  
  27:      searchService = TestBed.get(SearchService);
  28:  
  29:      spyOn(searchService, 'search').and.callFake(() => of({}));
  30:    }));
  31:  
  32:    beforeEach(() => {
  33:      fixture = TestBed.createComponent(TypeaheadComponent);
  34:      component = fixture.componentInstance;
  35:      fixture.detectChanges();
  36:    });
  37:  
  38:    // tests go here
  39:  
  40:  });
  41:  

Now that we've established that, here's the test I wrote to make sure the service function (search) was called:
   1:    describe('search', () => {
   2:      it('should search', () => {
   3:        component.triggerSearch('term');
   4:  
   5:        expect(searchService.search).toHaveBeenCalled();
   6:        expect(searchService.search).toHaveBeenCalledWith('term');
   7:      });
   8:    });
   9:  

Unfortunately, that's where the test was failing with an error message indicating that the spy should have been called, but it wasn't. Even looking at it now it still looks good to me. As I mentioned above, I added a bunch of logging to see what was going on and all the right spots were being hit, but my spy wasn't being called. It turns out I had to make a very small change that would "tick" the timer, causing the debounceTime function to allow everything to happen. Here's the updated test:
   1:    describe('search', () => {
   2:      it('should search', fakeAsync(() => {
   3:        component.triggerSearch('term');
   4:        tick(500);
   5:  
   6:        expect(searchService.search).toHaveBeenCalled();
   7:        expect(searchService.search).toHaveBeenCalledWith('term');
   8:      }));
   9:    });
  10:  

That was it. Wrap the test in the fakeAsync function and invoke a tick(500) after invoking the triggerSearch function. Now everything works as I expected.

Like I said at the beginning, my problem was that I assumed from the error message that something was wrong with Jasmine or the injection process in Angular. That's partly why it took me so long to figure out what was happening. Hopefully next time I run into this I remember to come back here and read this blog post.