I'm trying to see how dynamic code coverage for styles works using the devtools protocol. I'm manually evaluating a function which adds a class which was previously unused to the DOM which should ideally increase the coverage. But I keep getting inconsistent results in the coverage returned by takeCoverageDelta. Sometimes it logs [](wrong) and sometimes [{"styleSheetId":"35000.1","startOffset":0,"endOffset":31,"used":true}](correct)
I'm not sure if this is the right place to discuss this. Happy to move it to the Google groups if you can verify that my usage of CRI is not incorrect. Looks to me like some race condition that I'm unaware of or isnt documented in the chrome devtools docs.
Reproducible sample: I have a coverage.js which launches chrome, connects to remote debugging port, and opens index.html. This loads index.js and style.css.
// coverage.js
// Script to Launch and get CSS coverage
'use strict';
const CDP = require('chrome-remote-interface');
const ChromeLauncher = require('chrome-launcher');
const Path = require('path');
const styles = [];
const resources = [];
ChromeLauncher.launch({
port: 9222,
chromeFlags: ['--headless', '--disable-gpu'],
chromePath:
process.env.NODE_ENV === 'production' ? '/usr/bin/google-chrome' : undefined
})
.then(() => {
CDP().then(
async client => {
const { Page, Profiler, DOM, CSS, Runtime } = client;
// Enables all necessary domains
await Page.enable();
await Runtime.enable();
await DOM.enable();
await CSS.enable();
// Start capturing coverage data
await CSS.startRuleUsageTracking();
await Page.navigate({
url: `file://${Path.resolve(__dirname, './')}/index.html`
});
// Save the data for each style added to the document.
// This is because the rule usage data only includes styleSheetId.
// This helps map it back to the file name to which it belongs.
await CSS.styleSheetAdded(({ header }) => styles.push(header));
// Fires when all assets have finished loading
// This includes js, css, images, ads, etc.
await Page.loadEventFired();
const result = await Runtime.evaluate({
awaitPromise: true,
expression: 'coverMore()'
});
// Take a snapshot of all the coverage data ocne the load is complete
let { coverage: styleSheetCoverages } = await CSS.takeCoverageDelta();
// Sometimes logs []
// Sometimes logs [{"styleSheetId":"35000.1","startOffset":0,"endOffset":31,"used":true}]
console.log(JSON.stringify(styleSheetCoverages));
// Stop capturing coverage data
await CSS.stopRuleUsageTracking();
},
err => {
console.log('Error with remote interface', err);
}
);
})
.catch(err => {
console.log('Error with launcher', err);
});
<!-- index.html -->
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<div class="app-body"></div>
</div>
<script src="index.js"></script>
</body>
</html>
// index.js
const coverMore = () => {
const app = document.querySelector('.app');
app.classList.add('app-test4');
};
/* style.css */
.app-test4 {
display: flex;
}
| Component | Version
|-|-
| Operating system | macOS Sierra 10.12.6
| Node.js | 8.6.0
| Chrome/Chromium/... | Chrome using Chrome Launcher 0.8.1
| chrome-remote-interface | 0.25.1
Is Chrome running in a container? NO
First things first, coverMore does not return a Promise so I get an error here:
const result = await Runtime.evaluate({
awaitPromise: true, // <-- remove
expression: 'coverMore()'
});
@cyrus-and ohhh yeah. Sorry. I tried both things. With and without.
OK you're right this is a race condition: CSS.takeCoverageDelta() sometimes is called before that the DOM changes introduced by coverMore take effect.
Try this (with awaitPromise: true this time):
const coverMore = () => {
return new Promise(f => {
const app = document.querySelector('.app');
app.classList.add('app-test4');
setTimeout(f, 1000);
});
};
But we can do better (#238), define:
async function requestAnimationFrame(Runtime) {
await Runtime.evaluate({
expression: `new Promise((resolve) => window.requestAnimationFrame(resolve));`,
awaitPromise: true
});
}
then (original coverMore):
const result = await Runtime.evaluate({
expression: 'coverMore()'
});
await requestAnimationFrame(Runtime);
await requestAnimationFrame(Runtime);
let { coverage: styleSheetCoverages } = await CSS.takeCoverageDelta();
You can obviously move the requestAnimationFrame logic in index.js.
also the CSS.styleSheetAdded line should be run before the navigate, as you're setting up an event listener.
Thanks a lot for linking me to the issue and the code snippet. This really helps!
Couple of questions just to bolster my understanding here:
CSS.styleSheetAdded line work for me even though its after the navigate? It doesn't seem to skip any stylesheets per se.
- Is the race condition expected?
Yes it's expected because app.classList.add('app-test4') simply changes the class name (similarly to app.className += ' app-test4'), it doesn't wait for the change to be thoroughly applied, which it will happen shortly after.
- Why two rAF's?
@paulirish may give you a better answer but AFAICT requestAnimationFrame is called before the next repaint so by requesting two of them you can be sure that a full repaint happened.
- Why does the CSS.styleSheetAdded line work for me even though its after the navigate? It doesn't seem to skip any stylesheets per se.
(I misread the first time, that line should not be awaited, it's just an event listener, not a command.)
The answer is, because this time you were lucky :) this is another race condition; it may happen in fact that the first stylesheet is parsed before the CSS.styleSheetAdded listener is registered.
Thank you so much for the answers. A lot of things are more clear to me now. Just a small followup to your last answer, if i dont have to await CSS.styleSheetAdded since its just an event listener, why does await work for Page.loadEventFired? Whats the difference there?
@karanjthakkar you're welcome! The difference is explained here:
The only difference is that when callback is omitted the event is registered only once and a Promise object is returned.
Conversely if you add a callback the return value is unimportant and the callback is executed every time an event occurs; it's just a plain Node.js event emitter.
@cyrus-and Understood. I missed out on that line when you linked it previously. Again, thanks for explaining! Closing the issue :)
Most helpful comment
@karanjthakkar you're welcome! The difference is explained here:
Conversely if you add a callback the return value is unimportant and the callback is executed every time an event occurs; it's just a plain Node.js event emitter.