Saving a value and using it later in the command chain - cypress

I'm testing a drag scenario, want to get an elements position and use the value in a mousemove. I though that Cypress.env() was a way to save values, but it appears empty in the mousemove parameters. How do I save the initial data?
cy.get('.draggable')
.then($el => { Cypress.env('start', $el[0].getBoundingClientRect()) })
.trigger('mousemove', , { pageX: Cypress.env('start').x +20 })

The test commands take their values as the test is run, but the commands run later (asynchronously), so Cypress.env('start') is not yet defined when the mousemove command is queued.
You can add a .then() to delay queueing that command, and it will read the latest value.
cy.get('.draggable')
.then($el => { Cypress.env('start', $el[0].getBoundingClientRect()) })
.then($el => {
cy.wrap($el).trigger('mousemove', { pageX: Cypress.env('start').x +20 })
})

Related

Cypress returning Synchronous value within Async command?

So I think this is probably me mixing up sync/async code (Mainly because Cypress has told me so) but I have a function within a page object within Cypress that is searching for customer data. I need to use this data later on in my test case to confirm the values.
Here is my function:
searchCustomer(searchText: string) {
this.customerInput.type(searchText)
this.searchButton.click()
cy.wait('#{AliasedCustomerRequest}').then(intercept => {
const data = intercept.response.body.data
console.log('Response Data: \n')
console.log(data)
if (data.length > 0) {
{Click some drop downdowns }
return data < ----I think here is the problem
} else {
{Do other stuff }
}
})
}
and in my test case itself:
let customerData = searchAndSelectCustomerIfExist('Joe Schmoe')
//Do some stuff with customerData (Probably fill in some form fields and confirm values)
So You can see what I am trying to do, if we search and find a customer I need to store that data for my test case (so I can then run some cy.validate commands and check if the values exist/etc....)
Cypress basically told me I was wrong via the error message:
cy.then() failed because you are mixing up async and sync code.
In your callback function you invoked 1 or more cy commands but then
returned a synchronous value.
Cypress commands are asynchronous and it doesn't make sense to queue
cy commands and yet return a synchronous value.
You likely forgot to properly chain the cy commands using another
cy.then().
So obviously I am mixing up async/sync code. But since the return was within the .then() I was thinking this would work. But I assume in my test case that doesn't work since the commands run synchronously I assume?
Since you have Cypress commands inside the function, you need to return the chain and use .then() on the returned value.
Also you need to return something from the else branch that's not going to break the code that uses the method, e.g an empty array.
searchCustomer(searchText: string): Chainable<any[]> {
this.customerInput.type(searchText)
this.searchButton.click()
return cy.wait('#{AliasedCustomerRequest}').then(intercept => {
const data = intercept.response.body.data
console.log('Response Data: \n')
console.log(data)
if (data.length) {
{Click some drop downdowns }
return data
} else {
{Do other stuff }
return []
}
})
}
// using
searchCustomer('my-customer').then((data: any[]) => {
if (data.length) {
}
})
Finally "Click some drop downdowns" is asynchronous code, and you may get headaches calling that inside the search.
It would be better to do those actions after the result is passed back. That also makes your code cleaner (easier to understand) since searchCustomer() does only that, has no side effects.
you just need to add return before the cy.wait
here's a bare-bones example
it("test", () => {
function searchCustomer() {
return cy.wait(100).then(intercept => {
const data = {text: "my data"}
return data
})
}
const myCustomer = searchCustomer()
myCustomer.should("have.key", "text")
myCustomer.its("text").should("eq", "my data")
});

Cypress access alias with this.* doesn't works even with function

