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

Warn when defining alias in before hook - as they are not reusable in following tests. #665

Open SMenigat opened 7 years ago

SMenigat commented 7 years ago

Since this is my first Issue here, I want to use this opportunity to thank you guys for your hard work. I love and enjoy working with cypress so far and I'm really looking forward to upcoming releases.

Current behaviour:

It seems like, that a defined alias can only be used once under certain circumstances. The test code will be self explanatory about my approach.

Desired behaviour:

The defined alias should be reusable as expected. Since we are doing a lot of end to end testing, we are trying hard to achieve the best possible performance. As far as I know, requery-ing the dom with cy.get() is in general a bit expensive, so we try to avoid that. Making aliases reusable as much as possible, would also result in prettier, slimmer and easier to manage test code.

Test code:

describe('reusing alias', () => {
  before(() => {
    cy.visit('/my-page.html');
  });
  // ...
  describe('number input', () => {
    before(() => {
      cy.get('#my-input').as('input');
    });
    it('should be of type number', () => {
      cy.get('@input').should('have.attr', 'type', 'number');
    });
    it('should allow a minimum value of 1', () => {
      cy.get('@input').should('have.attr', 'min', '1');
    });
    it('should allow a maximum value of 3', () => {
      cy.get('@input').should('have.attr', 'max', '3');
    });
  });
});

Additional Info (images, stack traces, etc)

The given Example will lead to the following behaviour:

Maybe I am getting something wrong here. If so, feel free to correct my way of thinking here.

brian-mann commented 7 years ago

before code is only run once, and between the tests Cypress removes all of the aliases. So in subsequent tests it is not available.

That's why moving this code into a beforeEach will work.

jennifer-shehane commented 7 years ago

Some of this is discussed more in this issue. https://github.com/cypress-io/cypress/issues/583

brian-mann commented 7 years ago

I really wouldn't worry about performance of a cy.get, or trying to over optimize this.

Cypress runs every command asynchronously automatically, and that in itself is vastly more time consuming than any single cy.get.

SMenigat commented 7 years ago

Thanks for your quick response here.

Is there any kind of documentation / best practice paper that teaches me about performance?

I'm used to writing it's that are just executing one or maybe two expect statements, so that my test stay as precise as possible.

But it seems like that behaviour really has a bad impact on the test run times. That is why I'm experimenting around with that topic at the moment.

I measured as good as I could, and it seems like that this ...

describe('number input', () => {
  beforeEach(() => {
    cy.get('#my-input').as('input');
  });
  it('should be of type number', () => {
    cy.get('@input').should('have.attr', 'type', 'number');
  });
  it('should allow a minimum value of 1', () => {
    cy.get('@input').should('have.attr', 'min', '1');
  });
  it('should allow a maximum value of 3', () => {
    cy.get('@input').should('have.attr', 'max', '3');
  });
});

... is about twice as slow as this ...

it('should render number input with range 1 to 3', () => {
  cy
    .get('#my-input')
    .as('input')
    .should('have.attr', 'type', 'number')
    .should('have.attr', 'min', '1')
    .should('have.attr', 'max', '3');
});

Is it the vast amount of it statements or is it because of the amount of get queries? How can I analyse things like this?

This things add up real quick. For me it makes a big difference if checking the appearance of form costs me ~55 sec or ~20 sec.

brian-mann commented 7 years ago

It's much more slow to split them up into individual tests because Cypress performs a series of setup and teardown events in between tests.

e2e / integration tests are not like unit tests - you should absolutely 100% group all assertions related to the same DOM elements in a single test. There is no benefit splitting things out because with Cypress you already receive a ton of feedback about assertion failures.

Cypress tests is more about testing features than anything else. If you're testing that your form components adhere to attributes you could simply add all of them in a single test. Or better yet just use them and test their values as opposed to testing their attributes.

SMenigat commented 7 years ago

Thanks for your thoughts. I just state on this really quick.

