Testing rxjs subscription in react using jest - reactjs

I am trying to test the below code
Slider.tsx
import React, { useEffect, useState } from "react";
import { ISlide } from "./ISlide";
import styled, { css } from "styled-components";
import { BehaviorSubject, Observable, Subject, timer } from "rxjs";
import { repeatWhen, take, takeUntil, takeWhile, tap } from "rxjs/operators";
interface ISliderProps {
...
}
const SliderWrapper = styled.ul`
...
`;
const SliderItem = styled.li`
...
`;
export const Slider = (props: ISliderProps) => {
const {
slides,
activeSlide = 0,
onActiveItemChange,
itemsPerPage = 1,
delay = 5000,
interval = 5000,
loop = true
} = props;
const [currentActiveSlide, setCurrentActiveSlide] = useState(activeSlide);
const sliderCount = Math.ceil(slides.length / itemsPerPage);
const timerStart$: Observable<null> = new BehaviorSubject(null);
const timerStop$: Observable<null> = new Subject<null>();
const timer$ = timer(delay, interval).pipe(
takeWhile(() => loop),
takeUntil(timerStop$),
repeatWhen(() => timerStart$)
);
useEffect(() => {
const timerSubscription = timer$.subscribe({
next: (i) => {
const nextSlide = i >= sliderCount ? i % sliderCount : i;
onActiveItemChange(nextSlide);
setCurrentActiveSlide(nextSlide);
}
});
return () => timerSubscription.unsubscribe();
}, []);
const handleSliderItemClick = (i: number) => {
setCurrentActiveSlide(i);
timerStop$.next(null);
timer(delay).pipe(
take(1),
tap(timerStart$.next(null))
)
}
return (
<>
<SliderWrapper>
{currentActiveSlide}
{slides.map(({ id }, i) =>
<SliderItem
key={id}
aria-label={"scroll page number " + id}
onClick={() => handleSliderItemClick(i)}
active={i === currentActiveSlide}
/>)}
</SliderWrapper>
</>
);
};
Below is my test
import React from "react";
import { Slider } from "../../../components/slider/Slider";
import renderer, { act } from "react-test-renderer";
import { ISlide } from "../../../components/slider/ISlide";
it("renders without crashing", done => {
const slides: ISlide[] = [
{ id: 1 }, { id: 2 }, { id: 3 },
{ id: 4 }, { id: 5 }, { id: 6 },
{ id: 7 }, { id: 8 }
];
act(() => {
const component = renderer.create(<Slider itemsPerPage={3} slides={slides} activeSlide={0} />);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
done();
});
});
I have called done but the test keeps returning a warning
Jest did not exit one second after the test run has completed.
As expected the problem is in the subscription
● Timeout
67 |
68 | useEffect(() => {
> 69 | const timerSubscription = timer$.subscribe({
| ^
70 | next: (i) => {
71 | const nextSlide = i >= sliderCount ? i % sliderCount : i;
72 | onActiveItemChange(nextSlide);
How do I unsubscribe from this subscription?

Related

React not working with memo and tree structure

I've been going at it for 2 days and cannot figure it out :(
I have a tree-like conversation as you can see in the screenshot.
When a person types something in an empty input field, the Message is then added to the reducer array "conversationsData.messages". When that happens, the Replies component of each Message is listening to changes to only the ~3 replies of that message. If the replies change, then the Replies should rerender. Buuut... The problem is that every single Reply component, and thus every single message is being re-rendered which is causing lag.
Can you please help me get the memo to work properly?
ConversationManager/Conversation/Message.tsx
import React, { FunctionComponent, ReactElement, useRef, useState, useEffect, useMemo } from 'react'
import { useDispatch, useStore, useSelector } from 'react-redux'
import Autosuggest, { OnSuggestionSelected, ChangeEvent } from 'react-autosuggest'
import colors from '#common/colors'
import { updateMessage, removeMessage } from '#reducers/conversationsData'
import usePrevious from 'react-hooks-use-previous'
import MessageInterface, { MessageInitState } from '#interfaces/message'
import { RootState } from '#reducers/rootReducer'
import NewReply from './NewReply'
import { StyleSheet } from '#interfaces/common'
interface IMessageProps {
origMessage: MessageInterface,
isSubClone: boolean,
firstRender: boolean, // It's firstRender=true if we're rendering the message for the first time, "false" if it's a dynamic render
isStarter?: boolean
}
const MessageFunc = ({ origMessage, isSubClone, firstRender }: IMessageProps): ReactElement | null => {
if(!origMessage.id){
return null
}
const dispatch = useDispatch()
const store = useStore()
const state: RootState = store.getState()
const [inputSuggestions, setInputSuggestions] = useState<MessageInterface[]>([])
const [inputWidth, setInputWidth] = useState(0)
const $invisibleInput = useRef<HTMLInputElement>(null)
const isFirstRun = useRef(true)
const [localMessage, setLocalMessage] = useState<MessageInterface>(MessageInitState)
const previousLocalMessage = usePrevious<MessageInterface>(localMessage, MessageInitState)
useEffect(() => {
isFirstRun.current = true
setLocalMessage(origMessage)
}, [origMessage])
useEffect(() => {
if(!localMessage.id) return
if(isFirstRun.current == true){
setupInputWidth()
isFirstRun.current = false
}
if(previousLocalMessage.text != localMessage.text){
setupInputWidth()
}
if(previousLocalMessage.cloneId != localMessage.cloneId){
setupIfMessageClone()
}
}, [localMessage])
const characterMessages = state.conversationsData.messages.filter((m) => {
return m.characterId == origMessage.characterId
})
const parent: MessageInterface = characterMessages.find((m) => {
return m.id == origMessage.parentId
}) || MessageInitState
const setupIfMessageClone = () => { // This function is only relevant if this message is a clone of another one
if(!localMessage.cloneId) return
const cloneOf = characterMessages.find((m) => {
return m.id == localMessage.cloneId
}) || MessageInitState
setLocalMessage({
...localMessage,
text: cloneOf.text
})
}
const setupInputWidth = () => {
let width = $invisibleInput.current ? $invisibleInput.current.offsetWidth : 0
width = width + 30 // Let's make the input width a bit bigger
setInputWidth(width)
}
const _onFocus = () => {
// if(!localMessage.text){ // No message text, create a new one
// dispatch(updateMessage(localMessage))
// }
}
const _onBlur = () => {
if(localMessage.text){
dispatch(updateMessage(localMessage))
}
// No message, delete it from reducer
else {
dispatch(removeMessage(localMessage))
}
}
const _onChange = (event: React.FormEvent, { newValue }: ChangeEvent): void => {
setLocalMessage({
...localMessage,
cloneId: '',
text: newValue
})
}
const _suggestionSelected: OnSuggestionSelected<MessageInterface> = (event, { suggestion }) => {
setLocalMessage({
...localMessage,
cloneId: suggestion.id
})
}
const getSuggestions = (value: string): MessageInterface[] => {
const inputVal = value.trim().toLowerCase()
const inputLen = inputVal.length
return inputLen === 0 ? [] : characterMessages.filter(message =>
message.text.toLowerCase().slice(0, inputLen) === inputVal
)
}
if(!localMessage.id){
return null
}
else {
return (
<>
<li>
<div>
<Autosuggest
suggestions={inputSuggestions}
onSuggestionsFetchRequested={({ value }) => setInputSuggestions(getSuggestions(value))}
onSuggestionsClearRequested={() => setInputSuggestions([])}
getSuggestionValue={(suggestion) => suggestion.text}
onSuggestionSelected={_suggestionSelected}
renderSuggestion={(suggestion) => (
<div>
{suggestion.text}
</div>
)}
theme={{ ...autoSuggestTheme, input: {
...styles.input,
width: inputWidth,
borderBottomColor: localMessage.cloneId ? colors.purple : 'default',
borderBottomWidth: localMessage.cloneId ? 2 : 1
} }}
inputProps={{
value: localMessage.text,
onChange: _onChange,
onBlur: _onBlur,
onFocus: _onFocus,
className: 'form-control',
disabled: isSubClone
}}
/>
<span style={styles.invisibleSpan} ref={$invisibleInput}>{localMessage.text}</span>
</div>
<ul className="layer">
<Replies parentMessage={localMessage} isSubClone={isSubClone} />
</ul>
</li>
</>
)
}
}
const Message = React.memo(MessageFunc)
// const Message = MessageFunc
interface IRepliesProps {
parentMessage: MessageInterface,
isSubClone: boolean
}
const RepliesFunc: FunctionComponent<IRepliesProps> = ({
parentMessage, isSubClone
}: IRepliesProps): ReactElement | null => {
const previousParentMessage = usePrevious<MessageInterface>(parentMessage, MessageInitState)
const isFirstRun = useRef(true)
const replies: MessageInterface[] = useSelector((state: RootState) => state.conversationsData.messages.filter((m) => {
// If parent is regular message
if(!parentMessage.cloneId){
return m.parentId == parentMessage.id && m.characterId == parentMessage.characterId
}
// If parent is a clone, then replies need to come from the main clone
// else {
// return m.parentId == parentMessage.cloneId
// }
}))
if(replies.length){
return (
<>
{console.log('rendering Replies...')}
{replies.map((reply) => {
return (
<Message
origMessage={reply}
key={reply.id}
isSubClone={parentMessage.cloneId ? true : isSubClone}
firstRender={true}
/>
)
})}
{parentMessage.text && !parentMessage.cloneId && !isSubClone && (
<NewReply
parentMessage={parentMessage}
/>
)}
</>
)
}
else {
return null
}
}
// const Replies = React.memo(RepliesFunc)
const Replies = RepliesFunc
export default Message
const styles: StyleSheet = {
input: {
width: 0,
padding: 0,
paddingLeft: 10,
lineHeight: 25,
height: 25,
fontSize: 11,
boxShadow: 'none',
minWidth: 22
},
clone: {
borderBottomWidth: 2,
borderBottomColor: colors.purple
},
invisibleSpan: { // This is used for getting text width of input (for dynamic resizing of input fields)
opacity: 0,
position: 'absolute',
left: -9999,
top: -9999,
fontSize: 11
}
}
const autoSuggestTheme: StyleSheet = {
container: {
position: 'relative'
},
inputOpen: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
},
suggestionsContainer: {
display: 'none'
},
suggestionsContainerOpen: {
display: 'block',
position: 'absolute',
top: 25,
width: '100%',
minWidth: 400,
border: '1px solid #aaa',
backgroundColor: '#fff',
fontWeight: 300,
fontSize: 11,
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
zIndex: 2
},
suggestionsList: {
margin: 0,
padding: 0,
listStyleType: 'none'
},
suggestion: {
cursor: 'pointer',
padding: '5px 10px'
},
suggestionHighlighted: {
backgroundColor: '#ddd'
}
}
reducers/ConversationsData.ts
import { createSlice, PayloadAction } from '#reduxjs/toolkit'
import MessageInterface from '#interfaces/message'
import axios, { AxiosRequestConfig } from 'axios'
import conversationsDataJSON from '#data/conversationsData.json'
import { AppThunk } from '#reducers/store'
import _ from 'lodash'
interface IInitialState {
loaded: boolean,
messages: MessageInterface[]
}
export const initialState: IInitialState = {
loaded: false,
messages: []
}
export const charactersDataSlice = createSlice({
name: 'conversationsData',
initialState,
reducers: {
loadData: (state, action: PayloadAction<MessageInterface[]>) => {
return state = {
loaded: true,
messages:action.payload
}
},
add: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
state.messages.push(payload.message)
},
edit: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
const updatedConversations = state.messages.map(message => {
if(message.id == payload.message.id && message.characterId == payload.message.characterId){
return message = {
...payload.message,
text: payload.message.cloneId ? '' : payload.message.text // If there's a cloneId, don't save the text since the text comes from the clone parent
}
}
else {
return message
}
})
state.messages = updatedConversations
},
remove: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
_.remove(state.messages, (message) => {
return message.id == payload.message.id && message.characterId == payload.message.characterId
})
}
}
})
const { actions, reducer } = charactersDataSlice
const { loadData, edit, add, remove } = actions
// Thunk actions
// ---------
const loadConversationsData = (): AppThunk => {
return dispatch => {
const conversationsData: MessageInterface[] = conversationsDataJSON
dispatch(loadData(conversationsData))
}
}
const updateMessage = (message: MessageInterface): AppThunk => {
return (dispatch, getState) => {
const existingMessage: MessageInterface | undefined = getState().conversationsData.messages.find((m: MessageInterface) => {
return m.id == message.id && m.characterId == message.characterId
})
// If message exists, update it
if(existingMessage){
dispatch(edit({
message: message
}))
}
// else create a new message
else {
dispatch(add({
message: message
}))
}
setTimeout(() => {
dispatch(saveConversationsData())
}, 10)
}
}
const removeMessage = (message: MessageInterface): AppThunk => {
return (dispatch, getState) => {
const children: MessageInterface[] | [] = getState().conversationsData.messages.filter((m: MessageInterface) => {
return m.parentId == message.id && m.characterId == message.characterId
})
const hasChildren = children.length > 0
// If message has children, stop
if(hasChildren){
alert('This message has children. Will not kill this message. Remove the children first.')
}
// Otherwise, go ahead and kill message
else {
dispatch(remove({
message: message
}))
setTimeout(() => {
dispatch(saveConversationsData())
}, 10)
}
}
}
export const saveConversationsData = (): AppThunk => {
return (dispatch, getState) => {
const conversationsMessages = getState().conversationsData.messages
const conversationsMessagesJSON = JSON.stringify(conversationsMessages, null, '\t')
const options: AxiosRequestConfig = {
method: 'POST',
url: 'http://localhost:8888/api/update-conversations.php',
headers: { 'content-type': 'application/json; charset=UTF-8' },
data: conversationsMessagesJSON
}
axios(options)
.catch(error => console.error('Saving conversationsData error:', error))
}
}
// Exporting it all
// ---------
export { loadConversationsData, updateMessage, removeMessage }
export default reducer
interfaces/message.ts
export default interface MessageInterface {
id: string,
characterId: string,
text: string,
cloneId: string,
parentId: string
}
export const MessageInitState: MessageInterface = {
id: '',
characterId: '',
text: '',
cloneId: '',
parentId: ''
}
Because your selector uses Array.prototype.filter you create a new array every time the messages array changes for each component.
If you would store the data in the state as nested data you can prevent this from happening. For example: {id:1, message:'hello', replies:[{id:2, message:'world', replies:[]}]}.
A simpler way is to use the memoization of reselect to see if each element in the filtered array is the same as it was last time. This will require more resources than the nested solution as it will perform the filter on every change for every branch but won't re render needlessly.
Here is the simple example:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector, defaultMemoize } = Reselect;
const initialState = { messages: [] };
//action types
const ADD = 'ADD';
//helper crating id for messages
const id = ((id) => () => ++id)(0);
//action creators
const add = (parentId, message) => ({
type: ADD,
payload: { parentId, message, id: id() },
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
const { parentId, message, id } = payload;
return {
...state,
messages: state.messages.concat({
id,
parentId,
message,
}),
};
}
return state;
};
//selectors
const selectMessages = (state) => state.messages;
//curry creating selector function that closes over message id
// https://github.com/amsterdamharu/selectors
const createSelectMessageById = (messageId) =>
createSelector([selectMessages], (messages) =>
messages.find(({ id }) => id === messageId)
);
//used to check each item in the array is same as last
// time the function was called
const createMemoizeArray = (array) => {
const memArray = defaultMemoize((...array) => array);
return (array) => memArray.apply(null, array);
};
//curry creating selector function that closes over parentId
// https://github.com/amsterdamharu/selectors
const createSelectMessagesByParentId = (parentId) => {
//memoizedArray([1,2,3]) === memoizedArray([1,2,3]) is true
//https://github.com/reduxjs/reselect/issues/451#issuecomment-637521511
const memoizedArray = createMemoizeArray();
return createSelector([selectMessages], (messages) =>
memoizedArray(
messages.filter((m) => m.parentId === parentId)
)
);
};
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const AddMessage = ({ addMessage }) => {
const [reply, setReply] = React.useState('');
return (
<div>
<label>
message:
<input
type="text"
onChange={(e) => setReply(e.target.value)}
value={reply}
/>
</label>
<button onClick={() => addMessage(reply)}>Add</button>
</div>
);
};
const AddMessageContainer = React.memo(
function AddMessageContainer({ messageId }) {
const dispatch = useDispatch();
const addMessage = React.useCallback(
(message) => dispatch(add(messageId, message)),
//dispatch in deps should not be needed but
// my linter still complains about it
[dispatch, messageId]
);
return <AddMessage addMessage={addMessage} />;
}
);
const Message = ({ message, replies }) => {
console.log('in message render', message && message.message);
return (
<div>
{message ? <h1>{message.message}</h1> : ''}
{Boolean(replies.length) && (
<ul>
{replies.map(({ id }) => (
<MessageContainer key={id} messageId={id} />
))}
</ul>
)}
{/* too bad optional chaining (message?.id) does not work on SO */}
<AddMessageContainer
messageId={message && message.id}
/>
</div>
);
};
const MessageContainer = React.memo(
function MessageContainer({ messageId }) {
const selectMessage = React.useMemo(
() => createSelectMessageById(messageId),
[messageId]
);
const selectReplies = React.useMemo(
() => createSelectMessagesByParentId(messageId),
[messageId]
);
const message = useSelector(selectMessage);
const replies = useSelector(selectReplies);
return <Message message={message} replies={replies} />;
}
);
const App = () => {
return <MessageContainer />;
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>

Loop of Path and Sampler in Tone.js

I'm trying to create a simple metronome with Tone.js in my React app. But I can't get any sound. What am I doing wrong?
Exercise.ts
import React, { useRef, useEffect, useState } from 'react'
import { exercises } from 'data/exercises.json'
import { Typography, Grid, Button } from '#material-ui/core'
import { useParams } from 'react-router-dom'
import { Sampler, Loop, Part, Transport } from 'tone'
let part: Part
let loop: Loop
export default function ExercisePage() {
const [isLoaded, setIsLoaded] = useState(false)
// find exercise
let { id } = useParams()
let exercise = exercises.find(e => e.id === id)
const sampler = useRef<Sampler | null>(null)
useEffect(() => {
sampler.current = new Sampler(
{ A1: 'click_hi.ogg', B1: 'click_lo.ogg' },
() => {
setIsLoaded(true)
},
`${process.env.PUBLIC_URL}/audio/`
).toMaster()
part = new Part(
(time, note) => {
// #ts-ignore
sampler.current.triggerAttack(note.note, note.time, time, note.velocity)
},
[
{ time: '4n', note: 'A1', velocity: 1 },
{ time: '4n', note: 'B1', velocity: 0.5 },
{ time: '4n', note: 'B1', velocity: 0.5 },
{ time: '4n', note: 'B1', velocity: 0.5 },
]
)
loop = new Loop(() => {
part.start(0)
}, '1m')
Transport.start()
}, [])
const startLoop = () => {
loop.start(0)
}
return (
<Grid container direction="column">
<Grid item>
<Typography variant="h4">Exercise: {id}</Typography>
<Button
variant="contained"
color="secondary"
onClick={() => startLoop()}
>
Start
</Button>
</Grid>
</Grid>
)
}
Move Transport.start() to startLoop function
const startLoop = () => {
Transport.start()
loop.start(0)
}

GSAP animate elements in an array fetched from the server

I would like to animate each element in an array with TweenMax.staggerFrom. At the moment I created this working sample
import React, { useRef, useEffect createRef } from 'react';
import { TweenMax } from 'gsap';
const AnimateView = () => {
const data = [
{ title: 'Title 1', value: 1000 },
{ title: 'Title 2', value: 1100 },
{ title: 'Title 3', value: 1200 },
];
const elementsRef = useRef(data.map(() => createRef()));
useEffect(() => {
const elements = elementsRef.current.map(el => el.current);
TweenMax.staggerFrom(elements, 1, { scale: 0 }, 0.3);
}, []);
return (
<>
{data.map((item, index) => (
<div ref={elementsRef.current[index]}>{item.title}</div>
))}
</>
);
};
export default AnimateView;
The difference is that I want data array to be fetched from the server. I cannot figure out why in elementsRef I'm getting no attached refs. Below you can check what I wanted to achieve.
import React, { useRef, useEffect createRef } from 'react';
import { TweenMax } from 'gsap';
import { connect } from 'react-redux';
import { fetchData } from 'actions';
const AnimateView = ({ combineFetching, income }) => {
const elementsRef = useRef(income.length && income.map(() => createRef()));
useEffect(() => {
const elements = elementsRef.current.map(el => el.current);
TweenMax.staggerFrom(elements, 1, { scale: 0 }, 0.3);
}, []);
return (
<>
<button onClick={fetchData}>Click</button>
{income.map((item, index) => (
<div ref={elementsRef.current[index]}>{item.title}</div>
))}
</>
);
};
const mapDispatchToProps = state => ({
income: state.budget.income,
});
export default connect(
mapDispatchToProps,
{ fetchData },
)(AnimateView);
On Click I want to fetch data from database and animate each element in the array.

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])

