Search code examples
angulartypescriptunit-testingkarma-jasminengrx-store

Angular Unit Test Mock Selector not working


I am trying to mock a selector in my unit test like below:

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();
  });
});

Here is the component file

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;
  }
}

But when i run the spec i am getting the error as

TypeError: Cannot read property 'length' of undefined

enter image description here


Solution

  • I think the issue is with the mocking of selector using provideMockStore. I can see that you have used a lot of this.selectedRows.length (such as in setEditBackgroundColor()) which is being set based on the ngRx Selector.

    providers: [
            { provide: DevicesService, useValue: deviceServiceMock },
            { provide: ColumnApi, useClass: MockColumnApi },
             provideMockStore({
              selectors: [
                {
                  selector: selectLoginPagePending,
                  value: true
                }
              ]
            })
          ],
    
    

    for a selector like:

    export const selectLoginPagePending = createSelector(
      selectLoginPageState,
      (state: State) => state.pending;
    );
    

    Try this as per expected output of select('devices') and I think it should work.


    On side note, try not to make function calls from HTML as you have done in setEditBackgroundColor() and others, it impacts the performance and it'll be called in every ChangeDetection cycle (try putting console.log in such methods) . They will be called several times. Better use some map to set a object property and then render it on HTML