@@ -19,6 +19,10 @@ function parseFileName (filePath) {
1919 return path . resolve ( process . cwd ( ) , filePath )
2020}
2121
22+ function parseTokenList ( tokenString ) {
23+ return tokenString . split ( ',' ) . map ( token => token . trim ( ) . toLowerCase ( ) )
24+ }
25+
2226const STATUS_CODE = {
2327 ERROR : 111 ,
2428 OK : 0
@@ -32,7 +36,7 @@ commander
3236 . option ( '--lcov [path/to/output.lcov]' , 'the LCOV output file' , parseFileName )
3337 . option ( '--verbose' , 'verbose/debugging output' )
3438 . option ( '--ignore-source-map' , 'disable loading the sourcemap if one is found' )
35- . option ( '--cover -declarations' , 'try to cover CSS declarations as well as selectors (best-effort, difficult with sourcemaps)' )
39+ . option ( '--ignore -declarations [move-to,content] ' , 'A comma-separated list of declarations to ignore' , parseTokenList )
3640 . parse ( process . argv )
3741
3842// Validate args
@@ -69,11 +73,24 @@ try {
6973}
7074
7175const cssRules = [ ]
76+ const cssDeclarations = { } // so it is serializable to the browser
77+
7278cssTree . walkRules ( ast , ( rule ) => {
7379 if ( rule . type === 'Atrule' ) {
7480 // ignore
7581 } else if ( rule . type === 'Rule' ) {
7682 const converted = rule . prelude . children . map ( ( selector ) => {
83+ rule . block . children . each ( declaration => {
84+ if ( commander . ignoreDeclarations . indexOf ( declaration . property . toLowerCase ( ) ) >= 0 ) {
85+ return // skip because it is ignored
86+ }
87+ // Append to a list of locations
88+ const key = cssTree . translate ( declaration )
89+ let locs = cssDeclarations [ key ]
90+ locs = locs || [ ]
91+ locs . push ( declaration . loc )
92+ cssDeclarations [ key ] = locs
93+ } )
7794 return cssTree . translate ( selector )
7895 } )
7996 cssRules . push ( converted )
@@ -152,7 +169,7 @@ async function runCoverage () {
152169 } )
153170
154171 log . debug ( `Calculating coverage` )
155- const coverageOutput = await page . evaluate ( cssRules => {
172+ const { matchedSelectors : coverageOutput , supportedDeclarations } = await page . evaluate ( ( cssRules , cssDeclarations ) => {
156173 // This is the meat of the code. It runs inside the browser
157174 console . log ( `Starting evaluation` )
158175 const rules = cssRules
@@ -165,7 +182,7 @@ async function runCoverage () {
165182 window . Sizzle . selectors . pseudos [ pseudo ] = function ( elem ) { return elem }
166183 } )
167184
168- const ret = [ ]
185+ const matchedSelectors = [ ]
169186 rules . forEach ( function ( selectors ) {
170187 console . log ( `Checking selector: "${ JSON . stringify ( selectors ) } "` )
171188
@@ -194,20 +211,30 @@ async function runCoverage () {
194211
195212 console . log ( `Found ${ count } matche(s)` )
196213
197- ret . push ( [ count , selectors ] )
214+ matchedSelectors . push ( [ count , selectors ] )
198215 } )
199216
200217 console . log ( `Finished checking selectors` )
201- return ret
202- } , cssRules )
218+
219+ console . log ( `Checking if declarations are understandable by the browser` )
220+ const supportedDeclarations = [ ]
221+ for ( const decl of cssDeclarations ) {
222+ if ( window . CSS . supports ( decl ) ) {
223+ supportedDeclarations . push ( decl )
224+ } else {
225+ console . warn ( `Unsupported declaration ${ decl } ` )
226+ }
227+ }
228+ return { matchedSelectors, supportedDeclarations }
229+ } , cssRules , Object . keys ( cssDeclarations ) )
203230
204231 log . debug ( 'Closing browser' )
205232 await browser . close ( )
206233
207234 log . debug ( 'Finished evaluating selectors' )
208235 log . info ( 'Generating LCOV string...' )
209236
210- const lcovStr = await generateLcovStr ( coverageOutput )
237+ const lcovStr = await generateLcovStr ( coverageOutput , supportedDeclarations )
211238 if ( commander . lcov ) {
212239 fs . writeFileSync ( commander . lcov , lcovStr )
213240 } else {
@@ -223,7 +250,7 @@ runCoverage()
223250 process . exit ( STATUS_CODE . ERROR )
224251 } )
225252
226- async function generateLcovStr ( coverageOutput ) {
253+ async function generateLcovStr ( coverageOutput , supportedDeclarations ) {
227254 // coverageOutput is of the form:
228255 // [[1, ['body']], [400, ['div.foo']]]
229256 // where each entry is a pair of count, selectors
@@ -247,11 +274,28 @@ async function generateLcovStr (coverageOutput) {
247274 sourceMapPath = realConsumer . sourceMapPath
248275 }
249276
277+ function getStartInfo ( origStart , origEnd ) {
278+ const startInfo = sourceMapConsumer . originalPositionFor ( { line : origStart . line , column : origStart . column - 1 } )
279+ // const endInfo = sourceMapConsumer.originalPositionFor({line: origEnd.line, column: origEnd.column - 2})
280+
281+ // When there is no match, startInfo.source is null
282+ if ( ! startInfo . source /* || startInfo.source !== endInfo.source */ ) {
283+ console . error ( 'cssStart' , JSON . stringify ( origStart ) )
284+ origEnd && console . error ( 'cssEnd' , JSON . stringify ( origEnd ) )
285+ // console.error('sourceStart', JSON.stringify(startInfo));
286+ // console.error('sourceEnd', JSON.stringify(endInfo));
287+ throw new Error ( 'BUG: sourcemap might be invalid. Maybe try regenerating it?' )
288+ } else {
289+ if ( commander . verbose ) {
290+ console . error ( 'DEBUG: MATCHED this one' , JSON . stringify ( startInfo ) )
291+ }
292+ }
293+ return startInfo
294+ }
295+
250296 const files = { } // key is filename, value is [{startLine, endLine, count}]
251297 const ret = [ ] // each line in the lcov file. Joined at the end of the function
252298
253- const cssLines = CSS_STR . split ( '\n' )
254-
255299 function addCoverage ( fileName , count , startLine , endLine ) {
256300 // add it to the files
257301 if ( ! files [ fileName ] ) {
@@ -279,55 +323,8 @@ async function generateLcovStr (coverageOutput) {
279323 const origStart = rule . loc . start
280324 const origEnd = rule . loc . end
281325
282- if ( commander . coverDeclarations ) {
283- // Loop over every character between origStart and origEnd to make sure they are covered
284- // TODO: Do not duplicate-count lines just because this code runs character-by-character
285- let parseColumn = origStart . column
286- for ( let parseLine = origStart . line ; parseLine <= origEnd . line ; parseLine ++ ) {
287- const curLineText = cssLines [ parseLine - 1 ]
288- for ( let curColumn = parseColumn - 1 ; curColumn < curLineText . length ; curColumn ++ ) {
289- const info = sourceMapConsumer . originalPositionFor ( { line : parseLine , column : curColumn } )
290- // stop processing when we hit origEnd
291- if ( parseLine === origEnd . line && curColumn >= origEnd . column ) {
292- break
293- }
294- if ( / \s / . test ( curLineText [ curColumn ] ) ) {
295- continue
296- }
297- // console.error('PHIL ', curLineText[curColumn], {line: parseLine, column: curColumn}, info);
298- if ( info . source ) {
299- addCoverage ( info . source , count , info . line , info . line )
300- } else {
301- if ( commander . verbose ) {
302- console . error ( 'BUG: Could not look up source for this range:' )
303- console . error ( 'origStart' , origStart )
304- console . error ( 'origEnd' , origEnd )
305- console . error ( 'currIndexes' , { line : parseLine , column : curColumn } )
306- }
307- }
308- }
309- parseColumn = 1
310- }
311- } else {
312- // Just cover the selectors
313- const startInfo = sourceMapConsumer . originalPositionFor ( { line : origStart . line , column : origStart . column - 1 } )
314- // const endInfo = sourceMapConsumer.originalPositionFor({line: origEnd.line, column: origEnd.column - 2})
315-
316- // When there is no match, startInfo.source is null
317- if ( ! startInfo . source /* || startInfo.source !== endInfo.source */ ) {
318- console . error ( 'cssStart' , JSON . stringify ( origStart ) )
319- console . error ( 'cssEnd' , JSON . stringify ( origEnd ) )
320- // console.error('sourceStart', JSON.stringify(startInfo));
321- // console.error('sourceEnd', JSON.stringify(endInfo));
322- throw new Error ( 'BUG: sourcemap might be invalid. Maybe try regenerating it?' )
323- } else {
324- if ( commander . verbose ) {
325- console . error ( 'DEBUG: MATCHED this one' , JSON . stringify ( startInfo ) )
326- }
327- }
328-
329- addCoverage ( startInfo . source , count , startInfo . line , startInfo . line )
330- }
326+ const startInfo = getStartInfo ( origStart , origEnd )
327+ addCoverage ( startInfo . source , count , startInfo . line , startInfo . line )
331328 } else {
332329 // No sourceMap available
333330 fileName = commander . css
@@ -341,6 +338,15 @@ async function generateLcovStr (coverageOutput) {
341338 }
342339 } )
343340
341+ // Mark all the unsupported declarations
342+ const unsupportedDeclarations = Object . keys ( cssDeclarations ) . filter ( decl => supportedDeclarations . indexOf ( decl ) < 0 )
343+ for ( const decl of unsupportedDeclarations ) {
344+ for ( const loc of cssDeclarations [ decl ] ) {
345+ const startInfo = getStartInfo ( loc . start )
346+ addCoverage ( startInfo . source , 0 , startInfo . line , startInfo . line )
347+ }
348+ }
349+
344350 for ( const fileName in files ) {
345351 let nonZero = 0 // For summary info
346352 let allCounter = 0
0 commit comments