2

My React app has an Item component that should redirect users to the App component by clicking a button inside a react-router-dom Link component. I've followed lots of tutorials that have given clear examples of how to implement Link inside components that don't pass props. My guts tell me that my Item component props might be causing the Cannot destructure property 'basename' of 'React__namespace.useContext(...)' as it is null.] error because running tests on Jest are only possible when Link is not present in the code.

What I tried

  • Moved the RouteSwitch code to index.js
  • Moved the scripts that live in the components to src
  • Double-checked that I followed the tutorials correctly
  • Saw if this reactjs - Uncaught TypeError StackOverFlow answer is related to my problem

Can anybody point out what I'm missing here please?

item.js

import { Link } from "react-router-dom";

const Item = ({info},{addCart}) => {

  return(
    <>
      <Link to="/" ><button>X</button></Link>
      <img src={info.pic} alt={info.title}></img>
      <h1>{info.title}</h1>
      <p>{info.description}</p>
      <button onClick={()=>{addCart(info)}}>Add</button>
    </>
  )
};

export default Item;

routeswitch.js

import React from "react";
import { BrowserRouter,Routes,Route } from "react-router-dom";
import App from "./App"

const RouteSwitch = () =>{
  return(
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App/>}/>
      </Routes>
    </BrowserRouter>
  );
}

export default RouteSwitch;

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import RouteSwitch from "./routeswitch"
import './index.css';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouteSwitch/>
  </React.StrictMode>
);

item.test.js

import React from 'react';
import { render, cleanup,screen, getByAltText} from '@testing-library/react';
import '@testing-library/jest-dom'
import userEvent from '@testing-library/user-event';
import Item from './item';

afterEach(cleanup);
const details = {pic: `a pic`, title:`a title`, description:`a description`}
const addCartMock =jest.fn(); 

test(`Content renders`,()=>{
  render(<Item info={details} addCart={addCartMock}/>);

  const image = screen.getByAltText(`${details.title}`);
  expect(image).toBeInTheDocument();
  expect(image).toHaveAttribute(`src`,`${details.pic}`);
  expect(screen.getByRole(`heading`, {name:details.title})).toBeInTheDocument();
  expect(screen.getByText(`${details.description}`)).toBeInTheDocument();
  expect(screen.getByRole(`button`, {name: `Add`})).toBeInTheDocument();
});

test(`Add button click event`,()=>{
  render(<Item info={details} addCart={addCartMock(details)}/>);

  userEvent.click(screen.getByRole(`button`), {name: `Add`});
  expect(addCartMock).toHaveBeenCalledWith(details);
});
wavesinaroom
  • 105
  • 8
  • Does it happen only in tests? – Konrad Apr 11 '23 at 23:35
  • 1
    Typo in the `Item` component accessing the props, `const Item = ({info},{addCart}) => {` should be `const Item = ({info, addCart}) => {`... there is only one props object to destructure properties from. Another typo in the test, `addCart={addCartMock(details)}` immediately calls the function, you probably want `addCart={() => addCartMock(details)}`. Other than this it is unclear what any `Link` component has to do with anything in the post. – Drew Reese Apr 11 '23 at 23:36
  • I wondered that as well @Konrad and thanks to Drew Reese's answer below, I could confirm that the bug was actually the test. Thanks for suggesting that anyway! – wavesinaroom Apr 12 '23 at 20:20
  • 1
    Thanks @DrewReese! Definitely I didn't write the props in the right way. In fact your comment made me realize that I had a different idea of how props worked in my head. Glad that happened to get better at props. BTW when I put the `addCartMock` inside an arrow function my `expect(addCartMock).toHaveBeenCalledWith(details)` stopped working. I'm gonna check what's going on there more in depth – wavesinaroom Apr 12 '23 at 20:26
  • Oh @DrewReese! Link is aimed to be a button (X) that takes you back to the store main page. – wavesinaroom Apr 12 '23 at 20:38
  • 1
    Last thing `addCart={()=>addCartMock(details)}` now lives in the test code to void immediate function call. I had to change its test into `async` to be able to "click" on the button with `await userEvent.click(screen.queryByText(`Add`));` – wavesinaroom Apr 12 '23 at 20:43

2 Answers2

2

The issue with the Link in the unit tests is that it requires a routing context to be provided to it. Import the MemoryRouter and render the component into it so the Link component has a routing context available to it.

Example:

import React from 'react';
import { render, cleanup, screen, getByAltText } from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import Item from './item';

afterEach(cleanup);

const details = {
  pic: "a pic",
  title: "a title",
  description: "a description",
};
const addCartMock = jest.fn(); 

test("Content renders", () => {
  render(
    <MemoryRouter>
      <Item info={details} addCart={addCartMock} />
    </MemoryRouter>
  );

  const image = screen.getByAltText(`${details.title}`);
  expect(image).toBeInTheDocument();
  expect(image).toHaveAttribute("src",`${details.pic}`);
  expect(screen.getByRole("heading", { name: details.title })).toBeInTheDocument();
  expect(screen.getByText(`${details.description}`)).toBeInTheDocument();
  expect(screen.getByRole("button", { name: `Add` })).toBeInTheDocument();
});

test("Add button click event", () => {
  render(
    <MemoryRouter>
      <Item info={details} addCart={() => addCartMock(details)} />
    </MemoryRouter>
  );

  userEvent.click(screen.getByRole("button"), { name: "Add" });
  expect(addCartMock).toHaveBeenCalledWith(details);
});

You've also an issue accessing the props object in the Item component. There is only a single props object passed to React components and all props are destructured from it.

const Item = ({ info, addCart }) => {
  return (
    <>
      <Link to="/"><button>X</button></Link>
      <img src={info.pic} alt={info.title} />
      <h1>{info.title}</h1>
      <p>{info.description}</p>
      <button onClick={()=>{addCart(info)}}>Add</button>
    </>
  );
};
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thanks a lot Drew! I was hesitant to include the test code in the question but deep inside I was aware that there might've been an issue with the test rather than the code. Learned today that test need also a context which makes sense in order to simulate what I'm rendering. – wavesinaroom Apr 12 '23 at 20:28
0

Not sure if it's what is causing the challenge, but in your item.js you are destructuring the props wrongly. you can either desructure it directly, or take the props and use dot notations to get its children:

const Item = ({info, addCart}) => {

  return(
    <>
      // rest of your code
    </>
  )
};

OR

const Item = (props) => {

  return(
    <>
      <Link to="/" ><button>X</button></Link>
      <img src={props.info.pic} alt={props.info.title}></img>
      <h1>{props.info.title}</h1>
      <p>{props.info.description}</p>
      <button onClick={()=>{props.addCart(info)}}>Add</button>
    </>
  )
};

OR

const Item = (props) => {
  const {info, addCart} = props
  return(
    <>
      <Link to="/" ><button>X</button></Link>
      <img src={info.pic} alt={info.title}></img>
      <h1>{info.title}</h1>
      <p>{info.description}</p>
      <button onClick={()=>{addCart(info)}}>Add</button>
    </>
  )
};
Izebeafe
  • 182
  • 1
  • 4
  • Thanks Izebeafe! Your contribution is not the answer this time but as I commented above in my question section comments. I realized I had a wrong idea of how props worked so it was good to get your comment and Drew's to noticed that there was something else to be improved in my code. – wavesinaroom Apr 12 '23 at 20:31