Calls to Dispatcher.register return `undefined` - flux

new to Jest and having trouble with getting up and running. When calling Dispatcher.register, all I get is undefined.
I'm running Node.js 5.7.0 and Jest 0.9.2.
The following test fails:
BookedServicesStore-test.js:
jest.unmock("../BookedServicesStore");
jest.unmock("object-assign");
describe("BookedServicesStore", () => {
let BookedServicesStore;
let AppDispatcher;
beforeEach(() => {
BookedServicesStore = require("../BookedServicesStore");
AppDispatcher = require("../../dispatcher/AppDispatcher");
});
it("has a dispatch token", () => {
expect(BookedServicesStore.dispatchToken).not.toBeUndefined();
});
});
BookedServicesStore.js:
import AppDispatcher from "../dispatcher/AppDispatcher";
import { EventEmitter } from "events";
import assign from "object-assign";
const CHANGE_EVENT = "change";
const BookedServicesStore = assign({}, EventEmitter.prototype, {
emitChange() {
this.emit(CHANGE_EVENT);
},
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
// ...
});
BookedServicesStore.dispatchToken = AppDispatcher.register(action => {
// ...
});
module.exports = BookedServicesStore;
AppDispatcher.js:
import { Dispatcher } from "flux";
module.exports = new Dispatcher();
Not sure what's going on here. What is it that I'm missing?

Related

Why does this test code fail (It works in the application) - using testing-library/react-hooks

I'm having trouble writing test code for this application. Specifically, the code works in the app - it works exactly the way I want it to. But with all the crazy mocks and abstractions in the codebase I have to work with, for some reason the tests never seem to pass.
Here is the code I'm hoping to test:
// useLandAccessHistory.ts
import { RequestState, RootState, useAppDispatch, useAppSelector } from '../../../../../store';
import { fetchLandAccessHistory } from '../../../../../extension/LandAccess/module/thunks';
import { LAND_ACCESS_SLICE_NAME } from '../../../../../extension/LandAccess/module/constants';
import { models } from '#h2know-how/moata-land-management-sdk';
import { useEffect, useMemo } from 'react';
const landAccessHistoryTableSelector = (state: RootState) => {
const landAccess = state[LAND_ACCESS_SLICE_NAME];
const byTitleId = (titleId: number): models.LandAccessHistory[] | undefined =>
landAccess.landAccessHistories[titleId];
const { status, error } = landAccess.apiStatus.fetchLandAccessHistory;
return { status, error, byTitleId };
};
export const useLandAccessHistory = ({
projectId,
titleId,
}: {
projectId: number;
titleId: number;
}): { historyData?: models.LandAccessHistory[] } & RequestState => {
const dispatch = useAppDispatch();
const { status, error, byTitleId } = useAppSelector(landAccessHistoryTableSelector);
useEffect(() => {
const req = dispatch(
fetchLandAccessHistory({
projectId,
titleId,
})
);
return req.abort;
}, [dispatch, projectId, titleId]);
const historyData = useMemo(() => byTitleId(titleId), [byTitleId, titleId]);
return { historyData, status, error };
};
And here is the test suite that is failing.
// useLandAccessHistory.spec.ts
import React from 'react';
import { Provider } from 'react-redux';
import { configureStore } from '#reduxjs/toolkit';
import { renderHook } from '#testing-library/react-hooks';
import { waitFor } from '#testing-library/react';
import { useLocation } from 'react-router-dom';
import { useLandAccessHistory } from './useLandAccessHistory';
import { useAppDispatch, useAppSelector } from '../../../../../store';
import { landAccessSlice } from '../../../../../extension/LandAccess/module/landAccess';
import { LAND_ACCESS_SLICE_NAME } from '../../../../../extension/LandAccess/module/constants';
import { useUrlParams, useUrlSearchParams } from '../../../../../utils/hooks';
import { mockLandAccessHistories } from '../../../../../extension/LandAccess/test-helpers/factories';
import { landAccessAPI } from '../../../../../extension/LandAccess/api/landAccessAPI';
const mockHistories = mockLandAccessHistories({ projectId: 123, titleId: 345 }, 2);
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
jest.mock('../../../../../store');
jest.mock('../../../../../utils/hooks', () => ({
...jest.requireActual('../../../../../utils/hooks'),
useUrlParams: jest.fn(),
useUrlSearchParams: jest.fn(),
}));
jest.mock('../../../../../extension/LandAccess/api/landAccessAPI', () => ({
getLandAccessHistory: jest.fn(() => Promise.resolve(mockHistories)),
}));
describe('useLandAccessHistory', () => {
let dispatch: jest.Mock;
let abort: jest.Mock;
let store: any;
let wrapper: any;
beforeEach(() => {
(useLocation as jest.Mock).mockReturnValue({ pathname: '/route/123' });
store = configureStore({
reducer: { [LAND_ACCESS_SLICE_NAME]: landAccessSlice.reducer },
middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
});
dispatch = jest.fn((...params) => store.dispatch(...params));
abort = jest.fn();
dispatch.mockReturnValue({ abort });
wrapper = ({ children }) => <Provider store={store}>{children}</Provider>;
(useUrlParams as jest.Mock).mockReturnValue({ projectId: 123 });
(useUrlSearchParams as jest.Mock).mockReturnValue({ feature_id: 345 });
(useAppDispatch as jest.Mock).mockReturnValue(dispatch);
(useAppSelector as jest.Mock).mockImplementation((selectorFunction) => selectorFunction(store.getState()));
});
it('fetches the land access history', async () => {
const { result, rerender } = renderHook(() => useLandAccessHistory({ projectId: 123, titleId: 345 }), { wrapper });
await waitFor(() => {
expect(dispatch).toHaveBeenCalled(); // this line runs.
});
rerender();
await waitFor(() => {
expect(store.getState().landAccessHistories).toEqual(mockHistories); // the store does not contain the data
expect(result.current).toEqual({ error: null, status: 'fulfilled', historyData: mockHistories }); // the status is idle and history data is undefined
});
});
});
Here's what I've tried so far: so far as I can tell, dispatch runs, but it doesn't seem to actually call the mocked (or real) API function. It could be that dispatch has been called but has not yet resolved, but then wouldn't await waitFor and rerender() help solve that problem?

