-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add "scoreboard" test suite #479
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a39ec24
add babel-core dev dependency for Storybook
shawnbot 5edf32a
first pass at scoreboard script
shawnbot 27b96ac
add minimatch
shawnbot ab9cc3e
pass args through to ava from script/test-docs
shawnbot 4ba2368
refactor docs test: more async, one test per class
shawnbot c2226b6
scoreboard reorg; better docs test output parsing
shawnbot 77f987f
add primer.class_whitelist for doc tests
shawnbot c4b984d
add FIXME comment to deprecate .warning in primer-alerts
shawnbot 02a2370
add test script to run the scoreboard
shawnbot 200025c
generate the scoreboard before running tests
shawnbot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
const {basename, join, resolve} = require('path') | ||
const PromiseQueue = require('p-queue') | ||
const execa = require('execa') | ||
const globby = require('globby') | ||
const rootDir = resolve(__dirname, '../..') | ||
const lernaConfig = require(join(rootDir, 'lerna.json')) | ||
const modulesDir = join(rootDir, 'modules') | ||
require('console.table') | ||
|
||
const unique = list => Array.from(new Set(list)).sort() | ||
|
||
const matchAll = (pattern, text) => { | ||
const matches = [] | ||
let match | ||
while (match = pattern.exec(text)) { | ||
matches.push(match) | ||
} | ||
return matches | ||
} | ||
|
||
const checks = { | ||
'has stories': (module, key) => { | ||
return globby(join(module.path, '**/stories.js')) | ||
.then(files => ({ | ||
[key]: files.length > 0 ? 'yes' : 'no' | ||
})) | ||
}, | ||
'docs test': (module, key) => { | ||
return execa(join(rootDir, 'script/test-docs'), { | ||
cwd: module.path | ||
}) | ||
.then(result => ({[key]: 'pass'})) | ||
.catch(({stderr}) => { | ||
const pattern = /("\.[-\w]+") is not documented/g | ||
const matches = matchAll(pattern, stderr) | ||
.map(match => match[1]) | ||
let missing = matches ? Array.from(matches) : [] | ||
const max = 5 | ||
if (missing.length > max) { | ||
const more = missing.length - max | ||
missing = missing.slice(0, max).concat(`and ${more} more...`) | ||
} | ||
return { | ||
[key]: 'FAIL', | ||
'missing docs': unique(missing).join(', ') | ||
} | ||
}) | ||
} | ||
} | ||
|
||
const args = process.argv.slice(2) | ||
|
||
const modules = args.length | ||
? Promise.resolve(args) | ||
: globby(join(modulesDir, 'primer-*')) | ||
|
||
modules | ||
.then(moduleDirs => { | ||
console.log('Found %d module directories', moduleDirs.length) | ||
return moduleDirs | ||
.map(path => ({ | ||
path, | ||
name: basename(path), | ||
pkg: require(join(path, 'package.json')) | ||
})) | ||
.filter(({pkg}) => pkg.primer.module_type !== 'meta') | ||
}) | ||
.then(modules => { | ||
console.log('Filtered to %d modules (excluding meta-packages)', modules.length) | ||
|
||
const queue = new PromiseQueue({concurrency: 3}) | ||
|
||
for (const module of modules) { | ||
module.checks = {} | ||
for (const [name, check] of Object.entries(checks)) { | ||
queue.add(() => { | ||
// console.warn(`? check: ${module.name} ${name}`) | ||
return check(module, name) | ||
.then(result => { | ||
Object.assign(module.checks, result) | ||
}) | ||
}) | ||
} | ||
} | ||
|
||
console.warn(`Running ${queue.size} checks...`) | ||
return queue.onIdle().then(() => modules) | ||
}) | ||
.then(modules => { | ||
console.warn('ran tests on %d modules', modules.length) | ||
const rows = modules.map(({name, checks}) => { | ||
return Object.assign({'package': name}, checks) | ||
}) | ||
console.table(rows) | ||
}) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"private": true, | ||
"scripts": { | ||
"test": "node index.js" | ||
}, | ||
"devDependencies": { | ||
"console.table": "^0.10.0", | ||
"execa": "^0.10.0", | ||
"globby": "^6.1.0", | ||
"p-queue": "^2.4.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,6 +72,7 @@ | |
border-radius: 0; | ||
} | ||
|
||
// FIXME deprecate this | ||
.warning { | ||
padding: $em-spacer-5; | ||
margin-bottom: 0.8em; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
#!/bin/bash | ||
set -e | ||
$(dirname $0)/npm-run ava --verbose $(dirname $0)/../tests/modules/test-document-styles.js | ||
$(dirname $0)/npm-run ava --verbose $(dirname $0)/../tests/modules/test-document-styles.js "$@" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,73 +1,95 @@ | ||
const test = require("ava") | ||
const css = require(process.cwd()) | ||
const fs = require("fs") | ||
const glob = require("glob") | ||
const {join} = require('path') | ||
const fse = require('fs-extra') | ||
const globby = require('globby') | ||
const test = require('ava') | ||
const minimatch = require('minimatch') | ||
|
||
let selectors | ||
let classnames | ||
const cwd = process.cwd() | ||
const css = require(cwd) | ||
const pkg = require(join(cwd, 'package.json')) | ||
|
||
const unique = list => Array.from(new Set(list)).sort() | ||
|
||
/* | ||
* These are the regular expressions that match what we | ||
* expect to be class name instances in the docs. | ||
* Patterns should group the matched class name(s) such that: | ||
* | ||
* ```js | ||
* const [, klass, ] = pattern.exec(content) | ||
* ``` | ||
*/ | ||
const classPatterns = [ | ||
// HTML class attributes | ||
/class="([^"]+)"/ig, | ||
/:class ?=> "([^"]+)"/g, | ||
/class: "([^"]+)"/g, | ||
// assume that ERB helpers generate an element with the same class | ||
/<%= (\w+)\b/g, | ||
] | ||
|
||
const whitelistClasses = (pkg.primer ? pkg.primer.class_whitelist || [] : []) | ||
.concat('js*') | ||
|
||
const isWhitelisted = klass => { | ||
return whitelistClasses.some(glob => minimatch(klass, glob)) | ||
} | ||
|
||
// Find unique selectors from the cssstats selector list | ||
function uniqueSelectors(s) { | ||
s = s.map(s => { | ||
function uniqueSelectors(selectors) { | ||
return unique(selectors.map(s => { | ||
// split multi-selectors into last class used .foo .bar .baz | ||
return s.split(" ").pop() | ||
return s.split(' ').pop() | ||
}) | ||
.filter(s => { | ||
// remove any selector that aren't just regular classnames eg. ::hover [type] | ||
// only match exact class selectors | ||
return s.match(/^\.[a-z\-_]+$/ig) | ||
}) | ||
|
||
// return only the unique selectors | ||
return [...new Set(s)] | ||
})) | ||
} | ||
|
||
// From the given glob sources array, read the files and return found classnames | ||
function documentedClassnames(sources) { | ||
const classes = [] | ||
const files = sources.reduce((acc, pattern) => { | ||
return acc.concat(glob.sync(pattern)) | ||
}, []) | ||
|
||
files.forEach(file => { | ||
let match = null | ||
const content = fs.readFileSync(file, "utf8") | ||
|
||
classPatterns.forEach(pattern => { | ||
// match each pattern against the source | ||
while (match = pattern.exec(content)) { | ||
// get the matched classnames and split by whitespace into classes | ||
const klasses = match[1].trim().split(/\s+/) | ||
classes.push(...klasses) | ||
} | ||
function getDocumentedClassnames(sources) { | ||
return globby(sources) | ||
.then(paths => { | ||
return Promise.all(paths.map(path => { | ||
return fse.readFile(path, 'utf8') | ||
.then(content => ({path, content})) | ||
})) | ||
}) | ||
}) | ||
|
||
// return only the unique classnames | ||
return Array.from(new Set(classes)) | ||
.then(files => { | ||
return files.reduce((classes, {path, content}) => { | ||
classPatterns.forEach(pattern => { | ||
let match | ||
while (match = pattern.exec(content)) { | ||
// get the matched classnames and split by whitespace into classes | ||
let klasses = match[1].trim().split(/\s+/) | ||
classes.push(...klasses) | ||
} | ||
}) | ||
return classes | ||
}, []) | ||
}) | ||
.then(classes => unique(classes)) | ||
} | ||
|
||
// Before all the tests get the selectors and classnames | ||
const selectors = uniqueSelectors(css.cssstats.selectors.values) | ||
let classnames | ||
test.before(t => { | ||
selectors = uniqueSelectors(css.cssstats.selectors.values) | ||
classnames = documentedClassnames([ | ||
"docs/*.md", | ||
"README.md" | ||
]) | ||
return getDocumentedClassnames([ | ||
'docs/*.md', | ||
'README.md' | ||
]) | ||
.then(_ => (classnames = _)) | ||
}) | ||
|
||
test("Every selector class is documented", t => { | ||
const undocumented = [] | ||
selectors.forEach(selector => { | ||
if (!classnames.includes(selector.replace(/^\./, ""))) { | ||
undocumented.push(selector) | ||
selectors.forEach(selector => { | ||
const klass = selector.replace(/^\./, '') | ||
test(`Selector "${selector}" is documented/whitelisted`, t => { | ||
t.plan(1) | ||
if (isWhitelisted(klass)) { | ||
t.pass(`Selector "${selector}" is whitelisted`) | ||
} else { | ||
t.is(classnames.includes(klass), true, `Selector "${selector}" is not documented`) | ||
} | ||
}) | ||
t.is(undocumented.length, 0, `I did not find documentation for the "${undocumented.join(", ")}" selector(s) in the ${process.env.npm_package_name} module.`); | ||
}) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious why this was added? I don't see it being imported anywhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! I'll remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weird... this isn't in the
stories-to-markdown
branch anymore. GitHub bug??