Skip to content

Commit

Permalink
feat(tests): add missing AutoComplete unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding-SE committed Sep 24, 2019
1 parent 0aafdc9 commit 24487a4
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Editors } from '../index';
import { AutoCompleteEditor } from '../autoCompleteEditor';
import { AutocompleteOption, Column, FieldType, EditorArguments, GridOption, OperatorType, KeyCode } from '../../models';
import { AutocompleteOption, Column, EditorArgs, EditorArguments, GridOption, KeyCode, FieldType } from '../../models';

const KEY_CHAR_A = 97;
const containerId = 'demo-container';

// define a <div> container to simulate the grid container
Expand All @@ -12,13 +13,19 @@ const dataViewStub = {
};

const gridOptionMock = {
autoCommitEdit: false,
enableeditoring: true,
enableeditorTrimWhiteSpace: true,
} as GridOption;

const getEditorLockMock = {
commitCurrentEdit: jest.fn(),
};

const gridStub = {
getOptions: () => gridOptionMock,
getColumns: jest.fn(),
getEditorLock: () => getEditorLockMock,
getHeaderRowColumn: jest.fn(),
render: jest.fn(),
};
Expand Down Expand Up @@ -193,5 +200,225 @@ describe('AutoCompleteEditor', () => {

expect(editor.elementCollection).toEqual([{ value: 'male', label: 'Male' }, { value: 'female', label: 'Female' }]);
});

it('should return True when calling "isValueChanged()" method with previously dispatched keyboard event being char "a"', () => {
const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: KEY_CHAR_A, bubbles: true, cancelable: true });

editor = new AutoCompleteEditor(editorArguments);
const editorElm = divContainer.querySelector<HTMLInputElement>('input.editor-gender');

editorElm.focus();
editorElm.dispatchEvent(event);

expect(editor.isValueChanged()).toBe(true);
});

it('should return False when calling "isValueChanged()" method with previously dispatched keyboard event is same char as current value', () => {
const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: KEY_CHAR_A, bubbles: true, cancelable: true });

editor = new AutoCompleteEditor(editorArguments);
const editorElm = divContainer.querySelector<HTMLInputElement>('input.editor-gender');

editor.loadValue({ id: 123, gender: 'a', isActive: true });
editorElm.focus();
editorElm.dispatchEvent(event);

expect(editor.isValueChanged()).toBe(false);
});

it('should return True when calling "isValueChanged()" method with previously dispatched keyboard event as ENTER and "alwaysSaveOnEnterKey" is enabled', () => {
const event = new (window.window as any).KeyboardEvent('keydown', { keyCode: KeyCode.ENTER, bubbles: true, cancelable: true });
mockColumn.internalColumnEditor.alwaysSaveOnEnterKey = true;

editor = new AutoCompleteEditor(editorArguments);
const editorElm = divContainer.querySelector<HTMLInputElement>('input.editor-gender');

editorElm.focus();
editorElm.dispatchEvent(event);

expect(editor.isValueChanged()).toBe(true);
});

it('should call "focus()" method and expect the DOM element to be focused and selected', async () => {
editor = new AutoCompleteEditor(editorArguments);
const editorElm = editor.editorDomElement;
const spy = jest.spyOn(editorElm, 'focus');
editor.focus();

expect(spy).toHaveBeenCalled();
});

it('should return override the item data as an object found from the collection when calling "applyValue" that passes validation', () => {
mockColumn.internalColumnEditor.validator = null;
mockItemData = { id: 123, gender: 'female', isActive: true };

editor = new AutoCompleteEditor(editorArguments);
editor.applyValue(mockItemData, { value: 'female', label: 'female' });

expect(mockItemData).toEqual({ id: 123, gender: { value: 'female', label: 'female' }, isActive: true });
});

it('should return override the item data as a string found from the collection when calling "applyValue" that passes validation', () => {
mockColumn.internalColumnEditor.validator = null;
mockColumn.internalColumnEditor.collection = ['male', 'female'];
mockItemData = { id: 123, gender: 'female', isActive: true };

editor = new AutoCompleteEditor(editorArguments);
editor.applyValue(mockItemData, 'female');

expect(mockItemData).toEqual({ id: 123, gender: 'female', isActive: true });
});

