Cypress: End chain conditionally - javascript

I'm quite new to cypress, but I am wondering if there is some way to end a command chain conditionally? I know that conditional testing should be avoided in cypress, but I want try this out anyway.
What I've tried
I tried to solve it by passing in the chain to a custom command, but it doesn't seem to work.
Cypress.Commands.add('ifExists', { prevSubject: true }, (subject: object, element: string) => {
cy.get('body').then(($body) => {
if ($body.find(element).length) {
cy.wrap(object).end();
} else {
// something else
}
})
})
and also
Cypress.Commands.add('ifExists', { prevSubject: true }, (subject: JQuery<HTMLBodyElement>, element: string) => {
cy.wrap(subject).then(($body) => {
if ($body.find(element).length) {
return cy.wrap(subject);
} else {
return cy.wrap(subject).end();
}
})
})
And just a clarification on what I want from this command. I want to be able to add it to a chain like this:
cy.get('body > #specialThing').ifExists().then((thing) => ...)
or this:
cy.ifExists('body > #specialThing').then((thing) => ...)
All help and advice is appreciated!

I'd check out the Cypress documentation on conditional testing, in particular the part about element existence. The tl;dr is you just need to search for a more general element that yields something that you can then assert on.
y.get('body')
.then(($body) => {
// synchronously query from body
// to find which element was created
if ($body.find('#specialThing').length) {
// input was found, do something else here
return 'input'
}
// else assume it was textarea
return 'textarea'
})
.then((selector) => {
// selector is a string that represents
// the selector we could use to find it
cy.get(selector).type(`found the element by selector ${selector}`)
})

You can adapt the jQuery OR operator shown here Function to check if element exist in cypress.
You cannot it call using the cy.get('body > #specialThing').ifExists()... because the cy.get() will fail if the element isn't there, so this implies your custom command must be a parent command (no prevSubject).
Cypress.Commands.add('ifExists', { prevSubject: false }, (selector: string) => {
const orSelector = `${selector}, body`; // note comma, if "selector" fails return body
return Cypress.$(orSelector);
})
cy.ifExists('#specialThing').then((thing) => ...)
You may find it more useful to return null rather than body so that you can test the result more easily,
Cypress.Commands.add('ifExists', { prevSubject: false }, (selector: string) => {
const result = Cypress.$(selector);
return result.length ? cy.wrap(result) : null;
})
cy.ifExists('#specialThing').then((thing) => if (thing) ... else ... )
Warning this will inevitably lead to flaky tests. There is no retry in this command.
See Element existence
You cannot do conditional testing on the DOM unless you are either:
Server side rendering with no asynchronous JavaScript.
Using client side JavaScript that only ever does synchronous rendering.
It is crucial that you understand how your application works else you will write flaky tests.
Using cy.get('body').then(...) does not give you retry on the element you want to test conditionally.

Related

How do I get a text from a div class Cypress

