Cypress with MeteorJS: user does not stay logged in

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
6 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;
    });
});
5 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!

The reason why cypress logs out the user is down to it clearing local storage at the start of each test which wipes out the meteor login token.

I made my cypress tests work by caching a valid meteor login token from local storage at the end of my login function which will login once via the ui and cache the login token in a cypress function. Use a onbefore block at the start of each cypress test to write the cached token back into local storage for each cypress test case and each test will be logged in.

You need to remember to clear the cache if your test changes user by logging out. You can preventing having to logout by switching login tokens on the fly to become another user

Works quite reliably for me and simulates normal user behaviour - login once and reuse the token.

2 Likes

[quote=“marklynch, post:7, topic:50093”]
allSubscriptionsReady
[/quote
Thank you for this very useful code, can you please explain what allSubscriptionsReady exactly do ?

Sorry to reopen an old thread, but I’m having a hard time getting Cypress to wait for the login. How are you doing the async login with a beforeEach. Would love to see what an example of a test with your Commands. Thank you for this comment overall a life saver.

Never mind I’ve gotten it working. Turns out there just needed to be a cy.wait before things would register the login.

@stolinski do you plan on creating video tutorials about testing cypress with Meteor?

2 Likes

This is the issue with Cypress. You need those inefficient cy.wait calls for a lot of things.

If they supported real Promises, then we should just be awaiting the calls.

2 Likes

Yeah, I’ve found way to many situations where just putting in a wait fixed the problem. A bit distressing for consistancy.

Hey Scott, sorry, didn’t login to the forum for while. Wait’s are often needed (and I agree, it’s a bit worrying) but I don’t have any straight after login. Cypress is great overall but there are a few too many cases where things don’t seem to play out exactly the same on different setups or when you update the version. It’s improving though.

Here’s our before/beforeEach setup if it helps.

describe("TestedModule", () => {
    before(function() {
        cy.resetDb();
        cy.visit("/");
    });
    beforeEach(function() {
        cy.login();
    });
    afterEach(function() {
        cy.logout();
    });

If you have to wait for a successful login then you are probably doing it wrong. When your app logs in, some part of the up should update to indicate that the user has logged in.

In cypress, you should be doing a cy.get() to await a successful login.

There should never really be a use case for cy.wait(). If you find yourself having to use it then either your ui is a bad design as it provide no user feedback or you should be cy.getting another object which will inherently wait for it to appear.