How to store socket object of socket io in slice of redux toolkit?

How to store socket object of socket.io in slice of redux toolkit?
I would like to do something like:
const initialState = {
socket: null
}
const socketSlice = createSlice({
name: socket,
initialState,
reducers:{
createSocket(state, action){
state.socket = io("localhost:5000")
},
removeSocket(state, action){
state.socket = null
}
// ...
}
})
However, this gives the following error:
serializableStateInvariantMiddleware.ts:222 A non-serializable value was detected in the state
Help me...
I had the exact same issue and solved it using the following steps:
Create a socket client in which I have a single instance of socket which I use to perform all socket related functions:
import { io } from 'socket.io-client';
class SocketClient {
socket;
connect() {
this.socket = io.connect(process.env.SOCKET_HOST, { transports: ['websocket'] });
return new Promise((resolve, reject) => {
this.socket.on('connect', () => resolve());
this.socket.on('connect_error', (error) => reject(error));
});
}
disconnect() {
return new Promise((resolve) => {
this.socket.disconnect(() => {
this.socket = null;
resolve();
});
});
}
emit(event, data) {
return new Promise((resolve, reject) => {
if (!this.socket) return reject('No socket connection.');
return this.socket.emit(event, data, (response) => {
// Response is the optional callback that you can use with socket.io in every request. See 1 above.
if (response.error) {
console.error(response.error);
return reject(response.error);
}
return resolve();
});
});
}
on(event, fun) {
// No promise is needed here, but we're expecting one in the middleware.
return new Promise((resolve, reject) => {
if (!this.socket) return reject('No socket connection.');
this.socket.on(event, fun);
resolve();
});
}
}
export default SocketClient;
Import it into my index.jsx file and initialize it:
import SocketClient from './js/services/SocketClient';
export const socketClient = new SocketClient();
Here's the whole code of my index.jsx file:
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
//import meta image
import '#/public/assets/images/metaImage.jpg';
//styles
import '#/scss/global.scss';
//store
import store from '#/js/store/store';
//app
import App from './App';
//socket client
import SocketClient from './js/services/SocketClient';
export const socketClient = new SocketClient();
const container = document.getElementById('root'),
root = createRoot(container);
root.render(
<Provider store={store}>
<App />
</Provider>
);
I used createAsyncThunk function from #reduxjs/toolkit, because it automatically generates types like pending, fulfilled and rejected.
Here's how I structure my reducer slice to connect and disconnect from web socket in redux:
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import { socketClient } from '../../../../index';
const initialState = {
connectionStatus: '',
};
export const connectToSocket = createAsyncThunk('connectToSocket', async function () {
return await socketClient.connect();
});
export const disconnectFromSocket = createAsyncThunk('disconnectFromSocket', async function () {
return await socketClient.disconnect();
});
const appSlice = createSlice({
name: 'app',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(connectToSocket.pending, (state) => {
state.connectionStatus = 'connecting';
});
builder.addCase(connectToSocket.fulfilled, (state) => {
state.connectionStatus = 'connected';
});
builder.addCase(connectToSocket.rejected, (state) => {
state.connectionStatus = 'connection failed';
});
builder.addCase(disconnectFromSocket.pending, (state) => {
state.connectionStatus = 'disconnecting';
});
builder.addCase(disconnectFromSocket.fulfilled, (state) => {
state.connectionStatus = 'disconnected';
});
builder.addCase(disconnectFromSocket.rejected, (state) => {
state.connectionStatus = 'disconnection failed';
});
},
});
export default appSlice.reducer;
Here how I connect and disconnect in App.jsx file:
useEffect(() => {
dispatch(connectToSocket());
return () => {
if (connectionStatus === 'connected') {
dispatch(disconnectFromSocket());
}
};
//eslint-disable-next-line
}, [dispatch]);
You can do the following if you want to emit to web socket:
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import { socketClient } from '../../../../index';
const initialState = {
messageStatus: '', //ideally it should come from the BE
messages: [],
typingUsername: '',
};
export const sendMessage = createAsyncThunk('sendMessage', async function ({ message, username }) {
return await socketClient.emit('chat', { message, handle: username });
});
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(sendMessage.pending, (state) => {
state.messageStatus = 'Sending';
});
builder.addCase(sendMessage.fulfilled, (state) => {
state.messageStatus = 'Sent successfully';
});
builder.addCase(sendMessage.rejected, (state) => {
state.messageStatus = 'Send failed';
});
},
});
export default chatSlice.reducer;
You can do the following if you want to listen to an event from web socket:
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import { socketClient } from '../../../../index';
const initialState = {
messageStatus: '', //ideally it should come from the BE
messages: [],
typingUsername: '',
};
export const fetchMessages = createAsyncThunk(
'fetchMessages',
async function (_, { getState, dispatch }) {
console.log('state ', getState());
return await socketClient.on('chat', (receivedMessages) =>
dispatch({ type: 'chat/saveReceivedMessages', payload: { messages: receivedMessages } })
);
}
);
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {
saveReceivedMessages: (state, action) => {
state.messages.push(action.payload.messages);
state.typingUsername = '';
},
},
extraReducers: (builder) => {
builder.addCase(fetchMessages.pending, () => {
// add a state if you would like to
});
builder.addCase(fetchMessages.fulfilled, () => {
// add a state if you would like to
});
builder.addCase(fetchMessages.rejected, () => {
// add a state if you would like to
});
},
});
export default chatSlice.reducer;

