Cypress: Provide a "Cypress" way to access textContent (and/or innerText) - .text() command

Created on 11 Sep 2017  ยท  51Comments  ยท  Source: cypress-io/cypress

Desired behavior:

Add a .text() method similar to jQuery's text method to access textContent and/or innerText.

I guess for convenience, by default cy.text() should just return whatever $(...).text() returns.

It may also be good to provide a way to get the innerText of an HTMLElement as it returns a rendered text representation (removing sub-tags and whitespace that may be part of the node, but not visible once rendered). Perhaps a flag in the .text() method or via .its('innerText') or something.

pkdriver ready for work feature

Most helpful comment

@brknlpr you can just use jquery methods to access text content of elements - its our top FAQ question.

cy.get('element').invoke('text')

All 51 comments

Agreed. This is now on our radar.

I think the biggest issue here is that innerText is not necessarily normalized across all browsers. The question will be whether or not we just apply our own normalization (stripping whitespace, removing tags, etc) to the command as opposed to using the browser defaults.

I guess if you'd implement your own normalization, you can't call the feature innerText in any way or it would be confusing. If the feature would be named inner Text, you'd have to use the (current) browser implementation I guess.

any progress on this ?

@brknlpr you can just use jquery methods to access text content of elements - its our top FAQ question.

cy.get('element').invoke('text')

thanks a lot!

in the meantime, can you please add this to https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html , and/or link to the FAQ entry from there?

Thanx @brknlpr saved my life! :+1:

Not sure how this would work. Can some one provide an example. I don't like using 'contain' and would love to use the inner text if possible. How would this work?? cy.get('element').invoke('text')

Ok, I figured it out I found the link https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element%E2%80%99s-text-contents

Ok, it was bothering me to use contains because I wanted more of a valid check that would not possibly give me a false positive. For example if my text is 66.67% and use contains 6.67% my test will pass. I can see where that might be a problem where there is a row with 6.67% and I validate the wrong row and my test still passes, i realize the probability of that is slight, but still possible. I then started using 'have.text', and my test were failing because my cell elements has empty spaces in it. I fixed it by re-moving spaces in the table cell, but the cell was formatted that way by a developer and they could easily go in put the formatting back and break the test. I don't want my test to be that brittle. I then used this to prevent that problem,

        cy.get('#id-1').find('.budget-item-percent').invoke('text').then((text => {
            expect(text.trim()).to.eq('66.67%')
        }));

Is there any easier way to do this. Notice I am trimming the spaces so the if someone re-formats the it does not break my code (or so I hope)

I'm using the cy.get('element).invoke('text') to get the text of an h2 element in a test. The text of the element is Event 4 Spans Several Days instead the Cypress invoke call yields Event 4 spans several days making the text lower case and leading to some false positive assertions. Does anyone know why this would be?

Sounds like a (unrelated) bug. Maybe Cypress is trying to lowercase HTML tags (span in this case) where it shoudn't? I'd open a separate issue for this.

You can also create your own command if you think invoke('text') is too verbose:

Cypress.Commands.add("text", { prevSubject: true}, (subject, options) => {
  return subject.text();
});

and then use it like this:

cy.get('._19odbsb1')**.text()**.then(value => results.yearlyTotal = value);

@sahithya most likely you have text-transform:capitalize on that element. So the text content in the dom is lowercase, but you think it's capitalized

There is a plugin that offers a .text() command here: https://github.com/Lakitna/cypress-commands

More about the command can be found here https://github.com/Lakitna/cypress-commands/blob/develop/docs/text.md

When I use the plugin's .text() or when I use Cypress' .invoke("text") I get a Object (chainerId, firstCall)... which means when I try to parse into a number, JS rightfully complains that's NaN.

I just want to get a number string from a page and assert that it's greater than zero.

// I would like to use this syntax, but it fails because it compares a string with a number
โŒ cy.get(someSelectorHere).text().should("be.greaterThan", 0)

// This also fails because it returns a Chainable object, which to JavaScript is not a number (NaN)
โŒ expect(cy.get(someSelectorHere).invoke("text")).to.be.greaterThan(0)

// This hack works for this case, but it won't work for number ranges
โœ… cy.get(someSelectorHere).text().should("not.be", 0)

How can I do it properly?

You can always do something like this

cy.get(someSelector)
  .text()
  .then((str) => Number(str))
  .should("be.above", 2);

Do you think that .text() should cast to a number when doing that results in no information being lost? For example: Do not cast "01" to 1, but do cast "1" to 1.

If I add that an option will be required to turn this behaviour on/off. When the default is on then a major version release will be necessary.

