cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.72k stars 3.16k forks source link

Unable to get URL in Component Testing (@cypress/react) #21384

Closed danr-za closed 1 year ago

danr-za commented 2 years ago

Current behavior

I have a component that uses react-router for navigation and I am unable to test the URL. Currently, it always returns the test file asset path. Not sure this one is 100% related - https://github.com/cypress-io/cypress/issues/15106

Desired behavior

Should be able to get router's URL

Test code to reproduce

it('should navigate to root', () => {
  mount(<Navigation fallback="/" />)
  cy.findByRole('figure').click()
  cy.url().should('equal', '/')
})

Cypress Version

9.6.0

Other

@cypress/react - 5.12.4

mjhenkes commented 2 years ago

Hi @danr-za, Thanks for logging this. We are tantalizingly close to releasing the new component testing workflow with Cypress 10.0. After the 10.0 has been released we will revisit your issue and determine if it is still relevant post release.

ZachJW34 commented 1 year ago

CT tests do not accommodate route changes, that is an E2E concern. You should be able to stub the router, please check out our docs on the subject.

craigmiller160 commented 1 year ago

I just found this issue. I disagree with the position about testing URLs in component tests. I've been very excited by this feautre, I'm mainly using it for integration tests, ie testing the whole FE part of the app with intercepted endpoints. Long term I want to extend this to make the test code share-able between integration and e2e tests, but I'm not there yet.

Anyway, my point is that there is significant value in being able to see how the URL changes via SPA-router operations in component tests. I can confirm that the router itself works perfectly (I'm using a React app), I'm able to click on links and change the page, however I cannot validate the URL itself.

I realize this is a brand new feature (ie component testing) and I must comment the cypress team for an excellent job with it. Despite its "beta" status I have had virtually no issues using it. However, I strongly encourage you to continue expanding the feature set to add things like URL testing to it. Thank you.

ls-andrew-borstein commented 1 year ago

I've found a pretty decent workaround in the meantime, which is a similar approach to how react-router does their own internal unit testing. It's also similar to how Kent Dodds tests custom react hooks.

Create a component that renders values from react router (e.g. the path, params, etc) to the DOM, then assert about those values in the DOM.

And if you don't want the values to be visible in tests (to avoid messing with the UI or screenshots) but still accessible to cypress, you can do something like

/**
 * Renders invisible search params. Assert about the URL in component tests where location info is
 * not accessible to Cypress by default. This is useful for components that rely on react-router,
 * e.g. those that use searchParams internally. 
 */
export function RenderSearchParams() {
  const [searchParams] = useSearchParams();
  const paramsObj = Object.fromEntries(searchParams.entries());

  return (
    <div id="search-params">
      {Object.keys(paramsObj).map((key) => (
        <span id={`${key}:${paramsObj[key]}`} />
      ))}
    </div>
  );
}

and then in your tests

// Mount the component inside a MemoryRouter with a specific URL
cy.mount(
  <MemoryRouter initialEntries={['/?foo=bar&biz=baz']}>
    <RenderSearchParams />
    <SomeComponent />
  </MemoryRouter>
)

// Assert about the URL
cy.get('#search-params').within(() => {
  cy.get('#foo:bar')
  cy.get('#biz:baz')
});

or whatever type of selectors you want, so long as the DOM elements are visually hidden but still rendered in the DOM. I'm actually using data-testid attributes in conjunction with @testing-library matchers.

You can extract this into a custom command as well, e.g.

Cypress.Commands.add('mountRouterComponent', (component, route) => {
  const wrapped = (
    <MemoryRouter initialEntries={[route]}>
      <RenderSearchParams />
      {component}
    </MemoryRouter>
  );

  return cy.mount(wrapped);
});

and then use it like

cy.mountRouterComponent(<SomeComponent />, '/?foo=bar&biz=baz')

It would be nice to natively get info about the URL, but this seems like an OK workaround.

muratkeremozcan commented 1 year ago

Here's a recipe

https://github.com/muratkeremozcan/cypress-react-component-test-examples/blob/master/cypress/component/hooks/kyle-wds/react-router-useParams-component-test/react-router-test-useParams.cy.tsx