How to click unrendered virtualized element in TestCafe - reactjs

I'm using react-virtualized for a lengthy (1000+) list of items to select from. And I'm trying to set up an end to end test that requires clicking on one of the elements that are not currently rendered.
Ordinarily, I'd simply use something like:
await t.click(
ReactSelector('ListPage')
.findReact('ListItem')
.nth(873) // or .withText(...) or .withProps(...)
)
But because only a small subset of the ListItems are rendered, TestCafe fails to find the desired element.
I've been trying to figure out how to use TestCafe's ClientFunction to scroll the list container so the desired ListItem is rendered.
However, I'm running into a few issues:
Is there a way to share a Selector into the ClientFunction and modify the DOM element's scrollTop? Or do I have to re-query the element via the DOM directly?
Due to the ListItems being different heights, the scroll position is not a simple calculation of index x item height. How can I keep updating/scrolling within this function until the desired Selector is visible?

Is there a way to share a Selector into the ClientFunction and modify the DOM element's scrollTop?
There is a way to put Selector into the Client Function. Please refer to this example in the TestCafe documentation.
How can I keep updating/scrolling within this function until the desired Selector is visible?
You can use the TestCafe exist property to check if the element is rendered or not. The following example demonstrates the approach:
import { Selector } from 'testcafe';
fixture`Getting Started`.page('https://bvaughn.github.io/react-virtualized/#/components/List')
test('Test 1', async t => {
const dynamicRowHeightsInput = Selector('._1oXCrgdVudv-QMFo7eQCLb');
const listItem = Selector('._113CIjCFcgg_BK6pEtLzCZ');
const targetItem = listItem.withExactText('Tisha Wurster');
await t.click(dynamicRowHeightsInput);
while (!await targetItem.exists) {
const currentLastRenderdItemIndex = await listItem.count -1;
const currentLastRenderdItemText = await listItem.nth(currentLastRenderdItemIndex).textContent;
const currentLastRenderdItem = await listItem.withExactText(currentLastRenderdItemText);
await t.hover(currentLastRenderdItem);
}
await t
.hover(targetItem)
.click(targetItem);
await t.debug();
});
To scroll the list container I used the hover action with a last rendered listItem as a target element.

Related

How to work Drag-and-drop for nested array in useFieldArray

I am trying to implement the drag-n-drop feature on a nested array of inputs. I am using react-beautiful-dnd and react-hook-form libraries: useFieldArray hook to manage the form and useController hook for controlled input.
The problem I am facing is when i try to reorder any child input, change happens in the last item only. here is a sandbox.
To reproduce the issue, you need to add at least 2 top-level inputs along with 2 nested inputs for each of those. Next try to reorder the nested inputs of the 1st top-level item. As a result, child inputs of the 2nd item would change their positions, while the initially dragged inputs would revert back to their original position.
I tried to pass ref to nested inputs and used useImperativeHandle hook to expose move method of useFieldArray hook from child component to reorder. But everytime only the last child gets reordered
SOLVED
I was using the same ref for my nested field array to store reorder child function therefore when appending a new nested child field array, it was overriding the reorder function and only moving items belonging to the last nested field array.
To solve this, I had to pass a function instead of ref to the nested fields which creates different ref for each
function Parent () {
const childrenRef = useRef({});
const setReorder = useCallback(
(index, reorderCallback) => {
childrenRef.current[index] = reorderCallback;
},
[childrenRef]
);
...
return (
<Child
setReorder={setReorder}
/>
)
}
function Child ({ setReorder, nestIndex }) {
useEffect(() => {
setReorder(`child-${nestIndex}`, (from, to) => {
move(from, to);
});
}, [nestIndex, setReorder, move]);
}
Here is the working sandbox

How do I prevent unnecessary, repetitive side-effects from my React useEffect hook?