_oh, hi there @Lakitna ! I wasn't expecting the lib author to appear_ ๐Ÿ˜„๐Ÿ™‡โ€โ™‚๏ธ

I like your code example, it just adds a then and a simple conversion. I'll use it!

Hmm, I don't think .text() should do the cast "sometimes" based on loss of information, because then I need to learn its implementation details to use it.

I would prefer a .text() that returns a string, instead of a Chainable. But that would break compatibility. So I suggest you create a .textValue() that always returns a string. And then I can cast it as I desire, or use it directly in an expect(... .textValue()).to.be...` Do you think that's reasonable?

๐Ÿ˜„

Cypress commands by definition always return a Chainer, so making it return a string/array is impossible. In all other situations the jQuery .text() will probably suffice:

cy.get(singleElementSelector)
  .then((element) => {
    expect(element.text()).to.equal('foo');
  });

Impossible? Fine... ๐Ÿ˜ข
Then how about a .number() that _always_ does the casting that you mentioned? ๐Ÿ˜Ž

It's impossible because of how Cypress works. The code you write isn't
executed immediately. Instead it sets up a script to be executed later. See
my lecture slides here for more info :
https://codelikethis.com/lessons/javascript/cypress#anchor/nothing_happens_immediately

Because of this delayed execution, the only way to get fine grained access
to the actual values in the page -- to then do comparisons or conversions
-- is through a callback (like with ".then").

Hope this helps!

  • A

On Mon, Jun 24, 2019 at 11:11 AM Diogo Nunes notifications@github.com
wrote:

Impossible? Fine...
Then how about a .number() that always does the casting that you
mentioned?

โ€”
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/cypress-io/cypress/issues/630?email_source=notifications&email_token=AAACCTF3JTVXL7JXYVKZST3P4DP3NA5CNFSM4D2NG2EKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODYNIBNA#issuecomment-505053364,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAACCTAAT6745BHUOTQSAWDP4DP3NANCNFSM4D2NG2EA
.

>

Alex Chaffee - [email protected]
http://burlingtoncodeacademy.com
http://alexchaffee.com
http://codelikethis.com
http://twitter.com/alexch

That should be very doable. I could make it that it would always cast a value or array of values. It might warrant additional commands like .array() and .string(). I'm not totally sure we should want this yet. Maybe .cast('number') would be better?

Though I'm wondering if I can make it so .text() casts only if you need that for an upcoming assertion. I've written a way to find out what the upcoming assertions are for the .attribute() command so it should be possible (though very hacky).

It may be a good idea to discuss this over at cypress-commands instead. I'll create an issue.

I'm new to Cypress. Is there any nicer way to write something like this? It doesn't look great.

describe("My first test", () => {
    it("doesn't do much!", () => {
        cy.visit("http://mywebsite.com")

        cy.get(".cardBtn.question")
            .invoke("text")
            .then(initialText => {
                cy.get(".cardBtn.question")
                    .click()
                    .invoke("text")
                    .then(text => {
                        expect(text).not.to.eq(initialText)
                    })
            })
    })
})

Did I understand correctly?

// get the text of the button (A)
// click the button
// get the text of the button after the click (B)
// assert: A should be diff than B

Does this work for you?

describe("My first test", () => {
  it("doesn't do much!", () => {
    cy.visit("http://mywebsite.com")

    const textBeforeClick = cy.get(".cardBtn.question").invoke("text")
    cy.get(".cardBtn.question").click()
    const textAfterClick = cy.get(".cardBtn.question").invoke("text")

    expect(textBeforeClick).not.to.eq(textAfterClick)
  })
})

If you have eslint-plugin-cypress installed, it will complain that you shouldn't assign return values of Cypress commands...

  • Maybe you can use .as()?
  • Maybe you can cast that return value to a string, because that's what it really is?
  • Maybe you can try cypress-commands's .text() (soon it will support casting)

Did I understand correctly?

Yep, that's what I'm trying to do. I don't think the example you gave will work though. textBeforeClick and textAfterClick will be a chainer, since that's what invoke returns. How could I go about using .as() for this like you've suggested?

.as(), not sure this will work though:

cy.get(".cardBtn.question").invoke("text").as("textBeforeClick")
cy.get(".cardBtn.question").click()
cy.get(".cardBtn.question").invoke("text").as("textAfterClick")
cy.get("@textBeforeClick").should("not.eq", cy.get("@textAfterClick"))

I tried the .text() but it also returns a Chainer :(
We really need a .text().asString(). Sorry I can't help you more than this.

On mobile as I'm on holiday

I will finish the .to command when I return from holiday :)

Regardless, a solution lies in that an aliased value (defined with as) can be accessed with this in the scope of it

//...
.text()
.as('a');

//...
.text()
.should('not.equal', this.a);

Obviously I was not able to verify if this works but it should.

Edit: you can also nest inside a .should

```
.text()
.should((a) => {
//...
.text()
.should('not.equal', a)
});