it('should return item data with an empty string in its value when calling "applyValue" which fails the custom validation', () => {
mockColumn.internalColumnEditor.validator = (value: any, args: EditorArgs) => {
if (value.label.length < 10) {
return { valid: false, msg: 'Must be at least 10 chars long.' };
}
return { valid: true, msg: '' };
};
mockItemData = { id: 123, gender: 'female', isActive: true };

editor = new AutoCompleteEditor(editorArguments);
editor.applyValue(mockItemData, 'female');

expect(mockItemData).toEqual({ id: 123, gender: '', isActive: true });
});

it('should return DOM element value when "forceUserInput" is enabled and loaded value length is greater then minLength defined when calling "serializeValue"', () => {
mockColumn.internalColumnEditor.editorOptions = { forceUserInput: true, };
mockItemData = { id: 123, gender: { value: 'male', label: 'Male' }, isActive: true };

editor = new AutoCompleteEditor(editorArguments);
editor.loadValue(mockItemData);
editor.setValue('Female');
const output = editor.serializeValue();

expect(output).toBe('Female');
});

it('should return DOM element value when "forceUserInput" is enabled and loaded value length is greater then custom minLength defined when calling "serializeValue"', () => {
mockColumn.internalColumnEditor.editorOptions = { forceUserInput: true, minLength: 2 } as AutocompleteOption;
mockItemData = { id: 123, gender: { value: 'male', label: 'Male' }, isActive: true };

editor = new AutoCompleteEditor(editorArguments);
editor.loadValue(mockItemData);
editor.setValue('Female');
const output = editor.serializeValue();

expect(output).toBe('Female');
});

it('should return loaded value when "forceUserInput" is enabled and loaded value length is lower than minLength defined when calling "serializeValue"', () => {
mockColumn.internalColumnEditor.editorOptions = { forceUserInput: true, };
mockItemData = { id: 123, gender: { value: 'male', label: 'Male' }, isActive: true };

editor = new AutoCompleteEditor(editorArguments);
editor.loadValue(mockItemData);
editor.setValue('F');
const output = editor.serializeValue();

expect(output).toBe('Male');
});

it('should return correct object value even when defining a "customStructure" when calling "serializeValue"', () => {
mockColumn.internalColumnEditor.collection = [{ option: 'male', text: 'Male' }, { option: 'female', text: 'Female' }];
mockColumn.internalColumnEditor.customStructure = { value: 'option', label: 'text' };
mockItemData = { id: 123, gender: { option: 'female', text: 'Female' }, isActive: true };

editor = new AutoCompleteEditor(editorArguments);
editor.loadValue(mockItemData);
const output = editor.serializeValue();

expect(output).toBe('Female');
});

it('should return an object output when calling "serializeValue" with its column definition set to "FieldType.object"', () => {
mockColumn.type = FieldType.object;
mockColumn.internalColumnEditor.collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }];
mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true };

editor = new AutoCompleteEditor(editorArguments);
editor.loadValue(mockItemData);
const output = editor.serializeValue();

expect(output).toEqual({ value: 'f', label: 'Female' });
});

it('should call "getEditorLock" when "hasAutoCommitEdit" is enabled after calling "save()" method', async () => {
gridOptionMock.autoCommitEdit = true;
const spy = jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit');

editor = new AutoCompleteEditor(editorArguments);
editor.save();

expect(spy).toHaveBeenCalled();
});

it('should call "commitChanges" when "hasAutoCommitEdit" is disabled after calling "save()" method', async () => {
gridOptionMock.autoCommitEdit = false;
const spy = jest.spyOn(editorArguments, 'commitChanges');

editor = new AutoCompleteEditor(editorArguments);
editor.save();

expect(spy).toHaveBeenCalled();
});

it('should validate and return False when field is required and field is an empty string', () => {
mockColumn.internalColumnEditor.required = true;
editor = new AutoCompleteEditor(editorArguments);
const validation = editor.validate('');

expect(validation).toEqual({ valid: false, msg: 'Field is required' });
});

