我采取了新的立场,现在我必须保持angular4自定义组件,而不进行单元测试(我是一个角形菜鸟;)。
我在添加新功能(业力+茉莉花框架)之前正在编写单元测试,我正在绊倒这个错误的问题并且无法解决这个问题(尝试提供参数,更改测试定义,......):
Failed: Can't resolve all parameters for AutocompleteComponent: (?, ?, ?).
Error: Can't resolve all parameters for AutocompleteComponent: (?, ?, ?).
at syntaxError (spec.bundle.js:36817:34)
at CompileMetadataResolver._getDependenciesMetadata (spec.bundle.js:50893:35)
at CompileMetadataResolver._getTypeMetadata (spec.bundle.js:50761:26)
at CompileMetadataResolver.getNonNormalizedDirectiveMetadata (spec.bundle.js:50356:24)
at CompileMetadataResolver.loadDirectiveMetadata (spec.bundle.js:50220:25)
at spec.bundle.js:61930:72
at Array.forEach (<anonymous>)
at spec.bundle.js:61929:72
at Array.forEach (<anonymous>)
at JitCompiler._loadModules (spec.bundle.js:61926:75)
at JitCompiler._compileModuleAndAllComponents (spec.bundle.js:61909:36)
at JitCompiler.compileModuleAndAllComponentsAsync (spec.bundle.js:61841:37)
at TestingCompilerImpl.compileModuleAndAllComponentsAsync (spec.bundle.js:85709:31)
at TestBed.compileComponents (spec.bundle.js:20190:31)
at Function.TestBed.compileComponents (spec.bundle.js:20073:67)
at UserContext.<anonymous> (spec.bundle.js:86201:14)
at ZoneDelegate.invoke (spec.bundle.js:68195:26)
at AsyncTestZoneSpec.onInvoke (spec.bundle.js:71078:39)
at ProxyZoneSpec.onInvoke (spec.bundle.js:70777:39)
at ZoneDelegate.invoke (spec.bundle.js:68194:32)
at Zone.runGuarded (spec.bundle.js:67958:47)
at runInTestZone (spec.bundle.js:19557:25)
at UserContext.<anonymous> (spec.bundle.js:19496:13)
at ZoneDelegate.invoke (spec.bundle.js:68195:26)
at ProxyZoneSpec.onInvoke (spec.bundle.js:70780:39)
at ZoneDelegate.invoke (spec.bundle.js:68194:32)
at Zone.run (spec.bundle.js:67945:43)
at UserContext.<anonymous> (spec.bundle.js:70986:34)
at ZoneQueueRunner.jasmine.QueueRunner.ZoneQueueRunner.execute (spec.bundle.js:71016:42)
at ZoneQueueRunner.jasmine.QueueRunner.ZoneQueueRunner.execute (spec.bundle.js:71016:42)
at spec.bundle.js:71013:130
at ZoneDelegate.invokeTask (spec.bundle.js:68228:31)
at Zone.runTask (spec.bundle.js:67995:47)
at drainMicroTaskQueue (spec.bundle.js:68399:35)
at <anonymous>
组件:
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnChanges,
OnInit,
Output,
Renderer2,
SimpleChanges,
TemplateRef,
ViewChild
} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NgControl} from '@angular/forms';
import {Subject} from 'rxjs';
import {AbstractInput} from '../shared/abstract-input';
import {isNullOrUndefined, isString} from 'util';
import {IAutocompleteService} from './autocomplete-service';
import {TypeaheadMatch} from 'ngx-bootstrap';
import {HttpClient} from '@angular/common/http';
export const WH_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AutocompleteComponent),
multi: true
};
@Component({
selector: 'cs-autocomplete',
templateUrl: './autocomplete.component.html',
styleUrls: ['./autocomplete.component.scss'],
providers: [WH_AUTOCOMPLETE_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AutocompleteComponent extends AbstractInput implements AfterViewInit, ControlValueAccessor, OnChanges, OnInit {
values: Array<any> = [];
valueFilter: Observable<any>;
haveFocus: boolean;
@Input() width: string = this.DEFAULT_WIDTH;
@Input() editable: boolean = this.DEFAULT_EDITABLE;
@Input() numberOfValues: number = 1;
@Input() removeTag: boolean = false;
@Input() backendUrl: string;
@Input() dataSource: Array<any> = [];
filteredDataSource: Subject<any> = new Subject<any>();
@Input() typeaheadOptionField: string;
@Input() typeaheadOptionsLimit: number;
@Input() typeaheadMinLength: number = 2;
@Input() typeaheadWaitMs: number = 0;
@Input() typeaheadAsync: boolean = false;
@Input() typeaheadLatinize: boolean = true;
@Input() typeaheadSingleWords: boolean = true;
@Input() typeaheadWordDelimitaers: string = ' ';
@Input() typeaheadPhraseDelimiters: string = '\'"';
@Input() typeaheadItemTemplate: TemplateRef<any>;
@Input() nbResult: number = 10;
@Input() searchProperty: string;
@Input() service: IAutocompleteService<any>;
@Input() formControl: FormControl;
@Input() submitted: boolean;
@Input() autoSelectSingleResult: boolean = false;
@Input() params: any;
currentItemTemplate: TemplateRef<any>;
@Input() maxLength: string = 'number';
@Output() typeaheadLoading: EventEmitter<boolean> = new EventEmitter<boolean>();
@Output() typeaheadNoResults: EventEmitter<boolean> = new EventEmitter<boolean>();
@Output() selectionChanged: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('input') input: ElementRef;
@ViewChild('button') button: ElementRef;
@ViewChild(NgControl) ngControl: NgControl;
@ViewChild('defaultItemTemplate') defaultItemTemplate: TemplateRef<any>;
current: any = '';
shouldRemovedTag: boolean = true;
constructor(private http: HttpClient, cdr: ChangeDetectorRef, private renderer: Renderer2) {
super(cdr);
}
ngOnInit(): void {
this.currentItemTemplate = this.defaultItemTemplate;
if (this.typeaheadItemTemplate) {
this.currentItemTemplate = this.typeaheadItemTemplate;
}
}
ngOnChanges(changes: SimpleChanges): void {
super.ngOnChanges(changes);
if (changes.typeaheadItem) {
this.currentItemTemplate = this.typeaheadItemTemplate;
}
if (changes.dataSource) {
//console.log('dataSource', this.dataSource);
}
}
ngAfterViewInit() {
this.valueFilter = this.ngControl.valueChanges.filter(value => {
return value.length >= this.typeaheadMinLength;
});
if (!this.backendUrl && !this.service) {
this.valueFilter.subscribe(value => {
let filteredData = this.dataSource.filter(state => {
let s: string = state[this.typeaheadOptionField].toLowerCase();
return !value || s.startsWith(value.toLowerCase());
});
this.filteredDataSource.next(filteredData);
if (this.autoSelectSingleResult && filteredData.length === 1) {
this.internalSelect(filteredData[0], true);
}
});
} else {
this.valueFilter
.debounceTime(500)
.filter((filter) => {
return filter !== '' && this.numberOfValues > this.values.length;
})
.switchMap(filter => this.callService(filter))
.subscribe((data: any[]) => {
this.filteredDataSource.next(data);
if (this.autoSelectSingleResult && data.length === 1) {
this.internalSelect(data[0], true);
}
});
}
}
keyUp(event: KeyboardEvent) {
if (event.keyCode === 8 && this.current.length === 0 && this.values.length > 0) { // BACK Key
if (this.shouldRemovedTag) {
if (this.removeTag) {
this.values.pop()[this.typeaheadOptionField];
} else {
this.values.pop();
this.current = '';
}
this.updateModel();
}
this.shouldRemovedTag = !this.shouldRemovedTag;
} else if (event.keyCode === 32) { // SPACE Key
this.current = this.current.replace(/ +/g, ' ');
}
}
keyDown(event: KeyboardEvent) {
return this.values.length !== this.numberOfValues || event.keyCode === 9;
}
blur() {
this.current = '';
}
typeaheadOnSelectHandler(selected: TypeaheadMatch): void {
const selectedItem = (selected && selected.item) ? selected.item : '';
this.internalSelect(selectedItem, true);
}
typeaheadLoadingHandler($event: any): void {
this.typeaheadLoading.emit($event);
}
typeaheadNoResultsHandler($event: any): void {
this.typeaheadNoResults.emit($event);
}
removeTagHandler(tag: any): void {
let index = this.values.indexOf(tag);
this.values.splice(index, 1);
this.shouldRemovedTag = false;
this.updateModel();
this.selectionChanged.emit(null);
}
writeValue(values: any): void {
if (!isNullOrUndefined(values) && Array.isArray(values)) { // cas array non vide
this.values = values;
} else if (!isNullOrUndefined(values) && !Array.isArray(values) && !isString(values)) { // cas object non vide
this.values = [values];
} else if (!isNullOrUndefined(values) && !Array.isArray(values) && isString(values)) {
this.current = values;
} else if (isNullOrUndefined(values)) {
this.values = [];
}
this.cdr.markForCheck();
}
setDisabledState(isDisabled: boolean): void {
this.renderer.setProperty(this.input.nativeElement, 'disabled', isDisabled);
if (this.button) {
this.renderer.setProperty(this.button.nativeElement, 'disabled', isDisabled);
}
this.cdr.markForCheck();
}
onFocus() {
this.haveFocus = true;
}
onFocusOut() {
this.haveFocus = false;
}
public focus(): void {
if (!isNullOrUndefined(this.input)) {
this.input.nativeElement.focus();
}
}
private updateModel() {
if (this.numberOfValues > 1) {
this.onChangeCallback(this.values && this.values.length > 0 ? this.values : null);
} else {
this.onChangeCallback(this.values && this.values.length > 0 ? this.values[0] : null);
}
}
private callService(filter: string): Observable<any> {
if (this.backendUrl) {
return this.http.get(`${this.backendUrl}?q=${filter}`);
}
if (this.service) {
const map = new Map();
for (const p in this.params) {
map.set(p, this.params[p]);
}
return this.service.findAutocomplete(filter, this.searchProperty ? this.searchProperty : this.typeaheadOptionField, this.nbResult, map);
}
}
private internalSelect(selectedItem: any, emitEvents?: boolean): void {
if (this.values.indexOf(selectedItem) < 0) {
this.values.push(selectedItem);
}
this.current = '';
this.input.nativeElement.focus();
if (emitEvents) {
this.updateModel();
this.selectionChanged.emit(selectedItem);
}
}
}
和测试:
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ChangeDetectorRef, Renderer2 } from '@angular/core';
import { AutocompleteComponent } from './autocomplete.component';
describe('Component: Autocomplete', () => {
let component: AutocompleteComponent;
let fixture: ComponentFixture<AutocompleteComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [AutocompleteComponent]
})
.overrideTemplate(AutocompleteComponent, '')
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AutocompleteComponent);
fixture.detectChanges();
component = fixture.componentInstance;
});
fit('should create an instance', () => {
expect(component).toBeTruthy();
});
});
有什么建议吗?