Trying to access the alias value on the this () Context, I followed the docs from https://docs.cypress.io/api/commands/as#Fixture
In the .then callback the dataExample parameter is filled OK, but on this.example it is undefined.
describe('cy.as and this', function () {
it('Testing cy.as and this', function () {
cy.fixture('example.json').as('example')
.then(function (dataExample) {
cy.log('ACCESSING this, dataExample', this, dataExample);
});
cy.log(`ACCESSING this.example : ${JSON.stringify(this['example'])}`, this);
});
});
The first log outputs the following: with the dataExample correctly filled, but the contex.example undefined
The second log outside of .then, with this.example also undefined
If I move the cy.fixture line into a beforeEach() it does work. Cold somebody explain this behavior?
describe('alias', () => {
beforeEach(() => {
cy.fixture('example.json').as('example');
});
it('can access all aliases as properties', function () {
expect(this['example']).not.to.eq(undefined); // true
cy.log('THIS', this); // curious => [0]: Context {_runnable: undefined, test: undefined, example: undefined}
cy.log('THIS.example', this['example']); // {name: 'Using fixtures to represent data', email: 'hello#cypress.io',
// body: 'Fixtures are a great way to mock data for responses to routes'}
});
});
Cypress commands are asynchronous. Cypress does not execute a command immediately, it just puts one into a queue to be executed lately. So any command inside a single block of code will not be executed in this block. And the other block of code means any of this:
other it test
before/beforeAll/after/afterAll hooks
then/should callback
So if you want to use a value from a cypress command inside the same it test, you have to use a then callback in order to give Cypress time to execute the command.
If you use a cypress command in a beforeEach hook, the command will be executed by the time when it code starts.
A helpful way to think of the flow is that there are two passes:
first pass, runs the javascript of the test and executes plain JS but enqueues Cypress commands (and their callbacks)
second pass executes those commands
So when
cy.log(`ACCESSING this.example : ${JSON.stringify(this['example'])}`, this)
is enqueued in the first pass, the parameter this['example'] is evaluated immediately (value is undefined).
It's not storing the parameter expression to be evaluated in the second pass, which is what most people expect.
But you can defer the parameter evaluation by putting it inside another callback.
cy.then(() => {
// this callback code is enqueued
// so the evaluation of "this['example']" is deferred until the queue is running
cy.log(`ACCESSING this.example : ${JSON.stringify(this['example'])}`, this)
})
Gleb Bahmutov has a blog
How to replace the confusing cy.then command with the less confusing cy.later command

Test that an API call does NOT happen in Cypress

I've implemented API data caching in my app so that if data is already present it is not re-fetched.
I can intercept the initial fetch
cy.intercept('**/api/things').as('api');
cy.visit('/things')
cy.wait('#api') // passes
To test the cache is working I'd like to explicitly test the opposite.
How can I modify the cy.wait() behavior similar to the way .should('not.exist') modifies cy.get() to allow the negative logic to pass?
// data is cached from first route, how do I assert no call occurs?
cy.visit('/things2')
cy.wait('#api')
.should('not.have.been.called') // fails with "no calls were made"
Minimal reproducible example
<body>
<script>
setTimeout(() =>
fetch('https://jsonplaceholder.typicode.com/todos/1')
}, 300)
</script>
</body>
Since we test a negative, it's useful to first make the test fail. Serve the above HTML and use it to confirm the test fails, then remove the fetch() and the test should pass.
The add-on package cypress-if can change default command behavior.
cy.get(selector)
.if('exist').log('exists')
.else().log('does.not.exist')
Assume your API calls are made within 1 second of the action that would trigger them - the cy.visit().
cy.visit('/things2')
cy.wait('#alias', {timeout:1100})
.if(result => {
expect(result.name).to.eq('CypressError') // confirm error was thrown
})
You will need to overwrite the cy.wait() command to check for chained .if() command.
Cypress.Commands.overwrite('wait', (waitFn, subject, selector, options) => {
// Standard behavior for numeric waits
if (typeof selector === 'number') {
return waitFn(subject, selector, options)
}
// Modified alias wait with following if()
if (cy.state('current').attributes.next?.attributes.name === 'if') {
return waitFn(subject, selector, options).then((pass) => pass, (fail) => fail)
}
// Standard alias wait
return waitFn(subject, selector, options)
})
As yet only cy.get() and cy.contains() are overwritten by default.
Custom Command for same logic
If the if() syntax doesn't feel right, the same logic can be used in a custom command
Cypress.Commands.add('maybeWaitAlias', (selector, options) => {
const waitFn = Cypress.Commands._commands.wait.fn
// waitFn returns a Promise
// which Cypress resolves to the `pass` or `fail` values
// depending on which callback is invoked
return waitFn(cy.currentSubject(), selector, options)
.then((pass) => pass, (fail) => fail)
// by returning the `pass` or `fail` value
// we are stopping the "normal" test failure mechanism
// and allowing downstream commands to deal with the outcome
})
cy.visit('/things2')
cy.maybeWaitAlias('#alias', {timeout:1000})
.should(result => {
expect(result.name).to.eq('CypressError') // confirm error was thrown
})
I also tried cy.spy() but with a hard cy.wait() to avoid any latency in the app after the route change occurs.
const spy = cy.spy()
cy.intercept('**/api/things', spy)
cy.visit('/things2')
cy.wait(2000)
.then(() => expect(spy).not.to.have.been.called)
Running in a burn test of 100 iterations, this seems to be ok, but there is still a chance of flaky test with this approach, IMO.
A better way would be to poll the spy recursively:
const spy = cy.spy()
cy.intercept('**/api/things', spy)
cy.visit('/things2')
const waitForSpy = (spy, options, start = Date.now()) => {
const {timeout, interval = 30} = options;
if (spy.callCount > 0) {
return cy.wrap(spy.lastCall)
}
if ((Date.now() - start) > timeout) {
return cy.wrap(null)
}
return cy.wait(interval, {log:false})
.then(() => waitForSpy(spy, {timeout, interval}, start))
}
waitForSpy(spy, {timeout:2000})
.should('eq', null)
A neat little trick I learned from Gleb's Network course.
You will want use cy.spy() with your intercept and use cy.get() on the alias to be able to check no calls were made.
// initial fetch
cy.intercept('**/api/things').as('api');
cy.visit('/things')
cy.wait('#api')
cy.intercept('METHOD', '**/api/things', cy.spy().as('apiNotCalled'))
// trigger the fetch again but will not send since data is cached
cy.get('#apiNotCalled').should('not.been.called')

