Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve data in custom command #198

Closed
nicompte opened this issue Aug 11, 2016 · 19 comments
Closed

Resolve data in custom command #198

nicompte opened this issue Aug 11, 2016 · 19 comments
Assignees
Milestone

Comments

@nicompte
Copy link

Description
I cannot find a way to resolve data in a custom command, using a Cypress.Promise.

Code
In support/commands

Cypress.addParentCommand('signIn', function (email, password) {
  return new Cypress.Promise((resolve, reject) => {
    cy.request({
      method: 'POST',
      url: '/auth',
      body: {email, password}
    })
    .then(result => {
      resolve(result.body);
    }, reject)
  });
});

In my test:

describe('any test', function () {
  it('should work', function() {
    cy.signIn('user@email.com', 'password')
     .then(res => console.log(res))
  });
});

When I return the promise like above, cy.signIn times out. It does not fail when I do not return the promise, but I still can't access the request data.
Am I doing anything wrong?

@brian-mann
Copy link
Member

brian-mann commented Aug 17, 2016

I'll look into this - there are a few known issues with custom commands. You can however write the following much easier without using a promise. By default all cypress commands are awaited so you can just do...

Cypress.addParentCommand('signIn', function (email, password) {
  return cy.request({
    method: 'POST',
    url: '/auth',
    body: {email, password}
  })
  // this changes the subject to the body property from the cy.request object
  .its("body")
});

@brian-mann brian-mann self-assigned this Aug 17, 2016
@brian-mann
Copy link
Member

brian-mann commented Aug 17, 2016

Also sorry this took so long for a reply.

@nicompte
Copy link
Author

Thank you Brian, I see how it should work now, however, I cannot find a way to get the result, this code always logs null:

cy.signIn('my@email.com', 'password').then(function(res) {
  console.log(res);
});

No error though, so the body is found.

@brian-mann
Copy link
Member

Ah okay the reason this is happening is due to the way Cypress chaining works - there are rules about parent > child commands which ultimately resets the subject between new 'chains'. It makes sense in the general API but when writing custom commands this is basically a hidden landmine.

You need to do this...

Cypress.addParentCommand('signIn', function (email, password) {
  // chain onto the existing sets so that the subject
  // is not reset when attempting to consume the subject
  // from this custom command
  return cy.chain()
  .request({
    method: 'POST',
    url: '/auth',
    body: {email, password}
  })
  // this changes the subject to the body property from the cy.request object
  .its("body")
});

@nicompte
Copy link
Author

Hmm I have the same result with cy.chain(), still null and still no error.
It also shows up nicely in the console when I click on the - its on the left panel.

Command:   its
Property:  .body
On:        Object {body: Object, headers: Object, status: 200, duration: 299}
Returned:  Object {token: "d76cd89759", …}

This is on Cypress 0.16.5 by the way.

I found a workaround, writing/reading in the localStorage, but that's not the cleanest solution 😄

@brian-mann
Copy link
Member

This works for me when I added the cy.chain

Cypress.addParentCommand("signIn", function(email, password) {
  return cy.chain().request({
    method: "POST",
    url: "http://md5.jsontest.com?text=foo"
  }).its("body");
});

describe("custom command", function() {
  it("resolves with the body", function() {
    // the subject (body) is carried on and 
    // we can then add assertions about it
    cy.signIn().should("deep.eq", {
      md5: "acbd18db4cc2f85cedef654fccc4a4d8",
      original: "foo"
    });
  });
});

@brian-mann
Copy link
Member

Also I see what you're trying to do with cy.request and this is a really good idea. However in 0.16.x cy.request does not properly set cookies on the browser. It makes the request outside of the browser and you'll have to parse the response headers manually to set them on the browser.

However the good news is that in 0.17.x cy.request will automatically both SEND and SET cookies on the browser as if the request had been made directly out of the browser. So in essence cy.request will act just like a real XHR except it will continue to bypass all CORS restrictions.

