import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { dispatchKeyboardEvent, dispatchMouseEvent, dispatchTouchEvent } from '@ngneat/spectator';
import { of } from 'rxjs';
import { TypeaheadDirective, TypeaheadMatch, TypeaheadModule, TypeaheadOrder } from '../index';
interface State {
id: number;
name: string;
region: string;
}
@Component({
template: `
`
})
class TestTypeaheadComponent {
selectedState?: string;
states: State[] = [
{ id: 1, name: 'Alabama', region: 'South' },
{ id: 2, name: 'Alaska', region: 'West' },
{ id: 3, name: 'Arizona', region: 'West' },
{ id: 4, name: 'Arkansas', region: 'South' }
];
statesString: string[] = [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut'
];
onBlurEvent(event: TypeaheadMatch) {
return event;
}
}
describe('Directive: Typeahead', () => {
afterAll(async () => {
await new Promise(resolve => setTimeout(() => resolve(), 500)); // avoid jest open handle error
});
let fixture: ComponentFixture;
let component: TestTypeaheadComponent;
let directive: TypeaheadDirective;
let inputElement: HTMLInputElement;
beforeEach(waitForAsync(() =>
TestBed.configureTestingModule({
declarations: [TestTypeaheadComponent],
imports: [TypeaheadModule.forRoot(), BrowserAnimationsModule, FormsModule]
}).compileComponents()
));
beforeEach(() => {
fixture = TestBed.createComponent(TestTypeaheadComponent);
fixture.detectChanges();
component = fixture.componentInstance;
inputElement = fixture.debugElement.query(By.css('input'))
.nativeElement as HTMLInputElement;
// get the typeahead directive instance
const inputs = fixture.debugElement.queryAll(
By.directive(TypeaheadDirective)
);
directive = inputs.map(
(de: DebugElement) =>
de.injector.get(TypeaheadDirective)
)[0];
});
it('should be defined on the testing component', () => {
expect(directive).not.toBeNull();
});
describe('ngOnInit', () => {
it('should set a default value for typeaheadOptionsLimit', () => {
expect(directive.typeaheadOptionsLimit).toBe(20);
});
it('should set a default value for typeaheadMinLength', () => {
expect(directive.typeaheadMinLength).toBe(1);
});
it('should set a default value for typeaheadOrderBy', () => {
expect(directive.typeaheadOrderBy).toBeUndefined();
});
it('should get a value for typeaheadMinLength if user added it', () => {
directive.typeaheadMinLength = 4;
directive.ngOnInit();
expect(directive.typeaheadMinLength).toBe(4);
});
it('should set a default value for typeaheadAsync', () => {
expect(directive.typeaheadAsync).toBeFalsy();
});
it('should set a default value for typeaheadHideResultsOnBlur', () => {
expect(directive.typeaheadHideResultsOnBlur).toBeTruthy();
});
it('should not set the container reference', () => {
expect(directive._container).toBeFalsy();
});
it('should set a default value for typeaheadWaitMs', () => {
expect(directive.typeaheadWaitMs).toBe(0);
});
it('should set a default value for typeaheadSelectFirstItem', () => {
expect(directive.typeaheadSelectFirstItem).toBeTruthy();
});
it('should typeaheadAsync to true, if typeahead is an observable', () => {
directive.typeahead = of(component.states);
directive.ngOnInit();
expect(directive.typeaheadAsync).toBeTruthy();
});
it('should not render the typeahead-container', () => {
const typeaheadContainer = fixture.debugElement.query(
By.css('typeahead-container')
);
expect(typeaheadContainer).toBeNull();
});
it('should be called show method', fakeAsync(() => {
inputElement.value = 'a';
dispatchTouchEvent(inputElement, 'input');
tick();
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
})
);
it('and dropup equal true should be called show method', fakeAsync(() => {
directive.dropup = true;
inputElement.value = 'al';
dispatchTouchEvent(inputElement, 'input');
tick();
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
})
);
it('if value was changed to invalid should be called hide method', fakeAsync(() => {
inputElement.value = 'al';
dispatchTouchEvent(inputElement, 'input');
tick();
inputElement.value = ' ';
dispatchTouchEvent(inputElement, 'input');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
})
);
it('if value equal 0 should be called hide method', fakeAsync(() => {
inputElement.value = ' ';
dispatchTouchEvent(inputElement, 'input');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
})
);
it('if click event triggers on outside element should be called onOutsideClick method',
fakeAsync(() => {
inputElement.value = 'al';
dispatchTouchEvent(inputElement, 'input');
tick();
inputElement.value = ' ';
dispatchMouseEvent(document, 'click');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
})
);
it('should not throw an error on blur', fakeAsync(() => {
expect(directive._container).toBeFalsy();
expect(directive.matches).toEqual([]);
dispatchMouseEvent(inputElement, 'click');
tick();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(() => directive.onBlur()).not.toThrowError();
});
}));
});
describe('onFocus', () => {
it('should work if typeaheadMinLength equal 0', fakeAsync(() => {
directive.typeaheadMinLength = 0;
dispatchMouseEvent(inputElement, 'click');
tick();
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
}));
it('should not work if typeaheadMinLength equal 0', fakeAsync(() => {
dispatchMouseEvent(inputElement, 'click');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
}));
});
describe('changeModel tests', () => {
it('should set the selectedState value', () => {
directive.changeModel(
new TypeaheadMatch(
{ id: 1, name: 'Alabama', region: 'South' },
'Alabama'
)
);
expect(component.selectedState).toBe('Alabama');
});
});
describe('if typeaheadGroupField is not null', () => {
beforeEach(
fakeAsync(() => {
inputElement.value = 'Ala';
dispatchTouchEvent(inputElement, 'input');
directive.typeaheadGroupField = 'region';
fixture.detectChanges();
tick(100);
})
);
it(
'should result in a total of 4 matches, when "Ala" is entered',
fakeAsync(() => {
expect(directive.matches.length).toBe(4);
})
);
it(
'should result in 2 header matches, when "Ala" is entered',
fakeAsync(() => {
expect(directive.matches).toContainEqual(
new TypeaheadMatch('South', 'South', true)
);
expect(directive.matches).toContainEqual(
new TypeaheadMatch('West', 'West', true)
);
})
);
it(
'should result in 2 item matches, when "Ala" is entered',
fakeAsync(() => {
expect(directive.matches).toContainEqual(
new TypeaheadMatch(
{ id: 1, name: 'Alabama', region: 'South' },
'Alabama'
)
);
expect(directive.matches).toContainEqual(
new TypeaheadMatch(
{ id: 2, name: 'Alaska', region: 'West' },
'Alaska'
)
);
})
);
});
describe('onBlur', () => {
it('blur event should send the correct active item', fakeAsync(() => {
inputElement.value = 'Alab';
dispatchTouchEvent(inputElement, 'input');
tick();
jest.spyOn(fixture.componentInstance, 'onBlurEvent').mockImplementation((param: TypeaheadMatch) => {
expect(param.item.id).toBe(1);
return param;
});
directive.onBlur();
expect(directive._container?.isFocused).toBeFalsy();
fixture.detectChanges();
}));
});
describe('onChange', () => {
beforeEach(
fakeAsync(() => {
inputElement.value = 'Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
})
);
it('should render the typeahead-container child element', () => {
const typeaheadContainer = fixture.debugElement.nativeElement.querySelector(
'typeahead-container'
);
expect(typeaheadContainer).not.toBeNull();
});
it('should set the container reference', () => {
expect(directive._container).toBeTruthy();
});
it('should result in a total of 2 matches, when "Ala" is entered', fakeAsync(() => {
expect(directive.matches.length).toBe(2);
})
);
it('should result in 2 item matches, when "Ala" is entered', fakeAsync(() => {
expect(directive.matches).toContainEqual(
new TypeaheadMatch(
{ id: 1, name: 'Alabama', region: 'South' },
'Alabama'
)
);
expect(directive.matches).toContainEqual(
new TypeaheadMatch(
{ id: 2, name: 'Alaska', region: 'West' },
'Alaska'
)
);
})
);
it('should result in 2 item matches, when "Ala" is entered in async mode', fakeAsync(() => {
inputElement.value = 'Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
})
);
it('should result in 0 matches, when input does not match', fakeAsync(() => {
inputElement.value = 'foo';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(0);
})
);
it('should not display null item', fakeAsync(() => {
// eslint-disable-next-line
component.states.push({ id: 3, name: null, region: 'West' } as any);
inputElement.value = 'Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
})
);
it('should be triggered hide method if esc was clicked', fakeAsync(() => {
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
dispatchKeyboardEvent(inputElement, 'keyup', 'ESCAPE');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
}));
it('should be triggered hide method if enter was clicked', fakeAsync(() => {
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
dispatchKeyboardEvent(inputElement, 'keyup', 'ENTER');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
}));
it('should not be triggered prevActiveMatch method if up was clicked', fakeAsync(() => {
inputElement.value = ' ';
dispatchTouchEvent(inputElement, 'input');
tick();
dispatchKeyboardEvent(inputElement, 'keyup', 'UP_ARROW');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
}));
it('should be triggered prevActiveMatch method if up was clicked', fakeAsync(() => {
dispatchKeyboardEvent(inputElement, 'keyup', 'UP_ARROW');
tick();
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
}));
it('should not be triggered nextActiveMatch method if down was clicked', fakeAsync(() => {
inputElement.value = ' ';
dispatchTouchEvent(inputElement, 'input');
tick();
dispatchKeyboardEvent(inputElement, 'keyup', 'DOWN_ARROW');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
}));
it('should be triggered nextActiveMatch method if down was clicked', fakeAsync(() => {
dispatchKeyboardEvent(inputElement, 'keyup', 'DOWN_ARROW');
tick();
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
}));
it('should not close typeahead container if Ctrl was clicked', fakeAsync(() => {
dispatchKeyboardEvent(inputElement, 'keydown', 'INSERT');
tick();
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
}));
});
describe('onKeydown', () => {
beforeEach(
fakeAsync(() => {
inputElement.value = 'Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
})
);
it('should not be triggered show method', fakeAsync(() => {
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
dispatchKeyboardEvent(inputElement, 'keydown', 'TAB');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
}));
it('should not be triggered hide method', fakeAsync(() => {
inputElement.value = ' ';
dispatchTouchEvent(inputElement, 'input');
tick();
dispatchKeyboardEvent(inputElement, 'keydown', 'TAB');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
}));
it('should close container if Enter was clicked', fakeAsync(() => {
dispatchKeyboardEvent(inputElement, 'keydown', 'ENTER');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
}));
it('should not close typeahead container if Ctrl was clicked', fakeAsync(() => {
dispatchKeyboardEvent(inputElement, 'keydown', 'INSERT');
tick();
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
}));
it('should close typeahead container if Tab was clicked', fakeAsync(() => {
inputElement.value = 'Alab';
dispatchTouchEvent(inputElement, 'input');
tick();
dispatchKeyboardEvent(inputElement, 'keydown', 'TAB');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
}));
});
describe('if typeaheadHideResultsOnBlur is not null', () => {
beforeEach(
fakeAsync(() => {
inputElement.value = 'Ala';
dispatchTouchEvent(inputElement, 'input');
directive.typeaheadHideResultsOnBlur = false;
fixture.detectChanges();
tick(100);
})
);
it('equal true should be opened',
fakeAsync(() => {
dispatchMouseEvent(document, 'click');
tick();
expect(fixture.nativeElement.querySelector('.dropdown').classList).toContain('open');
})
);
it('equal false should be closed',
fakeAsync(() => {
directive.typeaheadHideResultsOnBlur = true;
dispatchMouseEvent(document, 'click');
tick();
expect(fixture.debugElement.query(By.css('typeahead-container'))).toBeNull();
})
);
});
describe('if typeaheadOrderBy is not null', () => {
describe('and source of options is an array of string should result in 2 items, when "Ala" is entered',
() => {
beforeEach(
fakeAsync(() => {
directive.typeahead = component.statesString;
directive.typeaheadOptionField = void 0;
inputElement.value = 'Ala';
fixture.detectChanges();
tick(100);
})
);
it('and order direction "asc". 1st - Alabama, 2sd - Alaska',
fakeAsync(() => {
directive.typeaheadOrderBy = { direction: 'asc' };
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item).toBe('Alabama');
expect(directive.matches[1].item).toBe('Alaska');
})
);
it(
'and order direction "desc". 1st - Alaska, 2sd - Alabama',
fakeAsync(() => {
directive.typeaheadOrderBy = { direction: 'desc' };
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item).toBe('Alaska');
expect(directive.matches[1].item).toBe('Alabama');
})
);
it('and typeaheadOrderBy is empty object, shouldn\'t break the app',
fakeAsync(() => {
directive.typeaheadOrderBy = {} as TypeaheadOrder;
console.error = jest.fn();
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(console.error).toHaveBeenCalled();
})
);
it('and order direction is not equal "asc" or "desc", shouldn\'t break the app',
fakeAsync(() => {
directive.typeaheadOrderBy = { direction: 'test' as 'asc' };
console.error = jest.fn();
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(console.error).toHaveBeenCalled();
})
);
it('and order field is setup, it shouldn\'t affect the result',
fakeAsync(() => {
directive.typeaheadOrderBy = { direction: 'asc', field: 'name' };
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
})
);
});
describe('and source of options is an array of objects', () => {
describe('should result in 2 items, when "Ala" is entered', () => {
beforeEach(
fakeAsync(() => {
inputElement.value = 'Ala';
fixture.detectChanges();
tick(100);
})
);
it('and order direction "asc", order field - "name". 1st - Alabama, 2sd - Alaska',
fakeAsync(() => {
directive.typeaheadOrderBy = { direction: 'asc', field: 'name' };
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item.name).toBe('Alabama');
expect(directive.matches[1].item.name).toBe('Alaska');
})
);
it('and order direction "desc", order field - "name". 1st - Alaska, 2sd - Alabama',
fakeAsync(() => {
directive.typeaheadOrderBy = { direction: 'desc', field: 'name' };
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item.name).toBe('Alaska');
expect(directive.matches[1].item.name).toBe('Alabama');
})
);
it(
'and order direction "desc", order field is null. 1st - Alabama, 2sd - Alaska. Lack of the field doesn\'t affect the result',
fakeAsync(() => {
// eslint-disable-next-line
directive.typeaheadOrderBy = { direction: 'desc', field: null } as any;
console.error = jest.fn();
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item.name).toBe('Alabama');
expect(directive.matches[1].item.name).toBe('Alaska');
expect(console.error).toHaveBeenCalled();
})
);
it(
'and order direction "desc", order field is "testing". 1st - Alabama, 2sd - Alaska. The wrong field doesn\'t affect the result',
fakeAsync(() => {
directive.typeaheadOrderBy = { direction: 'desc', field: 'test' };
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item.name).toBe('Alabama');
expect(directive.matches[1].item.name).toBe('Alaska');
})
);
});
describe('should result in 4 items, when "a" is entered', () => {
beforeEach(
fakeAsync(() => {
inputElement.value = 'a';
fixture.detectChanges();
tick(100);
})
);
it('and order direction "asc", order field - "region". Result = Alabama-Arkansas-Alaska-Arizona',
fakeAsync(() => {
directive.typeaheadOrderBy = { direction: 'asc', field: 'region' };
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(4);
expect(directive.matches[0].item.name).toBe('Alabama');
expect(directive.matches[1].item.name).toBe('Arkansas');
expect(directive.matches[2].item.name).toBe('Alaska');
expect(directive.matches[3].item.name).toBe('Arizona');
})
);
it('and order direction "desc", order field - "id". Result = Arkansas-Arizona-Alaska-Alabama',
fakeAsync(() => {
directive.typeaheadOrderBy = { direction: 'desc', field: 'id' };
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(4);
expect(directive.matches[0].item.name).toBe('Arkansas');
expect(directive.matches[1].item.name).toBe('Arizona');
expect(directive.matches[2].item.name).toBe('Alaska');
expect(directive.matches[3].item.name).toBe('Alabama');
})
);
});
});
});
describe('if typeaheadMultipleSearch is true', () => {
beforeEach(
fakeAsync(() => {
directive.typeahead = component.statesString;
directive.typeaheadMultipleSearch = true;
fixture.detectChanges();
tick(100);
})
);
it('and comma entered after one value is picked from typeahead dropdown, should show all available matches',
fakeAsync(() => {
inputElement.value = 'Alabama';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(1);
expect(directive.matches[0].item).toBe('Alabama');
inputElement.value = 'Alabama,';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(component.statesString.length);
}));
it(`and 'Ala' is entered after ',' or '|' when these used for typeaheadMultipleSearchDelimiters,
should give matches for Alaska and Alabama`,
fakeAsync(() => {
directive.typeaheadMultipleSearchDelimiters = ',|';
inputElement.value = 'Alabama';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(1);
expect(directive.matches[0].item).toBe('Alabama');
inputElement.value = 'Alabama,Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item).toBe('Alabama');
expect(directive.matches[1].item).toBe('Alaska');
inputElement.value = 'Alabama|Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item).toBe('Alabama');
expect(directive.matches[1].item).toBe('Alaska');
}));
it('and use, should give matches for Alaska and Alabama',
fakeAsync(() => {
inputElement.value = 'Alabama';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(1);
expect(directive.matches[0].item).toBe('Alabama');
inputElement.value = 'Alabama,Ala';
dispatchTouchEvent(inputElement, 'input');
fixture.detectChanges();
tick(100);
expect(directive.matches.length).toBe(2);
expect(directive.matches[0].item).toBe('Alabama');
expect(directive.matches[1].item).toBe('Alaska');
}));
it('and use comma for typeaheadWordDelimiters, should throw error',
fakeAsync(() => {
directive.typeaheadWordDelimiters = ',';
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).toThrowError();
}));
it('and use comma for typeaheadPhraseDelimiters, should throw error',
fakeAsync(() => {
directive.typeaheadPhraseDelimiters = ',';
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).toThrowError();
}));
it('and use space for typeaheadMultipleSearchDelimiters, should throw error',
fakeAsync(() => {
directive.typeaheadMultipleSearchDelimiters = ' ';
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).toThrowError();
}));
it('use space for typeaheadMultipleSearchDelimiters and \',\' for typeaheadWordDelimiters, should not throw error',
fakeAsync(() => {
directive.typeaheadMultipleSearchDelimiters = ' ';
directive.typeaheadWordDelimiters = ',';
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).not.toThrowError();
}));
it('and use space for typeaheadMultipleSearchDelimiters and typeaheadSingleWords is false, should not throw error',
fakeAsync(() => {
directive.typeaheadMultipleSearchDelimiters = ' ';
directive.typeaheadSingleWords = false;
fixture.detectChanges();
tick(100);
expect(() => directive.ngOnInit()).not.toThrowError();
}));
});
});