I am having trouble preventing unnecessary side-effects from a useEffect hook linked to a component that gets re-rendered/reused multiple times after being clicked. I want the side-effect to trigger an action once, but it is being triggered for every render.
I'm building a task viewer that displays nested task data. Please see screenshot #1 for reference.
For context, the main display shows the main tasks (see Event Data) and renders a clickable button if the task has sub-events. When this button is clicked, the selected main task is displayed at the top of the hierarchy view (see Event Hierarchy) and its sub-events are displayed below the hierarchy in a separate pane (see Event Details).
Like the main tasks, if these sub-events in 'Event Details' have their own sub-events, they are also rendered with a clickable button. When this sub-event button is clicked, this clicked sub-event is added to the bottom of the hierarchy, where the clicked main task is already displayed in bold. This selected sub-event's sub-events then replace the content in the 'Event Details' pane.
As the user clicks through the nested data, the clicked sub-event is added to the bottom of the hierarchy so that the user has an idea of where he is in the nested data and its sub-events displayed in 'Event Details'. All 'Event Hierarchy' and 'Event Details' data is cleared when the user selects a new main event or selects a new page.
The hierarchy events are held in an array managed via useState and every time another sub-event is clicked, it is added to this array. That's the idea, at least.
#1
My problem is this:
If I place my setHierarchy function inside a useEffect hook with the selectedTask as dependency, it renders the selectedTask in the hierarchy instantaneously, but the button component that triggers setHierarchy is re-rendered for every sub-event being displayed in 'Event Details' (as I want each event to be clickable) and in doing so, it adds that many copies of the event to my hierarchy array. This happens even though I am checking to see if the hierarchy array already contains the selected subevent before adding it. See result in screenshot #2.
I have tried various configurations of checking the array, but I cannot seem to stop it from adding these copies to and subsequently displaying them in the Hierarchy.
If I place the setHierarchy function inside my click handler, only one single event is added, but it executes before the selectedSubEvent has been updated. This means the hierarchy array is empty upon first render and stays one click 'behind' ie. a specific event is only displayed upon the following click event, after the click that selected it.
#2
This is all done inside my ExpandSubEvents button component (see code below) and also managed via a context provider.
I have tried moving the setHierarchy into a separate function, inside a useCallback, and triggering it from both the clickHandler and the useEffect that sets the selectedSubEvent. This did not resolve the issue.
I've also tried useRef to try and link it to the latest state. I'm not sure that's even doable/correct.
What am I doing wrong here? I am fairly new to coding, so any input on this would be much appreciated.
Sidenote: I suspect that my setup is perhaps beyond the intended scope of useContext. Is it? What can I do to make improvements? Is this perhaps in any way responsible for my issue?
Thank you for taking your time to read this far. I appreciate it!
Deon
ExpandSubEvents Component
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
import SubEventContext from '../../store/sub-event-context';
import classes from './ExpandSubEvents.module.css';
const ExpandSubEvents: React.FC<{
id: number;
subEvents: number;
}> = React.memo((props) => {
// Extract context
const subEventCtx = useContext(SubEventContext);
const {
subEvents,
subEventParentId,
selectedSubEvent,
hierarchy,
setSubEventParentId,
setFetchId,
setSelectedSubEvent,
setHierarchy,
} = subEventCtx;
// Get id of event for when it is clicked
const id = React.useMemo(() => props.id, [props.id]);
let eventIds: number[] = useMemo(() => [], []);
if (hierarchy) {
for (const event of hierarchy) {
eventIds.push(event.id);
}
}
// Set CSS classes to style button if it has sub-events
let subEventQuantity = props.subEvents;
let importedClasses = `${classes['sub-event-button']}`;
if (subEventQuantity === 0) {
importedClasses = `${classes['no-sub-events']}`;
}
// Push the event to the Hierarchy display
// NOTE Tried moving the setHierarchy to a separate function, but it did not make a difference
// const triggerHierarchy = useCallback(() => {
// if (!eventIds.includes(id))
// setHierarchy((prevState) => [...prevState, ...selectedSubEvent]);
// }, [eventIds, id, selectedSubEvent, setHierarchy]);
// Respond to subevent button click event
const clickHandler = useCallback(() => {
setSubEventParentId(id);
setFetchId(id);
// This setHierarchy works, but executes before the selectedSubEVent has been updated
// Furthermore, if a new subevent is selected, it checks if the NEW clicked one has been added
// BUT sends the OLD event still in selectedSubEvent to the hierarchy before IT has been updated
// meaning that the check does not stop the same event being added twice
if (!eventIds.includes(id))
setHierarchy((prevState) => [...prevState, ...selectedSubEvent]);
}, [
eventIds,
id,
selectedSubEvent,
setFetchId,
setHierarchy,
setSubEventParentId,
]);
// NOTE Tried useRef to get setHierarchy to use the latest selectedSubEvent
// const subEventRef = useRef<Event[]>([]);
// subEventRef.current = hierarchy;
// Trying to setHierarchy directly from its own useEffect
// useEffect(() => {
// if (!eventIds.includes(id))
// setHierarchy((prevState) => [...prevState, ...selectedSubEvent]);
// }, [eventIds, hierarchy, id, selectedSubEvent, setHierarchy]);
// Filter the event from the subEvent array and set it to selectedSubEvent
useEffect(() => {
setSelectedSubEvent(
subEvents.filter((subEvent) => subEvent.id === subEventParentId)
);
}, [setSelectedSubEvent, subEventParentId, subEvents]);
return (
<button onClick={clickHandler} className={importedClasses}>
{subEventQuantity}
</button>
);
});
export default ExpandSubEvents;

