how to wait for an event in a sub component in a chain of promises? - promise

I have a <slide-show> component that displays a list of images with timing, transitions etc. and emits a "finished" event when it's done.
Now I want to embed this component in another one that recurses in a tree of directories, sending a new list of images after each "finished" events. The code currently looks like this :
import { Component, Host, h, Prop, State, Event, EventEmitter } from '#stencil/core'
import * as path from 'path'
import isImage from 'is-image'
function waitForEvent(eventEmitter:EventEmitter<any>, eventType:string) {
return new Promise(function (resolve) {
eventEmitter.on(eventType, resolve)
})
}
#Component({
tag: 'slide-script',
styleUrl: 'slide-script.css',
shadow: true,
})
export class SlideScript {
#Prop() src: string
#State() images: Array<string>
#Event() next: EventEmitter<boolean>
componentWillLoad() {
this.process(this.src)
}
async process(dir: string) {
console.log(dir)
return fetch(path.join('dir', dir))
.then(response =>
response.json()
.then(data => {
this.images = data.files.filter(isImage)
this.images = this.images.map(im => path.join('img', dir, im))
// the above will start/update the slideshow
waitForEvent(this.next, "onFinished")
.then(() => {
data.subdirs.reduce(
async (prev: Promise<void>, sub: string) => {
await prev
return this.process(path.join(dir, sub))
},
Promise.resolve() // reduce initial value
)
})
})
)
}
handleFinished(e) {
console.log('finished')
this.next.emit(e)
}
render() {
return (
<Host>
<slide-show images={this.images} onFinished={(e) => this.handleFinished(e)} />
</Host>
);
}
}
the waitForEvent function does not work as stencil's EventEmitter is not a Node EventEmitter and has no .onmethod ...
How should I modify it ? or how to do it otherwise ? Thanks !

Ok, after roaming a bit on the Slack channel for StencilJS, I figured out I needed a deferas described in https://lea.verou.me/2016/12/resolve-promises-externally-with-this-one-weird-trick/
and the resulting code that successfully recurses in all directories is
import { Component, Host, h, Prop, State, Event, EventEmitter } from '#stencil/core'
import * as path from 'path'
import isImage from 'is-image'
function defer() {
var deferred = {
promise: null,
resolve: null,
reject: null
};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
}
#Component({
tag: 'slide-script',
styleUrl: 'slide-script.css',
shadow: true,
})
export class SlideScript {
#Prop() src: string
#State() images: Array<string>
#Event() next: EventEmitter<boolean>
componentWillLoad() {
this.process(this.src)
}
private defer: any
handleFinished(event) {
console.log('finished', event)
this.defer.resolve(true)
}
async process(dir: string) {
console.log(dir)
return fetch(path.join('dir', dir))
.then(response =>
response.json()
.then(data => {
this.images = data.files.filter(isImage)
this.images = this.images.map(im => path.join('img', dir, im))
this.defer = defer()
return this.defer.promise.then(() =>
data.subdirs.reduce((prev: Promise<void>, sub: string) =>
prev.then(() =>
this.process(path.join(dir, sub)) // recurse
),
Promise.resolve() // reduce initial value
)
)
})
)
}
render() {
return (
<Host>
<slide-show images={this.images} onFinished={this.handleFinished.bind(this)} />
</Host>
);
}
}

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;

update the state slice in redux lwc from the action payload

below is my code to update the state.customer slice. However when this runs, I see that the next state is not updated with the payload dispatched from the action. can some one please point out what I'm missing?
reducer:
const initialState1 = {"customer":{}};
const customer = (state = initialState1, action) => {
console.log('state:', state);
switch (action.type) {
case 'INIT_CUSTOMER_INFO':
return {
...state,
customer: action.payload
}
case 'UPD_CUSTOMER_INFO':
console.log(action);
return { ...customer, firstname: action.firstname }
default: return state;
}
}
export default customer;
action
export const initCustomer = customer => {
return {
type: 'INIT_CUSTOMER_INFO',
payload : customer
}
}
dispatch from LWC
import { LightningElement, track } from 'lwc';
import { connect } from 'c/connect';
import { updateCustomer, initCustomer } from 'c/actions';
import getJSONData from '#salesforce/apex/RGClass.getCartSummary';
const mapStateToProps = (state, ownProps) => ({
customer: state.customer
})
const mapDispatchToProps = (dispatch, ownProps) => ({
initCustomer : customer => dispatch(initCustomer(customer)),
updateCustomer : customer => dispatch(updateCustomer(customer))
})
export default class DigiForm extends LightningElement {
#track firstname;
showfirstname;
connectedCallback() {
//add the hook
connect(mapStateToProps, mapDispatchToProps)(this);//connects the
//api call
getJSONData()
.then(result => {
console.log('result:',result);
this.initCustomer(result);
})
.catch(error => {
console.log(error);
});
}
onContinue = () => {
let fn = this.template.querySelector('lightning-input').value;
console.log('firstname:', fn);
this.updateCustomer({ firstname : fn});
if(fn != null){
this.showfirstname = true;
}
}
}
Tried a couple of options but none worked. any help would be much appreciated!

How to listen emit event in parent component in vue3

I want to pass event from child comment to parent.
I did same thing in vue2 but i don't know how to that in vue3.
This one is child component setup method.
setup(props, { emit }) {
const router = useRouter();
const form = ref(
{
email: "ajay#gmail.com",
password: "123456789",
isLoading: false,
},
);
const user = ref("");
const error = ref("");
function login() {
User.login(this.form).then(() => {
emit('login', true);
// this.$root.$emit("login", true); -- vue2
localStorage.setItem("auth", "true");
router.push('/dashboard');
})
.catch(error => {});
}
return { form, login, user, error};
}
from here emit login method and i want to listen in parent comment.
this is parent component, emit.on method not working here
setup(props, { emit }) {
const router = useRouter();
const state = reactive({
isLoggedIn: false,
});
onMounted(async () => {
emit.on("login", () => { // `vue2` this.$root.$on("login"`
this.isLoggedIn = true;
});
});
In parent component you should add a handler for that emitted event :
<child #login="onLogin"></child>
setup(props, { emit }) {
const router = useRouter();
const state = reactive({
isLoggedIn: false,
});
function onLogin(){
state.isLoggedIn=true,
}
return{state,onLogin}
}
Or make a composable function named useAuth in separate file :
import {reactive} from 'vue'
const state = reactive({
isLoggedIn: false,
});
const useAuth=()=>{
function onLogin(){
state.isLogged=true;
}
return {state,onLogin}
}
export default useAuth();
then import the function inside the two components :
child :
import useAuth from './useAuth'
....
setup(props, { emit }) {
const router = useRouter();
const {useAuth} =useAuth();
....
function login() {
User.login(this.form).then(() => {
onLogin() //will call the nested function that set loggedIn to true
localStorage.setItem("auth", "true");
router.push('/dashboard');
})
.catch(error => {});
}
in parent :
import useAuth from './useAuth'
....
setup(props, { emit }) {
const router = useRouter();
const {state} =useAuth();
//it replaces your local state

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

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?

Resources