Problem with calling action method through dispatch with webext-redux in browser extension - react-redux

I'm trying to call apiAction in constructor method through the dispatch redux method in ReactJS Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './styles.scss'
import { fetchData, testSet } from '../../../../../event/src/cg-store/actions';
class AppDetails extends Component {
constructor(props) {
super(props);
this.state ={
testowaZmienna: ''
}
this.props.fetchData('5576900');
}
componentDidMount() {
document.addEventListener('click', () => {
this.props.addCount()
});
this.props.testSet()
this.props.fetchData('5576900');
console.log('dhsadhnaskjndaslndsadl-----------------------------------------')
}
render() {
const { error, test, count, testSetData, data } = this.props;
return (
<div>
TEST--------------------------
Count: {count}
Error: {error}
Test: {test}
TestSet: {testSetData}
Fetch: {data[0]}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
count: state.count,
test: state.cg.test,
data: state.cg.data,
error: state.cg.error,
testSetData: state.cg.testSet,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (offerId) => dispatch(fetchData(offerId)),
addCount: () => dispatch({
type: 'ADD_COUNT'
}),
testSet: () => dispatch(testSet()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AppDetails);
As you can see there is addCount, testSet and fetchData methods. addCount and testSet works but problem is with fetchData:
This is apiAction method:
const fetchProductsPending = () => {
return {
type: actionTypes.FETCH_DATA_PENDING
};
};
const fetchProductsSuccess = fetchedData => {
return {
type: actionTypes.FETCH_DATA_SUCCESS,
data: fetchedData
};
};
const fetchProductsError = errorMessage => {
return {
type: actionTypes.FETCH_DATA_ERROR,
error: errorMessage
};
};
export const testSet = () => {
return {
type: actionTypes.TEST_SET
};
};
export const fetchData = (offerId) => (dispatch) => {
console.log('Im inside fetch before set pending'); // It does not want to go here
dispatch(fetchProductsPending());
axios
.get(config.api.host + offerId, {
headers: {
"Content-Type": "application/json",
}
})
.then(response => {
return response.data;
})
.then(response => {
dispatch(fetchProductsSuccess(response.data));
console.log("Fetch data success: ----------------------");
console.log(response.data);
})
.catch(error => {
dispatch(fetchProductsError(error.statusText));
console.log("Fetch data success: ----------------------");
console.log(error);
});
};
So as you can see testSet works fine but fetchData does not want to work.
What I'm doing wrong?

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;

RxJs with Jest case fails but the `tap` output shows correctly

Following test case, will return the data correctly but JEST show as failed. The test is written using TestScheduler
Jest Result
expect(received).toEqual(expected) // deep equality
- Expected
+ Received
- Array [
- Object {
- "frame": 3,
- "notification": Notification {
- "error": undefined,
- "hasValue": true,
- "kind": "N",
- "value": Object {
- "type": "INITIALIZED",
- },
- },
- },
- ]
+ Array []
Code
import { ofType } from 'redux-observable';
import { mergeMap, map, tap } from 'rxjs/operators';
import { of, from } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
describe('routechange epic', () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
it('check apollo', () => {
const dependencies = {
apolloClient: {
mutate: ({ mutation, variables }: { mutation: any, variables: any }) =>
Promise.resolve({
data: { param: 'testA' }
})
},
};
const initializeOrg = (action$, state$, { apolloClient }) =>
action$
.pipe(
ofType('START'),
tap(act => console.log('---AAA', act)),
mergeMap(action =>
from(
apolloClient.mutate({
mutation: `something`,
variables: {
orgId: (action as any).params || ''
}
})
)
.pipe(
tap(x => console.log('----x', x)),
map(response => ({
type: 'INITIALIZED',
response,
}))
)
)
);
testScheduler.run(({ hot, cold, expectObservable }) => {
const action$ = hot('-a', {
a: { type: 'START', params: 'SomethingA' }
});
const state$ = null;
const output$ = initializeOrg(action$, state$, dependencies);
expectObservable(output$).toBe('---a', {
a: {
type: 'INITIALIZED'
}
})
});
});
});
We cannot use Promise.resolve as someone commented it.
It worked now with cold observable like below.
import { ofType } from 'redux-observable';
import { mergeMap, map, tap, toArray, take } from 'rxjs/operators';
import { of, from } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
describe('routechange epic', () => {
it('check apollo', async () => {
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
const initializeOrg = (action$, state$, { apolloClient }) =>
action$
.pipe(
ofType('START'),
tap(act => console.log('---AAA', act)),
mergeMap(action =>
from(
apolloClient.mutate({
mutation: `something`,
variables: {
orgId: (action as any).params || ''
}
})
)
.pipe(
tap(x => console.log('----x', x)),
map(response => ({
type: 'INITIALIZED',
response,
}))
)
)
);
testScheduler.run(({ hot, cold, expectObservable }) => {
const action$ = hot('-a', {
a: { type: 'START', params: 'SomethingA' }
});
const state$ = null;
const dependencies = {
apolloClient: {
mutate: ({ mutation, variables }: { mutation: any, variables: any }) =>
cold('--a|', {
a: { data: { param: 'testA' } }
})
},
};
const output$ = initializeOrg(action$, state$, dependencies);
expectObservable(output$).toBe('---a', {
a: {
type: 'INITIALIZED',
response: {
data: { param: 'testA' }
}
}
})
});
});
});

How to write an action that updates Redux's store?

I have a web app where use React-Redux. There is React table (list) that I need to populate with data from database. I use WebApi on the server and automatically generated (by TypeWriter) web-api on the client. The key parts of code looks as following:
1) Routing:
<Route path="/Dictionary/:dictionaryName" component={Dictionary} />
2) State:
export type SingleDictionaryState = Readonly<{
singleDictionary: WebApi.SingleDictionary[];
}>;
export const initialState: SingleDictionaryState = {
singleDictionary: [],
};
3) Reducer:
export const reducer: Reducer<SingleDictionaryState> = (state: SingleDictionaryState = initialState, action: AllActions): SingleDictionaryState => {
switch (action.type) {
case getType(actions.setSingleDictionaryValue):
return { ...state, ...action.payload };
}
return state;
};
4) Actions:
const actionsBasic = {
setSingleDictionaryValue: createAction('singleDictionary/setSingleDictionaryValue', (singleDictionary: any) => singleDictionary),
};
const actionsAsync = {
getDictionaryByName: (dictionaryName: string) => {
const currentState = store.getState().singleDictionary;
WebApi.api.dictionaryQuery.getDictionary(capitalizeForApi(dictionaryName));
},
};
export const actions = Object.assign(actionsBasic, actionsAsync);
const returnsOfActions = Object.values(actionsBasic).map($call);
export type AllActions = typeof returnsOfActions[number];
5) Container:
const mapStateToProps = (state: AppState, ownProps: OwnProps): StateProps => ({
dictionaryType: state.singleDictionary,
});
const mapDispatchToProps = (dispatch: Dispatch<any>): DispatchProps => ({
onLoad: (dictionaryName: string) => {
Actions.singleDictionary.getDictionaryByName(dictionaryName);
},
});
export default withRouter(connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)(DictionaryPage));
6) The client web-api:
class DictionaryQueryService {
getDictionary(name: string) {
const user = store.getState().oidc.user;
const headers = new Headers();
headers.append('Accept', 'application/json');
headers.append('Content-Type', 'application/json');
headers.append('Cache-Control', 'no-cache, no-store, must-revalidate');
headers.append('Pragma', 'no-cache');
headers.append('Expires', '0');
if (user) {
headers.append('Authorization', `Bearer ${user.access_token}`);
}
return () => {
return fetch(`api/dictionaries/${encodeURIComponent(name)}`, {
method: 'get',
headers,
})
.then(response => {
if (!response.ok) {
const traceId = response.headers.get("X-Trace-Id");
throw new ApiError(`${response.status} ${response.statusText}`, traceId);
}
return response.status == 204 ? null : response.json() as Promise<any[]>;
});
};
}
Actually, I'm not sure how to write my getDictionaryByName action.
Just my 2 cents. I use ES6 syntax, but Typescript would work as similar way.
actionTypes.js
export const RESET_DICTIONARIES = 'RESET_DICTIONARIES';
export const LOAD_DICTIONARIES_REQUEST = 'LOAD_DICTIONARIES_REQUEST';
export const LOAD_DICTIONARIES_REQUEST_SUCCESS = 'LOAD_DICTIONARIES_REQUEST_SUCCESS';
export const LOAD_DICTIONARIES_REQUEST_FAILURE = 'LOAD_DICTIONARIES_REQUEST_FAILURE';
dictionaryActions.js
/* Load Dictionariies*/
export function loadDictionariesRequestBegin() {
return {type: types.LOAD_DICTIONARIES_REQUEST};
}
export function loadDictionariesRequest(name) {
return function(dispatch) {
dispatch(loadDictionariesRequestBegin());
// eslint-disable-next-line no-undef
const request = new Request(`${YOUR_URL}/api/dictionaries/{name}`, {
method: 'get',
headers: new Headers({
'Content-type': 'application/json',
'Accept': 'application/json',
'Authorization': auth.getToken(),
})
});
return fetch(request)
.then(
response => {
if (!response.ok) {
dispatch(loadDictionariesRequestFailure(response.statusText));
throw new Error(response.statusText);
}
return response.json();
},
error => {
dispatch(loadDictionariesRequestFailure(error));
throw error;
})
.then(dictionaries=> {
if (dictionaries) {
dispatch(loadDictionariesRequestSuccess(dictionaries));
return dictionaries;
} else {
throw new Error('dictionaries NOT found in response');
}
});
};
}
export function loadDictionariesRequestSuccess(dictionaries) {
return {type: types.LOAD_DICTIONARIES_REQUEST_SUCCESS, dictionaries};
}
export function loadDictionariesRequestFailure(error) {
return {type: types.LOAD_DICTIONARIES_REQUEST_FAILURE, error};
}
dictionaryReducer.js
export default function dictionaryReducer(state = initialState.dictionaries, action) {
switch (action.type) {
case types.RESET_DICTIONARIES:
return {
...state,
loaded: false,
loading: false,
error: null,
};
/* load dictionaries*/
case types.LOAD_DICTIONARIES_REQUEST:
return {
...state,
error: null,
loaded: false,
loading: true
};
case types.LOAD_DICTIONARIES_REQUEST_SUCCESS:
return {
...state,
data: action.dictionaries,
error: null,
loaded: true,
loading: false
};
case types.LOAD_DICTIONARIES_REQUEST_FAILURE:
return {
...state,
loaded: false,
loading: false,
error: action.error
};
return state;
}
initialState.js
export default {
actions: {},
dictionaries: {
data: [],
loaded: false,
loading: false,
error: null,
},
}
dictionary client side API
this.props.actions
.loadDictionaryRequest(name)
.then(data => {
this.setState({ data: data, errorMessage: '' });
})
.then(() => {
this.props.actions.resetDictionaries();
})
.catch(error => {
...
});
Hope this may help.

Can't invoke the component's method

Using an EventBus on my Laravel-Vuejs project. I'm emitting an items-updated event from ItemCreate component after the successful Item creation. On the same page I'm using ItemList component which shows a list of created Items
Here is the codes:
app.js file
require('./bootstrap');
window.Vue = require('vue');
window.EventBus = new Vue();
Vue.component('item-list',
require('./components/entities/item/ItemList'));
Vue.component('item-create',
require('./components/entities/item/ItemCreate'));
const app = new Vue({
el: '#app'
});
ItemCreate.vue Component
export default {
data: function () {
return {
itemName: ''
}
},
methods: {sendItemData: function () {
axios.post('/dashboard/item', {
name: this.itemName
})
.then(response => {
if (response.status === 201) {
toastr.success('Item created successfully!', {timeout: 2000});
EventBus.$emit('items-updated');
}
})
.catch(error => {
toastr.error(error, 'Ooops! Something went wrong!');
})
}
}
}
ItemList.vue Component
export default {
data: function () {
return {
items: [],
}
},
methods: {
getItems: function () {
axios.get('/dashboard/items')
.then(response => {
this.items = response.data;
})
.catch(error => {
toastr.error(error, 'Ooops! Something went wrong!');
})
}
},
mounted() {
this.getItems();
EventBus.$on('items-updated', function () {
this.getItems();
});
}
}
It was a general JS mistake. Working code:
on ItemList.vue Component
export default {
data: function () {
return {
items: [],
}
},
methods: {
getItems: function () {
axios.get('/dashboard/items')
.then(response => {
this.items = response.data;
})
.catch(error => {
toastr.error(error, 'Ooops! Something went wrong!');
})
}
},
mounted() {
this.getItems();
let vm = this;
EventBus.$on('items-updated', function () {
vm.getItems();
});
}
}

Resources