There is no benefit splitting things out because with Cypress you already receive a ton of feedback about assertion failures.

I tried to do it this way to optimise my tests for the headless runs, where I can not simply analyse the failure over the beautiful GUI of yours. Yes, the dashboard (which I really like btw) already supplies a stack trace, but I thought it will be easier for us to analyse errors if we split things up better.

But this is def. not worth it if it comes with big performance trade-offs like this.

Can we look forward to performance optimisations on those teardown and setup events? Is there anything on the roadmap yet? Just curious here 🤓

brian-mann commented 7 years ago

A failure includes a screenshot + a video so you could likely derive it from there.

The setup and teardown involve network requests and talking to things like the browser so it's not really optimizable. We have a much bigger issue open for this which we refer to as Lifecycle Events which will be an API you use to control how and when Cypress does its teardown between tests. As of right now that is not exposed to you.

SMenigat commented 7 years ago

Thanks for your kind support on this. This thread gave me an idea on how I can manage to make our builds faster (for example by avoiding splitting up it statements unnecessarily). Maybe those infos should land in some way or form in the docs. I'm sure that a lot of people are looking to make their test suits more performant.

Btw: keep up the good work 👍 Love using your product.

jennifer-shehane commented 4 years ago

We continue to get feedback from users - unexpectedly running into this behavior. There was a proposal within our team to potentially warn if an .as is assigned within a before, so that at least people would be warned that aliases are cleared when used in before. Another case for displaying warning in command log. https://github.com/cypress-io/cypress/issues/4917

I'll be reopening this issue and changing the scope to warning when an alias is defined in a before.

sgronblo commented 4 years ago

The documentation here: https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Aliases is also mentioning using aliases with before

Using .then() callback functions to access the previous command values is great—but what happens when you’re running code in hooks like before or beforeEach?

pckhoi commented 4 years ago

Clearing aliases between each test is surprisingly inflexible for no good reason. I do much data setup in before all hook and save ids of generated objects in aliases. Doing the same setup for every tests will add 10s of seconds to each test. If your remedy is to add all related tests into one single test then you end up with ugly, long test that do too many things and hard to maintain.

Even after all hook forget all the previously set aliases. Then why have before all/after all hook at all? This is just asking for developers to circumvent the alias issue by using regular variable instead.

ketysek commented 4 years ago

Ach, I would love to prepare setup for my tests once in before hook. Would you consider to enable aliases? I think many users would appreciate it :-(

hehoo commented 4 years ago

One observation here is if the alias is a primitive value, it can be retrieved/reused in following cases. But if the value of alias is an object, then it is gone. So I do think there is a bug in clearing context or setting alias... More interesting thing is if I create an unnecessary describe/context to wrap all cases, then all alias can be retrieved/reused :) You can use this way to work around this issue.

GabrielMcNeilly commented 4 years ago

Yeah I would like to do the below for API testing but get an error saying the 'response' alias doesn't exist for the 2nd and 3rd 'it' statements. Currently my options are to either use 'before each' rather than 'before' which means calling the API 3 times or group them into 1 test which doesn't read very well in the editor or the cypress runner at all.

describe("Users - list", () => {
      before(() => {
          cy.request('users').as('response');
      });
      it('Response status code is correct', () => {
          // check status code of response
      });
      it('Response security headers are set', () => {
          // check security headers of response
      });
      it('Response body is correct', () => {
          // check body of response
      });
});
hacknlove commented 4 years ago

I also wanted to do some requests only once. I have tried this and it worked.

context('logged', () => {
  let WTFAlias
  before(() => {
    cy.fixture('login').then(login => {
      cy.request('POST', Cypress.env('API_URL') + 'login', login.mimaria).then(
        token => {
          WTFAlias = token
        }
      )
    })
  })
  beforeEach(() => {
    cy.request('POST', '/api/token', { token: WTFAlias.body.data.token })
  })
  ;['/user/settings', '/consumer/checkout', '/consumer/verify'].map(path => {
    context(path, () => {
      it('successfully loads', () => {
        cy.visit(path)
      })
    })
  })
})

