#cypress/react : How to launch a component under a given path / URL? - reactjs

I have a React component 'Foo' that uses react-router to decide which child to mount.
Let's say this component has 3 submodules: A, B, C.
In a 'normal' cypress test I would use cy.visit('www.test.com/foo/subModuleA') and I would get my site and it would display my Foo-component, which would render the module 'A'.
My component test however mounts this component directly, and cy.visit(...) doesn't work.
cy.visit from a component spec is not allowed
see https://github.com/bahmutov/#cypress/react/issues/286
(Sadly this link leads to nowhere)
For the time being I wrote myself a helper-component:
function PathEnforcer (props: React.PropsWithChildren<{ path: string }>) {
const history = useHistory()
useEffect(() => {
history.push(props.path)
}, [])
return <>{props.children}</>
}
This simply switches the path as soon as it mounts.
Now I can do something like this in my test:
mount(
<HashRouter>
<PathEnforcer path='subModuleA'>
<Foo />
</PathEnforcer>
<HashRouter>
)
And my component is displaying the right module.
This is a workaround that I can work with, but it can hardly be a long term best practice.
How can I configure the tests / execute a command that directly mounts this component given the right path?

I was running into the same issue and this question came up. With react-router v6 this works:
mount (
<MemoryRouter initialEntries={['/foo']}>
<Routes>
<Route path={'/:segment'} element={<YourComponent />}>
</Routes>
</MemoryRouter>
)

Related

React Router - Placing components by iteration not working

Given the following code:
const AdmNews = React.lazy(() => import('./components/adm/AdmNews'));
const AdmPeople = React.lazy(() => import('./components/adm/AdmPeople'));
const AdminRoutes = [
{path:"/route/admin/news",component:<AdmNews/>},
{path:"/route/admin/people",component:<AdmPeople/>},
]
{AdminRoutes.map((el)=>{
return <Route path={el.path}
element={
<React.Suspense fallback={<>...</>}>
{el.component}
</React.Suspense>
}/>
})}
I'm trying to iterate over objects in order to create the routes in react, and when I click on the matching buttons, they understand the path property, and it reacts accordingly, however, the component is not loading. Is is right to have the component inside the object as show above: component:<AdmNews/>? (No errors are shown.)
PD. If I place the code manually without iteration, it works, so I suspect of component:<AdmNews/> not being the right approach.

React router v6 testing library navigate relative fails

In my application i use relative navigation with react router v6 (we are currently migrating) and it works fine.
When i try to do some tests using Jest and testing-library the relative navigation fails and always resolve with the pathname '/'.
I use this wrapper to render my component under test to add it a Router.
const WrapTestComponentInRouter = (TestComponent, initialRoute = '/', history =['/']) => {
function WrappedComponent(props) {
return (
<MemoryRouter initialEntries={history}>
<Routes>
<Route path={initialRoute} element={<TestComponent {...props} />} />
<Route path="*" element={<Location />} />
</Routes>
</MemoryRouter>
);
}
return WrappedComponent;
};
function ComponentToTest() {
const location = useLocation();
console.log(location);
const path = useResolvedPath('..');
console.log(path);
return <button onClick={() => navigate('..')}>navigate</div>;
}
I use the hook useResolvedPath to compute the navigation result and log it. In my application the relative path computed is correct but in my test it is always equals to '/' whatever the current location is.
Does someone has encounter and solved this problem?
EDIT
Ok, i understand what is the problem. The useResolvedPath is using the routingContext to compute the relative route so if we use '..' it pop to the previous route in the route tree.
With that, if i want to test my navigation i need to reproduce my route tree. Another solution is to use the real relative path by using the method resolvePath('..', location)
The resolvePath compute relative path from a path
resolvePath('..', '/user/project') ---> '/user'
the navigate function also use the RoutingContext so if i want to do a relative from the current path i have to do
const location = useLocation();
navigate(resolvePath('..', location.pathname));

Why is my custom hook not re-initialised on path change?

