How to test component using spyFn in React Jest - reactjs

I want to test my component Actions when I pass actions to children. In a nutshell, every source like twitter or facebook has its own set of actions. I'd like to check that it is called or not using spy.
This is my Actions component
const targetMetric = 'account dropdown'
const availableActions = {
addQuery: {
facebook: '^facebook://page/',
twitter: true
},
exclude: {
blog: [
'^blog://user/eventregistry/',
'^eventregistry://user/'
],
news: [
'^news://user/eventregistry/',
'^eventregistry://user/'
],
twitter: true,
youtube: true
},
reportAsNews: {
youtube: true,
mastodon: true,
twitter: true
}
}
const requiredHandlers = {
exclude: [
'onExcludeProfile'
],
reportAsNews: [
'onReportAsNews'
]
}
class Actions extends React.PureComponent {
get actions() {
const { account, handlers } = this.props
const actions = {};
Object.keys(availableActions).forEach(key =>
actions[ key ] = false
)
Object.keys(actions).forEach(key => {
const value = (
!!account.uri
&&
availableActions[ key ][ account.type ]
)
if (!value) {
return
}
if (typeof value === 'boolean') {
actions[ key ] = value
return
}
if (typeof value === 'string') {
const re = new RegExp(value, 'i')
actions[ key ] = re.test(account.uri)
return
}
if (
typeof value === 'object'
&&
Array.isArray(value)
) {
actions[ key ] = value.some(v => {
const re = new RegExp(v, 'i')
return re.test(account.uri)
})
}
})
Object.keys(actions).forEach(key => {
if (!actions[ key ] || !requiredHandlers[ key ]) {
return
}
actions[ key ] = requiredHandlers[ key ].some(i => handlers[ i ])
})
if (actions.addQuery) {
actions.addQuery = Object
.keys(this.addQueryActions)
.some(key => this.addQueryActions[ key ])
}
return actions
}
get addQueryActions() {
const { availableLanguages = [], userIsAdmin } = this.context
const { caseItem, handlers } = this.props
const actions = {
addQueryToFilter: !caseItem.isPaused && !!handlers.onAddQuery,
addQueryToAccountList: userIsAdmin && !!handlers.onAddToAccountList
}
actions.addQueryToSearch = actions.addQueryToFilter && !!availableLanguages.length
return actions
}
get actor() {
return pick(
this.props.account,
[ 'uri', 'name', 'link' ]
)
}
onExclude = () => {
const { account, handlers, isCaseLocked } = this.props
if (isCaseLocked) {
return
}
handlers.onExcludeProfile(account)
}
onReportAsNews = () => this.props.handlers.onReportAsNews(this.actor)
onAddToAccountList = () => {
const { account, from, handlers } = this.props
handlers.onAddToAccountList(account, from)
}
onAddToQuery = type => ({ language } = {}) => {
const { account, caseItem, handlers } = this.props
const { id } = caseItem
const metrics = {
index: getId(),
language,
type
}
handlers.onAddQuery({
...metrics,
expression: this.expressionFromAccount(account),
hideSearch: true,
id,
})
return metrics
}
expressionFromAccount = account => ({
and: [
{ account }
]
})
trackExcludeEvent = () => {
const { account } = this.props
this.trackEvent(
events.excludeAccounts,
{
accountsAdded: 1,
source: account.type
}
)
}
trackCreateNewQueryEvent = ({ index, language, type }) => {
const eventNameMap = {
filters: events.createNewFilter,
queries: events.createNewSearch,
}
const metrics = {
queryId: index,
target: targetMetric
}
if (type === 'queries') {
metrics[ 'language' ] = language.toLowerCase()
}
this.trackEvent(
eventNameMap[ type ],
metrics
)
}
trackReportAsNewsEvent = () => (
this.trackEvent(
events.reportAsNews,
{ source: this.props.account.type }
)
)
trackEvent = (eventName, props = {}) => {
const { from, message } = this.props
eventTracker.track(
eventName,
{
...props,
from,
messageUri: message.uri
}
)
}
getLangMenuActions = ({ handlers, isCaseLocked }, { availableLanguages }) => {
if (
isCaseLocked
||
!availableLanguages
||
!handlers.onAddQuery
) {
return []
}
const onClick = compose(
this.trackCreateNewQueryEvent,
this.onAddToQuery('queries')
)
return availableLanguages.map(({ label, value: language }) => ({
handler: onClick.bind(this, { language }),
id: `add-account-to-search-lang-${language}`,
label
}))
}
getActions = () => {
const { isCaseLocked } = this.props
const { userIsAdmin } = this.context
const actions = []
if (this.actions.addQuery) {
const addQueryAction = {
id: 'add-account-to',
isInactive: isCaseLocked && !userIsAdmin,
label: i18n.t('SOURCES.DROPDOWN_ADD_TO'),
children: []
}
if (this.addQueryActions.addQueryToSearch) {
addQueryAction.children.push({
id: 'add-account-to-search',
isInactive: isCaseLocked,
label: i18n.t('SOURCES.DROPDOWN_NEW_SEARCH'),
children: this.getLangMenuActions(this.props, this.context)
})
}
if (this.addQueryActions.addQueryToFilter) {
addQueryAction.children.push({
handler: compose(
this.trackCreateNewQueryEvent,
this.onAddToQuery('filters')
),
id: 'add-account-to-filter',
isInactive: isCaseLocked,
label: i18n.t('SOURCES.DROPDOWN_NEW_FILTER')
})
}
if (this.addQueryActions.addQueryToAccountList) {
addQueryAction.children.push({
handler: this.onAddToAccountList,
id: 'add-account-to-account-list',
label: i18n.t('SOURCES.DROPDOWN_ACCOUNT_LIST')
})
}
actions.push(addQueryAction)
}
if (this.actions.reportAsNews) {
actions.push({
handler: compose(
this.onReportAsNews,
this.trackReportAsNewsEvent
),
id: 'report-as-news',
label: i18n.t('SOURCES.DROPDOWN_REPORT_AS_NEWS')
})
}
if (this.actions.exclude) {
actions.push({
handler: compose(
this.onExclude,
this.trackExcludeEvent
),
id: 'exclude-account',
isInactive: isCaseLocked,
label: i18n.t('SOURCES.DROPDOWN_EXCLUDE')
})
}
console.log(actions)
return actions
}
render() {
return this.props.children({
actions: this.getActions()
})
}
}
This is my test file
import expect from 'expect'
const injectActions = require('inject-loader!./actions')
const Actions = injectActions({
'cm/common/event-tracker': {
eventTracker: {
track: () => {},
clear: () => {}
},
events: {
createNewFilter: '...',
createNewSearch: '...',
excludeAccounts: '...',
reportAsNews: '...',
}
},
}).default
const handlers = {
onAddQuery: () => { },
onAddToAccountList: () => { },
onExcludeProfile: () => { },
onReportAsNews: () => { }
}
const testProps = {
twitter: {
account: {
name: 'Twitter account',
uri: 'twitter://status/12345',
type: 'twitter'
},
handlers,
},
facebookPage: {
account: {
name: 'Facebook page account',
uri: 'facebook://page/12345',
type: 'facebook'
},
handlers
}
}
describe('Actions component', () => {
let node
beforeEach(() => {
node = document.createElement('div')
})
afterEach(() => {
ReactDOM.unmountComponentAtNode(node)
})
it('returns empty actions array by default', () => {
const spyFn = expect.createSpy().andReturn(null)
ReactDOM.render(
<Actions>{spyFn}</Actions>,
node
)
expect(spyFn).toHaveBeenCalledWith({ actions: [] })
})
describe('Twitter', () => {
it('returns "Exclude" action', () => {
const { account, handlers } = testProps.twitter
const spyFn = expect.createSpy()
ReactDOM.render(
<Actions
account={account}
handlers={{
onExcludeProfile: handlers.onExcludeProfile
}}
isCaseLocked={false}
>
{spyFn}
</Actions>,
node
)
expect(spyFn).toHaveBeenCalledWith({ actions: [
{
handler: () => {},
id: 'exclude-account',
isInactive: false,
label: 'Exclude',
}
] })
})
})
First unit case works fine, but the second is wrong. Actually I don't need all object there too. I'd like to be only sure that it contains id: 'exclude-account' there.
Please guys about any help.

You can use expect.objectContaining(object)
matches any received object that recursively matches the expected properties
E.g.
describe('68770432', () => {
test('should pass', () => {
const spyFn = jest.fn();
spyFn({
actions: [{ handler: () => {}, id: 'exclude-account', isInactive: false, label: 'Exclude' }],
});
expect(spyFn).toBeCalledWith({
actions: [
expect.objectContaining({
id: 'exclude-account',
}),
],
});
});
});

Related

How to destroy google map on demand rides and deliveries in React

I was following the documentation to implement google map on demand rides and deliveries solution (ODRD) here.
And my Map component in React:
const MapComponent = ({ styles }) => {
const ref = useRef(null);
const tripId = useRef<string>('');
const locationProvider =
useRef<google.maps.journeySharing.FleetEngineTripLocationProvider>();
const [error, setError] = useState<string | undefined>();
const mapOptions = useRef<MapOptionsModel>({
showAnticipatedRoutePolyline: true,
showTakenRoutePolyline: true,
destinationMarker: ICON_OPTIONS.USE_DEFAULT,
vehicleMarker: ICON_OPTIONS.USE_DEFAULT,
});
const [trip, setTrip] = useState<TripModel>({
status: null,
dropOff: null,
waypoints: null,
});
const setTripId = (newTripId: string) => {
tripId.current = newTripId;
if (locationProvider.current) locationProvider.current.tripId = newTripId;
};
const setMapOptions = (newMapOptions: MapOptionsModel) => {
mapOptions.current.showAnticipatedRoutePolyline =
newMapOptions.showAnticipatedRoutePolyline;
mapOptions.current.showTakenRoutePolyline =
newMapOptions.showTakenRoutePolyline;
mapOptions.current.destinationMarker = newMapOptions.destinationMarker;
mapOptions.current.vehicleMarker = newMapOptions.vehicleMarker;
setTripId(tripId.current);
};
const authTokenFetcher = async () => {
const response = await fetch(
`${PROVIDER_URL}/token/consumer/${tripId.current}`
);
const responseJson = await response.json();
return {
token: responseJson.jwt,
expiresInSeconds: 3300,
};
};
useEffect(() => {
locationProvider.current =
new google.maps.journeySharing.FleetEngineTripLocationProvider({
projectId: PROVIDER_PROJECT_ID,
authTokenFetcher,
tripId: tripId.current,
pollingIntervalMillis: DEFAULT_POLLING_INTERVAL_MS,
});
locationProvider.current.addListener(
'error',
(e: google.maps.ErrorEvent) => {
setError(e.error.message);
}
);
locationProvider.current.addListener(
'update',
(
e: google.maps.journeySharing.FleetEngineTripLocationProviderUpdateEvent
) => {
if (e.trip) {
setTrip({
status: e.trip.status,
dropOff: e.trip.dropOffTime,
waypoints: e.trip.remainingWaypoints,
});
setError(undefined);
}
}
);
const mapViewOptions: google.maps.journeySharing.JourneySharingMapViewOptions =
{
element: ref.current as unknown as Element,
locationProvider: locationProvider.current,
anticipatedRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showAnticipatedRoutePolyline,
};
},
takenRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showTakenRoutePolyline,
};
},
destinationMarkerSetup: ({ defaultMarkerOptions }) => {
if (
mapOptions.current.destinationMarker !== ICON_OPTIONS.USE_DEFAULT
) {
defaultMarkerOptions.icon =
mapOptions.current.destinationMarker.icon;
}
return { markerOptions: defaultMarkerOptions };
},
vehicleMarkerSetup: ({ defaultMarkerOptions }) => {
if (mapOptions.current.vehicleMarker !== ICON_OPTIONS.USE_DEFAULT) {
// Preserve some default icon properties.
if (defaultMarkerOptions.icon) {
defaultMarkerOptions.icon = Object.assign(
defaultMarkerOptions.icon,
mapOptions.current.vehicleMarker.icon
);
}
}
return { markerOptions: defaultMarkerOptions };
},
};
const mapView = new google.maps.journeySharing.JourneySharingMapView(
mapViewOptions
);
// Provide default zoom & center so the map loads even if trip ID is bad or stale.
mapView.map.setOptions(DEFAULT_MAP_OPTIONS);
}, []);
return (
<div style={styles.map} ref={ref} />
);
};
And my App component like this:
import React from 'react';
import { Wrapper, Status } from '#googlemaps/react-wrapper';
import MapComponent from './src/components/MapComponent';
import { API_KEY } from './src/utils/consts';
const render = (status: Status) => <Text>{status}</Text>;
const App = () => {
return (
<Wrapper
apiKey={API_KEY}
render={render}
version={'beta'}
// #ts-ignore
libraries={['journeySharing']}
>
<MapComponent />
</Wrapper>
);
};
Everything will works fine but I do not know how to destroy the map when component unmount in React. That's why my App always call API update the trip info.
I was tried to use clean up function in useEffect:
useEffect(() => {
locationProvider.current =
new google.maps.journeySharing.FleetEngineTripLocationProvider({
projectId: PROVIDER_PROJECT_ID,
authTokenFetcher,
tripId: tripId.current,
pollingIntervalMillis: DEFAULT_POLLING_INTERVAL_MS,
});
locationProvider.current.addListener(
'error',
(e: google.maps.ErrorEvent) => {
setError(e.error.message);
}
);
const updateEvent = locationProvider.current.addListener(
'update',
(
e: google.maps.journeySharing.FleetEngineTripLocationProviderUpdateEvent
) => {
if (e.trip) {
setTrip({
status: e.trip.status,
dropOff: e.trip.dropOffTime,
waypoints: e.trip.remainingWaypoints,
});
setError(undefined);
}
}
);
const mapViewOptions: google.maps.journeySharing.JourneySharingMapViewOptions =
{
element: ref.current as unknown as Element,
locationProvider: locationProvider.current,
anticipatedRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showAnticipatedRoutePolyline,
};
},
takenRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showTakenRoutePolyline,
};
},
destinationMarkerSetup: ({ defaultMarkerOptions }) => {
if (
mapOptions.current.destinationMarker !== ICON_OPTIONS.USE_DEFAULT
) {
defaultMarkerOptions.icon =
mapOptions.current.destinationMarker.icon;
}
return { markerOptions: defaultMarkerOptions };
},
vehicleMarkerSetup: ({ defaultMarkerOptions }) => {
if (mapOptions.current.vehicleMarker !== ICON_OPTIONS.USE_DEFAULT) {
// Preserve some default icon properties.
if (defaultMarkerOptions.icon) {
defaultMarkerOptions.icon = Object.assign(
defaultMarkerOptions.icon,
mapOptions.current.vehicleMarker.icon
);
}
}
return { markerOptions: defaultMarkerOptions };
},
};
const mapView = new google.maps.journeySharing.JourneySharingMapView(
mapViewOptions
);
// Provide default zoom & center so the map loads even if trip ID is bad or stale.
mapView.map.setOptions(DEFAULT_MAP_OPTIONS);
return () => {
mapView.map = null // or mapView.map.setmap(null);
google.maps.event.removeListener(updateEvent);
};
}, []);
But it was not working. Hope anyone can help me find out this. Thanks