Explanation of the use case.

I have an API where I need to go to do the real login, but I also have a webapp only API that I need to call to set the https only cookie that the SSR will use.

So I want to do the real login just once, and reuse the token to create the cookies.

I could do the whole thing in beforeEach, but what would be the benefit of that? It makes much more sense to get the token once, and reuse it.

VoloBro commented 4 years ago

Up for the fix

patcon commented 4 years ago

fwiw, aliases created in before seem to work when the creation happens within a command executed in before(). Is this intentional, or expected to be preserved behaviour?

// cypress/support/commands.js
Cypress.Commands.add("createSurvey", () => {
  cy.login()
  cy.request({
    method: 'POST',
    url: Cypress.config().apiPath + '/survey',
    body: {foo: 'bar'}
  }).its('body.survey_id').as('surveyId').then(surveyId => {
    cy.visit(`/survey/${surveyId}`)
  })
})
// cypress/integration/aliases.spec.js
describe('Cypress Aliases', () => {
  before(function () {
    cy.createSurvey()
  })

  it('runs first test', function () {
    cy.log(this.surveyId)
  })

  it('runs second test', function () {
    cy.log(this.surveyId)
  })
})
anurag-roy commented 4 years ago

Is there any other way to share variables across tests?

ShawTim commented 4 years ago

we definitely need an alias with scope that can be shared across all test cases. this is my work around:

context("test", () => {
  let obj;

  before(() => {
    // do sth, e.g. call api, to get some data
    obj = data;
  });

  beforeEach(() => {
    cy.wrap(obj).as("obj");
  });

  it("test case 1", () => {
    cy.get("@obj").then((obj) => {
      ......
    });
  });

  it("test case 2", () => {
    cy.get("@obj").then((obj) => {
      ......
    });
  });

for sure i can directly access the variable but it's not recommended and i'm still looking forward to have alias with wider scope to support this and prepare for future support. it's not necessarily to be done in before, but tell us where to define a context-scope alias.

patcon commented 4 years ago

Assuming my approach above works (others are welcome to validate), then couldn't you just write a helper command to accommodate this for now?

// cypress/support/commands.js
Cypress.Commands.add("asPersistent", (alias, value) => {
  cy.wrap(value).as(alias)
})
// cypress/integration/foo.spec.js
describe('Something', () => {
  before(function () {
    cy.asPersistent('myAlias', {bar: 'baz'}).then(() => {
      // Allows accessing alias value in before()
      console.log(this.myAlias)
    }
  })

  it('runs first test', function () {
    // Alias accessible here
    cy.log(this.myAlias.bar)
  })

  it('runs second test', function () {
    // Alias accessible here
    cy.log(this.myAlias.bar)
  })

})

Abcmsaj commented 3 years ago

My 2c, I'm having similar issues now with aliases not persisting. I'd also like a way to stage my data in the before hook and let the aliases persist across the whole test, please. Staging data in the beforeEach hook will add 10-15 seconds per test as I'd need to reset the data each time (delete and recreate) - just not feasible when all I want is to call on a created user's GUID!

kartikmuchandi commented 3 years ago

We have seen many use cases above for the need to have an alias with a wider scope. Ill add another one here.

What I am trying to achieve is create a random email in the before hook and wrap it into an alias. Use that random email in various tests. I cannot call the random email generator in the before each hook, since it will generate a different email for every test.

I did a vague workaround by writing all the test under a single test. However I reached a blocker when I had to change the domain of the URL. Cypress dosent allow us to use different domains in the same test. So back to using multiple test, where I cannot use the same alias across tests.

Ginsusamurai commented 3 years ago

A lack of persisting aliases in any capacity is going to take much of the modular design that cypress encourages and throw it out the window. I need to visit a page and get a list of fields that will be used for the actual tests but I'm unable to expect those fields to be 100% static so i can't use a fixture. Possible solutions: 1) cram ALL the test cases in to a single a single test and make it harder to diagnose individual regression breaks and be harder to maintain 2) declare some JS variables and then define them later on and hope nothing breaks 3) put the site visit and data collection in the before each 4) Make a 1-off custom command