it('should validate and return True when field is required and field a valid object', () => {
mockColumn.internalColumnEditor.required = true;
editor = new AutoCompleteEditor(editorArguments);
const validation = editor.validate(mockItemData);

expect(validation).toEqual({ valid: true, msg: null });
});

describe('onSelect method', () => {
it('should expect "setValue" and "autoCommitEdit" to have been called with a string when item provided is a string', (done) => {
const spyCommitEdit = jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit');
gridOptionMock.autoCommitEdit = false;
mockColumn.internalColumnEditor.collection = ['male', 'female'];
mockItemData = { id: 123, gender: 'female', isActive: true };

editor = new AutoCompleteEditor(editorArguments);
const spySetValue = jest.spyOn(editor, 'setValue');
const output = editor.onSelect(null, { item: mockItemData.gender });

// HOW DO WE TRIGGER the jQuery UI autocomplete select event? The following works only on "autocompleteselect"
// but that doesn't trigger the "select" (onSelect) directly
// const editorElm = editor.editorDomElement;
// editorElm.on('autocompleteselect', (event, ui) => console.log(ui));
// editorElm[0].dispatchEvent(new (window.window as any).CustomEvent('autocompleteselect', { detail: { item: 'female' }, bubbles: true, cancelable: true }));

setTimeout(() => {
expect(output).toBe(false);
expect(spyCommitEdit).toHaveBeenCalled();
expect(spySetValue).toHaveBeenCalledWith('female');
done();
});
});

it('should expect "setValue" and "autoCommitEdit" to have been called with the string label when item provided is an object', () => {
const spyCommitEdit = jest.spyOn(gridStub.getEditorLock(), 'commitCurrentEdit');
gridOptionMock.autoCommitEdit = false;
mockColumn.internalColumnEditor.collection = [{ value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }];
mockItemData = { id: 123, gender: { value: 'f', label: 'Female' }, isActive: true };

editor = new AutoCompleteEditor(editorArguments);
const spySetValue = jest.spyOn(editor, 'setValue');
const output = editor.onSelect(null, { item: mockItemData.gender });

expect(output).toBe(false);
expect(spyCommitEdit).toHaveBeenCalled();
expect(spySetValue).toHaveBeenCalledWith('Female');
});
});
});
});
35 changes: 20 additions & 15 deletions src/app/modules/angular-slickgrid/editors/autoCompleteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { findOrDefault } from '../services/utilities';
// using external non-typed js libraries
declare var $: any;

// minimum length of chars to type before starting to start querying
const MIN_LENGTH = 3;