update observable object from array in observable object

I have documents id in the Information I am getting from service. I need documents Info to be attached to this parent information and return parent. Not able to return observables in loop.
The second switch map is working fine. The first is not executing if I return tInfo from map, it gives following error below.
Argument of type '(tInfo: { documents; documentsInfo:
any[]; }) => void' is not assignable to parameter of type '(value:
{ documents: any; documentsInfo: any[]; }, index: number) =>
ObservableInput'. Type 'void' is not assignable to type
'ObservableInput'
public transform(tId: any,name: string, args?: any): Observable<string> {
return this.sService.getTaxById(tId).pipe(
// tslint:disable-next-line: deprecation
switchMap((tInfo:{ documents ; documentsInfo: any [] }) => {
if (tInfo.documents) {
tInfo..documents.forEach(doc => {
return this.sService
.getFiles(doc.docId).pipe(
map((filesInfo: { FileName: ''; downloadUrl: '' }) => {
const fileObjnew =
{
docName: filesInfo.FileName,
downloadUrl: filesInfo.downloadUrl,
}
tInfo.documentsInfo.push(fileObjnew);
})
);
});
return of(taxInfo);
}
}),
// tslint:disable-next-line: deprecation
switchMap((Info: { cc: '' ; authority: '' ; cName: ''}) => {
if (Info.authority) {
const cc = taxInfo.cc;
return this.sService.getOrgs(cc).pipe(
map((orgsData: any) => {
console.log("Data============>", orgsData);
(orgsData || []).forEach(tax => {
if (orgsData.aid === tax.id) {
Info.cName = tax.parentName;
}
});
return Info;
})
);
}
}),
map((Info) => {
if (name === 'subsTemplate') {
return this.subsTemplate(Info);
}
})
);
}
In the first switchMap you have to return an observable in all code paths and to chain the getFiles sub-observables to your main observable, then return the observable combined by forkJoin.
You can try the following:
public transform(tId: any, name: string, args?: any): Observable<string> {
return this.sService.getTaxById(tId).pipe(
// tslint:disable-next-line: deprecation
switchMap((tInfo: { documents; documentsInfo: any[] }) => {
if (tInfo.documents) {
return forkJoin(
tInfo.documents.map((doc) =>
this.sService.getFiles(doc.docId).pipe(
map((filesInfo: { FileName: ''; downloadUrl: '' }) => {
const fileObjnew = {
docName: filesInfo.FileName,
downloadUrl: filesInfo.downloadUrl,
};
tInfo.documentsInfo.push(fileObjnew);
})
)
)
).pipe(mapTo(taxInfo));
}
return of(taxInfo);
}),
// tslint:disable-next-line: deprecation
switchMap((Info: { cc: ''; authority: ''; cName: '' }) => {
if (Info.authority) {
const cc = taxInfo.cc;
return this.sService.getOrgs(cc).pipe(
map((orgsData: any) => {
console.log('Data============>', orgsData);
(orgsData || []).forEach((tax) => {
if (orgsData.aid === tax.id) {
Info.cName = tax.parentName;
}
});
return Info;
})
);
}
return of(null);
}),
map((Info) => {
if (name === 'subsTemplate') {
return this.subsTemplate(Info);
}
})
);
}

