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

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

Open
verheyenkoen opened this issue Sep 11, 2017 · 64 comments
Labels
pkg/driver This is due to an issue in the packages/driver directory type: feature New feature that does not currently exist

Comments

@verheyenkoen
Copy link
Contributor

verheyenkoen commented Sep 11, 2017

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.

@jennifer-shehane jennifer-shehane added pkg/driver This is due to an issue in the packages/driver directory type: feature New feature that does not currently exist labels Sep 11, 2017
@brian-mann
Copy link
Member

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.

@verheyenkoen
Copy link
Contributor Author

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.

@brknlpr
Copy link

brknlpr commented May 8, 2018

any progress on this ?

@brian-mann
Copy link
Member

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

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

@brknlpr
Copy link

brknlpr commented May 9, 2018

thanks a lot!

@alexch
Copy link

alexch commented Jun 17, 2018

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?

@eduardogalbiati
Copy link

Thanx @brknlpr saved my life! 👍

@maccurt
Copy link

maccurt commented Jul 8, 2018

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')

@maccurt
Copy link

maccurt commented Jul 8, 2018

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

@maccurt
Copy link

maccurt commented Jul 8, 2018

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)

@jennifer-shehane jennifer-shehane added the stage: proposal 💡 No work has been done of this issue label Sep 14, 2018
@sahithya
Copy link

sahithya commented Jan 23, 2019

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?

@verheyenkoen
Copy link
Contributor Author

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.

@alcfeoh
Copy link

alcfeoh commented Jan 24, 2019

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);

@kuceb
Copy link
Contributor

kuceb commented Jan 25, 2019

@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

@jennifer-shehane jennifer-shehane added stage: ready for work The issue is reproducible and in scope and removed stage: proposal 💡 No work has been done of this issue labels Jan 31, 2019
@jennifer-shehane
Copy link
Member

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

@jennifer-shehane jennifer-shehane changed the title Provide a "Cypress" way to access textContent (and/or innerText) Provide a "Cypress" way to access textContent (and/or innerText) - .text() command Apr 25, 2019
@dialex
Copy link

dialex commented Jun 24, 2019

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?

@Lakitna
Copy link
Contributor

Lakitna commented Jun 24, 2019

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.

@dialex
Copy link

dialex commented Jun 24, 2019

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?

@Lakitna
Copy link
Contributor

Lakitna commented Jun 24, 2019

😄

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');
  });

@dialex
Copy link

dialex commented Jun 24, 2019

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

@alexch
Copy link

alexch commented Jun 24, 2019 via email

@rhamasv
Copy link

rhamasv commented Nov 27, 2019

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"

@Lakitna
Copy link
Contributor

Lakitna commented Nov 27, 2019

@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.

@rhamasv
Copy link

rhamasv commented Nov 27, 2019

@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.

@softwareCobbler
Copy link

softwareCobbler commented Mar 31, 2020

@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

@verheyenkoen
Copy link
Contributor Author

@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');

@tchu10
Copy link

tchu10 commented Apr 4, 2020

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

@verheyenkoen
Copy link
Contributor Author

@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.

@markrason
Copy link

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.

@Lakitna
Copy link
Contributor

Lakitna commented Sep 28, 2020

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

@bahmutov
Copy link
Contributor

@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

@mc185104
Copy link

@Doctor-Strange
Copy link

Doctor-Strange commented Apr 12, 2021

Here is my solution

cy.window().then((win) => {
      console.log( win.document.querySelector(".calss").textContent );  // the result is the context of the element
    });

@cypress-bot cypress-bot bot added stage: backlog and removed stage: ready for work The issue is reproducible and in scope stage: backlog labels Oct 8, 2021
@vjvibhanshu
Copy link

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.

I am also looking for this workaround
if anyone found it then please let me know

@verheyenkoen
Copy link
Contributor Author

@vjvibhanshu That will never work. The cy.get(...).then(...)-chain is executed in a deferred manner. Think of it as recorded instructions to execute later. After that is recorded (but not executed), the console.log(...) will be executed before whatever is in the chain. So userResponse would always be undefined at that point. That's just how Cypress works and will never change.

@vjvibhanshu
Copy link

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);

it is giving the output like this:
log: Object{5}

@vjvibhanshu
Copy link

@vjvibhanshu That will never work. The cy.get(...).then(...)-chain is executed in a deferred manner. Think of it as recorded instructions to execute later. After that is recorded (but not executed), the console.log(...) will be executed before whatever is in the chain. So userResponse would always be undefined at that point. That's just how Cypress works and will never change.

@verheyenkoen thanks for the insight
The reason I am searching for the work around is that, I have to compare the output(let say content) from the API as well as from UI so can you suggest to achieve the same.

@verheyenkoen
Copy link
Contributor Author

verheyenkoen commented Jan 13, 2022

@vjvibhanshu This isn't really the place for support (look here) but you could probably do something like this:

cy.request('{API url}').then(response => {
  cy.get('{selector}')
    .invoke('text') // this invokes the text method, so the equivalent of $(el).text()
    .should('eq', response.body.path.to.property) // JSON response body is automatically deserialized to an object
})

@vjvibhanshu
Copy link

vjvibhanshu commented Jan 13, 2022

@verheyenkoen Thanks for the code snip, However it is little messy I want to keep UI and API part separate
Also thanks for the support portal I will look into this

@bokukpark
Copy link

This is my solution.

    cy.get('#myroom_contents > div:nth-child(5) > table:nth-child(5) > tbody > tr > td.prod_cell').then(elem => {
      console.log(elem.text().trim())
    })

@whudson
Copy link

whudson commented Apr 1, 2022

I'm a little confused about this one, if .its() is supposed to get us the property from an object, why doesn't .its('innerText') already work out of the box? I was able to work around with .invoke('prop', 'innerText').

@AlexSdet
Copy link

AlexSdet commented Jun 3, 2022

This is ridiculous that I can't store text/value outside of chain.
Please make this possible:
var text = cy.get('element').text()
So I can use my text later on everywhere I want.

Also per Cypress documentation, here is one way that works:

beforeEach(() => {
  cy.get('button').invoke('text').as('text')
})
it('has access to text', function () {
  this.text // is now available
})

BUT if using alias outside of beforeEach it return "undefined" again:

it('has access to text', function () {
  cy.get('button').invoke('text').as('text')
  this.text // is NOT available
})

Why make it so complicated, whats the reason behind all this ?

@pavelloz
Copy link

pavelloz commented Mar 1, 2024

It has bitten me as well. Such commonly used operation feels like it should be bulletproof and very easy to use.

@Imravirathore
Copy link

In Cypress, you can access the textContent or innerText of an element using the .invoke('text') or .invoke('prop', 'innerText') commands.
cy.get('selector').invoke('text').then((text) => {
cy.log(text);
// You can also perform assertions on the text
expect(text).to.equal('expected text');
});

Know more : https://ravirathore.com/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg/driver This is due to an issue in the packages/driver directory type: feature New feature that does not currently exist
Projects
None yet
Development

No branches or pull requests