Cypress with MeteorJS: user does not stay logged in

Im trying to write E2E tests for my app, but first a user needs to be authenticated. This should not be a problem. Only a form needs to filled out and the login button should be pressed. As you can see in the image attached the user gets authenticated and can run fine through the first tests. However, the user gets logged out by itself at some point during the execution of the test functions.

When doing the tests manually the user never gets logged out automatically. I literally have no clue why this behaviour is happening.

Here are my tests:

 describe('RouteCard', function() {
  it('Renders Routes page', function() {
    cy.visit('http://localhost:3000/routes')

    cy.contains('Public Routes')
  })

  it('Can login', function() {
    const testUsername = 'debtdrie'
    const testPassword = 'absenceearly-morning'

    cy.get('#loginform').within(() => {
      cy.get('[name="idcard"]')
        .type(testUsername)

      cy.get('[name="password"]')
        .type(testPassword)

      cy.get('.btn').click()
    })
    cy.wait(1000)
  })

  const getFavCount = user => user && user.profile && user.profile.favorites && user.profile.favorites.length

  it('Can favorite route', function() {
    let initialFavoriteCount
    cy.window().then(win => {
      const user = win.Meteor.user()
      initialFavoriteCount = getFavCount(user)
    })

    cy.get('.is-not-favorite > .ion-ios-heart-outline').first().click()
    cy.wait(1000)

    cy.window().then(win => {
      const user = win.Meteor.user()
      const newFavoriteCount = getFavCount(user)

      expect(initialFavoriteCount + 1).to.equal(newFavoriteCount)
    })

    cy.wait(1000)
  })

  it('Can unfavorite route', function() {
    let initialFavoriteCount
    cy.window().then(win => {
      const user = win.Meteor.user()
      initialFavoriteCount = getFavCount(user)
    })

    cy.get('.is-favorite > .ion-ios-heart').first().click()
    cy.wait(1000)

    cy.window().then(win => {
      const user = win.Meteor.user()
      const newFavoriteCount = getFavCount(user)

      expect(initialFavoriteCount - 1).to.equal(newFavoriteCount)
    })
  })
})

cypress tests with MeteorJS

Cypress automatically clears all cookies before each test to prevent state from building up.

You can take advantage of Cypress.Cookies.preserveOnce() or even whitelist cookies by their name to preserve values across multiple tests. This enables you to preserve sessions through several tests.

So your auth cookies just get deleted by default. You either should execute login script in every test or just preserve auth cookies.

1 Like

Cypress docs recommend creating an automated, non-UI based flow for logging in and not to use a persistent session.

This makes the tests isolated and help you pinpoint issues, rather than have a single bug make all following tests potentially fail (or not fail, when they should).

2 Likes

@arggh I read about this too, but I still don’t know how to get it.
My first thought was that stubbing Meteor’s userId and user functions could do the trick, but I’m still figuring out how to do that (I just discovered Cypress a couple of days ago and it truly looks awesome and fun).
Do you have any recommendations on how to get there?

@ fullhdpixel as @arggh here points out, the Cypress people recommend against using the UI to login and run your tests. In fact, it is the first of a list of “Anti patterns” they’ve defined:

Yet there are moments in which this could be useful and in fact, necessary.
Brian Mann, CEO and founder of Cypress comments in a talk that he does this for testing the login screen only (meaning having a separate login.spec.js file). Here you test the real login works well and you take that confidence up to 100%. The point he makes is that using real login in subsequent tests only make those test slower (by a factor of 4x or 5x in his example) and adds 0% more confidence on its test suite. Hence the recommendation. Here the specific section of the video:

Now we only need to find out how to do it programatically :smile:

I did it like so:

  1. Create a module that is loaded only when you are running integration tests
  2. In this module, create a client side function createTestUser that calls Meteor’s Account.createUser with credentials you wish to use, so a fresh user is created for each test
  3. Assign this function to window
  4. Create a Cypress command login, something like this:
function login(win) {
   win.createTestUser().then(() => win.appReady = true);
}