/*
* An example of a 'detached' editor.
* KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter.
Expand Down Expand Up @@ -104,7 +107,7 @@ export class AutoCompleteEditor implements Editor {
}

focus() {
this._$editorElm.focus();
this._$editorElm.focus().select();
}

getValue() {
Expand Down Expand Up @@ -142,15 +145,16 @@ export class AutoCompleteEditor implements Editor {
}
}

serializeValue() {
// if user provided a custom structure, we will serialize the value returned from the object with custom structure
const minLength = typeof this.editorOptions.minLength !== 'undefined' ? this.editorOptions.minLength : 3;
serializeValue(): any {
// if you want to add the autocomplete functionality but want the user to be able to input a new option
if (this.editorOptions.forceUserInput) {
this._currentValue = this._$editorElm.val().length >= minLength ? this._$editorElm.val() : this._currentValue;
const minLength = this.editorOptions && this.editorOptions.hasOwnProperty('minLength') ? this.editorOptions.minLength : MIN_LENGTH;
this._currentValue = this._$editorElm.val().length > minLength ? this._$editorElm.val() : this._currentValue;
}
if (this.customStructure && this._currentValue.hasOwnProperty(this.labelName)) {
// if user provided a custom structure, we will serialize the value returned from the object with custom structure
if (this.customStructure && this._currentValue && this._currentValue.hasOwnProperty(this.labelName)) {
return this._currentValue[this.labelName];
} else if (this._currentValue.label) {
} else if (this._currentValue && this._currentValue.label) {
if (this.columnDef.type === FieldType.object) {
return {
[this.labelName]: this._currentValue.label,
Expand All @@ -165,15 +169,16 @@ export class AutoCompleteEditor implements Editor {
applyValue(item: any, state: any) {
let newValue = state;
const fieldName = this.columnDef && this.columnDef.field;

// if we have a collection defined, we will try to find the string within the collection and return it
if (Array.isArray(this.editorCollection) && this.editorCollection.length > 0) {
newValue = findOrDefault(this.editorCollection, (collectionItem: any) => {
if (collectionItem && typeof state === 'object' && collectionItem.hasOwnProperty(this.labelName)) {
return collectionItem[this.labelName].toString() === state[this.labelName].toString();
return (collectionItem.hasOwnProperty(this.labelName) && collectionItem[this.labelName].toString()) === (state.hasOwnProperty(this.labelName) && state[this.labelName].toString());
} else if (collectionItem && typeof state === 'string' && collectionItem.hasOwnProperty(this.labelName)) {
return collectionItem[this.labelName].toString() === state;
return (collectionItem.hasOwnProperty(this.labelName) && collectionItem[this.labelName].toString()) === state;
}
return collectionItem.toString() === state;
return collectionItem && collectionItem.toString() === state;
});
}

Expand All @@ -183,7 +188,7 @@ export class AutoCompleteEditor implements Editor {
item[fieldNameFromComplexObject || fieldName] = (validation && validation.valid) ? newValue : '';
}

isValueChanged() {
isValueChanged(): boolean {
const lastEvent = this._lastInputEvent && this._lastInputEvent.keyCode;
if (this.columnEditor && this.columnEditor.alwaysSaveOnEnterKey && lastEvent === KeyCode.ENTER) {
return true;
Expand Down Expand Up @@ -218,7 +223,9 @@ export class AutoCompleteEditor implements Editor {
// private functions
// ------------------

private onSelect(event: Event, ui: any) {
// this function should be PRIVATE but for unit tests purposes we'll make it public until a better solution is found
// a better solution would be to get the autocomplete DOM element to work with selection but I couldn't find how to do that in Jest
onSelect(event: Event, ui: any): boolean {
if (ui && ui.item) {
this._currentValue = ui && ui.item;
const itemLabel = typeof ui.item === 'string' ? ui.item : ui.item.label;
Expand Down Expand Up @@ -282,8 +289,6 @@ export class AutoCompleteEditor implements Editor {
});
}

setTimeout(() => {
this._$editorElm.focus().select();
}, 50);
setTimeout(() => this.focus(), 50);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -446,4 +446,32 @@ describe('AutoCompleteFilter', () => {
expect(filterCollection[1]).toEqual({ value: 'male', description: 'male' });
expect(filterCollection[2]).toEqual({ value: 'other', description: 'other' });
});

describe('onSelect method', () => {
it('should expect "setValue" and "autoCommitEdit" to have been called with a string when item provided is a string', () => {
const spyCallback = jest.spyOn(filterArguments, 'callback');
mockColumn.filter.collection = ['male', 'female'];

filter.init(filterArguments);
const spySetValue = jest.spyOn(filter, 'setValues');
const output = filter.onSelect(null, { item: 'female' });

expect(output).toBe(false);
expect(spySetValue).toHaveBeenCalledWith('female');
expect(spyCallback).toHaveBeenCalledWith(null, { columnDef: mockColumn, operator: 'EQ', searchTerms: ['female'], shouldTriggerQuery: true });
});

it('should expect "setValue" and "autoCommitEdit" to have been called with the string label when item provided is an object', () => {
const spyCallback = jest.spyOn(filterArguments, 'callback');
mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }];

filter.init(filterArguments);
const spySetValue = jest.spyOn(filter, 'setValues');
const output = filter.onSelect(null, { item: { value: 'f', label: 'Female' } });

expect(output).toBe(false);
expect(spySetValue).toHaveBeenCalledWith('Female');
expect(spyCallback).toHaveBeenCalledWith(null, { columnDef: mockColumn, operator: 'EQ', searchTerms: ['f'], shouldTriggerQuery: true });
});
});
});
Loading

0 comments on commit 24487a4

Please sign in to comment.