El selector simulado de prueba de unidad angular no funciona
Estoy tratando de burlarme de un selector en mi prueba unitaria como se muestra a continuación:
describe('Device List Component', () => {
let component: ListComponent;
let fixture: ComponentFixture<ListComponent>;
let deviceServiceMock: any;
let mockStore: MockStore<any>;
let devices;
beforeEach(async(() => {
deviceServiceMock = jasmine.createSpyObj('DevicesService', ['fetchDevices']);
deviceServiceMock.fetchDevices.and.returnValue(of(deviceState()));
TestBed.configureTestingModule({
declarations: [
ListComponent,
MockComponent(DataGridComponent),
],
imports: [
RouterTestingModule,
MockModule(SharedModule),
ToastrModule.forRoot({
preventDuplicates: true,
closeButton: true,
progressBar: true,
}),
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useClass: JsonTranslationLoader },
}),
],
providers: [
{ provide: DevicesService, useValue: deviceServiceMock },
{ provide: ColumnApi, useClass: MockColumnApi },
provideMockStore(),
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ListComponent);
component = fixture.componentInstance;
mockStore = TestBed.get(MockStore);
component.columnApi = TestBed.get(ColumnApi);
devices = mockStore.overrideSelector('devices', deviceState());
fixture.detectChanges();
});
});
Aquí está el archivo de componentes
export class ListComponent implements OnInit, OnDestroy {
columnDefs: DeviceColumns[];
defaultColumnDefs: any;
gridApi: any;
columnApi: any;
overlayNoRowsTemplate: string;
rowData: DeviceData[] = [];
pagination: DevicePagination;
globalSearch: string;
hasFloatingFilter: boolean;
frameworkComponents: any;
dropdownSettings: any = {};
dropDownList: Columns[] = [];
selectedItems: Columns[] = [];
shuffledColumns: any = [];
selectedRows: DeviceData[] = [];
rowSelection: string;
bsModalRef: BsModalRef;
isColumnsSorting: boolean;
hasRowAnimation = true;
multiSortKey = 'ctrl';
changeDetectorRef: ChangeDetectorRef;
deviceDeleteSubscription: Subscription;
currentPage = new Subject<number>();
search = new Subject<string>();
subscription: Subscription;
constructor(
private router: Router,
private devicesService: DevicesService,
public store: Store<any>,
private toast: ToastrService,
private ngZone: NgZone,
private translateService: TranslateService,
private globalTranslate: GlobalLanguageService,
private modalService: BsModalService,
changeDetectorRef: ChangeDetectorRef
) {
this.translateService.stream(['DEVICES.LIST', 'MULTISELECT']).subscribe((translations) => {
const listTranslations = translations['DEVICES.LIST'];
const multiSelectTranslations = translations['MULTISELECT'];
this.overlayNoRowsTemplate = `<span class="ag-overlay-loading-center">${listTranslations.NODEVICE}</span>`; this.dropdownSettings.selectAllText = multiSelectTranslations.SELECTALL; this.dropdownSettings.unSelectAllText = multiSelectTranslations.DESELECTALL; }); this.changeDetectorRef = changeDetectorRef; this.translateService.onLangChange.subscribe(() => { this.gridApi && this.gridApi.refreshHeader(); }); } ngOnInit() { this.loadStore(); this.initializeColumns(); this.pageSearch(); this.dropdownSettings = { singleSelection: false, idField: 'field', textField: 'key', selectAllText: 'Select All', unSelectAllText: 'UnSelect All', itemsShowLimit: 3, allowSearchFilter: true, enableCheckAll: true, }; this.store.dispatch(new DevicesActions.ClearCurretDevice()); } loadStore() { this.store.pipe(select('devices')).subscribe((val) => { const deviceList = val.devices.map((d) => { return { ...d, is_online: d.is_online ? 'Active' : 'Inactive', }; }); this.rowData = deviceList; this.pagination = val.pagination; this.globalSearch = val.globalSearch; this.hasFloatingFilter = val.hasFloatingFilter; this.dropDownList = val.shuffledColumns; this.selectedItems = val.shuffledColumns.filter((column: Columns) => !column.hide); this.selectedRows = val.selectedRows; }); } initializeColumns() { this.columnDefs = [ { headerName: 'S.No', translateKey: 'DEVICES.LIST.SNO', width: 100, resizable: false, sortable: false, suppressSizeToFit: true, valueGetter: (args) => this.getId(args), checkboxSelection: (params) => { console.log('params.columnApi.getRowGroupColumns()', params.columnApi.getRowGroupColumns()); return params.columnApi.getRowGroupColumns().length === 0; }, headerCheckboxSelection: (params) => { return params.columnApi.getRowGroupColumns().length === 0; }, }, ...gridColumns, ]; this.columnDefs = map(this.columnDefs, (columnDef) => { return extend({}, columnDef, { headerValueGetter: this.localizeHeader.bind(this) }); }); this.shuffledColumns.push(this.columnDefs[0]); this.dropDownList.forEach((column, colIndex) => { this.columnDefs.forEach((data) => { if (data.field === column.field) { data.hide = column.hide; data.sort = column.sort; data.width = column.width; data.minWidth = column.minWidth; this.shuffledColumns.splice(colIndex + 1, 0, data); } }); }); this.columnDefs = this.shuffledColumns; this.rowSelection = 'multiple'; this.defaultColumnDefs = { suppressMenu: true, suppressMovable: true, sortable: true, resizable: true, }; this.frameworkComponents = { FloatingFilterComponent: FloatingFilterComponent }; } localizeHeader(params: any) { return this.globalTranslate.getTranslation(params.colDef.translateKey); } getId(args: any): any { return ( this.pagination.per_page * this.pagination.prev_page + parseInt(args.node.rowIndex, 10) + 1 ); } pageSearch() { this.subscription = this.search.subscribe((value) => { this.store.dispatch(new DevicesActions.GlobalSearch(value)); if (value.length === 0) { this.clearSelectedRows(); this.loadData(); } }); } OnGridReady(params) { this.gridApi = params.api; this.columnApi = params.columnApi; this.loadData(); } loadData() { this.devicesService.fetchDevices(this.gridApi); } gotoAddDevice() { this.router.navigate(['/devices/new']); } searchDevices() { this.store.dispatch(new DevicesActions.UpdateCurrentPage(1)); this.clearSelectedRows(); this.loadData(); } clearSelectedRows() { this.store.dispatch(new DevicesActions.ClearSelectedRows()); } onItemSelect(item: DropDownColumns) { this.store.dispatch(new DevicesActions.ColumnSelect(item.field)); this.columnApi.setColumnVisible(item.field, true); } onSelectAll(items: any) { this.store.dispatch(new DevicesActions.ColumnsSelectAll()); items.map((item) => this.columnApi.setColumnVisible(item.field, true)); } onItemUnSelect(item: DropDownColumns) { this.store.dispatch(new DevicesActions.ColumnDeSelect(item.field)); this.columnApi.setColumnVisible(item.field, false); } onDeSelectAll() { this.store.dispatch(new DevicesActions.ColumnsDeSelectAll()); this.dropDownList.map((item) => this.columnApi.setColumnVisible(item.field, false)); } SortedColumns(params: SortedColumns[]) { const columnsId = []; params.map((param: SortedColumns) => { columnsId.push(param.id); }); const shuffledColumns = columnsId.map((columnId) => this.dropDownList.find((data) => data.field === columnId) ); this.store.dispatch(new DevicesActions.ShuffledColumns(shuffledColumns)); this.columnApi.moveColumns(columnsId, 1); } hasDevices() { return this.rowData.length > 0 ? true : false; } updatePage() { if (this.pagination.current_page.toString() === '') { this.pagination.current_page = 1; } this.store.dispatch(new DevicesActions.UpdateCurrentPage(this.pagination.current_page)); this.clearSelectedRows(); this.loadData(); } previousPage() { this.store.dispatch(new DevicesActions.UpdateCurrentPage(this.pagination.prev_page)); this.clearSelectedRows(); this.loadData(); } firstPage() { this.store.dispatch(new DevicesActions.UpdateCurrentPage(1)); this.clearSelectedRows(); this.loadData(); } lastPage() { this.store.dispatch(new DevicesActions.UpdateCurrentPage(this.pagination.total_pages)); this.clearSelectedRows(); this.loadData(); } nextPage() { if (!this.pagination.is_last_page) { this.store.dispatch(new DevicesActions.UpdateCurrentPage(this.pagination.next_page)); this.clearSelectedRows(); this.loadData(); } } toggleFloatingFilter() { this.hasFloatingFilter = !this.hasFloatingFilter; this.store.dispatch(new DevicesActions.UpdateFloatingFilter(this.hasFloatingFilter)); this.clearSelectedRows(); this.gridApi.setRowData(this.rowData); if (!this.hasFloatingFilter) { this.gridApi.setFilterModel(null); this.store.dispatch(new DevicesActions.ClearColumnSearch()); this.loadData(); } setTimeout(() => { this.gridApi.refreshHeader(); }, 0); window.location.reload(); } isSortingEnabled() { this.isColumnsSorting = this.dropDownList.some((column) => column.sort !== ''); return this.isColumnsSorting; } setSortingBackgroundColor() { return this.isColumnsSorting ? COLOR_PRIMARY : COLOR_SECONDARY; } setSortingIconColor() { return this.isColumnsSorting ? ICON_ENABLED : ICON_DISABLED; } clearSort() { this.gridApi.setSortModel(null); this.store.dispatch(new DevicesActions.ClearColumnsSort()); this.loadData(); } resizeColumns() { const allColumnIds = []; this.columnApi.getColumnState().forEach((column) => { allColumnIds.push(column.colId); }); this.columnApi.autoSizeColumns(allColumnIds, false); } onRowDataChanged() { if (this.gridApi) { this.gridApi.forEachNode((node: any) => { const selectNode = this.selectedRows.some((row) => row.id === node.data.id); if (selectNode) { node.setSelected(true); } }); } } onSelectionChanged() { this.selectedRows = this.gridApi.getSelectedRows(); console.log('selected rows', this.selectedRows); this.store.dispatch(new DevicesActions.UpdateSelectedRows(this.selectedRows)); this.changeDetectorRef.detectChanges(); } onSortChanged(params) { this.store.dispatch(new DevicesActions.UpdateColumnsSort(params)); this.clearSelectedRows(); this.loadData(); } onColumnResized() { const updatedColumns: ColumnWidth[] = []; this.columnApi.getColumnState().forEach((column) => { updatedColumns.push({ field: column.colId, width: column.width }); }); this.store.dispatch(new DevicesActions.UpdateColumnWidth(updatedColumns)); } gotoDetailView(params: any) { const id = params.id; this.ngZone.run(() => this.router.navigate(['/devices', id])); } isDeleteEnabled() { return this.selectedRows.length === 0 ? true : false; } setDeleteBackgroundColor() { return this.selectedRows.length !== 0 ? COLOR_PRIMARY : COLOR_SECONDARY; } setDeleteIconColor() { return this.selectedRows.length !== 0 ? ICON_ENABLED : ICON_DISABLED; } openModal() { const initialState = { title: 'Delete Device', message: 'Do you really want to delete the device? This process cannot be undone', }; if (this.selectedRows.length > 1) { (initialState.title = 'Delete Devices'), (initialState.message = `Do you really want to delete ${this.selectedRows.length} devices? This process cannot be undone`);
}
this.bsModalRef = this.modalService.show(ModalDeleteComponent, { initialState });
this.bsModalRef.content.delete.subscribe((canDelete: boolean) => {
if (canDelete) {
this.deleteDevices();
}
this.bsModalRef.hide();
});
}
ngOnDestroy() {
this.deviceDeleteSubscription?.unsubscribe();
}
deleteDevices() {
const selectedIds = this.selectedRows.map((row) => row.id).toString();
const params = {
ids: selectedIds,
};
this.deviceDeleteSubscription = this.devicesService.deleteDevices(params).subscribe(
(data) => {
const ids = selectedIds.split(',').map(Number);
this.clearSelectedRows();
this.store.dispatch(new DevicesActions.DeleteDevices(ids));
if (this.rowData.length === 0) {
this.store.dispatch(new DevicesActions.UpdateCurrentPage(1));
this.loadData();
}
this.toast.success('Deleted successfully');
setTimeout(() => {
window.location.reload();
}, 500);
},
(error) => {
this.toast.error(error.message);
}
);
}
editConfiguration() {
this.store.dispatch(new DevicesActions.SetEditDevice(this.selectedRows[0]));
this.router.navigate(['/devices', 'edit', this.selectedRows[0].id]);
}
isEditEnabled() {
return this.selectedRows.length !== 1 ? true : false;
}
setEditBackgroundColor() {
return this.selectedRows.length === 1 ? COLOR_PRIMARY : COLOR_SECONDARY;
}
setEditIconColor() {
return this.selectedRows.length === 1 ? ICON_ENABLED : ICON_DISABLED;
}
}
Pero cuando ejecuto la especificación, obtengo el error como
TypeError: no se puede leer la propiedad 'longitud' de indefinido
Respuestas
Creo que el problema es burlarse del selector provideMockStore. Puedo ver que ha usado una gran cantidad de this.selectedRows.length(como en setEditBackgroundColor()) que se está configurando en función del selector ngRx.
providers: [
{ provide: DevicesService, useValue: deviceServiceMock },
{ provide: ColumnApi, useClass: MockColumnApi },
provideMockStore({
selectors: [
{
selector: selectLoginPagePending,
value: true
}
]
})
],
para un selector como:
export const selectLoginPagePending = createSelector(
selectLoginPageState,
(state: State) => state.pending;
);
Pruebe esto según la salida esperada de select('devices')y creo que debería funcionar.
En la nota al margen, trate de no realizar llamadas a funciones desde HTML como lo ha hecho en setEditBackgroundColor()y otros, impacta el rendimiento y será llamado en cada ciclo de ChangeDetection (intente poner console.logtales métodos). Serán llamados varias veces. Es mejor usar algunos mappara establecer una propiedad de objeto y luego renderizarlo en HTML
En lugar de selectores, configuré la tienda con valores iniciales como los siguientes
const mockInitialAppState = {
devices: deviceState(),
};
beforeEach(() => {
deviceServiceMock = jasmine.createSpyObj('DevicesService', ['fetchDevices', 'deleteDevices']);
deviceServiceMock.fetchDevices.and.returnValue(of(deviceState()));
deviceServiceMock.deleteDevices.and.returnValue(of({}));
TestBed.configureTestingModule({
declarations: [ListComponent, MockComponent(DataGridComponent)],
imports: [
RouterTestingModule,
MockModule(SharedModule),
ToastrModule.forRoot({
preventDuplicates: true,
closeButton: true,
progressBar: true,
}),
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useClass: JsonTranslationLoader },
}),
],
providers: [
{ provide: DevicesService, useValue: deviceServiceMock },
{ provide: ColumnApi, useClass: MockColumnApi },
{ provide: GridApi, useClass: MockGridApi },
provideMockStore({
initialState: { ...mockInitialAppState },
}),
],
}).compileComponents();
});
Lo que hace que la prueba pase. Sin pasar por el estado inicial, no necesito inicializar las filas seleccionadas.