Cypress.Commands.add('login', () => {
  cy.visit('/'); // load the app
  cy.window().then(login); // create user
  cy.window().should('have.property', 'appReady', true); // wait for the user to get created
});
  1. Use this command in the beforeEach hooks of your Cypress tests
4 Likes

That’s an interesting way to do it. We just login with a user that are in our test fixtures and we reset the db to these with each spec. Here’s some commands you can grab - some of which we grabbed from various forum posts and github issues over the months we’ve been using cypress. Apologies if I can’t remember who to attribute all the various bits to but I think @mitar & @florianbienefelt helped :slight_smile:


Cypress.Commands.add("resetDb", () => {
    const mongoport = 3001;
    cy.exec(
        `mongorestore --host localhost:${mongoport}  --drop --gzip --archive=./tests/testFixtures.agz`
    );
});

Cypress.Commands.add("getMeteor", () =>
    cy.window().then(({Meteor}) => {
        if (!Meteor) {
            // We visit the app so that we get the Window instance of the app
            // from which we get the `Meteor` instance used in tests
            cy.visit("/");
            return cy.window().then(({Meteor: MeteorSecondTry}) => MeteorSecondTry);
        }
        return Meteor;
    })
);

Cypress.Commands.add("callMethod", (method, ...params) => {
    Cypress.log({
        name: "Calling method",
        consoleProps: () => ({name: method, params})
    });

    cy.getMeteor().then(
        Meteor =>
            new Cypress.Promise((resolve, reject) => {
                Meteor.call(method, ...params, (err, result) => {
                    if (err) {
                        reject(err);
                    }

                    resolve(result);
                });
            })
    );
});

Cypress.Commands.add("login", () => {
    Cypress.log({
        name: "Logging in"
    });

    cy.getMeteor().then(
        Meteor =>
            new Cypress.Promise((resolve, reject) => {
                Meteor.loginWithPassword("you@example.com", "password123", (err, result) => {
                    if (err) {
                        reject(err);
                    }
                    resolve(result);
                });
            })
    );
});

Cypress.Commands.add("logout", () => {
    Cypress.log({
        name: "Logging out"
    });

    cy.getMeteor().then(
        Meteor =>
            new Cypress.Promise((resolve, reject) => {
                Meteor.logout((err, result) => {
                    if (err) {
                        reject(err);
                    }
                    resolve(result);
                });
            })
    );
});

Cypress.Commands.add("allSubscriptionsReady", (options = {}) => {
    const log = {
        name: "allSubscriptionsReady"
    };

    const getValue = () => {
        const DDP = cy.state("window").DDP;

        if (DDP._allSubscriptionsReady()) {
            return true;
        } else {
            return null;
        }
    };

    const resolveValue = () => {
        return Cypress.Promise.try(getValue).then(value => {
            return cy.verifyUpcomingAssertions(value, options, {
                onRetry: resolveValue
            });
        });
    };

    return resolveValue().then(value => {
        Cypress.log(log);
        return value;
    });
});
3 Likes

I just solved this all by myself with very little effort, so there’s very likely a better way out there :slight_smile:

The reasoning at the time was something along these two points:

  1. I didn’t want to create the user fixtures manually in the first place
  2. Creating the user(s) this way liberates me from the burden of manually updating the fixtures when ever our app logic or user creation flow changes.

Thanks for sharing the snippets!

Thanks for sharing your solution @arggh
I’m guessing your createUser function also logs in ? Otherwise I don’t fully get where is the login happening.

Thank a lot @marklynch ! I’ve added the login/logout ones from your snippets :). I’m getting logged in programmatically now :).

I’d already grabbed some of those from this code:

Still wondering if there is some way to stub Meteor itself (I’ve been trying using cy.stub once I get the Meteor object with one of your commands, but still no luck).
I think that it should be theoretically the fastest mean to login in a test that does not intent to test the login functionality per-se.
I also thought that if I can achieve that, other tests might be eased out. For example, playing with Meteor.status to simulate the different states of the connection with the server and verify how the application responds to that.

Regards and thanks for the quick responses people!

1 Like

Yes, it logs you in if the call was succesful. More here.

I understand now. Thanks!