1 will be a nightmare 2 will probably work but will take me time to debug and make consistent 3 just isn't a viable option 4 MIGHT work but clutters up the actions with things I'll only need for one specific test

This all seems like a lot of work to bypass an inability to persist an alias

bahmutov commented 3 years ago

Morgan, can you create an example test that you are trying to write please? Then we can see what you are trying to do and maybe how the test could be written in a more intuitive style

Sent from my iPhone

On Jan 5, 2021, at 18:25, Morgan Heinemann notifications@github.com wrote:

 A lack of persisting aliases in any capacity is going to take much of the modular design that cypress encourages and throw it out the window. I need to visit a page and get a list of fields that will be used for the actual tests but I'm unable to expect those fields to be 100% static so i can't use a fixture. Possible solutions:

cram ALL the test cases in to a single a single test and make it harder to diagnose individual regression breaks and be harder to maintain declare some JS variables and then define them later on and hope nothing breaks put the site visit and data collection in the before each Make a 1-off custom command 1 will be a nightmare 2 will probably work but will take me time to debug and make consistent 3 just isn't a viable option 4 MIGHT work but clutters up the actions with things I'll only need for one specific test

This all seems like a lot of work to bypass an inability to persist an alias

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or unsubscribe.

Ginsusamurai commented 3 years ago

some of this is my "working" code and some is "what I'd like to do" Getting the source data each test isn't an effective use of time. I'm not sure, currently, how many tests are going to be involved in the user creation and having granular info on a failure is ideal. Just this example has a loop to confirm that I don't have pre-existing emails polluting my data, the actual tests will come later after I figure out how to persist my domain name information for longer than 1 test.

describe("create users", function() {
  before(function(){         // this 'before' handling getting all my test data once would be ideal
    cy.fixture('master_credentials').as('creds').then(function (creds) {
      cy.adminLogin(creds);
      cy.visit(`${this.creds.adminSite.url}${this.creds.adminSite.port}/ep-admin/domain/domain/`)
      cy.get(".field-name").then(function(domains){
        let domainClean = [];      
        for(let i = 0; i < domains.length; i++){
          if(!baseDomains.includes(domains[i].innerText)){
            domainClean.push(domains[i].innerText)
          }
        }
        cy.wrap(domainClean).as('testDomains')
      })
    })
  })

  beforeEach(function () {
    // just to keep my tests separate I would log in for each
    cy.get('@creds').then(function (creds) {
      cy.adminLogin(creds);
    })
  })

  it("create users for each test domain", function(){
    cy.visit('http://admin.localtest.me:7000/ep-admin/common/user/')
    cy.get(".field-email").then(function(emailAddresses){
      let existingAddresses = []
      for(let i = 0; i < emailAddresses.length; i++){
        existingAddresses.push(emailAddresses[i].innerText)
      }
      cy.get('@testDomains').then((val1)=>{
        val1.forEach(function(val,ind){
          let email = `${val.split('.',1)[0]}@testdomain.com`
          expect(existingAddresses).to.not.include(email)
        })
      })
    })
  })

  // more tests using domains as templates or existing emails to go here
})
patcon commented 3 years ago

@Ginsusamurai have you tried my suggestion above? https://github.com/cypress-io/cypress/issues/665#issuecomment-692910590

I've got aliases working fine with like 3 extra lines and a simple new convention. Yes, it could be more intuitive, but I don't think anyone is blocked if they're already in this issue reading :)

Working example

Command setup

https://github.com/pol-is/polis/blob/fc0c11642e4d94e1f9fbcd37ca07a27e06f40f72/e2e/cypress/support/commands.js#L92-L101

Command usage (in before())