.as(), not sure this will work though:

cy.get(".cardBtn.question").invoke("text").as("textBeforeClick")
cy.get(".cardBtn.question").click()
cy.get(".cardBtn.question").invoke("text").as("textAfterClick")
cy.get("@textBeforeClick").should("not.eq", cy.get("@textAfterClick"))

I tried the .text() but it also returns a Chainer :(
We really need a .text().asString(). Sorry I can't help you more than this.

Try this one:

cy.get(".cardBtn.question").invoke("text").then (textBeforeClick => {
   cy.get(".cardBtn.question").click();
   cy.get(".cardBtn.question").invoke("text").should("not.eq",textBeforeClick)
})

Cypress is interpreting IDE formating:

 <h1>
           Not found
 </h1>          

 cy.get('h1').invoke('text').should('eq', 'Not found');

Result:
Error: AssertionError: expected '\n Not found\n ' to equal 'Not found'

Whenever I change to:
<h1>Not found</h1>
It will pass

Guys, I believe the question was much simpler, how do we retrieve a value from the screen (or from an object) and store it within a variable (string/number/whatever), for e.g.:
let userResponse: string;

let userResponse: string;
cy
        .get('*[class*=response][class*=user]')
        .then(($currentUserRequest) => {
        userResponse = $currentUserRequest.text();
        console.log('userResponse: ' + userResponse);   //THIS WORKS
});

//THIS DOES NOT WORK - BUT WE WANT TO WORK - so I can use it later, outside of the .then clause
console.log('userResponse: ' + userResponse);  //Variable 'userResponse' is used before being assigned.

@brknlpr you can just use jquery methods to access text content of elements - its our top FAQ question.

cy.get('element').invoke('text')

You cannot assign this to a variable, returns a chainable error:

//error: Type 'Chainable<JQuery<HTMLElement>>' is not assignable to type 'string'.
const myText: string = cy.get('element').invoke('text') //returns [Object object]

@lotosotol That will never work. Whenever you chain of cy you are "recording" behaviour instead of actually executing code, so whatever you define there is delayed in execution. Therefore the userResponse variable will not be assigned to at the console.log line.

More info on that here.

@verheyenkoen so how can I store a variable from the app for later use then?

@lotosotol I guess you need to work with .then or .should in these cases.

Ok, it seems that I figured out, the point was to grab a value from the screen and store it within a global variable, did it this way:

// global variable:
let userResponse: string;

describe('pass a screen value across describes and its', function () {
    before(function() {
        // read the content of a element in the app's front end
        // get actual user's response/request from the screen
        cy.get('*[class*=response][class*=user]').invoke('text').as("userRequest");
    });

    it('assign the text of the element to my variable', function () {
        // assign the text of the element to the global variable :
        userResponse = this.userRequest;
        console.log('userResponse #1: ' + userResponse); //WORKS
    });

    it('see if "userResponse" var is still available - another it / same describe', function () {
        console.log('userResponse #2: ' + userResponse); //WORKS
    });
});

describe('another describe & it - check the variable still holds the value', function () {
    it('check again if "userResponse" var is still available - another describe & it', function () {
        console.log('userResponse #3: ' + userResponse); //WORKS
    });
});

Since this is one of cypresses "top FAQ question" I'd suggest that you really think about a solution that fits for "beginners". Disadvantage of current implementation in Cypress:

cy.get returns an array of elements, even when called with an id. I know this is very fine for experienced type save developers, because, cy.get() _always_ returns and array. But for beginners this is very counter intuitive.

   cy.get('#someId').then(e => {  
      /* e is now an ARRAY of DOM elements */  
      var v1 = e.textContent   // this does NOT work
      var v2 = e[0].textContent   // this must be done
   }

I totally agree with the suggestion of the OP: cy.text(selector) is a handy shortcut (for beginners).

@lotosotol that's not a great solution as it relies on the order of execution of your tests. A good test should pass even when it's run in complete isolation. I'm not sure why you want to store the value in a global variable when you can get the same effect by using an alias:

describe('...', function() {
    beforeEach(function() {
        cy.get('div')
            .text()
            .as('valueINeedEverywhere');
    });

    it('can use the value I need everywhere', function() {
        expect(this.valueINeedEverywhere).to.not.be.empty;
    });

    describe('Some nested suite', function() {
        it('can use the value I need everywhere', function() {
            expect(this.valueINeedEverywhere).to.not.be.empty;
        });
    });
});

@Doogiemuc:

cy.get returns an array of elements, even when called with an id.

The reason for this is actually quite simple. A command that yields an element always wraps the element in jQuery. A jQuery object like this always has an array-like structure, even if there is only a single value.

This is not something you notice if you stick with pre-made commands but is something that might trip you up if you start going deeper (using .then(), .should() or custom commands).


To go back to the original topic of this issue: You can use the .text() command as it exists in the package cypress-commands and the Cypress team is free to copy that command into the Cypress repo (as offered here #3946). However, the Cypress team has a lot more exciting things on their backlog, so I don't think they will add a command like this anytime soon.

Is it not possible to add another options to the its('option') command? So that we could just go cy.get('myElement').its('innerText').should('eq', 'myText');

its.('trimText') would be super nice as well.

Ok, it seems that I figured out, the point was to grab a value from the screen and store it within a global variable, did it this way:

// global variable:
let userResponse: string;

describe('pass a screen value across describes and its', function () {
    before(function() {
        // read the content of a element in the app's front end
        // get actual user's response/request from the screen
        cy.get('*[class*=response][class*=user]').invoke('text').as("userRequest");
    });

    it('assign the text of the element to my variable', function () {
        // assign the text of the element to the global variable :
        userResponse = this.userRequest;
        console.log('userResponse #1: ' + userResponse); //WORKS
    });

    it('see if "userResponse" var is still available - another it / same describe', function () {
        console.log('userResponse #2: ' + userResponse); //WORKS
    });
});

describe('another describe & it - check the variable still holds the value', function () {
    it('check again if "userResponse" var is still available - another describe & it', function () {
        console.log('userResponse #3: ' + userResponse); //WORKS
    });
});

Any idea why this wouldn't work for me ?

it.only('should compare text',()=>{

    cy.get('h6').first().invoke('text').as('txt')

    cy.log(this.txt)
})

Guys I think i found a viable workaround while cypress doesn't allow you to store the values in a const or let it does in for an array. Below works for me

it.only('should compare text',()=>{

    let txt = []
    cy.get('h6').invoke('text').then((x)=>{
        txt.push(x)
    })
    cy.log(txt)
})

})

I am trying to get the text of a group of elements and populate them into a map so that I can access the values in the following page to do some validation.However the map object gives undefined once out of the invoke().then() functionality.
The "cy.get(...).text()" returns "TypeError: cy.get(...).text is not a function"

@rhamasv Note that .text() is not a default command. Instead, it's part of cypress-commands which is not affiliated with Cypress itself. Did you follow the installation instructions of cypress-commands here https://github.com/lakitna/cypress-commands#installation ?

If you have, please create an issue in the cypress-commands repository.

@Lakitna Thanks for your response.I forgot to mention .text() function already works for some elements in our existing framework (still trying to found out if cypress-commands is already plugged in our framework) and throws the error I mentioned above for elements with a (hyperlink) tags.

@brknlpr you can just use jquery methods to access text content of elements - its our top FAQ question.

cy.get('element').invoke('text')

Is this retried until some timeout, or is the retry loop limited to only waiting on get to succeed? e.g., assert on some innerText value that is waiting to populate with a server response

@brknlpr I guess there's no retry mechanism there as the jQuery .text() method will always return a value. If you want that behavior you can probably use the have.text chai assertion with .should() like so, which should retry with a default 5000ms timeout:

cy.get('element').should('have.text', 'lorem ipsum');

I'm still kinda new to the syntax, but how do I return an element from an array of elements with a text match?

@tchu10 Kinda getting off-topic here (direct these kinds of questions to the Gitter chat) but for that you probably need the cy.contains(...) method. Bear in mind that Cypress commands never return actual elements but yield the result to the next command in the chain. So if you chain a .should(...) method to that for asserting the element that would work.

Bear in mind that Cypress commands never return actual elements but yield the result to the next command in the chain. So if you chain a .should(...) method to that for asserting the element that would work.

I know this is not the best place to ask questions, but how to verify if text (on button or label) has changed or was updated? We don't know in test what the target value should be - and thus using cy.contains() makes no sense.

@markrason These are the kinds of questions you can best ask in Gitter https://gitter.im/cypress-io/cypress

@markrason also you can use our documentation search to find the answer in https://docs.cypress.io/faq/questions/using-cypress-faq.html#How-do-I-get-an-element%E2%80%99s-text-contents

Was this page helpful?
0 / 5 - 0 ratings

Related issues

carloscheddar picture carloscheddar  ยท  3Comments

verheyenkoen picture verheyenkoen  ยท  3Comments

stormherz picture stormherz  ยท  3Comments

brian-mann picture brian-mann  ยท  3Comments

egucciar picture egucciar  ยท  3Comments