The Problem
I need a way to cy.get elements by trying multiple selectors with retries/timeouts. I'm aware this is not really the intended way to use Cypress and I've read Conditional Testing
but unfortunately this is a requirement. It's intended to provide fallback identifiers.
My attempts so far haven't worked well. I've tried using 'normal' javascript promises with async/await but then cypress complains about mixing promises and commands.
I've commented some test cases below with my expectations and what actually happens.
Example HTML
<!-- example.html -->
<button id="button-1" onclick="this.style.color='red'">my button</button>
Test Cases / My Function
beforeEach(() => {
cy.visit('./example.html')
})
function getWithMultipleSelectors(selectors) {
return new Cypress.Promise(resolve => {
cy.wrap(selectors).each(selector => {
getWithRetries(selector).then(element => {
if (element) resolve(element)
})
// how do I exit out of this .each() early?
// I only know if I found something inside .then() so I can't just do `return false`
})
})
}
function getWithRetries(selector, retries = 3) {
return new Cypress.Promise(resolve => {
cy.wrap([...Array(retries).keys()]).each(attempt => {
cy.log(`attempt nr ${attempt}`)
const element = cy.$$(selector)
cy.log(element, selector)
if (element.length === 1) {
resolve(element[0])
return false // ends .each()
}
cy.wait(1000) // wait before next attempt
})
})
}
// just a sanity check that the button can indeed be found
it('normal get function finds #button-1', () => {
cy.get('#button-1').should('exist').click()
})
// to see what happens if you check existence of null or undefined
// as expected they are considered to not exist
it('cy.wrap() null and undefined', () => {
cy.wrap(undefined).should('not.exist')
cy.wrap(null).should('not.exist')
})
// ends with "expected undefined to exist in the DOM" which somehow passes
// but fails when trying to click()
it('finds the button with one selector', () => {
getWithMultipleSelectors(['#button-1']).then(element => {
cy.wrap(element).should('exist').click()
})
})
// ends with "expected undefined to exist in the DOM" which somehow passes
// but fails when trying to click()
it('finds the button with two selectors', () => {
getWithMultipleSelectors(['#does-not-exist', '#button-1']).then(element => {
cy.wrap(element).should('exist').click()
})
})
// this test should FAIL but it doesn't
it('fails if no selector matches', () => {
getWithMultipleSelectors(['#does-not-exist']).then(element => {
cy.wrap(element).should('not.exist').click()
})
})
Versions Used
Cypress package version: 7.5.0
Cypress binary version: 7.5.0
Electron version: 12.0.0-beta.14
Bundled Node version:
14.15.1
Added this function that seems to be working for me for getting an element by any of the selectors in an array.
Cypress.Commands.add('getMulti', (selectors) => {
cy.document().then(($document) => {
selectors.forEach(selector => {
if($document.querySelector(selector)){
return cy.get(selector).first()
}
})
})
})
Related
So, i am new to RXJS, and i have checked a lot of stackoverflow and documentation before coming here and asking this, but i'm finding a hard time to make my logic work.
I have an Observable that will fetch a collection of documents and return them, and i use the pipe operator to make some changes, like using the map operator to change the object. So far, everything is fine.
The problem is here. Afterward, i need to run an "http request" for every document, in order to get specific data about them ("tags"). The http request is of course made as an Observable too, that needs to get subscribed on to fetch the data. However, the subscription takes some time, and the resulting object afterward doesn't have the required data.
let myFunction.pipe(
// mapping to add missing data needed for the front-end
map((results) => ({
...results,
documents: results._embedded.documents.map((document) => ({
...document,
tags: []
})),
})),
// mapping to loop through each document, and use the observable to get the tags with the document id
map((results) => {
let documents = results.documents.map((document: Document) => {
// get Tags for each document
let tagsToReturn = []
this.getDocumentTags(document.id)
.pipe(
// map function to return only the ids for each document, and not the complete tag object
map((tagsArray) => {
const modifiedTagsArray = tagsArray.map((tagObject: any) => {
if (tagObject !== undefined) {
return tagObject.id
}
})
return modifiedTagsArray
})
)
// the subscription to "actually" get the tags
.subscribe((tagsArray: number[]) => {
// Here the tags are found, but the latter code is executed first
// document.tags = tagsArray
tagsToReturn = tagsArray
})
// console.log(JSON.stringify(document))
// Here the tags are not found yet
console.log(JSON.stringify(tagsToReturn))
return { ...document, tags: tagsToReturn }
})
// I then, normally return the new documents with the tags for each document, but it is empty because the subscribe didn't return yet.
return {
_links: results._links,
page: results.page,
documents: documents,
}
}),
map((results) => {
results.documents.forEach((doc) => {
return this.addObservablesToDocument(doc)
})
return results
})
)
I have tried some solutions with switchmap, forkjoin, concat...etc but it didn't work, or i didn't find the correct way to use them. This is why i'm asking if there is a way to stop or another way to handle this problem.
I have tried using different operators like: mergemap, concat, switchmap to swich to the new request, but afterward, i can't have the global object.
I mostly tried to replicate/readapt this in some ways
By using mergemap combined with forkjoin, i was able to replicate what you were looking for.
Not really sure of how i can explain this, because i'm also not an expert coming to Rxjs, but i used the code from : this stackoverflow answer that i adapted
How i understand it is that, when using mergeMap in the pipe flow, you make sur that everything that get returned there, will be executed by the calling "subscribe()",then the mergeMap returns a forkJoin which is an observable for each document tags
I hope this can help
.pipe(
// mapping to add missing data needed for the front-end
map((results) => ({
...results,
documents: results._embedded.documents.map((document) => ({
...document,
tags: []
})),
})),
/******** Added Code *********/
mergeMap((result: ResultsNew<Document>) => {
let allTags = result._embedded.documents.map((document) =>
this.getDocumentTags(document.id).pipe(
map((tagsArray) => tagsArray.map((tagObject: any) => tagObject.id))
)
)
return forkJoin(...allTags).pipe(
map((idDataArray) => {
result._embedded.documents.forEach((eachDocument, index) => {
eachDocument.tags = idDataArray[index]
})
return {
page: result.page,
_links: result._links,
documents: result._embedded.documents,
}
})
)
}),
/******** Added Code *********/
map((results) => {
results.documents.forEach((doc) => {
return this.addObservablesToDocument(doc)
})
return results
})
)
I am trying to verify if the user is inside that list that I capture by axios, the issue is that I have used the FILTER option but it always returns undefined or [], being that if the user exists in that array.
I can't think what else to do, because I validate if it is by console.log() the variable with which I ask and if it brings data.
created() {
this.getStagesDefault()
this.getSalesman()
this.getStagesAmountByUser()
},
methods: {
async getSalesman(){
const { data } = await axios.get('salesman')
this.employees = data.data
},
getStagesAmountByUser(){
console.log(this.user['id'])
var objectUser = this.employees.filter(elem => {
return elem.id === this.user['id']
})
console.log(objectUser)
},
Console
Vue data
The method getSalesman is asynchronous, meaning that getStagesAmountByUser will start executing before getSalesman finishes.
Two ways to fix the problem:
Await the getSalesman method, but you have to make the created method async as well. Change the code as follows:
async created() {
this.getStagesDefault()
await this.getSalesman()
this.getStagesAmountByUser()
}
Attach a .then to the getSalesman function, and start the next one inside the .then. Change the code as follows:
created() {
this.getStagesDefault()
this.getSalesman().then(() => this.getStagesAmountByUser())
}
getSalesman is an async method. At the time of the filter, the array being filtered is still empty.
this.getSalesman() // this runs later
this.getStagesAmountByUser() // this runs right away
Have the methods run sequentially by awaiting the async method:
await this.getSalesman()
this.getStagesAmountByUser()
You can avoid the inefficient clientside filtering if you pass the id to the backend and only select by that id.
Additionally, created only gets called once unless you destroy the component which is also inefficient, so watch when user.id changes then call your method again.
Plus don't forget you must wrap any async code in a try/catch else you will get uncaught errors when a user/salesman is not found etc, you can replace console.error then with something which tells the user the error.
{
data: () => ({
employee: {}
}),
watch: {
'user.id' (v) {
if (v) this.getEmployee()
}
},
created() {
this.getEmployee()
},
methods: {
getEmployee() {
if (typeof this.user.id === 'undefined') return
try {
const {
data
} = await axios.get(`salesman/${this.user.id}`)
this.employee = data.data
} catch (e) {
console.error(e)
}
}
}
}
My question is, how do you override the variable in the async function which is out of the scope of this?
I read here that the problem is the lack of a callback. After adding the callback, the variable outside the scope in which it was changed (but still the variable itself is in the correct scope) returns "undefined". What am I doing wrong?
Test:
const savedVariableCallback = (variable) =>
console.log(`Variable saved ${variable}`);
describe(() => {
...
it("Sample input type", () => {
let fixValue;
cy.fixture("example.json").then(({ email }) => {
actionsPage
.selectSampleInput()
.then((input) => {
checkAmountOfElements(input, 1);
checkVisiblity(input);
})
.type(email)
.then((input) => {
checkIfValue(input, email);
fixValue = "Nice work";
savedVariableCallback(fixValue);
});
});
cy.log(`fixValue is: ${fixValue}`);
});
})
I expect the first log to show Variable saved Nice work and the second log to show fixValue is: Nice work for variables. But for now, I get in the first log Variable saved Nice work but in second I get undefined.
I want to have that variable to be accessible in it() method scope.
Edit: Since the reference didn`t work I suggest approaching it with an allias
const savedVariableCallback = (variable) =>
console.log(`Variable saved ${variable}`);
describe(() => {
...
it("Sample input type", () => {
cy.fixture("example.json").then(({ email }) => {
actionsPage
.selectSampleInput()
.then((input) => {
checkAmountOfElements(input, 1);
checkVisiblity(input);
})
.type(email)
.then((input) => {
checkIfValue(input, email);
let fixValue = "Nice work";
savedVariableCallback(fixValue);
cy.wrap(fixValue).as('fixValue')
});
});
cy.get('#fixValue')
.then(fixValue => {
cy.log(`fixValue is: ${fixValue.value}`);
})
});
})
If you change
cy.log(`fixValue is: ${fixValue}`)
to
console.log(`fixValue is: ${fixValue}`)
you can see the order of logging
fixValue is: undefined
Variable saved Nice work
so cy.log() grabs the value of fixValue at the time the command is added to the queue, that is before cy.fixture() has run, even though it actually runs after the cy.fixture().
You can defer it until cy.fixture() is complete by adding another .then()
cy.fixture("example.json").then(data => {
...
})
.then(() => {
// enqueued after the fixture code
cy.log(`fixValue is: ${fixValue}`) // now it sees the changed value
})
but of course everything downstream that needs to use fixValue must be inside the .then() callback.
You can also defer the "grabbing" of the value
cy.then(() => cy.log(`fixValue is: ${fixValue}`))
or split cy.fixture() into a before(), which will resolve the variable before the test begins
let fixValue;
before(() => {
cy.fixture("example.json").then(data => {
...
fixValue = ...
})
})
it('tests', () => {
cy.log(`fixValue is: ${fixValue}`) // sees the changed value
})
This is a follow up question to this question.
I have a function called assertTruthy for Jest. assertTruthy(msg, fn, args), expects a message, a function and arguments and should pass if the thing that is returned when the function is invoked with the arguments is truthy and fail if its not.
I want to extend it to also support Jest's only and skip.
Here is what I wrote:
assertTruthy.skip = ({
message = '',
fn = undefined,
args,
} = {}) => {
it.skip(message, () => {
expect(fn(args)).toBeTruthy();
});
};
assertTruthy.only = ({
message = '',
fn = undefined,
args,
} = {}) => {
it.only(message, () => {
expect(fn(args)).toBeTruthy();
});
};
How would I test these functions?
Here is what I tried, which works, but I'm not sure if this is correct.
describe('skip()', () => {
test('it skips the function', () => {
it.skip = jest.fn();
assertTruthy.skip({
message: 'something',
fn: () => true,
args: undefined,
});
expect(it.skip).toHaveBeenCalledTimes(1);
});
});
This looks like a fair enough test that your assertTruthy skip and only call Jest's it skip and only methods.
You might want to assert that it also calls them with the arguments you expect using toHaveBeenCalledWith.
Can you please elaborate more, what you want to achieve and I did't get it properly why you want to extend jest skip and only methods to achieve the same thing that you are already testing.
But if you only want to test if a function not been invoked/executed with toHaveBeenCalledTimes() based on the arguments to be truthy/falsy then you are doing it right.
I'm having issues getting Chai's expect.to.throw to work in a test for my node.js app. The test keeps failing on the thrown error, but If I wrap the test case in try and catch and assert on the caught error, it works.
Does expect.to.throw not work like I think it should or something?
it('should throw an error if you try to get an undefined property', function (done) {
var params = { a: 'test', b: 'test', c: 'test' };
var model = new TestModel(MOCK_REQUEST, params);
// neither of these work
expect(model.get('z')).to.throw('Property does not exist in model schema.');
expect(model.get('z')).to.throw(new Error('Property does not exist in model schema.'));
// this works
try {
model.get('z');
}
catch(err) {
expect(err).to.eql(new Error('Property does not exist in model schema.'));
}
done();
});
The failure:
19 passing (25ms)
1 failing
1) Model Base should throw an error if you try to get an undefined property:
Error: Property does not exist in model schema.
You have to pass a function to expect. Like this:
expect(model.get.bind(model, 'z')).to.throw('Property does not exist in model schema.');
expect(model.get.bind(model, 'z')).to.throw(new Error('Property does not exist in model schema.'));
The way you are doing it, you are passing to expect the result of calling model.get('z'). But to test whether something is thrown, you have to pass a function to expect, which expect will call itself. The bind method used above creates a new function which when called will call model.get with this set to the value of model and the first argument set to 'z'.
A good explanation of bind can be found here.
As this answer says, you can also just wrap your code in an anonymous function like this:
expect(function(){
model.get('z');
}).to.throw('Property does not exist in model schema.');
And if you are already using ES6/ES2015 then you can also use an arrow function. It is basically the same as using a normal anonymous function but shorter.
expect(() => model.get('z')).to.throw('Property does not exist in model schema.');
This question has many, many duplicates, including questions not mentioning the Chai assertion library. Here are the basics collected together:
The assertion must call the function, instead of it evaluating immediately.
assert.throws(x.y.z);
// FAIL. x.y.z throws an exception, which immediately exits the
// enclosing block, so assert.throw() not called.
assert.throws(()=>x.y.z);
// assert.throw() is called with a function, which only throws
// when assert.throw executes the function.
assert.throws(function () { x.y.z });
// if you cannot use ES6 at work
function badReference() { x.y.z }; assert.throws(badReference);
// for the verbose
assert.throws(()=>model.get(z));
// the specific example given.
homegrownAssertThrows(model.get, z);
// a style common in Python, but not in JavaScript
You can check for specific errors using any assertion library:
Node
assert.throws(() => x.y.z);
assert.throws(() => x.y.z, ReferenceError);
assert.throws(() => x.y.z, ReferenceError, /is not defined/);
assert.throws(() => x.y.z, /is not defined/);
assert.doesNotThrow(() => 42);
assert.throws(() => x.y.z, Error);
assert.throws(() => model.get.z, /Property does not exist in model schema./)
Should
should.throws(() => x.y.z);
should.throws(() => x.y.z, ReferenceError);
should.throws(() => x.y.z, ReferenceError, /is not defined/);
should.throws(() => x.y.z, /is not defined/);
should.doesNotThrow(() => 42);
should.throws(() => x.y.z, Error);
should.throws(() => model.get.z, /Property does not exist in model schema./)
Chai Expect
expect(() => x.y.z).to.throw();
expect(() => x.y.z).to.throw(ReferenceError);
expect(() => x.y.z).to.throw(ReferenceError, /is not defined/);
expect(() => x.y.z).to.throw(/is not defined/);
expect(() => 42).not.to.throw();
expect(() => x.y.z).to.throw(Error);
expect(() => model.get.z).to.throw(/Property does not exist in model schema./);
You must handle exceptions that 'escape' the test
it('should handle escaped errors', function () {
try {
expect(() => x.y.z).not.to.throw(RangeError);
} catch (err) {
expect(err).to.be.a(ReferenceError);
}
});
This can look confusing at first. Like riding a bike, it just 'clicks' forever once it clicks.
examples from doc... ;)
because you rely on this context:
which is lost when the function is invoked by .throw
there’s no way for it to know what this is supposed to be
you have to use one of these options:
wrap the method or function call inside of another function
bind the context
// wrap the method or function call inside of another function
expect(function () { cat.meow(); }).to.throw(); // Function expression
expect(() => cat.meow()).to.throw(); // ES6 arrow function
// bind the context
expect(cat.meow.bind(cat)).to.throw(); // Bind
One other possible implementation, more cumbersome than the .bind() solution, but one that helps to make the point that expect() requires a function that provides a this context to the covered function, you can use a call(), e.g.,
expect(function() {model.get.call(model, 'z');}).to.throw('...');
I have found a nice way around it:
// The test, BDD style
it ("unsupported site", () => {
The.function(myFunc)
.with.arguments({url:"https://www.ebay.com/"})
.should.throw(/unsupported/);
});
// The function that does the magic: (lang:TypeScript)
export const The = {
'function': (func:Function) => ({
'with': ({
'arguments': function (...args:any) {
return () => func(...args);
}
})
})
};
It's much more readable then my old version:
it ("unsupported site", () => {
const args = {url:"https://www.ebay.com/"}; //Arrange
function check_unsupported_site() { myFunc(args) } //Act
check_unsupported_site.should.throw(/unsupported/) //Assert
});