Alright, I want to get a text from a div and a class. The html looks like this:
<div class="inventory_item_name">Backpack</div>
And my Code is this:
const article = cy.get('.inventory_item_price').then((theElement) => {
theElement.text();
});
The problem: When I do cy.log(article) I get Object{5}
Just like Alex mentioned in the comment, you cannot return a value from cy commands. But you can do so in the .then block just like:
it("should have text of 'Backpack'", () => {
// I have changed the selector since the class name in your HTML is ".inventory_item_name" not ".inventory_item_price"
cy.get(".inventory_item_name").then(($el) => {
const text = $el.text(); // Now you have the text "Backpack"
// Do the assertion here
expect(text).to.eq("Backpack");
});
});
You can learn more about why Cypress doesn't return value in this documentation
As cypress documentation says
You cannot assign or work with the return values of any
Cypress command. Commands are enqueued and run asynchronously.
// this won't work the way you think it does
const button = cy.get('button')
const form = cy.get('form')
button.click()
For more information refer : variables-and-aliases
You have a few options. But you need to keep in mind the asynchronous nature of cypress
Use alias (if you want to share the article variable in other tests ) so the code will look like this,
Example:
it("Test 1", () => {
cy.get('.inventory_item_price').invoke('text').as('article')
})
//Now you can use this.article in all the other test cases
//If you are using aliases remember Not to use arrow functions on hooks
it("Test 2", function(){
//this will log Backpack
cy.log(this.article)
})
Use .then, As mentioned by #konekoya in the answer, Since cypress is asynchronous you need to use .then to execute a task after a previous task is completed.
Example:
cy.get('.inventory_item_price').invoke('text').then(article=> {
//you can use articale variable as much as you want inside .then
//you can't use it outside
const value = articale ;
cy.log(article)
cy.log(value )
})
Note: to get inner Text you there are several methods. You can do the same thing using,
cy.get('.inventory_item_price').then(article => {
const value = articale.text() ;
cy.log(article.text())
cy.log(value)
})
You can't assign text values like this.
const article = cy.get('.inventory_item_price').then((theElement) => {
theElement.text();
});
The reason is the bellow code does not return a text element.
cy.get('.inventory_item_price').then((theElement) => {
theElement.text();
});
So you need to change it like this.
cy.get('.inventory_item_price').then((theElement) => {
const article = theElement.text();
//not you can use article
});
You can return the article text with below code. Please also look at the link where I have shared the answer for more details.
Storing an element's text in Cypress outside of a chainer method
return cy.get('.inventory_item_price').then((theElement) => {
return theElement.text();
}).then(article => {
cy.log(`article is ${article}`);
})

flow error accessing optional prop after truthy check

flow 0.67.1 (but behavior continues to exist in 0.73.1)
Example:
type PropOptional = {
prop?: ComplexType
};
type ComplexType = {
callable: () => void,
anotherCallable: () => void
};
function usePropOptional(arg1: PropOptional) {
if (arg1.prop) {
arg1.prop.callable();
arg1.prop.anotherCallable();
arg1.prop.callable();
}
};
The function checks for the presence of arg1.prop before accessing any properties on arg1.prop. This should be sufficient to verify that arg1.prop is defined.
Flow is fine with the first time an arg1.prop property is accessed, which is the call to arg1.prop.callable() on the first line inside the if block. However, flow generates errors on subsequent attempts to access arg1.prop properties in the exact same if block:
arg1.prop.anotherCallable();
arg1.prop.callable();
I am forced to either prepend each line with a rote arg1.prop && truthy check, or reassign arg1.prop to a local variable inside the if block:
function usePropOptional(arg1: PropOptional) {
if (arg1.prop) {
const reallyExists = arg1.prop;
reallyExists.callable();
reallyExists.anotherCallable();
reallyExists.callable();
}
};
This doesn't feel right. What am I doing wrong or missing?
You can check this in a flow repl here on flow.org.
This is documented in FlowType's Type Refinement section:
Refinement Invalidations
It is also possible to invalidate refinements, for example:
// #flow
function otherMethod() { /* ... */ }
function method(value: { prop?: string }) {
if (value.prop) {
otherMethod();
// $ExpectError
value.prop.charAt(0);
}
}
The reason for this is that we don’t know that otherMethod() hasn’t
done something to our value.
...
There’s a straightforward way to get around this. Store the value
before calling another method and use the stored value instead. This
way you can prevent the refinement from invalidating.
// #flow
function otherMethod() { /* ... */ }
function method(value: { prop?: string }) {
if (value.prop) {
var prop = value.prop;
otherMethod();
prop.charAt(0);
}
}
So the workaround in your final case appears to be the suggested way to avoid this problem.

Logical OR for expected results in Jest