I am using ReactRouter to route the application (BrowserRouter at the top level) with a Switch that includes all the routes.
In my use-case I want to be able to handle paths that include the path-parameters (bedId) and navigate between different sub-paths (i.e. /beds/:bedId/, /beds/:bedId/info/) as well asa case where the path is (/beds/-).
I also want to be able to direct user to a different "bed" while they are already on some bed, so /beds/bed1/info -> /beds/bed2, and so on...
My BedView component is responsible for routing within that /beds/:bedId path like so:
// App.jsx (fragment)
<Switch>
<Route
path="/"
exact
render={() => (<Redirect to="/beds/-"/>)}
/>
<Route
path="/beds/-"
exact
component={SomeOtherComponent}
/>
<Route
path="/beds/:bedId"
component={BedView}
/>
</Switch>
The problem occurs when I try to use a hook that relies on the current path-parameter to fetch the latest data (i.e. call to /beds/bed1 will result in a call to http://myapi.com/beds/bed1/timeseries). The useLatestData hook is called from the BedView component, which look like so:
// BedView.jsx (fragment)
export default function BedView(props) {
const {bedId} = props.match.params;
let {path, url} = useRouteMatch('/beds/:bedId');
const bedData = useLatestData({
path: `/beds/${bedId}/timeseries`,
checksumPath: `/checksum/timeseries`,
refresh: false
});```
if(!bedData){
return <Loading/>;
}
return (
<Switch>
<Route exact path={path}>
<Redirect to={`${url}/info`}/>
</Route>
<Route exact path={`${path}/info`} >
<SomeComponent info={bedData.info} />
</Route>
</Switch>
}
...and the useLatestData hook is available here.
The problem is the fact that upon redirecting from /beds/bed1/info to /beds/bed2/info, the hook does not update its props, even though the BedView component seems to be re-rendering. I have created a version of the hook that 'patches' the problem by adding an useEffect hook in my custom hook, to detect the change in path (as supplied in the function arguments) and set data to null, but then the behaviour changes on the BedView.jsx's end - making the following check fail:
if(!bedData){
return <Loading/>;
}
I'm not entirely sure which part is the culprit here and any guidance would be much appreciated! As far as I'm aware, the hook is no re-initialised because the path change still results in the same component. There is also one caveat, once I change the BrowserRouter to include the forceRefresh flag, everything works fine. Naturally, I don't want my page to refresh with every redirect though...
try this:
const {bedId} = props.match.params;
let {path, url} = props.match;

React. How to redirect early in a component lifecycle

This seems so simple, but I am new to react and trying different approaches and nothing is working for me. (BTW I am a regular contributor but working on my clients machine and can't remember my so password.)
The version of react router is 4.0, and state is stored using redux.
The scenario is that we are changing the order of routing in our application and would like to redirect any users that have Urls with the old structure to the new Url structure. I have tried the following (also note that I have "scrubbed" the names of the page, function calls and variables):
There is a trigger component for the section I need to direct from, with routing set up like this:
<Route path='page/:pageGuidGuid' component={PageTrigger}> </Route>
In the trigger component, ComponentWillMount makes a request that returns a 404 if the link is from the previous component, although it does redirect to the correct route. I am checking for and triggering the redirect in getInitialState, but the component keeps going through the lifecycle and ComponentWillMount is called, resulting in a 404 and then redirects to the correct page.
const PageTrigger = connectToStores(React.createClass({
getInitialState() {
this.checkForRedirect();
return {};
},
componentWillMount() {
if (this.props.params.guid) {
this.setCaseByGuid(this.props.params.guid);
}
},
checkFrorRedirect() {
/* logic to determine if it should redirect */
browserHistory.push(redirectUrl);
}
}
I have also tried creating a custom route...
<CaseRedirectRoute path='(cases/:caseGuid)' component={CaseTrigger}>
</CaseRedirectRoute>
And in a separate module (based off a login sample)
const CaseRedirectRoute = ({ component: Component, ...rest }) => (
<Route
{...rest} render={props => (
checkForCaseRedirect(...props) ? (
<Redirect to={{
pathname: getCaseUrlRedirectUrl(...props),
state: { from: props.location }
}} />
) : (
<Component {...props} />
)
)} />
);
I have WithRouter imported from react router. When I try to run this I am getting an error inside the react framework:
Uncaught Type Error: Cannot read property 'createRouteFromReactElement' of undefined
at RouteUtils.js:68
at forEachSingleChild (ReactChildren.js:52)
at traverseAllChildrenImpl (traverseAllChildren.js:98)
at traverseAllChildrenImpl (traverseAllChildren.js:114)
at traverseAllChildren (traverseAllChildren.js:186)
at Object.forEachChildren [as forEach] (ReactChildren.js:70)
at createRoutesFromReactChildren (RouteUtils.js:65)
at Function.createRouteFromReactElement (RouteUtils.js:35)
at RouteUtils.js:69
at forEachSingleChild (ReactChildren.js:52)
I've tried redirecting from app.js and page.js, but the PageTrigger is the first component having state set. I either need to figure out how to stop execution after the redirect, or figure out why my custom route keeps blowing up. Any help would be appreciated.
I'm not sure about your setup, but I would implement this redirect as this:
<Route path='oldurl/:p1/:p2' render={
routeProps => (
<Redirect to={'/newUrlHere/'+routeProps.match.params.p1} />
)
} />
So, if the route matches (you may specify exact match if necessary), when the Redirect "renders" it stops rendering and should start the rendering from the new URL. You may build your URL in the Redirect.to property as you wish

Get props (or global variables)

I'm attempting to set some global variables on my app which I want to be available to all components. Lets say for example that I want a 'language' and a 'status' property to be passed to each component. This property won't be rendered to the page, instead it will be added to the props for each component, this will be so I can check for that variable when each component loads and output the appropriate styles and languages.
I was hoping it would be something simple like adding props to the router, however no matter what I try, the props come back as 'undefined' on my child components (only the main layoutWrapper component gets the props). Here is how it looks so far:
//app.js
var LayoutWrapper = React.createClass({
render: function () {
return (
<Layout status="available" />
);
}
});
ReactDOM.render(
<Router history={hashHistory}>
<Route path="/" language="en-gb" component={LayoutWrapper}>
<IndexRoute component={Index}></IndexRoute>
</Route>
</Router>,
app);
When handling global level state data, it's recommended you use some kind of state framework like Flux. I'd recommend Redux as it does a great job of reducing boilerplate code to easily pass down app state to any connected component (and subsequently subscribe these components to any changes in the state).
What you are doing fails because there's no consistent way of creating "global" props; you could use the context variable but this is an unstable feature that is not recommended for production use. Otherwise, you have to manually pass down your props from parent to child explicitly.

Resources