https://github.com/pol-is/polis/blob/fc0c11642e4d94e1f9fbcd37ca07a27e06f40f72/e2e/cypress/integration/polis/client-participation/social-login.spec.js#L1-L15

The above is very specific to me, but it can be generalized into an abstract command as I said in my above comment.

Pls do let me know if this doesn't work for you.

Note for skimmers

I believe the above is a solution.

Ginsusamurai commented 3 years ago

Thanks @patcon , I must have missed that. Got that implemented in a few minutes and it's doing what I need and is easier than I thought it would be. I noticed I can't add objects to context within one test and have it show in another but at least primitives work and this gets me going. I appreciate the help.

edit: After more testing, this doesn't actually work out. I can get the context to stick around for 1 test but then lose it again.

fourcolors commented 3 years ago

Assuming my approach above works (others are welcome to validate), then couldn't you just write a helper command to accommodate this for now?

// cypress/support/commands.js
Cypress.Commands.add("asPersistent", (alias, value) => {
  cy.wrap(value).as(alias)
})
// cypress/integration/foo.spec.js
describe('Something', () => {
  before(function () {
    cy.asPersistent('myAlias', {bar: 'baz'}).then(() => {
      // Allows accessing alias value in before()
      console.log(this.myAlias)
    }
  })

  it('runs first test', function () {
    // Alias accessible here
    cy.log(this.myAlias.bar)
  })

  it('runs second test', function () {
    // Alias accessible here
    cy.log(this.myAlias.bar)
  })

})

I think the major advantage of using as is it's ability to be chained, in this case I think it would make since just to add a variable.

patcon commented 3 years ago

Sorry, I'm not totally clear on the nature of your thoughtful push-back -- I use something comparable to this to pass around dynamic values derived from content of pages visited in before(); to be specific, pages visited during the command execution itself. It might be possible to do with js vars, but that's even more off-road from cypress best practices, as far as I understand

alex-vungle commented 3 years ago

One observation here is if the alias is a primitive value, it can be retrieved/reused in following cases. But if the value of alias is an object, then it is gone. So I do think there is a bug in clearing context or setting alias... More interesting thing is if I create an unnecessary describe/context to wrap all cases, then all alias can be retrieved/reused :) You can use this way to work around this issue.

I have the observation on Cypress v6.8.0, in my test scenario, I'd like to store the object to an alias in before() , so that other test cases can access it, however I noticed it can only be accessed in the first test case, if I store primitive values like string, integer, they can be retrived in all test cases, so there must be a bug when sharing object with alias in before()

CleverLili commented 3 years ago

Like a lot of you, I also need to have variables across tests. For example, I created x number of users and want to store their data to be used and referenced across the other tests as I prefer to have small "it" blocks. I don't have a lot of experience with Cypress (in fact, I've only been using it for the past 24hrs). But here is my solution, hope it helps others.

I added the following in the commands


let globals = {};
Cypress.Commands.add('setGlobalAlias', {
    prevSubject: true
}, (subject, method) =>
{
    globals[method] = subject;
    return subject;
})

Cypress.Commands.add('getGlobalAlias', (alias) =>
{
    return globals[alias];
})

and then I use it this way.


cy.wait("@getSession").setGlobalAlias('student1');

cy.getGlobalAlias('student1').then(function (student)
{
    cy.log(student)
})