Zustand state does not re-render component or passes data correctly to then display filtered items

I'm using Zustand to store state, everything is working fine apart from this. When i click on the Song Buttons i want that to filter from the list.
Currently on fresh load it displays 3 songs. When clicking the button it should filter (and it does for first instance) but as soon as i click another button to filter again then nothing happens.
So if i chose / click Song 1 and Song 2 it should only show these songs.
I think the logic i wrote for that is correct but i must be doing something wrong with re-rendering.
Sorry i know people like to upload example here but i always find it hard with React files, so for this case I'm using https://codesandbox.io/s/damp-waterfall-e63mn?file=/src/App.js
Full code:
import { useEffect, useState } from 'react'
import create from 'zustand'
import { albums } from './albums'
export default function Home() {
const {
getFetchedData,
setFetchedData,
getAttrData,
setAttrData,
getAlbumData,
getButtonFilter,
setButtonFilter,
setAlbumData,
testState,
} = stateFetchData()
useEffect(() => {
if (getFetchedData) setAttrData(getFetchedData.feed.entry)
}, [getFetchedData, setAttrData])
useEffect(() => {
setAlbumData(getButtonFilter)
}, [getButtonFilter, setAlbumData])
// useEffect(() => {
// console.log('testState', testState)
// console.log('getAlbumData', getAlbumData)
// }, [getAlbumData, testState])
useEffect(() => {
setFetchedData()
}, [setFetchedData])
return (
<div>
<div>Filter to Show: {JSON.stringify(getButtonFilter)}</div>
<div>
{getAttrData.map((props, idx) => {
return (
<FilterButton
key={idx}
attr={props}
getDataProp={getButtonFilter}
setDataProp={setButtonFilter}
/>
)
})}
</div>
<div>
{getAlbumData?.feed?.entry?.map((props, idx) => {
return (
<div key={idx}>
<h1>{props.title.label}</h1>
</div>
)
})}
</div>
</div>
)
}
const FilterButton = ({ attr, getDataProp, setDataProp }) => {
const [filter, setFilter] = useState(false)
const filterAlbums = async (e) => {
const currentTarget = e.currentTarget.innerHTML
setFilter(!filter)
if (!filter) setDataProp([...getDataProp, currentTarget])
else setDataProp(getDataProp.filter((str) => str !== currentTarget))
}
return <button onClick={filterAlbums}>{attr.album}</button>
}
const stateFetchData = create((set) => ({
getFetchedData: albums,
setFetchedData: async () => {
set((state) => ({ ...state, getAlbumData: state.getFetchedData }))
},
getAttrData: [],
setAttrData: (data) => {
const tempArr = []
for (const iterator of data) {
tempArr.push({ album: iterator.category.attributes.label, status: false })
}
set((state) => ({ ...state, getAttrData: tempArr }))
},
getButtonFilter: [],
setButtonFilter: (data) => set((state) => ({ ...state, getButtonFilter: data })),
testState: {
feed: { entry: [] },
},
getAlbumData: [],
setAlbumData: (data) => {
set((state) => {
console.log('🚀 ~ file: index.js ~ line 107 ~ state', state)
const filter = state.getAlbumData.feed?.entry.filter((item) =>
data.includes(item.category.attributes.label),
)
return {
...state,
getAlbumData: {
...state.getAlbumData,
feed: {
...state.getAlbumData.feed,
entry: filter,
},
},
}
})
},
}))
Sample data:
export const albums = {
feed: {
entry: [
{ title: { label: 'Song 1' }, category: { attributes: { label: 'Song 1' } } },
{ title: { label: 'Song 2' }, category: { attributes: { label: 'Song 2' } } },
{ title: { label: 'Song 3' }, category: { attributes: { label: 'Song 3' } } },
],
},
}