Why does Object.keys(this.refs) not return all keys?

Hi,
so I've redacted some sensitive information from the screen shot, but you can see enough to see my problem.
Now, I'm trying to build the UI for a site that gets data from a weather station.
I'm trying to use react-google-maps' InfoBox, which disables mouse events by default.
It seems that to enable mouse events, you must wait until the DOM is loaded, and then add the event handlers.
react-google-maps' InfoBox fires an onDomReady event (perhaps even upon adding more divs) but seems to never fire an onContentChanged event (I've looked in the node_modules code).
The content I'm putting in the InfoBox is basically a div with a string ref for each type of weather data. Sometimes there comes along a new type of weather data so I want to put that in also, and have the ref be available / usable.
However, immediately after the new divs have been added (and the DOM has been updated to show them), when I try to console log the DOM nodes (the refs refer to the nodes because they are divs and not a custom built component) the latest added ones are undefined.
They do become a div (not undefined) a few renders later.
I've contemplated that this may be because
1) the DOM is not being updated before I'm trying to access the refs, but indeed the UI shows the new divs,
2) string refs are deprecated (React 16.5),
but they work for the divs in comonentDidMount and eventually for new divs in componentDidUpdate,
3) executing the code within the return value of render may be run asynchronously with componentDidMount, but I also tried setTimeout with 3000 ms to the same effect,
4) of something to do with enumerable properties, but getOwnProperties behaves the same way.
In the end I decided I'll console log this.refs and Object.keys(this.refs) within the same few lines of code (shown in the screen shot), and you can see that within one console log statement (where Object.keys was used in the previous line) that while this.refs is an object with 8 keys, the two most recently added refs don't appear in Object.keys(this.refs).
This is probably a super complex interaction between react-google-maps' InfoBox, React's refs, and JavaScript's Object.keys, but it seems like it should be simple and confuses me to a loss.
Can anyone shed some light on why this might be happening??
The code looks something alike:
class SensorInfoWindow extends React.Component {
handleIconClick = () => {
// do stuff here
}
componentDidMount() {
this.addClickHandlers();
}
componentDidUpdate() {
this.addClickHandlers();
}
addClickHandlers = () => {
const keys = Object.keys(this.refs);
for(let i=0; i<keys.length; i++) {
const key = keys[i];
let element = this.refs[key];
if (element !== undefined)
element.addEventListener('click', this.handleIconClick);
}
}
render() {
const { thissensor, allsensors } = this.props;
let divsToAddHandlersTo = [];
const sensorkeys = Object.keys(allsensors);
for (let i=0; i<sensorkeys.length; i++) {
divsToAddHandlersTo.push(
<div
ref={'stringref' + i}
/>
{/* children here, using InfoBox */}
</div>
);
}
return (
<div>
{divsToAddHandlersTo}
</div>
);
}
}
This is, in essence, the component.

