Skip to content

Commit f315d76

Browse files
authored
Merge pull request #12 from openstax/unsupported-declarations
feat: mark unsupported declarations
2 parents 66e2df8 + 543cd3f commit f315d76

File tree

6 files changed

+75
-550
lines changed

6 files changed

+75
-550
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "1.3.1",
44
"scripts": {
55
"pretest": "sass --source-map ./test/test.scss ./test/test.css",
6-
"test": "./bin/css-coverage.js --cover-declarations --html ./test/test.html --css ./test/test.css --lcov ./test/test.lcov",
6+
"test": "./bin/css-coverage.js --html ./test/test.html --css ./test/test.css --lcov ./test/test.lcov --ignore-declarations 'move-to,move-foobar'",
77
"test-debug": "node --inspect-brk ./bin/css-coverage.js --cover-declarations --html ./test/test.html --css ./test/test.css --lcov ./test/test.lcov",
88
"posttest": "standard --fix"
99
},

src/runCoverage.js

Lines changed: 65 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
2226
const 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

7175
const cssRules = []
76+
const cssDeclarations = {} // so it is serializable to the browser
77+
7278
cssTree.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

test/test.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/test.css.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)