why is a React class component converted to functional component with hooks not picking up state in event handler on document

I tried to refactor the code here using a functional component instead of a class component and am seeing that the state the copy event handler picks up is the initial state. I tried adding other copy event handlers and found the same behavior and was wondering how I can address this so that it can pick up the current state instead.
import React, { useState, useEffect, Component } from "react";
import ReactDOM from "react-dom";
import { range } from 'lodash';
import ReactDataGrid from 'react-data-grid'; // Tested with v5.0.4, earlier versions MAY NOT HAVE cellRangeSelection
import "./styles.css";
function App() {
return (
<div className="App">
<MyDataGrid />
</div>
);
}
const columns = [
{ key: 'id', name: 'ID', editable: true },
{ key: 'title', name: 'Title', editable: true },
{ key: 'count', name: 'Complete', editable: true },
{ key: 'sarah', name: 'Sarah', editable: true },
{ key: 'jessica', name: 'Jessica', editable: true },
];
const initialRows = Array.from(Array(1000).keys(), (_, x) => (
{ id: x, title: x * 2, count: x * 3, sarah: x * 4, jessica: x * 5 }
));
const defaultParsePaste = str => (
str.split(/\r\n|\n|\r/)
.map(row => row.split('\t'))
);
const MyDataGrid = props => {
const [state, setState] = useState({
rows: initialRows,
topLeft: {},
botRight: {},
});
useEffect(() => {
// Copy paste event handler
document.addEventListener('copy', handleCopy);
document.addEventListener('paste', handlePaste);
return () => {
document.removeEventListener('copy', handleCopy);
document.removeEventListener('paste', handlePaste);
}
}, [])
const rowGetter = (i) => {
const { rows } = state;
return rows[i];
}
const updateRows = (startIdx, newRows) => {
setState((state) => {
const rows = state.rows.slice();
for (let i = 0; i < newRows.length; i++) {
if (startIdx + i < rows.length) {
rows[startIdx + i] = { ...rows[startIdx + i], ...newRows[i] };
}
}
return { rows };
});
}
const handleCopy = (e) => {
console.debug('handleCopy Called');
e.preventDefault();
const { topLeft, botRight } = state;
// Loop through each row
const text = range(topLeft.rowIdx, botRight.rowIdx + 1).map(
// Loop through each column
rowIdx => columns.slice(topLeft.colIdx, botRight.colIdx + 1).map(
// Grab the row values and make a text string
col => rowGetter(rowIdx)[col.key],
).join('\t'),
).join('\n');
console.debug('text', text);
e.clipboardData.setData('text/plain', text);
}
const handlePaste = (e) => {
console.debug('handlePaste Called');
e.preventDefault();
const { topLeft } = state;
const newRows = [];
const pasteData = defaultParsePaste(e.clipboardData.getData('text/plain'));
console.debug('pasteData', pasteData);
pasteData.forEach((row) => {
const rowData = {};
// Merge the values from pasting and the keys from the columns
columns.slice(topLeft.colIdx, topLeft.colIdx + row.length)
.forEach((col, j) => {
// Create the key-value pair for the row
rowData[col.key] = row[j];
});
// Push the new row to the changes
newRows.push(rowData);
});
console.debug('newRows', newRows);
updateRows(topLeft.rowIdx, newRows);
}
const onGridRowsUpdated = ({ fromRow, toRow, updated, action }) => {
console.debug('onGridRowsUpdated!', action);
console.debug('updated', updated);
if (action !== 'COPY_PASTE') {
setState((state) => {
const rows = state.rows.slice();
for (let i = fromRow; i <= toRow; i++) {
rows[i] = { ...rows[i], ...updated };
}
return { rows };
});
}
};
const setSelection = (args) => {
console.log('setting... >>', args)
setState({
...state,
topLeft: {
rowIdx: args.topLeft.rowIdx,
colIdx: args.topLeft.idx,
},
botRight: {
rowIdx: args.bottomRight.rowIdx,
colIdx: args.bottomRight.idx,
},
});
};
const { rows } = state;
return (
<div>
<ReactDataGrid
columns={columns}
rowGetter={i => rows[i]}
rowsCount={rows.length}
onGridRowsUpdated={onGridRowsUpdated}
enableCellSelect
minColumnWidth={40}
cellRangeSelection={{
onComplete: setSelection,
}}
/>
</div>
);
}
export default MyDataGrid;
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The second parameter that you pass to useEffect tells react to skip the effect unless one of the items in the array has changed. You passed in an empty array, so other than the initial render, it will never update. Thus, you set up the event listeners with functions that have the original state in their closure, and nothing else.
To get it to update as state changes, either remove the array, or fil it with variables that your code depends on:
useEffect(() => {
document.addEventListener('copy', handleCopy);
document.addEventListener('paste', handlePaste);
return () => {
document.removeEventListener('copy', handleCopy);
document.removeEventListener('paste', handlePaste);
}
}, [state])

