11

When I write my Cypress e2e tests for my Angular application, I often use the visit() command like this:

.visit('/foo/bar')

This get's the job done, i.e. Cypress navigates to /foo/bar, but the entire application reloads. This is very slow, and does not mimic actual user behaviour.

Is it possible to navigate/visit the Angular application, without full page reloads?

I did try:

cy.window().then((win) => {
   win.history.pushState({}, '', '/foo/bar')
})

But angular does not react to this.

DauleDK
  • 3,313
  • 11
  • 55
  • 98
  • There is a blog post explaining how to interact with AngularJS from Cypress: https://www.cypress.io/blog/2017/11/15/Control-Angular-Application-From-E2E-Tests It's for 1.x, but maybe some of the concepts are still relevant. – Zach Bloomquist Mar 21 '19 at 15:06
  • 1
    A workaround is to actually click the navigation buttons in your menu. – totymedli Jun 27 '19 at 01:37
  • Hi @totymedli - sure that clicking the buttons is a workaround, but not very good for my teams usecase. I added an answer with the solution we are using now. – DauleDK Jun 27 '19 at 05:36

4 Answers4

8

I solved this by adding a custom cypress command that calls a method on the Angular applications app.component.ts. The solution look like this Updated to Ivy:

app.component.ts

export class AppComponent {
    constructor(
        private router: Router,
        private ngZone: NgZone,
    ) {}

    // Method Cypress will call
    public navigateByUrl(url: string) {
        this.ngZone.run(() => {
            this.router.navigateByUrl(url);
        });
    }
}

cypress/support/commands.ts

// add new command to the existing Cypress interface
declare global {
    namespace Cypress {
        interface Chainable {
            visitAngular: (url: string) => Chainable<Window>;
        }
    }
}

// Custom function
export function visitAngular(url: string) {
    cy.get('body').then($body => {
        try {
            const el = $body.find('app-root')[0];
            const win = el.ownerDocument.defaultView;
            const componentInstance = win.ng.getComponent(el);
            cy.log(`Angular nav to '${url}' `);
            componentInstance.navigateByUrl(url);
            cy.url().should('contain', url);
        } catch (error) {
            cy.log(`Cypress nav to '${url}' `);
            cy.visit(url);
        }
    });
}

Cypress.Commands.add('visitAngular', visitAngular);

cypress/support/index.d.ts

interface Window {
    ng: {
        getComponent: (element: any) => any;
    };
}

We have used this for 2 months now, and it works great in local development, speeding up test executions with x3. But in CI it's another story.

DauleDK
  • 3,313
  • 11
  • 55
  • 98
  • Hi @DauleDK, is there a problem with this approach in CI/CD pipeline? – Chandermani Sep 21 '20 at 11:07
  • 1
    The "problem" here is that different spec files are isolated when running CI. So this approach does not really speed things up. We actually moved away from the above approach, and just acknowledged that Cypress is build for "big" it specs, and hence navigation only happens in one time anyways (per spec). – DauleDK Sep 21 '20 at 11:11
6

You can make it work in a CI enviroment registering a global function an call that instead of the angular component:

app.component.ts

export class AppComponent {
    constructor(
        private router: Router,
        private ngZone: NgZone,
    ) {
        // Method Cypress will call
        if ((window as any).Cypress) {
            (window as any).cypressNavigateByUrl = (url: string) => this.cypressNavigateByUrl(url);
        }
    }

    public cypressNavigateByUrl(url: string) {
        this.ngZone.run(() => {
            this.router.navigateByUrl(url);
        });
    }
}

cypress/support/commands.ts

Cypress.Commands.add('visitAngular', (url) => {
    cy.window().then((win) => {
        win.cypressNavigateByUrl(url);
    });
});
Diego Busacca
  • 61
  • 1
  • 3
  • 1
    Hi @Diego - this is an awesome tip. Thanks. – DauleDK Nov 18 '19 at 14:53
  • not only is this awesome and should be part of the official documentation, but it solves another problem: often when `cy.stub()`ing some service, the injected instance is lost on each `cy.visit('/')` so with this way of navigating, we can finally mock root-injected services without jumping through the `window.then()` hoops that used to be necessary. – Cee McSharpface Apr 24 '23 at 15:32
2

If you are using hash-based routing, you could manually manipulate the URL:

cy.window().then(win => win.location.hash = "/foo/bar")
ZachB
  • 13,051
  • 4
  • 61
  • 89
0

For NuxtJs, this is my solution, I create a specific command. Dont forget to put "experimentalSessionSupport": false in cypress.json

cypress/support/commands.ts

// For typescript
declare global {
  namespace Cypress {
    interface Chainable {
      push(path: string): Chainable<void>
    }
  }
}


Cypress.Commands.add('push', (path: string) => {
  cy.url().then((url) => {
    if (url.includes('blank')) return cy.visit(path) // If your application is not loaded 
    cy.window()
      .its('$nuxt')
      .then((nuxt) => nuxt.$router.push(path))
    return cy.url().should('include', path) // Wait until page change
  })
})

Usage

describe('My e2e test', () => {
  it('Should do something', () => {
    cy.push('/') // Use push instead of visit
  })
})

Drawback

Be careful, we should improve this command to avoid keeping the context between test. Maybe reset store, or init page etc.

Thibaut Mottet
  • 414
  • 4
  • 15