I have a function in Cypress support/index.js that is meant to get the dimensions of the cy.document
outerWidth
and outerHeight
, then return them for future use in a test. My problem is that when the test runs and the values are compared with others the assertion says the values are NaN
. I checked by console logging the value at the point of the assertion and it was empty, so I must be doing something wrong, I'm just not sure what. My function is below, any help gratefully received, thanks.
function getViewport() {
var viewport = {}
cy.document().then((doc) => {
let width = Cypress.$(doc).outerWidth()
let height = Cypress.$(doc).outerHeight()
viewport['bottom'] = height
viewport['height'] = height
viewport['left'] = 0
viewport['right'] = width
viewport['top'] = 0
viewport['width'] = width
viewport['x'] = 0
viewport['y'] = 0
}).then(() => {
return viewport
})
return viewport
}
The code that calls getViewport()
is
export const getRect = (obj) => {
var rect
if (obj == 'viewport') {
rect = getViewport()
} else {
rect = getElement(obj)
if (Cypress.config('parseLayoutToInt')) { rect = parseAllToInt(rect) }
}
return rect
}
And that is called by a custom command, where subject
is prevSubject
and the element is the string "viewport"
Cypress.Commands.add('isInside', { prevSubject: true }, (subject, element, expected) => {
var minuend, subtrahend, diff
minuend = getRect(element)
subtrahend = getRect(subject)
diff = getRectDiff(minuend, subtrahend, expected);
expect(diff).to.deep.equal(expected);
})
Like @NoriSte said, the cy
commands are asynchronous thus you can't mix them with sync code.
What you want to do is something like:
function getViewport() {
return cy.document().then( doc => {
rect = /* do something synchronous */
return rect;
});
}
Anyway, to answer the original question (in the title), there's a couple of patterns I use to store a value for later use in cypress:
wrap next commands in the then
callback:
cy.document().then( doc => {
return doc.documentElement.getBoundingClientRect();
}).then( viewportRect => {
cy.doSomething(viewportRect);
cy.doSomethingElse();
});
cache to a variable and access the cached value from inside an enqueued command:
let viewportRect;
cy.document().then( doc => {
return doc.documentElement.getBoundingClientRect();
}).then( rect => viewportRect = rect );
cy.doSomething();
// this is important -- you need to access the `viewportRect`
// asynchronously, else it will be undefined at the time of access
// because it's itself assigned asynchronously in the first command'd callback
cy.then(() => {
doSomething(viewportRect);
});
Ad the actual problem in your question (if I understood it correctly), I've made a solution you can learn from:
const getRect = (selector) => {
if (selector == 'viewport') {
return cy.document().then( doc => {
return doc.documentElement.getBoundingClientRect();
});
} else if ( typeof selector === 'string' ) {
return cy.get(selector).then( $elem => {
return $elem[0].getBoundingClientRect();
});
// assume DOM elem
} else {
return cy.wrap(selector).then( elem => {
return Cypress.$(elem)[0].getBoundingClientRect();
});
}
};
const isInside = (containerRect, childRect) => {
if ( !containerRect || !childRect ) return false;
return (
childRect.top >= containerRect.top &&
childRect.bottom <= containerRect.bottom &&
childRect.left >= containerRect.left &&
childRect.right <= containerRect.right
);
};
Cypress.Commands.add('isInside', { prevSubject: true }, (child, container, expected) => {
return getRect(child).then( childRect => {
getRect(container).then( containerRect => {
expect(isInside(containerRect, childRect)).to.equal(expected);
});
});
});
describe('test', () => {
it('test', () => {
cy.document().then( doc => {
doc.body.innerHTML = `
<div class="one"></div>
<div class="two"></div>
<style>
.one, .two {
position: absolute;
}
.one {
background: rgba(255,0,0,0.3);
width: 400px;
height: 400px;
}
.two {
background: rgba(0,0,255,0.3);
width: 200px;
height: 200px;
}
</style>
`;
});
cy.get('.two').isInside('.one', true);
cy.get('.one').isInside('.two', false);
});
it('test2', () => {
cy.document().then( doc => {
doc.body.innerHTML = `
<div class="one"></div>
<div class="two"></div>
<style>
body, html { margin: 0; padding: 0 }
.one, .two {
position: absolute;
}
.one {
background: rgba(255,0,0,0.3);
width: 400px;
height: 400px;
}
.two {
background: rgba(0,0,255,0.3);
width: 200px;
height: 200px;
left: 300px;
}
</style>
`;
});
cy.get('.two').isInside('.one', false);
cy.get('.one').isInside('.two', false);
});
it('test3', () => {
cy.document().then( doc => {
doc.body.innerHTML = `
<div class="one"></div>
<style>
body, html { margin: 0; padding: 0 }
.one {
position: absolute;
background: rgba(255,0,0,0.3);
width: 400px;
height: 400px;
left: -100px;
}
</style>
`;
});
cy.get('.one').isInside('viewport', false);
});
});
Why there is a synchronous return in your getViewport
function? I'm speaking about the last return viewport
function getViewport() {
var viewport = {}
cy.document().then((doc) => {
...
})
return viewport // <-- ?????
}
doing so, all the cy.document().then((doc)
etc. code is useless.
I don't know if this is the problem, but I can't run your code locally because it misses a lot of functions. Could you share a "working” GitHub repo to make some more tests?
I ran into this problem as well, and opted for a solution with async/await:
function getDocument() {
return new Promise(resolve => {
cy.document().then(d => {
console.log('deeee', d);
resolve(d);
});
});
}
describe('Stuff', () => {
it('Sees the toasty character', async () => {
const document = await getDocument();
// Your test code here
});
});
Even though Cypress commands aren't really promises, you can create your own promise, and resolve it when ready. Then await
that promise in your test code.
Hope it helps!