How to test function component with a react-data-grid

I have created a data grid with react using React-data-grid (link)
My code is as follows:
import React, { useState } from "react";
import ReactDataGrid from 'react-data-grid';
const ROW_COUNT = 20;
const MIN_WIDTH = 100;
const defaultColumnProperties = {
resizable: true,
sortable: true
};
const columns = [
{
key: "eventTypeNameI18n",
name: "Type",
},
{
key: "nameI18n",
name: "Name",
width: 160
},
{
key: "dateCreated",
name: "Time",
width: 220
},
{
key: "locationNameI18n",
name: "Location",
width: 200
}
].map(col => ({...col, ...defaultColumnProperties}));
const sortRows = (initialRows, sortColumn, sortDirection) => rows => {
const comparer = (a, b) => {
if (sortDirection === "ASC") {
return a[sortColumn] > b[sortColumn] ? 1 : -1;
}
else if (sortDirection === "DESC") {
return a[sortColumn] < b[sortColumn] ? 1 : -1;
}
};
return sortDirection === "NONE" ? initialRows : [...rows].sort(comparer);
};
function DataGrid({initialRows}) {
const [rows, setRows] = useState(initialRows);
return (
<ReactDataGrid
id="EventDataGrid"
columns={columns}
rowGetter={i => rows[i]}
rowsCount={ROW_COUNT}
minColumnWidth={MIN_WIDTH}
onGridSort={(sortColumn, sortDirection) =>
setRows(sortRows(initialRows, sortColumn, sortDirection))
}
/>
);
}
export default DataGrid;
I am new to writing unit tests and have been writing very basic unit tests as of recent. I am wondering what is the best way to test the onGridSort method using Jest/Enzyme
In my tests I currently have the following:
import React from 'react';
import ReactDOM from 'react-dom';
import Enzyme, {mount} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import EventsGrid from '../EventsGrid';
import sampleData from './sampleData/transformedEventSample';
Enzyme.configure({adapter: new Adapter()});
describe('Tests for <EventDataGrid/>', () => {
it('sort data grid by ascending ', () => {
const wrapper = mount(<EventsGrid initialRows={sampleData}/>);
const instance = wrapper.instance();
jest.spyOn(instance, 'sortRows');
const column = 'eventTypeNameI18n';
const sortDirection = 'ASC';
wrapper.find('#EventDataGrid').at(1).prop('onGridSort')(column, sortDirection);
expect(instance.sortRows).toHaveBeenCalled();
});
it('sort data grid by descending', () => {
const wrapper = mount(<EventsGrid initialRows={sampleData}/>);
const instance = wrapper.instance();
jest.spyOn(instance, 'sortRows');
const column = 'eventTypeNameI18n';
const sortDirection = 'DESC';
wrapper.find('#EventDataGrid').at(1).prop('onGridSort')(column, sortDirection);
expect(instance.sortRows).toHaveBeenCalled();
});
});
I want to test to make sure that the grid is ordered correctly (ordered by ascending/descending)

Resources