Playwright: Is there an equivalent to cy.within() and cy.each()? - cypress

I'm new to Playwright, I'm trying to convert some Cypress tests but I'm having issues with iterating over elements with the same selectors.
Is there a way to convert these two functions to Playwrite:
I call this one with different strings and it looks for the given string in one of the elements:
assertCardPresent(expected) {
cy.get(selectors.cards).within(() => {
cy.get('h5').contains(expected)
})
I call this and it loops through all the cards to ensure they have a button with some text:
assertEachCardContainsAButton() {
cy.get(selectors.cardButtons).each(($button) => {
cy.get($button).should('have.text', 'find out more')
})
}

Related

How can I save the text of an element for later usage in the test

I want to get the text of a displayed time on a website.
The object looks like this
<strong class="date-time ng-tns-c226-30 ng-star-inserted">Dec 14, 2022, 5:07:08 PM</strong>
I want to change the timezone on the website and then get the text-content of it again and compare whether the changes are correct.
I've tried this:
cy.get('.date-time').invoke("text").as("oldtext")
cy.log("saved text: " + this.oldtext)
//Do some changes
cy.get('.date-time').invoke("text").as("newtext")
cy.log("saved text: " + this.newtext)
//Do comparison with the two strings
I have also tried doing this with resolving the promise that .invoke() returns but I have not found a way to get the text as string outside of a .then() block
let currentTime;
cy.get('.date-time') // select the element that displays the time
.then(element => {
currentTime = element.text()// get the text content of the element
cy.log("inside: " + element.text()) // log the time inside the callback function
});
cy.log("outside: " + currentTime)
But this always resulted in the output of the log being as follows
log inside: Dec 14, 2022, 5:07:08 PM
log outside: undefined
To compare old and new text, you can track an element's changing property by saving the old values on the element.
cy.get('.date-time')
.then($el => $el.oldtext = $el.text()) // save the text at this time
//Do some changes
cy.get('.date-time')
.then($el => {
expect($el.oldtext).not.to.eq($el.text())
})
It's very useful for multiple "snapshots", like testing (x,y) positions in drag-and-drop. In this case, there may be many values, and you can just save them in an array.
BUT be careful if testing a React page and //Do some change causes the element to be re-created.
In that case, you loose the old value, and you are better off saving the value externally
let oldtext;
cy.get('.date-time')
.then($el => oldtext = $el.text()) // save the text at this time
//Do some changes
cy.get('.date-time')
.then($el => {
expect(oldtext).not.to.eq($el.text())
})
cy.log() has some interesting behavior when it enqueues the values to log, namely that it will evaluate the string when it is enqueued and not when it is actually run.
For example, the following:
let myVar = "foo";
cy.log(myVar) // prints "foo"
.then(() => {
myVar = "bar";
})
.log(myVar) // prints "foo"
To avoid this, this easiest way using aliases would be to only use cy.log() inside of a command in a Cypress chain, so it isn't enqueued before the value is set.
cy.get('.date-time').invoke("text").as("oldtext")
cy.get('#oldtext').then((oldText) => {
cy.log(oldText);
});
If you need to compare the oldText and newText, you can do so by nesting the cy.get() commands.
cy.get('#oldText').then((oldText) => {
cy.get('#newText').then((newText) => {
cy.log(oldText);
cy.log(newText);
});
});
If you instead did not want to use aliases at all, you could store the values as Cypress.env() variables.
cy.get('.date-time').invoke("text").then((oldText) => {
Cypress.env('oldText', oldText);
});
// changes
cy.get('.date-time').invoke("text").then((newText) => {
Cypress.env('newText', newText);
});
cy.then(() => {
expect(Cypress.env('oldText')).to.not.eql(Cypress.env('newText'));
});

How to query values from elements on web page and pair them up

I have a list of products on a web page some of which have discounts. I want to get the old price and new price as pairs so I can compare the values, calculate the discount etc. I'm having trouble joining the values and returning them out of the selectors.
The HTML is something like <span class="el1"></span><span class="el2"></span>.
My test is
cy.get('.el1').then($el1 => {
cy.get('.el2').then($el2 => {
return [$el1, $el2]
})
})
// analyze pairs here
A .map() on the element lists will give you pairs.
Once you have the result, put it in an alias.
cy.get('.el1').then($el1 => {
cy.get('.el2').then($el2 => {
const pairs = [...$el1].map((e, i) => {
return [+e.innerText, +[...$el2][i].innerText];
})
cy.wrap(pairs).as('pairs')
})
})
cy.get('#pairs')
.then(pairs => {
// analyse the pairs
})

How to override 'data-testid' in the 'findByTestId function from Cypress Testing Library

Most of my existing codebase uses a 'id' only in few places 'data-testId' attribute present.
tried this code
import { configure } from '#testing-library/cypress';
configure({ testIdAttribute: ['data-testId','id'] });
But, still its not working.
Is there any way to use 'id' value in any of the testing-library functions.
My HTML code is something like:
<div class="some random class name" id="userprofile-open" role="button">SB</div>
I want click that element with this code:
cy.findByTestId("userprofile-open", { timeout: 120000 }).click();
I don't think you can configure testing-library with an array of ids, ref API configuration,
import { configure } from '#testing-library/cypress'
configure({ testIdAttribute: 'id' })
But even this fails. Instead you have to use the Cypress command to change the attribute name (only one name is allowed).
cy.configureCypressTestingLibrary({ testIdAttribute: 'id' })
To use either/or attribute name you can change the attribute name on the fly, wrapping it in a custom command (based on Custom Queries)
Cypress.Commands.add('findByTestIdOrId', (idToFind) => {
let result;
const { queryHelpers } = require('#testing-library/dom');
let queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'data-testId');
result = queryAllByTestId(Cypress.$('body')[0], idToFind)
if (result.length) return result;
queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'id');
result = queryAllByTestId(Cypress.$('body')[0], idToFind);
if (result.length) return result;
throw `Unable to find an element by: [data-test-id="${idToFind}"] or [id="${idToFind}"]`
})
cy.findByTestIdOrId('my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
Note this custom command works only for synchronous DOM.
If you need to have Cypress retry and search for either/or attribute, don't use testing-library in the custom command.
Instead use Cypress .should() to enable retry
Cypress.Commands.add('findByTestIdOrId', (selector, idToFind) => {
cy.get(selector)
.should('satisfy', $els => {
const attrs = [...$els].reduce((acc, el) => {
const id = el.id || el.getAttribute('data-test-id') // either/or attribute
if (id) {
acc.push(id)
}
return acc
}, [])
return attrs.some(attr => attr === idToFind); // retries when false
})
.first(); // may be more than one
})
cy.findByTestIdOrId('div', 'my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
The usual cypress way - which has an inherent check on the element visibility and existence as well as included retries for a period of time is using cy.get()
If you want to select element using property like data-id you need this sintax: cy.get('[propertyName="propertyValue"]')
If you want select an element by CSS selector you just pass CSS selector like this:
cy.get('#id')

cypress: How can manage the application flow, if the element xpath is not present

I have the following scenario:
if the element is present, i have to do one activity and if not present will do another activity.
cy.xpath("//div[text()= 'button').its('length').then(res=> {
if (res > 0) {
return 1;
}
else {
cy.log ("Element is not present")
}
}
)} '''
if element is present = Code is working fine,
if the element xpath is not present = it try to search the element xpath (//div[text()= 'button') and throwing the error as 'Timed out retrying: Expected to find element: undefined, but never found it.'
if element is not present, Is there any way, i can handle the code ,
When using xpath you can (sort of) make it conditional by wrapping the xpath selector with count().
cy.xpath("count(//div[text()= 'button'])") // ok with async content
.then(count => {
if (count) {
//return 1; // not useful, returns a "chainer"
// ...but you can perform the required test here, e.g
cy.xpath("//div[text()= 'button']").click()
} else {
cy.log('not found')
}
})
The shorter syntax using built-in jQuery might be
const exists = !!Cypress.$("div:contains('button')").length
if (exists) {
cy.xpath("//div[text()= 'button']").click()
} else {
cy.log('not found')
}
Note that this is a partial match to 'button', where-as the xpath syntax is an exact match.
Also note - using Cypress.$ by-passes retry-ability, so it should not be used where the text is asynchronous.
From docs
This is a great way to synchronously query for elements when debugging from Developer Tools.
The implication is that it's more for debugging after the page has loaded.
The best practice is to try to construct the test and the app's data in such a way that you know that the button is present.
You can do something like this. With Cypress.$, you can validate the presence of the element with the help of the length attribute and then do further actions.
cy.get('body').then($body => {
const ele = $body.find('selector');
if (Cypress.$(ele).length == 0) {
//Do Something element is not present
}
else if (Cypress.$(ele).length > 0) {
//Do Something when element is present
}
})

Excel DNA attempting to write array from UDF only writes single value

I am attempting to write either an array function or a dynamic array to Excel via a UDF written in Excel DNA (v.0.34). My result is always a single value instead of the array. What am I doing wrong?
[ExcelFunction(Name = "WriteTestArray")]
public static object[,] WriteTestArray()
{
try
{
return new object[2, 2] { { "one", "two" }, { "three", "four" } };
}
catch
{
return new object[,] { { ExcelError.ExcelErrorValue } };
}
}
For array functions to work with Excel (before the 'dynamic array' support that comes one day in a future version) you need to select the target range, then type in your formula and press Ctrl+Shift+Enter to commit it as an array formula. It will be indicated by curly brackets when displayed - e.g. {=MyFunc(...)}

Resources