Cypress: History navigate method

Created on 5 May 2016  路  11Comments  路  Source: cypress-io/cypress

Description

cypress.visit reset state between pages, but if your application is isomorphic (universal), you should test application in two modes (prerendered and SPA). It's painful if your work with stubbed API, because cypress can stub only xhr requests. In this situation, i create blank page that contains only assets, wait until application will be ready for SPA mode, and navigate to test target page.

In this situation will be nice if cypress expose cy.navigate method that just write record in history.

Most helpful comment

Most of routing systems, e.g. stock Angular routing, is based on HistoryAPI feature. Clicking on link a[href] is captured and translated to pushState of HistoryAPI.

So, URL of page changes without page reloading. It's very usefull for SPA.

cy.visit is the functional equivalent to opening a new tab in your browser and typing in a fresh url

Opening new tab is not common way to use SPA. Navigation in SPA is always programatic.

I think, it would be great, if you can extend visit with option like state:true that will be translated to pushState if a "previous" page has already opened.

All 11 comments

Okay let me clarify a few things because I'm not 100% certain what the problem is or what the solution is.

cy.visit reset state between pages

cy.visit does not actually reset any state. cy.visit is the functional equivalent to opening a new tab in your browser and typing in a fresh url. If you already have Cookies or Local Storage then those will apply. However Cypress does reset state between tests, which is why you may be mixing that up with the cy.visit command.

It's painful if your work with stubbed API, because cypress can stub only xhr requests.

I'm not sure what you mean here. How are XHR's problematic?

i create blank page that contains only assets, wait until application will be ready for SPA mode, and navigate to test target page.

Also not sure what you mean.

I do understand that while you can do cy.visit, and cy.go there isn't an API for partially changing the URL such as changing the pathname or hash. However this is easily achievable with regular Javascript that you can do right now.

Remember that in Cypress you have access to everything on your page. It runs in the same run loop as your application code.

So for instance if you wanted to update the pathname or hash of the current URL:

cy
  .visit("http://localhost:8080/app/#/some/hash/page")
  .window().then(function(win){
    // change the hash without causing
    // a full page refresh
    win.location.hash = "/users/1"
  })
  .url().should("eq", "http://localhost:8080/app/#/users/1") // => true

If you can clarify a few points we can see if there are some new cy commands we need to add to better handle your use case.

Also feel free to come into our Gitter channel and we can talk about it live.

I'm not sure what you mean here. How are XHR's problematic?

At first (server) render in isomorphic app all request made node.js server by http module, no xhr here, and you can't stub it.

So for instance if you wanted to update the pathname or hash of the current URL

https://developer.mozilla.org/ru/docs/Web/API/History_API
You can check pushState/replaceState methods. Cypress don't have behaviors for it.

Okay I think I understand this a little bit better. In your case since the first page load is rendered entirely by the server you're saying you have no ability to mock anything that is sent down, since its a complete HTML page.

We have some new commands coming down the pipeline to help with this, cy.msg and cy.exec will allow you to communicate directly with your backend prior to navigating in the browser. This would enable you to seed the database or mock things out in your backend.

However I'm still confused as to how pushState and replaceState would affect anything.

Could you write your tests (even pseudo code) to show us what you _wish_ you could do. Hopefully this can show us what you'd like to accomplish.

Most of routing systems, e.g. stock Angular routing, is based on HistoryAPI feature. Clicking on link a[href] is captured and translated to pushState of HistoryAPI.

So, URL of page changes without page reloading. It's very usefull for SPA.

cy.visit is the functional equivalent to opening a new tab in your browser and typing in a fresh url

Opening new tab is not common way to use SPA. Navigation in SPA is always programatic.

I think, it would be great, if you can extend visit with option like state:true that will be translated to pushState if a "previous" page has already opened.

Hey @KhodeN, could you open a new issue for this explaining your use case? This is a rather old issue that was closed due to it being stale. We'd like to see what you're trying to achieve today.

@KhodeN cy.visit is really just about typing the URL into the browser and then the browser "does what its programmed to do".

I don't really see any inherent advantage to making the visit use pushState instead of doing a real navigation because it would only affect things when using multiple cy.visit in a single test.

If you want to expose this, you could just do it programmatically via your application, or even just with cy.window() by using the pushState API's directly.

In the case of cy.visit there would be nothing to "listen to" to know when the page is done. Things line onBeforeLoad and onLoad would be meaningless in that context.

Yeah thinking about this more... you could just use the API's directly via cy.window() and we wouldn't have to do anything since there's not really any added benefit that cy.visit could offer you.

袨泻, I agree, it is framework specific feature and could be done with custom code.

For example, in Angular (6) I have done this way:

Cypress.Commands.add('navigate', (...args) => {
  return cy.window()
    .then(w => {
      w._cy_navigate(...args);
    });
});

Cypress.Commands.add('navigateByUrl', (url) => {
  return cy.navigate([url]);
});

And application code to support this:

export class AppComponent {
  constructor(router: Router, ngZone: NgZone) {
    (<any>window)._cy_navigate = (...args) => {
      ngZone.run(() => {
        router.navigate(...args);
      });
    };
  }
}

It gives significant performance improvements, so almost every page loads in milliseconds, not in seconds (with full loading and initialisation). Yes, it is quite inconvenient for pure testing, but time is money)

I think the big reason is speed. In a next.js app, cy.visit can be very slow because its being SSRd, and dev compilation of SSR pages is quite slow. So a method to invoke pushstate would be very helpful.

But it does seem framework-specific as previously mentioned...


@KhodeN Thanks, your approach works well. Big speed improvement.

Here is the next.js approach:

Cypress.Commands.add('navigate', (...args) => {
  return cy.window().then(w => {
    w.next.router.push(...args)
  })
})

Cypress.Commands.add('navigateByUrl', url => {
  return cy.navigate(url)
})

It seems though that there is a problem with the waiting. cy.visit waits for page navigate event and waits. This approach does not. @brian-mann What is the best way to implement this?

@vjpr This is the original visit implementation, you can take a look at. We listen to the 'load' event before resolving. https://github.com/cypress-io/cypress/blob/develop/packages/driver/src/cy/commands/navigation.coffee#L458

@vjpr Cypress is just javascript - you have native access to everything. You don't ever need Cypress to create special commands in order to interact with native JS API's.

For instance... if you wanted to invoke pushState...

cy.window().then((win) => {
  win.history.pushState(...)
})

// or even more terse

cy.window().its('history').invoke('pushState', ...)

Nothing special is needed. You can interact with anything you want natively.

I solved this in my own app which uses react-router. You can see my solution here: https://github.com/cypress-io/cypress/issues/3120

Was this page helpful?
0 / 5 - 0 ratings