Umzug Migration UP with Aws Lambda function not working

I have built a test app using nestjs + Sequelize ORM + docker database (as of now local). As per documentation, I am using umzug library and AWS Lambda SAM template and triggering lambda handler. Below is the code for it. Connection Pooling is implemented to reuse existing sequelize connection. Below is the lambdaEntry.ts file where I trigger umzug.up() function. It is triggering but not migrating files.
When done from command prompt node migrate up it works correctly. I am testing using sam invoke command to test it.
require('ts-node/register');
import { Server } from 'http';
import { NestFactory } from '#nestjs/core';
import { Context } from 'aws-lambda';
import * as serverlessExpress from 'aws-serverless-express';
import * as express from 'express';
import { ExpressAdapter } from '#nestjs/platform-express';
import { eventContext } from 'aws-serverless-express/middleware';
import { AppModule } from './app.module';
import sharedBootstrap from './sharedBootstrap';
const { Sequelize } = require('sequelize');
const { Umzug, SequelizeStorage } = require('umzug');
import configuration from '.././config/config';
const fs = require('fs');
let lambdaProxy: Server;
let sequelize = null;
async function bootstrap() {
const expressServer = express();
const nestApp = await NestFactory.create(
AppModule,
new ExpressAdapter(expressServer),
);
nestApp.use(eventContext());
sharedBootstrap(nestApp);
await nestApp.init();
return serverlessExpress.createServer(expressServer);
}
export const handler = (event: any, context: Context) => {
if (!lambdaProxy) {
bootstrap().then((server) => {
lambdaProxy = server;
serverlessExpress.proxy(lambdaProxy, event, context);
(async () => {
if (!sequelize) {
console.log('New connection::');
sequelize = await loadSequelize();
} else {
sequelize.connectionManager.initPools();
if (sequelize.connectionManager.hasOwnProperty('getConnection')) {
delete sequelize.connectionManager.getConnection;
}
}
try {
console.log('MIGRATOR::');
const umzug = new Umzug({
migrations: { glob: 'src/migrations/*.ts' },
context: sequelize.getQueryInterface(),
storage: new SequelizeStorage({ sequelize }),
logger: console,
});
await umzug
.pending()
.then((migrations: any) => {
console.log('pending ? : ', JSON.stringify(migrations));
//test for file exists.
for (const migration of migrations) {
try {
if (fs.existsSync(migration.path)) {
console.log('file exists');
}
} catch (err) {
console.log('file does not exists');
console.error(err);
}
}
async () => {
//BELOW FUNCTION IS TRIGGERING BUT NOT GETTING MIGRATION LOADED.
await umzug.up();
};
})
.catch((e: any) => console.log('error2 ? ', e));
} finally {
await sequelize.connectionManager.close();
}
})();
});
} else {
serverlessExpress.proxy(lambdaProxy, event, context);
}
};
async function loadSequelize() {
const sequelize = new Sequelize(
configuration.database,
configuration.username,
configuration.password,
{
dialect: 'mysql',
host: configuration.host,
port: Number(configuration.port),
pool: {
max: 2,
min: 0,
idle: 0,
acquire: 3000,
evict: 600,
},
},
);
await sequelize.authenticate();
return sequelize;
}
I am able to solve the issue after lot of tries. I seperated out the sequelize connection code and called it from app side and triggered from lambdaentry
lambdaEntry.js file.
async function bootstrap(uuid = null) {
console.log('Calling bootstrap');
const expressServer = express();
const nestApp = await NestFactory.create(
AppModule,
new ExpressAdapter(expressServer),
);
nestApp.use(eventContext());
sharedBootstrap(nestApp);
await nestApp.init();
try {
// Write a function in Service (ex: purhaslistservice) and trigger the service with umzug up from here.
const migrateResult1 = await nestApp.get(PurchaseListService).migrate('down');
console.log(migrateResult1);
const migrateResult2 = await nestApp.get(PurchaseListService).migrate('up');
console.log(migrateResult2);
} catch (err) {
throw err;
}
return serverlessExpress.createServer(expressServer);
}
export const handler = (event: any, context: Context) => {
if (!lambdaProxy) {
bootstrap(uuid).then((server) => {
lambdaProxy = server;
serverlessExpress.proxy(lambdaProxy, event, context);
});
} else {
serverlessExpress.proxy(lambdaProxy, event, context);
}
};
/code/src/purchaselist/purchaselist.service.ts
async migrate(id: string): Promise<any> {
console.log('migrate script triggered', id);
const sequelize = PurchaseListItem.sequelize;
const umzug = new Umzug({
migrations: { glob: 'src/migrations/*.{ts,js}' },
context: sequelize.getQueryInterface(),
storage: new SequelizeStorage({ sequelize }),
logger: console,
});
let consoleDisplay = 'Umzug LOGS:::<br/>';
switch (id) {
default:
case 'up':
await umzug.up().then(function (migrations) {
console.log('Umzug Migrations UP::<br/>', migrations);
consoleDisplay +=
'Umzug Migrations UP::<br/>' + JSON.stringify(migrations);
});
break;
}
return consoleDisplay;
}

