baruchvlz / resq Goto Github PK
View Code? Open in Web Editor NEWReact Element Selector Query (RESQ) - Query React components and children by component name or HTML selector
License: MIT License
React Element Selector Query (RESQ) - Query React components and children by component name or HTML selector
License: MIT License
resq$('Button:first-child')
resq$('Button:nth-child(3)')
resq$('Button:last-child')
Scenario:
// imports
const MyComponent = () => (
<>
My Component
<div>Nested Div</div>
<div>Nested Div</div>
<section>Nested Div</section>
</>
);
const App = () => (
<div>
<MyComponent />
</div>
);
ReactDOM.render(<App />, document.querySelector("#root"));
const myComponentChildren = await resq$$('MyComponent *')
/*
outputs:
[
{
children: Array,
name: String,
node: <div />,
props: Object,
state: Object
},
{
children: Array,
name: String,
node: <div />,
props: Object,
state: Object
},
{
children: Array,
name: String,
node: <section />,
props: Object,
state: Object
}
]
*/
Currently, if we want to select a styled component (for example) we would have to first find out the displayName from the React dev tools and then select it with the proper HoCs wrapped around.
Given this example component
// index.jsx
// imports
export default const Button = styled.a`...`
// wrapping the HoCs
resq$('Styled(Button)', document.getElementById('root'))
// ideally
resq$('Button', document.getElementById('root'))
// in a perfect world, both would work
resq$('Styled(Button)', document.getElementById('root'))
resq$('Button', document.getElementById('root'))
This is not the best solution considering that it can be confusing having to download React dev tools, get used to it, and then find the component, etc.
If we strip these HoCs from the displayName property before matching the selectors, then we wouldn't need to wrap the HoCs around our selector component.
We have a React component named EditableCallRoutingTableDialog that contains within it another of our React components named EditableCallRoutingTable. The relevant snippets showing the code for these components is shown below. We added a call to React.useEffect
in the EditableCallRoutingTable component as follows:
React.useEffect(() => {
console.log(resq$$('EditableCallRoutingTabl*', document.getElementById('root')));
}, []);
in order to see what resq$$
returns for our components and got this output:
r(2) [{…}, {…}]
0:
children: [{…}]
name: "EditableCallRoutingTableDialog"
node: div.MuiDialog-root
props: {announcements: Array(11), routes: Array(2), rows: Array(1), basicRoutingService: false, dialogOpen: true, …}
state: {memoizedState: {…}, baseState: null, baseQueue: null, queue: null, next: {…}}
[[Prototype]]: Object
1:
children: (2) [{…}, {…}]
isFragment: true
name: "EditableCallRoutingTable"
node: []
props: {announcements: Array(11), routes: Array(2), rows: Array(1), basicRoutingService: false, editedCells: Set(0), …}
state: {memoizedState: {…}, baseState: null, baseQueue: null, queue: null, next: {…}}
[[Prototype]]: Object
length: 2
lastIndex: (...)
lastItem: (...)
[[Prototype]]: e
From the output above for EditableCallRoutingTableDialog we see that the node
field is returned as div.MuiDialog-root
which matches what we expected.
However, the node
field for the EditableCallRoutingTable seems to be an empty array when we expected it to be a div
.
If we remove the Snackbar component from the end of the EditableCallRoutingTable code then the node
is returned as:
node: div.MuiPaper-root.MuiCard-root.makeStyles-tableContainer-2595.MuiPaper-elevation1.MuiPaper-rounded
which looks correct.
Are we using your library incorrectly or is this a bug?
We are using:
...
export const EditableCallRoutingTableDialog: React.FC<EditableCallRoutingTableDialogProps> = ({
announcements,
routes,
rows,
basicRoutingService,
dialogOpen,
onRefreshAnnouncementsClicked,
onUploadAnnouncementClicked,
useLazyGetAnnouncementSignedGetUrlQuery,
onSaveClick,
onCancelClick
}) => {
...
return (
<Dialog
PaperProps={{ classes: paperClasses }}
fullScreen
maxWidth={false}
open={!!dialogOpen}
onClose={() => false}
disableEscapeKeyDown
>
<DialogTitle className={classes.dialogTitle} title="Edit" />
<DialogContent className={classes.dialogContent}>
<section className={classes.headerContents}>...</section>
<EditableCallRoutingTable
announcements={announcements}
routes={routes}
rows={rows}
basicRoutingService={basicRoutingService}
editedCells={editedCells}
changedRows={changedRows}
changedRouteOrdinals={changedRouteOrdinals}
setHasChanged={setHasChanged}
onRefreshAnnouncementsClicked={onRefreshAnnouncementsClicked}
onUploadAnnouncementClicked={onUploadAnnouncementClicked}
useLazyGetAnnouncementSignedGetUrlQuery={useLazyGetAnnouncementSignedGetUrlQuery}
/>
</DialogContent>
</Dialog>
);
};
...
export const EditableCallRoutingTable: React.FC<EditableCallRoutingTableProps> = ({
announcements,
routes,
rows,
basicRoutingService,
editedCells,
changedRows,
changedRouteOrdinals,
setHasChanged,
onRefreshAnnouncementsClicked,
onUploadAnnouncementClicked,
useLazyGetAnnouncementSignedGetUrlQuery
}) => {
...
React.useEffect(() => {
console.log(resq$$('EditableCallRoutingTabl*', document.getElementById('root')));
}, []);
return (
<>
<Card className={classes.tableContainer}>
<div className={classes.table}>
<DataTable customStyles={rdtCustomStyles(theme, disabledColumns)} columns={columns} data={data} />
</div>
</Card>
<Snackbar
variant="error"
message={getAnnouncementSignedGetUrlQueryResult.error?.message}
show={!!playingAudio && !!getAnnouncementSignedGetUrlQueryResult.error?.message}
verticalAnchor="top"
onClose={() => setPlayingAudio(undefined)}
/>
</>
);
};
I use @material-ui/icons.
And test it with WebDriverIO's react$.
In resq/utils/findStateNode, element.stateNode of material-ui-icon component is null, element.child.stateNode too.
I found element.child.child.child.child.stateNode is svg.
I think findStateNode function need recursive.
Current implementation of the resq$
and resq$$
commands use only the first creation of the virtual dom. If the vdom ever changes, these changes are not reflected in the tree.
Related to Webdriverio issue 3916.
If react is mounted asynchronously. Using react$ selector throws the error:
javascript error: Could not find the root element of your application
.
I see two cases for mounting react asynchronously:
When running resq on http://todomvc.com/examples/react/#/ it has problems finding the root element. When trying to fetch an element it returns an error saying: Could not find the root element of your application
By utilizing ReactWorkTags we can simplify identifying the type of component. This will allow us to easily identify the name of the component as well as be able to filter out wrappers like Providers and Consumers.
The current bundled version is not useful as it still requires some babel packages. I got a much smaller bundle that assigns to the window object by using webpack and the following config:
const path = require('path')
module.exports = {
mode: 'production',
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'resq.bundle.js',
library: ['window', 'resq'],
},
}
To be able to use it with WebdriverIO it would be great there would be a bundle that assigns itself to the window object. With that I can inject it using the executeScript
command and run queries from the window object, e.g. https://github.com/webdriverio/webdriverio/pull/3797/files#diff-d58e668c0402c2fa779bbf9c90c308a3R5
Currently we just throw an error. We should let the user know that one of the possibilities is an anonymous component along side saying that the component was not found
Hey @baruchvlz ,
thanks for working on this! I looked the code and wanted to share some idea. Maybe I get the time to contribute to this project and make PRs for them.
index.js
- await waitToLoadReact(5000)
: maybe we can make the timeout configurable but 5000ms seams a reasonable defaultgetRootComponent
- do all root elements have the id '#root'
?root
we could let waitToLoadReact
return the root element because it will look for it anyway, no?Hello, I'm playing with your library, and I noticed that the properties created for the interface RESQNode must be wrong. There are no byProps or byState properties. It is possible to bend component to any and then call byProps and it will work, but it would be better if your javascript functions are offered the same typescript types.
const nodes = resq$(name);
const component = (nodes as any).byProps({a: 1});
Would be great if you can fix this, thanks!
The error message is this: TypeError: Argument not optional
The "problem" (for IE11) is that createTreeWalker is called with only one argument.
The error is here:
Line 9 in 59c4c51
The documentation proving my point: https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker
When passing a matcher with multiple keys, .byProps
/.byState
returns nodes that match any one of the given keys.
Example:
const filtered = myComponent.byProps({ prop1: 123, prop2: 'abc' })
console.log(filtered)
/*
{
name: 'MyComponent',
props: {
prop1: 123,
prop2: 'def',
},
// ...
}
*/
.byProps
/.byState
returns nodes that match ALL of the given keys.
Example:
const filtered = myComponent.byProps({ prop1: 123, prop2: 'abc' })
console.log(filtered)
/*
{
name: 'MyComponent',
props: {
prop1: 123,
prop2: 'abc',
},
// ...
}
*/
Failing test case: ooorayray@b502b83
Hi @baruchvlz
The JSX:
import React from 'react';
import { getProducts } from '../products';
class AProduct extends React.Component {
constructor(props) {
super(props);
this.state = {
myName: props.name,
orderCount: 0,
};
}
order() {
this.setState({
orderCount: this.state.orderCount + 1,
});
}
render() {
return (
<div className="product">
<span className="name">{this.state.myName}</span>
<span className="ordered">{this.state.orderCount}</span>
<button className="order" onClick={this.order.bind(this)}>
Order
</button>
</div>
);
}
}
const Products = ({ products }) => (
<React.Fragment>
{products.map((product) => (
<AProduct key={product.id} name={product.name} value="product" />
))}
</React.Fragment>
);
class ProductsContainer extends React.Component {
state = {
products: [],
};
componentDidMount() {
// for now use promises
return getProducts().then((products) => {
this.setState({
products,
});
});
}
render() {
return (
<div className="product-container">
<Products products={this.state.products} />
</div>
);
}
}
export default ProductsContainer;
FETCH Products:
export const getProducts = () => {
console.log('fetch products');
return fetch('http://myapi.com/products')
.then((r) => r.json())
.then((json) => {
console.log('products', json.products);
return json.products;
});
};
The outcome:
Expectation:
As per (Type definition)[https://github.com/baruchvlz/resq#type-definition], node
returns the real HTMLElement so resq$ /resq$$
should consider the node
rather than the RESQ object
Now, surprisingly, for a different react example, everything works as expected.
So, what's really happening for the above example that RESQ is behaving differently?
Recently came across an issue with our react app and webdriver.io returning elements without the underlying node. The node value is null
as shown below:
This seems to point to $resq. More details here:
webdriverio/webdriverio#4129
Any ideas what could be the issue?
The function waitToLoadReact
is resolving even when the global.rootReactElement
is undefined instead of retrying till the timeout.
https://github.com/baruchvlz/resq/blob/master/src/waitToLoadReact.js
Even when the findReactInstance
function returns undefined the promise is resolved.
This issue is provoking abhinaba-ghosh/cypress-react-selector#104 in the cypress-react-selector module.
In our case we have a next.js project.
Repro steps would be just:
cy.visit("/")
cy.waitForReact(1000, `#__next`);
cy.react("Logo"); // Some dummy component mounted in the index page
The bug is reproducible 1 of 10 times in a run of total 100 tests(we just generate the above test 100 times to repro the bug).
To be able to see the "real" component names in the compiled version of my webapp (https://hexed.it) I'm using displayName
, like here:
export const GridItem: React.FunctionComponent<GridItemProps> = (props) => {
// [...]
};
GridItem.displayName = 'GridItem';
While the ReactDevTools is showning the displayName
of the component correctly, resq
is returning the compiled 1-letter-name
.
If I change the getElementName()
function:
Line 16 in 465ab5e
return isFunction(type) ? (type.displayName || type.name) : type
it works fine.
According to an issue reported on the downstream cypress-react-selector package abhinaba-ghosh/cypress-react-selector#320 (comment) there seems to be a problem with the latest 1.10.2 release of resq with React 18. The reason seems to be a change in react component/object key naming.
Can you please support React 18? Thank you very much!
I am not sure if creating separate issue for all of these makes sense, but these are roughly the concerns I have while looking through the code and underlying FiberNode structure -
stateNode
) with the simplified output (being subset).document.querySelector("#outer").querySelector("#inner")
.classA classB
is supported, but definitely not something like classA:last-child
<Optional>
<Optional>
Optional marked items are good to have and can be build on top, but other things can be blocker for some usecases and can't be solved in userland.
When creating a RESQNode
instance, we keep nodes
in memory in order to be able to do resq$('SomeComponent').byProps({ /* ... */ })
.
These nodes should be a private property in the class
When getting the state using getElementState(element.memoizedState)
here in the source code, it seems like only the first useState value is considered instead of gathering all values of useState calls together into an array using the next
field on the memoizedState
object which when not null
, holds the value of the next useState
call
Hi guys, this utility is the most awesome thing I've tried out this week.
Since you seem to be knowledgable of react stuff I just wanted to ask if you know some generaliseable way to trigger a rerender? Because if I find a component with this and change its props the real DOM doesn't get updated and it would be nice to be able to do that. Are there any methods I could call?
Hi @baruchvlz
When I am trying to access a component in a specified DOM space, often getting this error. Note, waitForReact
is completed successfully.
The repository where you can reproduce this bug:
https://github.com/abhinaba-ghosh/cypress-react-chain-queries
Resq version: 1.9.2
Platform: Catalina [10.15.7]
Hi,
I was playing with resq and I have encountered functional differences in different browser. Chrome and Firefox greatly handles the react component identification part. But, not IE11.
I have changed res$$ to resqTT, as IE can't handle those symbols. But, this is not a showstopper.
I have attached the findings for two different browsers for same website:
website under test: https://react-shopping-cart-67954.firebaseapp.com/
Chrome:
you can clearly see that I am getting the element and element count is 1
*IE11:
Now, element size is 0
Any help to mitigate this issue will be a great help. I need to run resq in IE anyway.
This might be a bit tricky but definitely solve able. It would be cool if resq
could fetch the sourcemaps of the bundled react files and allow to fetch the original component names even though the code is minified.
Steps to reproduce
in browser console
resq.resq$$('*', document.getElementById('root')).byProps({name: '5'})
Actual Result
returned 44 elements
Expected Result
one element
WebdriverIO steps to reproduce
browser.url('https://ahfarmer.github.io/calculator/')
const el = browser.react$('*', { props: { name: '5' } })
console.log(el.getTagName(), el.getText())
Actual Result
div 0
AC
+/-
%
÷
7
8
9
x
4
5
6
-
1
2
3
+
0
.
=
Expected Result
div 5
RESQ version: 1.8.0
If root is used while fetching the resq nodes, it is able to identify the child elements. But, if non-root element is used, it is failing to identify the same. The attached screenshots will be more descriptive:
Fetch Commands:
So, in first command it is not able to identify the child elements. Once the root is used, both the child products got identified.
I know the library is built to provide the Node information to the end user by quering React properties. I was just thinking about any possibilities of updating props and states using some crazy commands - setProps or setState. With Enzyme, it can be possible by mounting/shallowing the target component.But currently there is no solution where somebody doesn't have the access to the source code, like in e2e testing or integration testing.
Type error throwed when resq isn't able to find a react component
hi,
I am getting window not defined error while importing the resq module. I am doing nothing fancy, just running:
import { resq$ } from 'resq';
console.log(resq$('MyComponent', null));
npm script: ts-node test.ts
Error thrown:
ReferenceError: window is not defined
at Object.<anonymous> (C:\Dev Room\Projects\node_modules\resq\dist\index.js:1:221)
at Module._compile (internal/modules/cjs/loader.js:701:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
at Module.load (internal/modules/cjs/loader.js:600:32)
at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
at Function.Module._load (internal/modules/cjs/loader.js:531:3)
at Module.require (internal/modules/cjs/loader.js:637:17)
at require (internal/modules/cjs/helpers.js:22:18)
at Object.<anonymous> (C:\Dev Room\Projects\test.ts:3:16)
at Module._compile (internal/modules/cjs/loader.js:701:30)
Add type definitions in a index.d.ts
file in root directory
React allows for the state to be any type (number, string, object, etc). Current implementation of byState
only supports objects.
When calling window.resq.resq$$
on a page that doesn't entail a React app the call returns a string saying Could not find the root element of your application
where it actually should throw an error.
Both filters (byProps
and byState
) have an exact
flag which will deep evaluate the objects and return true only if all values match.
This information is missing from the documentation.
Simple github page for a nicer looking documentation.
PR: #68
React.Fragment
and those using HTML elements as wrappersOptional
try/catch
this way we don't need @babel/runtime
and we can reduce the size of the library by 40% (bundlephobia)A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.