my preference is if the Cypress team would add a param to .alias() to set the scope as in .alias('bozo', true) or .alias('bozo', {global:true}

astahmer commented 3 years ago

So, being new to Cypress I've just faced this problem and was quite surprised there is no "clean solution" yet, especially one that would be endorsed or provided directly from the Cypress API.

Anyway, here's what I ended up doing in order to re-use aliases, so I can keep my tests kinda easy to read.

I've got a function that returns some alias getters and use it like this at the top of my test file. const aliases = getAliases();

Then in each tests I just call the alias getter whenever I need it.

aliases.homeSubmitBtn();
cy.get('@homeSubmitBtn').should('be.disabled');
aliases.flowTypeRSelect();
  cy.get('@flowTypeRSelect').find('.rs__control').click();

...etc.

This pattern can even work for an alias getter that use another alias, you just need to call alias getter dependencies in the "final" alias getter.

Here an example of how the alias getters function looks like, it could be in another utils file or something, in my case I just defined it at the bottom of the file since the function is hoisted.

function getAliases() {
  const flowTypeInput = () => cy.get('input[name="flowType"]').as('flowTypeInput');
  const flowTypeRSelect = () => {
    // Re-using another alias is as simple as that
    flowTypeInput();
    cy.get('@flowTypeInput').closest('.rs-container').as('flowTypeRSelect');
  };
  const homeSubmitBtn = ;

  return {
    homeSubmitBtn: () =>
    cy.get('.homepage-main .button-action-container button[type="submit"]').as('homeSubmitBtn'),
    articleInput: () => cy.get('input[name="article"]').as('articleInput'),
    flowTypeInput,
    flowTypeRSelect,
  };
}
kkapka commented 3 years ago

I found this article very useful -> https://medium.com/quick-code/passing-variables-between-tests-in-cypress-3bea0eb821fb

jahglow commented 2 years ago

Well, might be slightly off-the-topic but I stumled upon this in my research of how to avoid repetitive tasks like navigating with ui to the same url b/w tests which might include 2-6 route actions which involve data fetching for all intermediate routes and takes time as well and came up with a function which can be executed in beforeEach hook rather than in before. It allowed us shave off a couple of seconds in each subsequent test and reduce wasteful graphql requests on intermediate routes mounts

declare namespace Cypress {
  interface Chainable {
    /**
     * Used in `beforeEach` to use web navigation only during first test execution, all subsequesnt tests use the location as product of the function passed as the first argument
     * @param fn a function containing the navigation logic. The location after the function code is run will be reused in subsequent calls. This function is run only during first execution
     */
    revisit(fn: () => void): Chainable<void>;
  }
}

Cypress.Commands.add(
  'revisit',
  function (this: { lastVisitedUrl?: string }, fn: () => void) {
    if (this.lastVisitedUrl) cy.visit(this.lastVisitedUrl);
    else {
      fn();
      cy.location().its('href').as('lastVisitedUrl');
    }
  },
);

Now in the suite only the first test will have the full user navigation of clicking the links, while the second and all subsequent tests of the suite will use the href and navigate there straight away as if by permalink. Mind it if you're using any contexts passed around by navigating to routes via router context as those will not be available using this method

describe('Workshops', function () {
   before(() => {
    //auth logic
  });

  beforeEach(() => {
    cy.revisit(() => {
      //navigation execued only once
      cy.visitMenu({ top: 'enterpriseSettings', side: 'productionStructure' }); //visit('/'), click top menu (url changes) and side menu (url changes)
      cy.getTab('workshops').click(); // click tab on page (url changes)
      cy.get('#addButton').click(); //click add button to go to add page (url changes)
    });
    cy.getScreen('workshopCreate').as('createScreen');
  });

  it('first add test', () => {
  // came here by cicking through navigtion
  });
  it('second add test', () => {
  // came here by full url
  })
  it('third add test', () => {
  // came here by full url
  })
 // yep, there are more tests down below for the same add page because it has complex logic

PS: using get('@aliasName') was not an ption because in the first run it didn't exist and cypress error on the beforeEach hook, hence the this.aliasName and the named function to specify the type for this.aliasName

codeincontext commented 2 years ago

We've found that aliases set in a before block are persisted in some test files and not others. It turns out that tests within a describe block will maintain the this. aliases of the before block. I don't know if this is by design or a bug

Fails:

const testUser = { id: "test" };

describe("My test", () => {
  before(() => {
    cy.wrap(testUser).as("user");
  });

  it("loads first time", function () {
    cy.wrap(this.user).should("eq", testUser);
  });

  it("loads second time", function () {
    // Fails
    cy.wrap(this.user).should("eq", testUser);
  });
});

Works:

const testUser = { id: "test" };

describe("My test", () => {
  before(() => {
    cy.wrap(testUser).as("user");
  });

  describe("Keeping aliased state", () => {
    it("loads first time", function () {
      cy.wrap(this.user).should("eq", testUser);
    });
    it("loads second time", function () {
      // Works
      cy.wrap(this.user).should("eq", testUser);
    });
  });
});
SirAndrii commented 2 years ago

deleted

dp-franklin commented 2 years ago

codeincorect's solution didn't work for me (v10.3.1) so I came up with another solution that instead involves dumping and restoring aliases. Here is a full example:

describe("My Tests", () => {
  const store = {};

  before(() => {
    cy.wrap("bar").as("foo");
    cy.dumpAliases(store);
  });

  beforeEach(() => {
    cy.restoreAliases(store);
  });

  it("test1", () => { cy.get("@foo").should("eq", "bar"); });
  it("test2", () => { cy.get("@foo").should("eq", "bar"); });
  it("test3", () => { cy.get("@foo").should("eq", "bar"); });

  after(() => {
    cy.restoreAliases(store);
    cy.get("@foo").then((foo) => {
      cy.log("Foo is ", foo);
    });
  });
});

/**
 * Dumps all the aliases from the current context into the store.
 * Typically used in `before(() => ...)`
 *
 * Workaround for aliases set in `before` being unavailable after the first test.
 * See https://github.com/cypress-io/cypress/issues/665
 */

Cypress.Commands.add("dumpAliases", function (store: Record<string, any>, aliases?: string[]) {
  if (aliases == null) {
    const { currentTest, test, _runnable, ...rest } = this;
    Object.assign(store, rest);
  } else {
    for (const alias of aliases) {
      store[alias] = this[alias];
    }
  }
});

/**
 * Restores the aliases from the given store
 * Typically used in `beforeEach(() => ...)`
 *
 * Workaround for aliases set in `before` being unavailable after the first test
 * See https://github.com/cypress-io/cypress/issues/665
 */
Cypress.Commands.add("restoreAliases", function (store: Record<string, any>, aliases?: string[]) {
  if (aliases == null) {
    for (const [alias, value] of Object.entries(store)) {
      cy.wrap(value).as(alias);
    }
  } else {
    for (const alias of aliases) {
      cy.wrap(store[alias]).as(alias);
    }
  }
});
marklagendijk commented 11 months ago

When separate its use separate domains, the solutions above don't work. I believe in such case the only solution is to use a task, since those live in the execution context of the node process, which is the highest level (all other levels can be reloaded).

In your cypress.config.ts:

const globalAliases = {};

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on: any, config: any) {
      on("task", {
        globalAlias(newValues?: Record<string, any>): Record<string, any> {
          if (newValues) {
            Object.assign(globalAliases, newValues);
          }
          return globalAliases;
        },
      });
      return config;
    },
 }
})    

We could use this task directly from the tests, but for convenience it is nice to create the following commands, that wrap the task calls:

Cypress.Commands.add(
  "asGlobal",
  {
    prevSubject: true,
  },
  (subject: any, alias: string) =>
    cy.task("globalAlias", { [alias]: subject }).then(() => subject)
);

Cypress.Commands.add("getGlobal", (alias: string) => {
  return cy
    .task("globalAlias")
    .then((globalAliases: any) => globalAliases[alias]);
});

declare global {
  namespace Cypress {
    interface cy {
      getGlobal(alias: string): Chainable<any>;
    }

    interface Chainable<Subject> {
      asGlobal(alias: string): Chainable<Subject>;
    }
  }
}

We can then use the commands in our tests:

  it("should do something", () => {
     // .. some logic
     cy.title().asGlobal("title");  
  });

  it("should do something else", () => {
     cy.getGlobal("title").then(title => {
        // Use title
     });
  });