I’m writing a plugin for a website. It is going to add elements to the DOM that are styled via the plugin’s CSS. I expect the styling to be limited to the plugin, i.e. no elements outside the plugin should change their appearance once the plugin is included on the web page.
I’m running integration tests using cypress. How can I assert that all the pre-existing elements’ styles stay the same when the plugin is included on the page? I have access to the page before and after the plugin has been loaded.
This is what I thought should work:
cy.visit('theURL');
getStyles().then(oldStyles => { // Get the styles of the elements
mountPlugin(); // Mount the plugin (including CSS)
getStyles().then(newStyles => { // Get the (possibly changed) styles
newStyles.forEach((newStyle, i) => // Compare each element’s style after
expect(newStyle).to.equal(oldStyles[i]) //+ mounting to the state before mounting
);
});
});
function getStyles() {
return cy.get('.el-on-the-page *').then((elements) => { // Get all elements below a certain root
const styles: CSSStyleDeclaration[] = []
elements.each((_, el) => { // Get each element’s style
styles.push(window.getComputedStyle(el)); //+ and put them it an array
});
return styles; // Return the styles
});
}
The line expect(newStyle).to.equal(oldStyles[i])
fails because oldStyles[i]
contains numeric keys that only list property names. For instance,
// oldStyles[i] for some i
{
cssText: "animation-delay: 0s; animation-direction: normal; […more]"
length: 281
parentRule: null
cssFloat: "none"
0: "animation-delay" // <-- These elements only list property names, not values
... //+
280: "line-break" //+
alignContent: "normal" // <-- Only here there are actual property values
... //+
zoom: "1" //+
...
}
I fix this by looping through the CSS keys manually and checking if the key is a number. However, these numeric keys only appear in the oldStyles
, not in newStyles
. I’m writing this because this looks fishy to me, and I assume that the error might already be there.
// Instead of newStyles.foreach(…) in the first snippet
newStyles.forEach((newStyle, i) => {
for (const key in newStyle) {
if(isNaN(Number(key))) {
expect(newStyle[key]).to.equal(oldStyles[i][key]);
}
}
});
I’m making the implicit assumption here that the DOM is actually loaded and has applied the styles. From my understanding getLinkListStyles
’s call to cy.get
should be scheduled to run only after cy.visit
has waited for the window to fire the load
event.
From the Cypress documentation:
cy.visit()
resolves when the remote page fires itsload
event.
However, with the above workaround employed, I get an empty string for the CSS rules in oldStyles
. For instance:
//oldStyles[i] for some i
{
cssText: "animation-delay: ; animation-direction: ; animation-duration: ; […more]"
length: 0
parentRule: null
cssFloat: ""
alignContent: ""
...
}
Note that this behaviour does not change when I explicitly use a callback with cy.visit
, i.e.:
cy.visit(Cypress.env('theURL')).then(()=>{
getStyles().then((oldStyles) => {
// (rest as above)
Neither does cy.wait(15000)
at the beginning of getStyles()
:
function getStyles() {
cy.wait(15000); // The page has definitely loaded and applied all styles by now
cy.get('.el-on-the-page *').then((elements) => {
...
I can't answer the question about empty property values, the workaround should not affect things. If I understand correctly, you get property values when not using the workaround?
Numeric keys
These are almost certainly indexes into the cssText style which is the inline styles.
There are exactly the same number of numeric keys as there are entries in cssText
, and the values match up to the LHS of the key-value pairs in cssText
.
Missing numeric keys on 2nd getStyles()
Are you sure?
If I run your code without the plugin mount, I get a failure, because it compares the object references,
getStyles().then(oldStyles => {
// no plugin mounted
getStyles().then(newStyles => {
newStyles.forEach((newStyle, i) =>
expect(newStyle).to.equal(oldStyles[i])
);
});
but if I use .to.deep.equal
it succeeds
getStyles().then(oldStyles => {
// no plugin mounted
getStyles().then(newStyles => {
newStyles.forEach((newStyle, i) =>
expect(newStyle).to.deep.equal(oldStyles[i])
);
});
getComputedStyle() returns a live object
The returned style is a live CSSStyleDeclaration object, which updates automatically when the element's styles are changed.
so you would need clone the result before comparing, even if the plugin changed something when you compare they would be identical.
I'd suggest apply JSON/stringify()
to the result and compare the strings, it's pretty fast, also removes the need to deep-equal.
function getStyles() {
return cy.get('.el-on-the-page *').then((elements) => {
const styles = []
elements.each((_, el) => {
styles.push(window.getComputedStyle(el));
});
return JSON.stringify(styles);
});
}
getStyles().then(oldStyles => {
mountPlugin();
getStyles().then(newStyles => {
expect(newStyles).to.equal(oldStyles);
});
});