test(select/segment): adds unit test for select and segment

This commit is contained in:
Manuel Mtz-Almeida
2017-03-28 17:57:16 +02:00
parent 9be5751eeb
commit 3d569eb88a
20 changed files with 197 additions and 112 deletions

View File

@ -58,13 +58,13 @@ export class PageOne {
buttons: [
{
text: 'Cancel',
handler: (data: any) => {
handler: (data) => {
console.log('Cancel clicked');
}
},
{
text: 'Save',
handler: (data: any) => {
handler: (data) => {
console.log('Saved clicked');
}
}

View File

@ -28,5 +28,5 @@ export interface AlertButton {
text?: string;
role?: string;
cssClass?: string;
handler?: Function;
handler?: (value: any) => boolean|void;
};

View File

@ -11,9 +11,7 @@ import { PageOneModule } from '../pages/page-one/page-one.module';
],
imports: [
BrowserModule,
IonicModule.forRoot(AppComponent, {
mode: 'ios'
}),
IonicModule.forRoot(AppComponent, {}),
PageOneModule
],
bootstrap: [IonicApp]

View File

@ -94,8 +94,7 @@ export class Checkbox extends BaseInput<boolean> implements IonicTapInput, After
renderer: Renderer,
private _cd: ChangeDetectorRef
) {
super(config, elementRef, renderer, 'checkbox', form, item, null);
this._value = false;
super(config, elementRef, renderer, 'checkbox', false, form, item, null);
}
/**
@ -110,7 +109,6 @@ export class Checkbox extends BaseInput<boolean> implements IonicTapInput, After
*/
@HostListener('click', ['$event'])
_click(ev: UIEvent) {
console.debug('checkbox, checked');
ev.preventDefault();
ev.stopPropagation();
this.value = !this.value;
@ -122,6 +120,7 @@ export class Checkbox extends BaseInput<boolean> implements IonicTapInput, After
_inputNormalize(val: any): boolean {
return isTrueProperty(val);
}
/**
* @hidden
*/
@ -129,11 +128,4 @@ export class Checkbox extends BaseInput<boolean> implements IonicTapInput, After
this._item && this._item.setElementClass('item-checkbox-checked', val);
}
/**
* @hidden
*/
_inputUpdated() {
this._cd.detectChanges();
}
}

View File

@ -273,12 +273,11 @@ export const DATETIME_VALUE_ACCESSOR: any = {
providers: [DATETIME_VALUE_ACCESSOR],
encapsulation: ViewEncapsulation.None,
})
export class DateTime extends BaseInput<any> implements AfterContentInit, ControlValueAccessor, OnDestroy {
export class DateTime extends BaseInput<DateTimeData> implements AfterContentInit, ControlValueAccessor, OnDestroy {
_text: string = '';
_min: DateTimeData;
_max: DateTimeData;
_timeValue: DateTimeData = {};
_locale: LocaleData = {};
_picker: Picker;
@ -426,7 +425,7 @@ export class DateTime extends BaseInput<any> implements AfterContentInit, Contro
@Optional() item: Item,
@Optional() private _pickerCtrl: PickerController
) {
super(config, elementRef, renderer, 'datetime', form, item, null);
super(config, elementRef, renderer, 'datetime', {}, form, item, null);
}
/**
@ -448,10 +447,17 @@ export class DateTime extends BaseInput<any> implements AfterContentInit, Contro
* @hidden
*/
_inputUpdated() {
updateDate(this._timeValue, this.value);
this.updateText();
}
/**
* @hidden
*/
_inputNormalize(val: any): DateTimeData {
updateDate(this._value, val);
return this._value;
}
/**
* @hidden
*/
@ -743,7 +749,7 @@ export class DateTime extends BaseInput<any> implements AfterContentInit, Contro
* @hidden
*/
getValue(): DateTimeData {
return this._timeValue;
return this._value;
}
/**

View File

@ -604,10 +604,6 @@ describe('DateTime', () => {
datetime.setValue(null);
expect(datetime.getValue()).toEqual({});
datetime.setValue('1994-12-15T13:47:20.789Z');
datetime.setValue(undefined);
expect(datetime.getValue()).toEqual({});
datetime.setValue('1994-12-15T13:47:20.789Z');
datetime.setValue('');
expect(datetime.getValue()).toEqual({});

View File

@ -132,7 +132,7 @@ export class TextInput extends BaseInput<string> implements IonicFormInput {
@Optional() public ngControl: NgControl,
private _dom: DomController
) {
super(config, elementRef, renderer, 'input', form, item, ngControl);
super(config, elementRef, renderer, 'input', '', form, item, ngControl);
this._nav = <NavControllerBase>nav;

View File

@ -10,7 +10,6 @@ import { BaseInput } from '../../util/base-input';
import { Item } from '../item/item';
import { Platform } from '../../platform/platform';
import { PointerCoordinates, pointerCoord } from '../../util/dom';
import { TimeoutDebouncer } from '../../util/debouncer';
import { UIEventManager } from '../../gestures/ui-event-manager';
@ -139,7 +138,6 @@ export class Range extends BaseInput<any> implements AfterViewInit, ControlValue
_barL: string;
_barR: string;
_debouncer: TimeoutDebouncer = new TimeoutDebouncer(0);
_events: UIEventManager;
@ViewChild('slider') public _slider: ElementRef;
@ -268,9 +266,8 @@ export class Range extends BaseInput<any> implements AfterViewInit, ControlValue
private _dom: DomController,
private _cd: ChangeDetectorRef
) {
super(config, elementRef, renderer, 'range', form, item, null);
super(config, elementRef, renderer, 'range', 0, form, item, null);
this._events = new UIEventManager(_plt);
this._value = 0;
}
/**

View File

@ -7,6 +7,7 @@ import { commonInputTest, NUMBER_CORPUS } from '../../../util/input-tester';
describe('Range', () => {
it('should pass common test', () => {
// TODO, validate range inside bounds
const range = createRange();
range._slider = mockElementRef();
commonInputTest(range, {

View File

@ -62,7 +62,6 @@ export class Searchbar extends BaseInput<string> {
_autocomplete: string = 'off';
_autocorrect: string = 'off';
_isActive: boolean = false;
_debouncer: TimeoutDebouncer = new TimeoutDebouncer(250);
_showCancelButton: boolean = false;
_animated: boolean = false;
@ -165,7 +164,8 @@ export class Searchbar extends BaseInput<string> {
renderer: Renderer,
@Optional() ngControl: NgControl
) {
super(config, elementRef, renderer, 'searchbar', null, null, ngControl);
super(config, elementRef, renderer, 'searchbar', '', null, null, ngControl);
this.debounce = 250;
}
@ViewChild('searchbarInput') _searchbarInput: ElementRef;

View File

@ -77,7 +77,7 @@ export class Segment extends BaseInput<string> {
renderer: Renderer,
@Optional() ngControl: NgControl
) {
super(config, elementRef, renderer, 'segment', null, null, ngControl);
super(config, elementRef, renderer, 'segment', null, null, null, ngControl);
}
/**

View File

@ -0,0 +1,31 @@
import { QueryList } from '@angular/core';
import { Segment } from '../segment';
import { SegmentButton } from '../segment-button';
import { mockConfig, mockElementRef, mockRenderer } from '../../../util/mock-providers';
import { commonInputTest } from '../../../util/input-tester';
describe('Segment', () => {
it('should pass common test', () => {
const config = mockConfig();
const elementRef = mockElementRef();
const renderer = mockRenderer();
const segment = new Segment(config, elementRef, renderer, null);
segment._buttons = new QueryList<SegmentButton>();
commonInputTest(segment, {
defaultValue: null,
corpus: [
['option1', 'option1'],
['option2', 'option2'],
['option3', 'option3'],
['option4', 'option4'],
['', ''],
]
});
});
});

View File

@ -7,7 +7,7 @@ import { App } from '../app/app';
import { Config } from '../../config/config';
import { Form } from '../../util/form';
import { BaseInput } from '../../util/base-input';
import { isCheckedProperty, isTrueProperty, isBlank, deepCopy } from '../../util/util';
import { isCheckedProperty, isTrueProperty, isBlank, deepCopy, deepEqual } from '../../util/util';
import { Item } from '../item/item';
import { NavController } from '../../navigation/nav-controller';
import { Option } from '../option/option';
@ -195,8 +195,7 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
@Optional() item: Item,
@Optional() private _nav: NavController
) {
super(config, elementRef, renderer, 'select', form, item, null);
this._value = [];
super(config, elementRef, renderer, 'select', [], form, item, null);
}
@ -259,7 +258,7 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
this.interface = 'alert';
}
let overlay: any;
let overlay: ActionSheet | Alert;
if (this.interface === 'action-sheet') {
selectOptions.buttons = selectOptions.buttons.concat(options.map(input => {
return {
@ -321,7 +320,7 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
overlay.addButton({
text: this.okText,
handler: (selectedValues: any) => this.value = selectedValues
handler: (selectedValues) => this.value = selectedValues
});
}
@ -373,20 +372,17 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
}
_inputNormalize(val: any): string[] {
if (val === null) {
if (isBlank(val)) {
return [];
}
if (Array.isArray(val)) {
return val;
}
return isBlank(val) ? [] : [val];
return [val + ''];
}
_inputShouldChange(val: string[]): boolean {
if (val.length === 0 && this._value.length === 0) {
return false;
}
return super._inputShouldChange(val);
return !deepEqual(this._value, val);
}
/**

View File

@ -1,7 +1,7 @@
import { Select } from '../select';
import { mockApp, mockConfig, mockElementRef, mockRenderer, mockItem, mockForm } from '../../../util/mock-providers';
import { commonInputTest, BOOLEAN_CORPUS } from '../../../util/input-tester';
import { commonInputTest } from '../../../util/input-tester';
describe('Select', () => {
@ -16,8 +16,13 @@ describe('Select', () => {
const select = new Select(app, form, config, elementRef, renderer, item, null);
commonInputTest(select, {
defaultValue: false,
corpus: BOOLEAN_CORPUS
defaultValue: [],
corpus: [
[['hola'], ['hola']],
[null, []],
['hola', ['hola']],
[['hola', 'adios'], ['hola', 'adios']]
]
});
});

View File

@ -20,7 +20,7 @@ describe('Toggle', () => {
commonInputTest(toggle, {
defaultValue: false,
corpus: BOOLEAN_CORPUS
corpus: BOOLEAN_CORPUS,
});
});

View File

@ -109,8 +109,7 @@ export class Toggle extends BaseInput<boolean> implements IonicTapInput, AfterVi
private _domCtrl: DomController,
private _cd: ChangeDetectorRef
) {
super(config, elementRef, renderer, 'toggle', form, item, null);
this._value = false;
super(config, elementRef, renderer, 'toggle', false, form, item, null);
}
/**

View File

@ -2,7 +2,7 @@ import { ElementRef, EventEmitter, Input, Output, Renderer } from '@angular/core
import { ControlValueAccessor } from '@angular/forms';
import { NgControl } from '@angular/forms';
import { isPresent, isArray, isTrueProperty, assert } from './util';
import { isPresent, isUndefined, isArray, isTrueProperty, deepCopy, assert } from './util';
import { Ion } from '../components/ion';
import { Config } from '../config/config';
import { Item } from '../components/item/item';
@ -30,13 +30,13 @@ export interface CommonInput<T> extends ControlValueAccessor {
export class BaseInput<T> extends Ion implements CommonInput<T> {
_value: T = null;
_value: T;
_onChanged: Function;
_onTouched: Function;
_isFocus: boolean = false;
_labelId: string;
_disabled: boolean = false;
_debouncer: TimeoutDebouncer;
_debouncer: TimeoutDebouncer = new TimeoutDebouncer(0);
_init: boolean = false;
id: string;
@ -66,18 +66,19 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
this.setDisabledState(val);
}
constructor(
config: Config,
elementRef: ElementRef,
renderer: Renderer,
name: string,
private _defaultValue: T,
public _form: Form,
public _item: Item,
ngControl: NgControl
) {
super(config, elementRef, renderer, name);
_form && _form.register(this);
this._value = deepCopy(this._defaultValue);
if (_item) {
this.id = name + '-' + _item.registerInput(name);
@ -103,7 +104,7 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
// 1. Updates the value
// 2. Calls _inputUpdated()
// 3. Dispatch onChange events
setValue(val: T) {
setValue(val: any) {
this.value = val;
}
@ -123,19 +124,26 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
}
_writeValue(val: any): boolean {
const normalized = this._inputNormalize(val);
const shouldUpdate = this._inputShouldChange(normalized);
if (shouldUpdate) {
console.debug('BaseInput: value changed:', normalized, this);
this._value = normalized;
this._inputCheckHasValue(normalized);
this._inputUpdated();
if (this._init) {
this.ionChange.emit(this);
}
return true;
if (isUndefined(val)) {
return false;
}
const normalized = (val === null)
? deepCopy(this._defaultValue)
: this._inputNormalize(val);
const notUpdate = isUndefined(normalized) || !this._inputShouldChange(normalized);
if (notUpdate) {
return false;
}
console.debug('BaseInput: value changed:', normalized, this);
this._value = normalized;
this._inputCheckHasValue(normalized);
this._inputUpdated();
if (this._init) {
this._debouncer.debounce(() => this.ionChange.emit(this));
}
return false;
return true;
}
/**
@ -224,12 +232,11 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
if (!this._item) {
return;
}
let hasValue: boolean;
if (isArray(val)) {
hasValue = val.length > 0;
} else {
hasValue = isPresent(val);
}
const hasValue = isArray(val)
? val.length > 0
: isPresent(val);
this._item.setElementClass('input-has-value', hasValue);
}
@ -249,7 +256,7 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
* @hidden
*/
_inputShouldChange(val: T): boolean {
return (typeof val !== 'undefined') && this._value !== val;
return this._value !== val;
}
/**

View File

@ -21,7 +21,7 @@ export const NUMBER_CORPUS: any[] = [
[123456789, 123456789],
['1.1234', 1.1234],
['123456789', 123456789],
['-123456789', -123456789],
['-123456789', -123456789]
];
export const BOOLEAN_CORPUS: any[] = [
@ -30,13 +30,11 @@ export const BOOLEAN_CORPUS: any[] = [
['', true],
['false', false],
['true', true],
['hola', false]
];
export const ANY_CORPUS: any[] = [
[true, true],
[false, false],
[null, null],
[0, 0],
['', ''],
[' ', ' '],
@ -68,16 +66,36 @@ function testInput<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean)
}
function testState<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
assert(input._init === isInit, 'input must be init');
assert(!input._isFocus && !input.isFocus(), 'should not be focus');
assert(input.value === config.defaultValue, 'default value is wrong');
assertEqual(input._init, isInit, 'input must be init');
assertEqual(input._isFocus, false, 'should not be focus');
assertEqual(input.isFocus(), false, 'should not be focus');
assertEqual(input.value, config.defaultValue, 'default value is wrong');
let blurCount = 0;
let focusCount = 0;
const subBlur = input.ionBlur.subscribe((ev: any) => {
assertEqual(ev, input, 'ionBlur argument is wrong');
blurCount++;
});
const subFocus = input.ionFocus.subscribe((ev: any) => {
assertEqual(ev, input, 'ionFocus argument is wrong');
focusCount++;
});
input._setFocus();
assert(input._isFocus && input.isFocus(), 'should be focus');
input._setFocus(); // it should not crash
assertEqual(input._isFocus, true, 'should be focus');
assertEqual(input.isFocus(), true, 'should be focus');
input._setFocus();
input._setBlur();
assert(!input._isFocus && !input.isFocus(), 'should not be focus');
assertEqual(input._isFocus, false, 'should be not focus');
assertEqual(input.isFocus(), false, 'should be not focus');
input._setBlur(); // it should not crash
assertEqual(focusCount, 1, 'ionFocus was not called correctly');
assertEqual(blurCount, 1, 'ionBlur was not called correctly');
subBlur.unsubscribe();
subFocus.unsubscribe();
}
function testWriteValue<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
@ -87,27 +105,28 @@ function testWriteValue<T>(input: BaseInput<T>, config: TestConfig, isInit: bool
let OnChangeCalled = 0;
let OnTouchedCalled = 0;
input.value = config.defaultValue;
// Test ionChange
let sub = input.ionChange.subscribe((ev: any) => {
assert(ionChangeCalled === 0, 'internal error');
assert(ev === input, 'ev is not the input');
assert(test[1] === ev.value, 'value does not match');
assertEqual(ionChangeCalled, 0, 'ionChange: internal error');
assertEqual(ev, input, 'ionChange: ev is not the input');
assertEqual(ev.value, test[1], 'ionChange: value does not match');
ionChangeCalled++;
});
// Test registerOnChange
input.registerOnChange((ev: any) => {
assert(OnChangeCalled === 0, 'internal error');
assert(ev === input.value, 'ev output does not match');
assert(test[1] === input.value, 'value does not match');
assertEqual(OnChangeCalled, 0, 'registerOnChange: internal error');
assertEqual(input.value, ev, 'registerOnChange: ev output does not match');
assertEqual(input.value, test[1], 'registerOnChange: value does not match');
OnChangeCalled++;
});
// Test registerOnChange
input.registerOnTouched(() => {
assert(OnTouchedCalled === 0, 'internal error');
assertEqual(OnTouchedCalled, 0, 'registerOnTouched: internal error');
OnTouchedCalled++;
});
@ -115,28 +134,45 @@ function testWriteValue<T>(input: BaseInput<T>, config: TestConfig, isInit: bool
for (i = 0; i < config.corpus.length; i++) {
test = config.corpus[i];
input.value = test[0];
assert(input.value === test[1], 'input/output does not match');
assertEqual(input.value, test[1], 'loop: input/output does not match');
if (isInit) {
assert(ionChangeCalled === 1, 'ionChange error');
assertEqual(ionChangeCalled, 1, 'loop: ionChange error');
} else {
assert(ionChangeCalled === 0, 'ionChange error');
assertEqual(ionChangeCalled, 0, 'loop: ionChange error');
}
assert(OnChangeCalled === 1, 'OnChangeCalled was not called');
assert(OnTouchedCalled === 1, 'OnTouchedCalled was not called');
assertEqual(OnChangeCalled, 1, 'loop: OnChangeCalled was not called');
assertEqual(OnTouchedCalled, 1, 'loop: OnTouchedCalled was not called');
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
console.log(test[0], input.value);
// Set same value (it should not redispatch)
input.value = test[0];
assert(ionChangeCalled === 0, 'ionChange should not be called');
assert(OnChangeCalled === 0, 'OnChangeCalled should not be called');
assertEqual(ionChangeCalled, 0, 'loop: ionChange should not be called');
assertEqual(OnChangeCalled, 0, 'loop: OnChangeCalled should not be called');
// TODO OnTouchedCalled?
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
}
// Test undefined
input.value = undefined;
assertEqual(input.value, test[1], 'undefined should not change the value');
assertEqual(ionChangeCalled, 0, 'undefined: ionChange should not be called');
assertEqual(OnChangeCalled, 0, 'undefined: OnChangeCalled should not be called');
assertEqual(OnTouchedCalled, 0, 'undefined: OnTouchedCalled should not be called');
// Test null (reset)
test = [null, config.defaultValue];
input.value = null;
assertEqual(input.value, config.defaultValue, 'null: wrong default value');
assertEqual(OnChangeCalled, 1, 'null: OnChangeCalled was not called');
assertEqual(OnTouchedCalled, 1, 'null: OnTouchedCalled was not called');
input.registerOnChange(null);
input.registerOnTouched(null);
sub.unsubscribe();
input.value = config.defaultValue;
}
function testNgModelChange<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
@ -148,9 +184,10 @@ function testNgModelChange<T>(input: BaseInput<T>, config: TestConfig, isInit: b
// Test ionChange
let sub = input.ionChange.subscribe((ev: any) => {
assert(ionChangeCalled === 0, 'internal error');
assert(ev === input, 'ev output does not match');
assert(test[1] === ev.value, 'value does not match');
assertEqual(ionChangeCalled, 0, 'internal error');
assertEqual(ev, input, 'ev output does not match');
assertEqual(test[1], ev.value, 'value does not match');
ionChangeCalled++;
});
@ -169,21 +206,21 @@ function testNgModelChange<T>(input: BaseInput<T>, config: TestConfig, isInit: b
test = config.corpus[i];
input.writeValue(test[0]);
assert(input.value === test[1], 'input/output does not match');
assertEqual(input.value, test[1], 'input/output does not match');
if (isInit) {
assert(ionChangeCalled === 1, 'ionChange error');
assertEqual(ionChangeCalled, 1, 'ionChange error');
} else {
assert(ionChangeCalled === 0, 'ionChange error');
assertEqual(ionChangeCalled, 0, 'ionChange error');
}
assert(OnChangeCalled === 0, 'OnChangeCalled should not be called');
assert(OnTouchedCalled === 0, 'OnTouchedCalled should not be called');
assertEqual(OnChangeCalled, 0, 'OnChangeCalled should not be called');
assertEqual(OnTouchedCalled, 0, 'OnTouchedCalled should not be called');
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
// Set same value (it should not redispatch)
input.writeValue(test[0]);
input.value = test[0];
assert(ionChangeCalled === 0, 'ionChange should not be called');
assert(OnChangeCalled === 0, 'OnChangeCalled should not be called');
assertEqual(ionChangeCalled, 0, 'ionChange should not be called');
assertEqual(OnChangeCalled, 0, 'OnChangeCalled should not be called');
// TODO OnTouchedCalled?
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
@ -195,7 +232,19 @@ function testNgModelChange<T>(input: BaseInput<T>, config: TestConfig, isInit: b
input.value = config.defaultValue;
}
function assertEqual(a: any, b: any, message: string) {
if (!equal(a, b)) {
assert(false, a + ' != ' + b + ' ' + message);
}
}
function equal(a: any, b: any): boolean {
if (a === b) {
return true;
}
// return false;
return JSON.stringify(a) === JSON.stringify(b);
}

View File

@ -51,5 +51,5 @@ function mockInput(form: any, item: any, ngControl: any): BaseInput<any> {
config = mockConfig(null, '/', platform);
elementRef = mockElementRef();
renderer = mockRenderer();
return new BaseInput(config, elementRef, renderer, 'input', form, item, ngControl);
return new BaseInput(config, elementRef, renderer, 'input', null, form, item, ngControl);
}

View File

@ -16,6 +16,14 @@ export function deepCopy(obj: any) {
return JSON.parse(JSON.stringify(obj));
}
/** @hidden */
export function deepEqual(a: any, b: any) {
if (a === b) {
return true;
}
return JSON.stringify(a) === JSON.stringify(b);
}
/** @hidden */
export function debounce(fn: Function, wait: number, immediate: boolean = false): any {
var timeout: number, args: any, context: any, timestamp: number, result: any;