Angular unit testing for the promises

I am trying to write the test cases for below method :-
The below is the component code for adding the product
COMPONENT:
import { HttpClient } from '#angular/common/http';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '#angular/forms';
import { Observable } from 'rxjs';
import { Constants } from '../../../../../../utils/constants';
import { FileUploadService } from '../../../services/file-upload.service';
import { Component, EventEmitter, Input, OnInit, Output } from '#angular/core';
import { ManagePackageService } from '../../../services/manage-package.service';
import { DomSanitizer } from '#angular/platform-browser';
import { Package } from '../../../models/package.model';
import { ProductPackageMapping } from '../../../models/product-package-mapping.model';
import { Product } from '../../../models/product.model';
import { NotificationAlertService } from '../../../../shared/services/notification-alert.service';
import { ManageProductService } from '../../../services/manage-product.service';
import { DocumentDTO } from '../../../models/document-dto';
import { saveAs } from 'file-saver';
import { ConfirmationDialogService } from '../../../../shared/services/confirmation-dialog.service';
#Component({
selector: 'app-add-product',
templateUrl: './add-product.component.html',
styleUrls: ['./add-product.component.scss']
})
export class AddProductComponent implements OnInit {
productDetailForm: FormGroup;
addTitle: boolean = true;
isView: boolean = true;
submitted: boolean = false;
status: boolean = true;
check: boolean = false;
changevar: string;
#Input() id: Number;
#Output() changeIndicator = new EventEmitter<string>();
#Input() list: ProductPackageMapping;
#Output() dataLoaded = new EventEmitter<string>();
fileToUpload: File | null = null;
fileArray: Array<any> = [];
productData: any = [];
productDataTemp: any[];
fData = new FormData();
packagePlan: Array<any> = [];
productDto = new Product();
packageDto = new Package();
productPackageMappingDto = new ProductPackageMapping();
documentDto: Array<any> = [];
isDocId: boolean = false;
productForm: FormGroup;
activeStatus: boolean = true;
requiredFileType: string;
public NAME_MAX_LENGTH = 50;
isValidFormSubmitted: boolean;
#Input() allRolesFromParent: Product[] = [];
selectedFiles?: FileList;
message: string[] = [];
fileInfos?: Observable<any>;
documentList: Array<any> = [];
ppm: any;
fileUrl: any;
constructor(private http: HttpClient,
private formBuilder: FormBuilder,
private uploadService: FileUploadService,
private manageProduct: ManageProductService,
private managePackageService: ManagePackageService,
private sanitizer: DomSanitizer,
private notifyService: NotificationAlertService,
private confirmationDialogService: ConfirmationDialogService
) { }
ngOnInit(): void {
this.createForm();
this.fileInfos = this.uploadService.getFiles();
this.fData = new FormData();
this.subscriptionPackages()
}
closeModal() {
this.status = true;
this.check = false;
this.changevar = 'cancel';
this.changeIndicator.emit(this.changevar);
this.productForm.reset();
this.fData.delete('ppm');
this.fData.delete('file');
this.emit();
}
closeEditModel() {
this.status = true;
this.documentList = [];
this.changevar = "cancel";
this.changeIndicator.emit(this.changevar);
this.addTitle = true;
this.productForm.reset();
this.fData.delete("ppm");
this.fData.delete("file");
this.emit();
}
onChangeSwitch(event) {
if (event.target.checked == true) {
this.activeStatus = true;
} else {
this.activeStatus = false;
}
}
createForm() {
this.productForm = new FormGroup({
name: new FormControl(this.productDto.name, [
Validators.required,
Validators.maxLength(this.NAME_MAX_LENGTH),
Validators.pattern(Constants.NO_WHITE_SPACE_PATTERN),
]),
url: new FormControl(this.productDto.demoURL, Validators.required),
version: new FormControl(this.productDto.productVersion, Validators.required),
projectType: new FormControl(this.productDto.projectType, Validators.required),
plansName: new FormControl(this.packageDto.id, Validators.required),
desc: new FormControl(this.productDto.productDescription, Validators.required),
active: new FormControl(this.productDto.active, Validators.required),
documents: this.formBuilder.array([]),
});
}
ngOnChanges() {
this.productData = this.list;
if (this.id) {
this.addTitle = false;
this.productDataTemp = this.productData.filter((vl) => (this.id == vl.id));
this.productDataTemp.forEach(obj => {
this.productDto.ppmId = obj.ppmId;
this.productDto.id = obj.id;
this.productDto.name = obj.name;
this.productDto.demoURL = obj.demoURL;
this.productDto.productDescription = obj.productDescription;
this.productDto.projectType = obj.type;
this.productDto.active = obj.active;
this.productDto.productVersion = obj.version;
this.packageDto.id = obj.pkgInfo.id;
this.documentList = obj.listDocuments
})
this.createForm();
} else {
this.productDto = new Product();
this.packageDto = new Package();
this.documentDto = [];
this.documentList = [];
}
this.documentList.length == 0 ? this.isDocId = false : this.isDocId = true;
}
get f() {
return this.productForm.controls;
}
choosePackages(e) {
}
subscriptionPackages() {
this.managePackageService.getPackages().subscribe(
(response) => {
this.packagePlan = response;
},
(httpErrorRes) => {
}
);
}
get documentsFormArray(): FormArray {
return this.f['documents'] as FormArray;
}
addDocument() {
if (this.documentsFormArray.length < 5) {
this.documentsFormArray.push(this.formBuilder.group({
id: new FormControl(''),
docName: new FormControl('', Validators.required),
docType: new FormControl('', Validators.required),
attachment: new FormControl('', Validators.required)
}))
}
}
deleteRow(i: number) {
if (this.id) {
delete this.fileArray[i];
this.documentsFormArray.removeAt(i);
this.fileArray.splice(i, 1);
}
else {
delete this.documentsFormArray[i];
this.documentsFormArray.removeAt(i);
this.fileArray.splice(i, 1);
}
}
getFiles(fileArray) {
this.fileArray.forEach(files => {
this.fData.append("file", files);
})
}
selectFiles(event): void {
this.selectedFiles = event.target.files;
}
onFormSubmit() {
this.submitted = true;
if (this.fileArray.length > 0) {
this.getFiles(this.fileArray);
}
if (this.productForm.value.id === null) {
this.productDto.id = null;
}
this.productDto.name = this.productForm.value.name;
this.productDto.demoURL = this.productForm.value.url;
this.productDto.productVersion = this.productForm.value.version;
this.productDto.projectType = this.productForm.value.projectType;
this.productDto.productDescription = this.productForm.value.desc;
this.productDto.active = this.productForm.value.active;
this.packageDto.id = this.productForm.value.plansName;
if (this.documentsFormArray && this.documentsFormArray.value && this.documentsFormArray.value.length > 0) {
this.documentsFormArray.value.forEach(element => {
this.documentDto.push(element);
});
}
this.productPackageMappingDto.id = this.productDto.ppmId ? this.productDto.ppmId : null;
this.productPackageMappingDto.products = this.productDto;
this.productPackageMappingDto.packages = this.packageDto;
this.productPackageMappingDto.document = this.documentDto;
this.ppm = new Blob([JSON.stringify(this.productPackageMappingDto)], { type: "application/json" });
this.fData.has("file") ? true : this.fData.append("file", '');
this.fData.append("ppm", this.ppm);
this.confirmationDialogService.confirm('Are you sure ?', 'Do you really want to Activate/ Deactivate Package ?')
.then((confirmed) => {
if (confirmed) {
this.manageProduct.createProduct(this.fData).subscribe(
(response) => {
this.notifyService.showSuccess("Data saved successfully !!", "Success")
this.closeModal();
this.fData.delete("ppm");
this.fData.delete("file");
this.productDto = new Product();
this.packageDto = new Package();
this.documentDto = [];
this.documentList = [];
this.fileArray = [];
this.emit();
},
(httpErrorRes) => {
this.fData.delete("ppm");
this.fData.delete("file");
this.notifyService.showError(httpErrorRes.error, "Error");
}
);
}
else {
this.fData.delete("ppm");
this.fData.delete("file");
}
})
}
upload(event: any) {
let files = event.target.files[0];
this.fileArray.push(files);
}
downloadFile(fileData: DocumentDTO): void {
this.manageProduct.download(fileData.fileName)
.subscribe(blob => saveAs(blob, fileData.fileName));
}
emit() {
this.dataLoaded.emit('AddProductComponent')
}
}
The below is the spec file for the above component
COMPONENT.SPEC.TS
import { HttpClientTestingModule } from '#angular/common/http/testing';
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { NgSelectModule } from '#ng-select/ng-select';
import { TranslateModule } from '#ngx-translate/core';
import { ToastrModule } from 'ngx-toastr';
import { Observable, of, throwError } from 'rxjs';
import { AppConfig } from '../../../../../app.config';
import { ConfirmationDialogService } from '../../../../configurations/services/confirmation-dialog.service';
import { NotificationAlertService } from '../../../../shared/services/notification-alert.service';
import { DocumentDTO } from '../../../models/document-dto';
import { ManagePackageService } from '../../../services/manage-package.service';
import { ManageProductService } from '../../../services/manage-product.service';
import { AddProductComponent } from './add-product.component';
describe('AddProductComponent', () => {
let component: AddProductComponent;
let fixture: ComponentFixture<AddProductComponent>;
let packageServiceStub = jasmine.createSpyObj('ManagePackageService', ['getPackages']);
let confirmationDialougeStub = jasmine.createSpyObj('ConfirmationDialogService', ['confirm']);
let manageProductStub = jasmine.createSpyObj('ManageProductService', ['createProduct','download']);
let notificationStub = jasmine.createSpyObj('NotificationAlertService', ['showSuccess', 'showError']);
let fileData = new DocumentDTO()
class MockUserService {
getPackages() {
return [{'name':'basic', 'id':1}, {'name':'advance', 'id':2}, {'name':'premium', 'id':3}];
}
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule,ReactiveFormsModule,FormsModule,ToastrModule.forRoot(),
TranslateModule.forRoot(), NgSelectModule],
declarations: [ AddProductComponent ],
providers :[AppConfig,
{ provide: ManageProductService, useValue: manageProductStub },
{ provide: ManagePackageService, useValue: packageServiceStub }]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddProductComponent);
component = fixture.componentInstance;
const mockpackageResponse = [];
packageServiceStub.getPackages.and.returnValue(of(mockpackageResponse));
component.activeStatus = true;
component.productDataTemp = [{'ppmId': 2, 'id': 2, 'name': 'ttvrtv', 'productDescription': 'vttvt', 'projectType': null}]
component.productData = [{'ppmId': 1, 'id': 1, 'name': 'tvrtv', 'productDescription': 'vtvt', 'projectType': null},
{'ppmId': 2, 'id': 2, 'name': 'ttvrtv', 'productDescription': 'vttvt', 'projectType': null}]
component.id = 2;
component.fileArray = ['dfdfdf','dfdsdf']
component.packagePlan = [{'name':'basic', 'id':1}, {'name':'advance', 'id':2}, {'name':'premium', 'id':3}];
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create closeModal()', () => {
const eventTrue = {
target: {
checked: true,
}
};
const eventFalse = {
target: {
checked: false,
}
};
component.closeModal();
component.closeEditModel();
component.onChangeSwitch(eventTrue)
component.onChangeSwitch(eventFalse)
});
// it('should create ngOnChanges', () => {
// component.ngOnChanges();
// });
it('should create choosePackages', () => {
const event = {};
component.choosePackages(event);
});
it('should create subscriptionPackages()', () => {
component.subscriptionPackages();
expect(component.packagePlan).toBeTruthy();
});
// it('should handle error subscriptionPackages', () => {
// spyOn(window, 'alert');
// packageServiceStub.getPackages.and.returnValue(throwError({ error: 'some error'}));
// component.subscriptionPackages();
// });
it('should create addDocument()', () => {
component.addDocument();
});
it('should create deleteRow(i: number)', () => {
component.id = 1;
component.deleteRow(1);
});
it('should create deleteRow(i: number) nullity check', () => {
component.id = null;
component.deleteRow(null);
});
it('should create selectFiles', () => {
const eventSelectFiles = {
target: {
files: 'ss',
}
};
component.selectFiles(eventSelectFiles);
});
it('should create onFormSubmit length>0', () => {
component.fileArray.length = 2;
component.onFormSubmit();
});
it('should create onFormSubmit nullity check', () => {
component.productForm.value.id = null
component.onFormSubmit();
});
// it('should create onFormSubmit else part', (done) => {
// const confirmationResponse = {}; // Keep is as your response
// let spy = spyOn( confirmationDialougeStub,'confirm').and.returnValue(Promise.resolve(true));
// // fixture.whenStable().then(confirmed => {
// // fixture.detectChanges();
// // component.onFormSubmit();
// // })
// spy.calls.mostRecent().returnValue.then(() => {
// fixture.detectChanges();
// //component.onFormSubmit();
// done();
// });
// });
// it('should create onFormSubmit else part', () => {
// confirmationDialougeStub.confirm.and.returnValue({
// closePromise : {
// then : function(callback) {
// callback({value: true});
// }
// }
// });
// });
it('should create upload download', () => {
const eventFile = {
target: {
files: true,
}
};
component.upload(eventFile);
});
});
PROBLEM :- I have some below pointers regarding Jasmine and trying to figure it out the way to achieve the same.
I am not able to figured out the way to cover or test what the promises return
this.confirmationDialogService.confirm('Are you sure ?', 'Do you really want to Activate/ Deactivate Package ?')
.then((confirmed) => {
In the code coverage, the above lines are not covered and I don't know what went wrong, I have tried so many ways and referred many of the posts from stackoverflow itself but that weren't work for my use case.
how to write test cases for a method having foreach(), filter(), map()
how to write test cases for a method having observables, subscribe and promises
Please suggest the best practices.
Your component is quite long, I will show you how to you can cover the following lines with Promises and Observables.
this.confirmationDialogService.confirm('Are you sure ?', 'Do you really want to Activate/ Deactivate Package ?')
.then((confirmed) => {
if (confirmed) {
this.manageProduct.createProduct(this.fData).subscribe(
Make the following changes:
// dialog is spelt wrong here
let confirmationDialogStub = jasmine.createSpyObj('ConfirmationDialogService', ['confirm']);
....
providers: [
...
// provide the mock for confiramtion dialog service
{ provide: ConfirmationDialogService, usevalue: confirmationDialogStub },
{ provide: ManageProductService, useValue: manageProductStub },
{ provide: ManagePackageService, useValue: packageServiceStub }
],
// fakeAsync because we need tick (wait for promises to resolve before asserting)
it('does abc', fakeAsync(() => {
// mock the value for confirmation with a promise
confirmationDialogStub.confirm.and.returnValue(Promise.resovle(true));
manageProductStub.createProduct.and.returnValue(of({/* mock however you wish */}));
// call the method
component.onFormSubmit();
// wait for all promises to resolve with tick
tick();
// do your assertions
}));
For forEach, filter, map, make sure you mock an array. That the array is there.
That being said, I highly recommend you check out the following resources, they will help you a lot.
https://testing-angular.com/
^ A well written e-book.
https://www.pluralsight.com/courses/unit-testing-angular
^ A really good class on unit testing in Angular.

Observable epics not running service call

I try to follow the example in the documentation to test epic:
Epic
import { ofType } from 'redux-observable';
import { combineEpics } from 'redux-observable';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import { switchMap } from 'rxjs/add/operator/switchMap';
import { from } from 'rxjs/observable/from';
import { of } from 'rxjs/observable/of';
import { fromPromise } from 'rxjs/observable/fromPromise';
import {
getTopStories
} from '../../../utils/service-helper';
import { type, actions } from './action';
export const getHackernewsStoryEpic = (action$, store) =>
action$.ofType(type.GET_HACKERNEWS_STORIES_REQUEST)
.switchMap(
action => {
return from(getTopStories())
.takeUntil(action$.ofType(type.GET_HACKERNEWS_STORIES_REQUEST_CANCEL))
.map(result => actions.getHackernewsStoriesRequestSuccess(result))
.catch((error) => actions.getHackernewsStoriesRequestFailure(error))
}
);
export default combineEpics(
getHackernewsStoryEpic
);
Get getTopStories is service call which talks to hackernews API:
export const getTopStories = async () => await getRequest('/topstories.json');
My test looks like this:
describe('Hackernews stories epic', () => {
describe('getHackernewsStoryEpic', () => {
let store;
beforeEach(() => {
store = mockStore();
});
afterEach(() => {
nock.cleanAll();
epicMiddleware.replaceEpic(storiesEpic);
});
it('should return success on request success', async () => {
store.dispatch({ type: type.GET_HACKERNEWS_STORIES_REQUEST });
expect(store.getActions()).toEqual([
{ type: type.GET_HACKERNEWS_STORIES_REQUEST },
{ type: type.GET_HACKERNEWS_STORIES_SUCCESS }
]);
});
});
});
Looking at the test it fails as one action is trigger and getTopStories() is never trigger (nock is not complaining that there is no mock) and not getting next action. I think I missing something as from should run async call?
it('should return success on request success', async () => {
const mock = require('../../../../data/hackernews/topstories.json');
nock(__ROOT_API__)
.defaultReplyHeaders({ 'access-control-allow-origin': '*' })
.get('/topstories.json')
.reply(200, mock);
const action$ = ActionsObservable.of(
{type: type.GET_HACKERNEWS_STORIES_REQUEST}
);
const expectedAction = [actions.getHackernewsStoriesRequestSuccess(mock)]
await getHackernewsStoryEpic(action$)
.toArray()
.toPromise()
.then(actualOutputActions => {
expect(actualOutputActions).toEqual(expectedAction)
});
});

Resources