Action not getting called on Testing - React

My code:
componentWillReceiveProps(nextProps) {
if (get(nextProps.signInSliderDetails, 'showWelcomePage')) {
this.timer = setTimeout(() => {
this.onPrimaryCloseButtonClick();
}, 3000);
}
}
onClickOfCreateAccountButton() {
const el = document.getElementsByClassName('SignInSlider-loginSlider')[0];
const el1 = document.getElementsByClassName('SignInSlider-createAccountSlider')[0];
el.classList.add('SignInSlider-animate-show');
el.classList.remove('SignInSlider-animate-hide');
setTimeout(() => {
this.props.signInSliderActions.openCreateAccountPage();
el1.classList.add('SignInSlider-animate-show');
}, 5);
}
onClickPasswordReset() {
const el = document.getElementsByClassName('SignInSlider-loginSlider')[0];
const el1 = document.getElementsByClassName('SignInSlider-passwordSlider')[0];
el.classList.add('SignInSlider-animate-show');
el.classList.remove('SignInSlider-animate-hide');
setTimeout(() => {
this.props.signInSliderActions.openForgotPasswordResetPage();
el1.classList.add('SignInSlider-animate-show');
}, 5);
}
onBackButtonClick() {
const el = document.getElementsByClassName('SignInSlider-loginSlider')[0];
const el1 = document.getElementsByClassName('SignInSlider-createAccountSlider')[0];
const el2 = document.getElementsByClassName('SignInSlider-passwordSlider')[0];
el1.classList.remove('SignInSlider-animate-show');
el2.classList.remove('SignInSlider-animate-show');
setTimeout(() => {
this.props.signInSliderActions.resetSignInSlider();
el.classList.add('SignInSlider-animate-hide');
}, 5);
}
onPrimaryCloseButtonClick() {
this.removeSliderClasses();
this.timer && clearTimeout(this.timer);
this.props.signInSliderActions.resetSignInSlider();
this.props.onPrimaryCloseButtonClick(this.props.signInSliderDetails.showWelcomePage);
}
removeSliderClasses() {
const el = document.getElementsByClassName('SignInSlider-loginSlider')[0];
const el1 = document.getElementsByClassName('SignInSlider-createAccountSlider')[0];
const el2 = document.getElementsByClassName('SignInSlider-passwordSlider')[0];
el.classList.remove('SignInSlider-animate-show');
el1.classList.remove('SignInSlider-animate-show');
el2.classList.remove('SignInSlider-animate-show');
}
render() {
const { deviceType, preferences, messagesTexts, signInSliderDetails } = this.props;
const { showCreateAccountPage, showWelcomePage, showForgotPasswordPage, createAccount,
signInDetails, passwordResetResponse } = signInSliderDetails;
const passwordResetDetails = { passwordResetResponse };
const hideBackArrowCloseButton = !(showCreateAccountPage || showForgotPasswordPage);
// Need to have a new logic. Not Token Provider here
const firstName = TokenProvider.get('DP_USER_NAME');
return (
<SlidePanel
isOpenPanel={this.props.isOpenPanel}
onClosePanel={!hideBackArrowCloseButton && this.onBackButtonClick}
onPrimaryCloseButtonClick={this.onPrimaryCloseButtonClick}
panelTitle={!hideBackArrowCloseButton && 'Back to Sign-In'}
hideBackArrowCloseButton={hideBackArrowCloseButton}
isPrimaryCloseButtonRequired>
<div className={cx('signInSliderPanel')}>
<div className={cx('loginSlider')}>
{ !showCreateAccountPage && !showWelcomePage && !showForgotPasswordPage &&
<LoginWrapper
signInDetails={signInDetails}
deviceType={deviceType}
preferences={preferences}
messagesTexts={messagesTexts}
source="account"
actions={this.props.signInActions}
onClickOfCreateAccountButton={this.onClickOfCreateAccountButton}
onClickPasswordReset={this.onClickPasswordReset}
isSignInSliderReq
/> }
</div>
<div className={cx('createAccountSlider')}>
{showCreateAccountPage &&
<CreateAccountWrapper
createAccount={createAccount}
isSignInSliderReq
deviceType={deviceType}
messagesTexts={this.props.messagesTexts}
preferences={this.props.preferences}
actions={this.props.createAccountActions}/> } </div>
<div className={cx('passwordSlider')}>
{showForgotPasswordPage &&
<PasswordResetWrapper
isSignInSliderReq
messagesTexts={messagesTexts.passwordReset}
preferences={preferences}
createAccountActions={this.props.createAccountActions}
actions={this.props.passwordResetActions}
passwordResetDetails={passwordResetDetails}
signInSliderActions={this.props.signInSliderActions}
onPrimaryCloseButtonClick={this.onPrimaryCloseButtonClick}
deviceType
/>} </div>
<div className={cx('welcomeSlider')}>
{ showWelcomePage &&
<Welcome
messagesTexts={messagesTexts.signInSlider}
firstName={firstName} />} </div>
</div>
</SlidePanel>
);
}
}
My Test file:
const actions = {
signInActions: sinon.spy(),
createAccountActions: sinon.spy(),
passwordResetActions: sinon.spy(),
signInSliderActions: {
resetSignInSlider: sinon.spy(),
openForgotPasswordResetPage: sinon.spy(),
openCreateAccountPage: sinon.spy(),
},
};
const props = {
actions,
isOpenPanel: false,
onPrimaryCloseButtonClick: sinon.spy(),
preferences: preferences.common,
messagesTexts,
deviceType,
signInSliderDetails,
isSignInSliderReq: true,
};
describe('Shallow Render', () => {
let wrapper;
beforeEach(() => {
const gl = global.window.document.getElementsByClassName.returns({ className: '' });
gl.returns([elStub]);
wrapper = shallow(
<SignInSlider
{...props}
/>,
);
});
afterEach(() => {
global.window.document.getElementsByClassName.reset();
elStub.classList.add.reset();
elStub.classList.remove.reset();
wrapper.unmount();
});
it('it rendered successfully', () => {
expect(wrapper.find('SlidePanel')).to.exists;
expect(wrapper.find('LoginWrapper')).to.exists;
wrapper.find('LoginWrapper').prop('onClickOfCreateAccountButton')();
wrapper.find('LoginWrapper').prop('onClickPasswordReset')();
});
it('it shows createAccountPage SignInSlider', () => {
wrapper.setProps({
signInSliderDetails: {
createAccount: {},
signInDetails: {},
passwordResetDetails: {
passwordResetResponse: {},
},
showCreateAccountPage: true,
showWelcomePage: false,
showForgotPasswordPage: false,
},
});
wrapper.update();
expect(wrapper.find('CreateAccountWrapper')).to.exists;
});
it('it shows Forgot password page SignInSlider', () => {
wrapper.setProps({
signInSliderDetails: {
createAccount: {},
signInDetails: {},
passwordResetDetails: {
passwordResetResponse: {},
},
showCreateAccountPage: false,
showWelcomePage: false,
showForgotPasswordPage: true,
},
});
wrapper.update();
expect(wrapper.find('PasswordResetWrapper')).to.exists;
wrapper.find('PasswordResetWrapper').prop('onPrimaryCloseButtonClick')();
wrapper.find('SlidePanel').prop('onBackButtonClick')();
});
it('it shows Welcome page SignInSlider', () => {
wrapper.setProps({
signInSliderDetails: {
createAccount: {},
signInDetails: {},
passwordResetDetails: {
passwordResetResponse: {},
},
showCreateAccountPage: false,
showWelcomePage: true,
showForgotPasswordPage: false,
},
firstName: 'Vini',
});
wrapper.update();
setTimeout(() => {
expect(props.onPrimaryCloseButtonClick).to.have.been.called;
}, 3000);
expect(wrapper.find('Welcome')).to.exists;
});
});
Please help.

Resources