how to subscribe to the last observable and execute a funtion in rxjs?

Hi I am having a small code snippet in my project .
it basically subscribe to an angular form group and calls a method generatePerformanceGraph that draws an svg.
the form group has around 6 form controls . the problem is some times when i change the value of a form control , it will set another values to another form controls. As a result when i change some form control value it causes generatePerformanceGraph to called multiple times . How can i prevent this problem .
in short basically what i want is when there is a change in form group ,I would like to subscribe to the last observable and then execute the generatePerformanceGraph once.
this.formGroup.valueChanges.subscribe(formValue => {
if(this.formGroup.valid) {
this.generatePerformanceGraph(formValue);
}
});
I have tried the following how ever it didnt work out well.
this.formGroup.valueChanges.
pipe(
distinctUntilChanged()
) .subscribe( formValue => {
if(this.formGroup.valid) {
this.generatePerformanceGraph(formValue);
}
});
Try debounceTime, the time is up to you in ms. The debounceTime ignores events that happen within 200ms of each other and only accept the last one.
https://www.learnrxjs.io/learn-rxjs/operators/filtering/debouncetime
this.formGroup.valueChanges.
pipe(
debounceTime(200),
) .subscribe( formValue => {
if(this.formGroup.valid) {
this.generatePerformanceGraph(formValue);
}
});
To go the distinctUntilChanged way, you have to do it a different way because formValue is an object.
this.formGroup.valueChanges.
pipe(
distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)),
) .subscribe( formValue => {
if(this.formGroup.valid) {
this.generatePerformanceGraph(formValue);
}
});

Need correct call to Promise reduce (when.reduce )

I have a processor function that takes a "cmd" object and returns a promise where the resolution is the same "cmd" object passed in (with a response key added). reduce here is when.reduce
reduce = require('when').reduce;
//return processor(cmds[0])
return reduce(cmds, function(processor, cmd) {
Debug.L1('running processor for component ', cmd.component)
return processor(cmd)
})
.then(cmds => {
Debug.L1('cmds with responses\n', cmds)
let response = cmds.map(cmd => {
return cmd.response
})
console.log('the complete response is\n', response)
});
This does nothing, it does get to the .then but the array of promises never fires, never see the Debug running processor...
If I run just a single processor it works great cmd[0], cmds[1], etc.
return processor(cmds[0])
//return reduce(cmds, function(processor,cmd) {
// Debug.L1('running processor for component ', cmd.component)
// return processor(cmd) })
What am I missing here? Their api and wiki examples aren't giving me any insight.
IMPORTANT UPDATE:
The answer below does work but throws unhandled rejection errors. The culprit is the when library. It seems no longer active and has not been updated since node 6. I switched to bluebird and it works fine without any change to the code outlined below.
I'm still not sure what you are looking for, but it might be
reduce(cmds, function(responses, cmd) {
return processor(cmd).then(function(response) {
responses.push(response); // or whatever
return responses;
});
}, []).then(function(responses) {
…
});
Before trying to understand when.reduce, you might want to have a look at the non-promise array reduce.

Resources