It will be the best explain in on example
expected(someNumber).toBe(1).or.toBe(-2).or.toBe(22) // expect result is 1 or -2 or 22
This is bad syntax, but can do sth like that in jest?
A simple way around this is to use the standard .toContain() matcher (https://jestjs.io/docs/en/expect#tocontainitem) and reverse the expect statement:
expect([1, -2, 22]).toContain(someNumber);
If you really needed to do exactly that, I suppose you could put the logical comparisons inside the expect call, e.g.
expect(someNumber === 1 || someNumber === -2 || someNumber === 22).toBeTruthy();
If this is just for a "quick and dirty" check, this might suffice.
However, as suggested by several comments under your question, there seem to be several "code smells" that make both your initial problem as well as the above solution seem like an inappropriate way of conducting a test.
First, in terms of my proposed solution, that use of toBeTruthy is a corruption of the way Jasmine/Jest matchers are meant to be used. It's a bit like using expect(someNumber === 42).toBeTruthy(); instead of expect(someNumber).toBe(42). The structure of Jest/Jasmine tests is to provide the actual value in the expect call (i.e. expect(actualValue)) and the expected value in the matcher (e.g. toBe(expectedValue) or toBeTruthy() where expectedValue and true are the expected values respectively). In the case above, the actual value is (inappropriately) provided in the expect call, with the toBeTruthy matcher simply verifying this fact.
It might be that you need to separate your tests. For example, perhaps you have a function (e.g. called yourFunction) that you are testing that provides (at least) 3 different possible discrete outputs. I would presume that the value of the output depends on the value of the input. If that is the case, you should probably test all input/output combinations separately, e.g.
it('should return 1 for "input A" ', () => {
const someNumber = yourFunction("input A");
expect(someNumber).toBe(1);
});
it('should return -2 for "input B" ', () => {
const someNumber = yourFunction("input B");
expect(someNumber).toBe(-2);
});
it('should return 22 for "input C" ', () => {
const someNumber = yourFunction("input C");
expect(someNumber).toBe(22);
});
..or at least...
it('should return the appropriate values for the appropriate input ', () => {
let someNumber;
someNumber = yourFunction("input A");
expect(someNumber).toBe(1);
someNumber = yourFunction("input B");
expect(someNumber).toBe(-2);
someNumber = yourFunction("input C");
expect(someNumber).toBe(22);
});
One of the positive consequences of doing this is that, if your code changes in the future such that, e.g. one (but only one) of the conditions changes (in terms of either input or output), you only need to update one of three simpler tests instead of the single more complicated aggregate test. Additionally, with the tests separated this way, a failing test will more quickly tell you exactly where the problem is, e.g. with "input A", "input B", or "input C".
Alternatively, you may need to actually refactor yourFunction, i.e. the code-under-test itself. Do you really want to have a particular function in your code returning three separate discrete values depending on different input? Perhaps so, but I would examine the code separately to see if it needs to be re-written. It's hard to comment on this further without knowing more details about yourFunction.
To avoid putting all the logical comparisons in one statement and using toBeTruthy(), you can use nested try/catch statements:
try {
expect(someNumber).toBe(1)
}
catch{
try {
expect(someNumber).toBe(-2)
}
catch{
expect(someNumber).toBe(22)
}
}
To make it more convenient and more readable, you can put this into a helper function:
function expect_or(...tests) {
if (!tests || !Array.isArray(tests)) return;
try {
tests.shift()?.();
} catch (e) {
if (tests.length) expect_or(...tests);
else throw e;
}
}
NB: With Typescript replace line 1 with function expect_or(...tests: (() => void)[]) { to add types to the function parameter.
and use it like this:
expect_or(
() => expect(someNumber).toBe(1),
() => expect(someNumber).toBe(-2),
() => expect(someNumber).toBe(22)
);
As #JrGiant suggested, there could be a toBeOneOf, however, it is easy top implement your own matcher:
Example in TypeScript:
expect.extend({
toBeOneOf(received: any, items: Array<any>) {
const pass = items.includes(received);
const message = () =>
`expected ${received} to be contained in array [${items}]`;
if (pass) {
return {
message,
pass: true
};
}
return {
message,
pass: false
};
}
});
// Declare that jest contains toBeOneOf
// If you are not using TypeScript, remove this "declare global" altogether
declare global {
namespace jest {
interface Matchers<R> {
toBeOneOf(items: Array<any>): CustomMatcherResult;
}
}
}
describe("arrays", () => {
describe("getRandomItemFromArray", () => {
it("should return one of the expected - 1", () => {
expect(getRandomItemFromArray([1, 2])).toBeOneOf([1, 2])
});
});
});
I was also looking for a solution for the expect.oneOf issue. You may want to checkout d4nyll's solution.
Here is an example of how it could work.
expect(myfunction()).toBeOneOf([1, -2, 22]);
I recommend using the .toContain(item) matcher. The documentation can be found here.
The below code should work well:
expect([1, -2, 22]).toContain(someNumber);

Rethinkdb create if not exists

What I'm trying to do is the following:
Check if a record with a filter criteria exists
If it does, do nothing
If it does not, create it with some default settings.
Now I could do it with 2 queries:
function ensureDocumentExists(connection, criteria, defaults) {
return r.table('tbl')
.filter(criteria)
.coerceTo('array') // please correct me if there's a better way
.run(connection)
.then(([record]) => {
if (record) {
return Promise.resolve() // Record exists, we are good
} else {
return r.table('tbl') // Record is not there we create it
.insert(defaults)
.run(connection)
}
})
}
But the fact that r.branch and r.replace exists, suggest me that this would be possible in a single run. Is it? I was thinking something like this:
function ensureDocumentExists(connection, criteria, defaults) {
return r.table('tbl')
.filter(criteria)
.replace(doc => r.branch(
r.exists(doc), // If doc exists (I'm just making this up)
doc, // Don't touch it
defaults // Else create defaults
)).run(connection)
}
But I'm not sure if replace is the right method for this, and also no idea how to check if the given row exists.
Figured it out:
function ensureDocumentExists(connection, criteria, defaults) {
return r.table('tbl')
.filter(criteria)
.isEmpty() // isEmpty for the rescue
.do(empty => r.branch(
empty, // equivalent of if(empty)
r.table('tbl').insert(defaults), // insert defaults
null // else return whatever
).run(connection)
})
}

Loop in js for specific value without if

I use the following code which is working great but I wonder if in JS there is a way to avoid the if and to do it inside the loop, I want to use also lodash if it helps
for (provider in config.providers[0]) {
if (provider === "save") {
....
You can chain calls together using _.chain, filter by a value, and then use each to call a function for each filtered result. However, you have to add a final .value() call at the end for it to evaluate the expression you just built.
I'd argue that for short, simple conditional blocks, an if statement is easier and more readable. I'd use lodash- and more specifically chaining- if you are combining multiple operations or performing sophisticated filtering, sorting, etc. over an object or collection.
var providers = ['hello', 'world', 'save'];
_.chain(providers)
.filter(function(provider) {
return provider === 'save';
}).each(function(p) {
document.write(p); // your code here
}).value();
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.8.0/lodash.js"></script>
Edit: My mistake; filter does not have an overload where you can just supply a literal value. If you want to do literal value checking you have to supply a function as in my amended answer above.
I'd argue that what you have there is pretty good, clean and readable, but since you mentioned lodash, I will give it a try.
_.each(_.filter(config.providers[0], p => p === 'save'), p => {
// Do something with p
...
});
Note that the arrow function/lambda of ECMAScript 6 doesn't come to Chrome until version 45.
Basically, you are testing to see if config.providers[0], which is an object, contains a property called save (or some other dynamic value, I'm using a variable called provider to store that value in my example code below).
You can use this instead of using a for .. in .. loop:
var provider = 'save';
if (config.providers[0][provider] !== undefined) {
...
}
Or using #initialxy's (better!) suggestion:
if (provider in config.providers[0]) {
...
}
How about:
for (provider in config.providers[0].filter(function(a) {return a === "save"}) {
...
}
Strategy, you are looking for some kind of strategy pattern as,
Currenlty the save is hardcoded but what will you do if its coming from other varible – Al Bundy
var actions = {
save: function() {
alert('saved with args: ' + JSON.stringify(arguments))
},
delete: function() {
alert('deleted')
},
default: function() {
alert('action not supported')
}
}
var config = {
providers: [{
'save': function() {
return {
action: 'save',
args: 'some arguments'
}
},
notSupported: function() {}
}]
}
for (provider in config.providers[0]) {
(actions[provider] || actions['default'])(config.providers[0][provider]())
}
Push „Run code snippet” button will shows two pop-ups - be carefull
It is not clearly stated by the original poster whether the desired output
should be a single save - or an array containing all occurrences of
save.
This answer shows a solution to the latter case.
const providers = ['save', 'hello', 'world', 'save'];
const saves = [];
_.forEach(_.filter(providers, elem => { return elem==='save' }),
provider => { saves.push(provider); });
console.log(saves);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.19/lodash.js"></script>

Categories