Can React.createRef() be created dynamically from an array of state objects?

I have a list of styled radio buttons fixed inside the sidebar that, when clicked, scroll a corresponding component from the main area into view.
Both the radio buttons list and the components inside the main area are mapped from the state array called usedComponents that contains objects of component properties and ids. Let's say the array contains 10 objects and each object looks something like this:
{
id: 1,
group: "header",
componentType: "headerLogo",
componentName: "Header 01",
//...rest of the component properties
}
In order to achieve scroll into view on radio button click I had to create references to components inside the constructor. I manually created references for each component contained inside usedComponents state array and tied them to componentType like this:
this.headerLogo = React.createRef();
this.headerLogoNavigation = React.createRef();
//...etc.
I then passed the reference to each corresponding component and in my selectComponentHandler I set scrollIntoView to call the radio button id which corresponds to the main area component reference.
The selectComponentHandler looks like this:
selectComponentHandler = e => {
const value = Number(e.target.value);
const id = e.target.id; //returns componentType. For example "headerLogo"
this.setState(prevState => {
let selected = prevState.usedComponents
.filter(item => item.id === value)
.shift();
let unSelected = prevState.usedComponents
.filter(item => item.selected === true)
.shift();
if (unSelected) {
unSelected.selected = false;
}
selected.selected = true;
return { unSelected, selected };
});
this[id].current.scrollIntoView({ block: "start", behavior: "smooth" });
};
However, I'm using react-beautiful-dnd in order to be able to add new components by dragging them to the main area, and whenever I add the new component of the type that is already contained inside the usedComponents array, it has the same reference as the old component of the same type. This makes the scrollIntoView always scroll to the component of the same type that is first in line, no matter which one I select.
Is there a way to create references dynamically for all the components inside the usedComponents array, by perhaps mapping them, and update them whenever a new component is inserted. Maybe tie them to ids?
Edit:
I tried mapping references inside the constructor like this:
this.state.usedComponents.map(component => {
const id = component.id.toString();
return (this[id] = React.createRef());
});
It works for the components that are already inside the used Components array, however I still don't know how to update the references when the new object is inserted into the usedComponents array via drag and drop. Newly inserted components basically don't have references.
Like I was mentioning in the comments, I wouldn't go the route of dynamic refs. The scope / size of these refs could grow large and there is a much better way to handle this. Just use the DOM.
When rendering you can just put the components ID on your html element that you want to scroll to
<div id={component.id}> ... </div>
And then when you want to scroll to that element just query the DOM for that element and scroll to it
const elemToScrollTo = document.getElementById(component.id)
if (!!elemToScrollTo) {
elemToScrollTo.scrollIntoView()
}

Puppeteer - how to click on an inner element

I'm using Puppeteer to do e2e tests for an app built with React and Material-UI.
In my login form I'm trying to click on the login button but I get loginBtn.click is not a function - that's because Material UI (version 0.21.0) is wrapping the <RaisedButton> with an extra div so I can only reach the outer element. I need a way to access the inner element and then simulate the click event.
I've tried this:
const loginBtn = await page.$eval(
'.login-form-button',
(element) => element.innerHTML
);
await loginBtn.click();
But it seems like element.innerHTML is just a text and not clickable.
Any ideas how can I get the inner element?
page.$eval selects an element (.login-form-button) and passes that element to the second argument ((element) => element.innerHTML), which will in your case return the HTML of the node as a string. Therefore loginBtn is not a button but a string. To click an element you might want to use page.click.
If you have a more complex term to execute I recommend using page.evaluate. In your case you could use it like this to query the element and click its first child element:
await page.evaluate(() => {
document.querySelector('.login-form-button').firstElementChild.click();
});

Resources