@nicompte
Copy link
Author

Ah, I found what was causing the error! You code works fine, but the following one does not:

Cypress.addParentCommand("signIn", function(email, password) {
  return cy.chain().request({
    method: "POST",
    url: "http://md5.jsontest.com?text=foo"
  }).its("body");
});

describe("custom command", function() {
  it("resolves with the body", function() {
    // the subject (body) is carried on and 
    // we can then add assertions about it
    cy.signIn().should("deep.eq", {
      md5: "acbd18db4cc2f85cedef654fccc4a4d8",
      original: "foo"
    });
    cy.visit('https://cypress.io');
  });
});

The only difference is the cy.visit('https://cypress.io'); after using cy.signIn(). I can wrap the rest of the test in the signIn(...).then(function() {...});, but I'm not sure it's the right way to go.

@ChristophP
Copy link

ChristophP commented Nov 29, 2016

We also had problems with returning values from custom commands. It took us a while to find this thread about cy.chain(). Our commands are working now. @brian-mann could you please update the docs for the chain method because I couldn't find it in the docs. I would like to understand how it works specifically.

@ChristophP
Copy link

ChristophP commented Dec 6, 2016

@Hey @brian-mann, could you please explain in depth how the chain() command works(we can't find it in the docs)? My colleague @pehota and I got some issues with returning subjects from custom commands fixes by using it, but now ran into other issues where the subject is null like @nicompte said. We also suspect that it is caused by other calls to visit() but we can just guess because we don't fully understand how the chaining works.

@brian-mann
Copy link
Member

Yah these API's are expected to change as I've never been happy with the implementation.

The way it currently works is that everytime you call off of the cy global it will reset the previous subject to null so that querying resets.

This is how you can start a new "series" of commands without being affected by the previous.

The problem is that during a custom command you "may" want to chain off of the existing series, or allow the final resolved subject to be chainable.

For this you need to use chain so...

Cypress.addParentCommand("login", function(){
  cy
  .chain() // reattaches itself to whatever was the previous cy call
  .some()
  .other()
  .commands()
})

cy.login().then(function(subject){
  // now we can access the subject from the custom command
})

@pehota
Copy link

pehota commented Dec 7, 2016

Hi, @brian-mann,
We're trying to create an abstraction layer of sorts using custom commands. E.g. navigating to different parts of the site, creating system objects, etc. Many custom commands use other custom commands.
The lost subject issue appears when we try to chain several custom commands.
I still can't fully understand how chaining is supposed to work so that subject doesn't get lost between commands.

An example of how we're trying to use custom commands would be:

Cypress.addParentCommand('createSystemObject', () => {
  cy
  .goToPage1()
  .fillAndSubmitFormAtPage1()
  .url()
  .then((url) => {
    const newObjId = extractIdFromUrl(url);
    return newObjId;
  });
});

Cypress.addChildCommand('useSystemObject', (objId) => {
  console.log('objId', objId);
});

Then executing

cy
  .createSystemObject()
  .useSystemObject();

prints objId, null in the console.

We tried using chain in all custom commands but the result was still the same.

Any help will be appreciated.
Thanks

@ChristophP
Copy link

Hey Brian,
thanks for your quick reply. Unfortunately we still have some issues where the subject is null even though we used chain(), like @pehota described above.

@brian-mann
Copy link
Member

brian-mann commented Dec 7, 2016

Okay we will look into this, but we'll need a bit of time.

For the moment, just avoid using custom commands and switch to using regular javascript functions to compose your custom commands / page objects.

With the new ES2015 support you can easily create util helpers and import them into your spec files.

Also can you create a very small repo that exhibits this problem? It would help us look at the issue much faster if you can put together a small use case showing several custom commands in use together.

Another solution is basically just avoid using the page object pattern altogether because there are generally other better approaches. For instance, there really shouldn't ever be this much duplication in your scripts to ever need to reuse them in different spec files.

When I see commands like fillAndSubmitFormAtPage1 I think - why would this ever need to be shared? You'll likely want to write e2e tests around this specific page, but not wrap this into a reusable command because if you want other pages to use its behavior - don't use the UI. Just hit the endpoints directly in the backend using cy.request which avoids having to load the entire application, navigate between pages, fill out forms, etc.

Using cy.request would be orders of magnitude faster, less brittle, way less duplication, and avoids needing to compose or share reusable objects (which mostly leads to indirection anyway).

We are making progress on our recipes and will hope to shed more light on this issue in the long term. With Cypress you don't need to bring old baggage from Selenium (like page objects).

If you can share your project (if its not private) or put together more use cases we can discuss this approach more in detail.

@brian-mann
Copy link
Member

@pehota and @ChristophP I cannot recreate your issue. Adding cy.chain to all custom commands works. Here is some example code...

Cypress.addParentCommand("a", function(){
  cy
    .chain()
    .wrap({foo: "bar"})
    .its("foo")
})

Cypress.addChildCommand("b", function(subj){
  cy
    .chain()
    .wrap(subj)
    .should("eq", "bar")
})

it("can chain subjects", function(){
  cy
    .a()
    .b()
    .should("eq", "bar")
})

@ChristophP
Copy link

ChristophP commented Feb 21, 2017

Hi @brian-mann, thanks for your reply. So the thing is I cannot reproduce the issue 100%. It started ocurring when I was in the middle of writing tests. And stuck around for a while and stopped when I refactored some things. My feeling is that the issue was caused by some other error in my code.
I have two theories about what could be the cause:

  1. I was using a chain where a subsequent parent command resets the subject to null(I am again hypothesizing here because I am not sure how the internals of the chaining work). This was kind of my setup:
cy
  .customCommand1()               //  <--- returns some data as subject
  .customCommand2()               //  <--- should get the subject from previous command, but gets null     
  .customCommand3WithVisit() //  <--- because this calls visit() internally and starts a new chain
  1. The error was coming from somewhere else in my code and instead of erroring there cypress continued but started acting weird. This is a behaviour a have witnessed several times in Cypress. The error messages sometimes don't match the error. Stuff that I came across, when something that should have been a refererence error in my beforehook was in my code:
  • cookies weren't cleared anymore
  • my beforeeach hook was skipped without warning or error because I was using a variable that was not defined(Reference Error)
  • Cypress actually cut off the beginning(/* .....) of a comment block and but the rest in the compiled JS. I wish I had taken a screenshot but the code in the browser console had the end of my comment in it but not the beginning which then resulted in a Syntax Error.
    My beforeEach hook that caused it looked kinda like this.
let id;
beforeEach('does setup', () => {
  cy
     .window()
     .then(win => {
        win.client.createResource()
     })
     .then(result => {
        id = res.id   // save id to be able to call client.deleteResource(id) in the afterEach hook
        someFunction(resourceId)  //resouceId is not defined and should throw a Reference Error
     })
     .visitResource(id)   // calls .visit(`/resource/${id}`);
});

Does this help you at all? Sorry I can't say more about reproducing the errors. Let me know if I can do something to help.

@jennifer-shehane
Copy link
Member

We are planning to move the custom command interface to be on Cypress.Commands instead of the Cypress object. See #436

@brian-mann
Copy link
Member

An interesting use case to throw into the mix: https://gitter.im/cypress-io/cypress?at=5953bf62bf7e6af22c81c5db

Situation where user wants should to retry but by inserting a custom command there is no retry logic. Likely needs to be something declarative in the command builder's API.

@brian-mann
Copy link
Member

Fixed in 0.20.0

@brian-mann brian-mann added this to the 0.20.0 milestone Sep 13, 2017
@cypress-io cypress-io locked as resolved and limited conversation to collaborators Dec 6, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants