Skip to content

Commit 569f02c

Browse files
committed
Update check-rules.js
1 parent 7708bf6 commit 569f02c

File tree

1 file changed

+157
-16
lines changed

1 file changed

+157
-16
lines changed

tests/check-rules.js

Lines changed: 157 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,168 @@ const fs = require('fs')
44
const assert = require('assert')
55
const path = require('path')
66

7+
const RuleTester = require('eslint').RuleTester
8+
const ruleTester = new RuleTester({env: {es2020: true}, parserOptions: {sourceType: 'module'}})
9+
10+
function rulesFromDir(dir) {
11+
try {
12+
return fs.readdirSync(`./${dir}`).map(f => path.basename(f, path.extname(f)))
13+
} catch {
14+
return []
15+
}
16+
}
17+
18+
function makeTitle(name) {
19+
return name
20+
.replace(/-/g, ' ')
21+
.replace(/\w\S*/g, x => x.charAt(0).toUpperCase() + x.substr(1))
22+
.replace(/\b(The|An?|And|To|In|On|With)\b/g, x => x.toLowerCase())
23+
.replace(/\b(Dom)\b/g, x => x.toUpperCase())
24+
}
25+
26+
function* extractCodeblocks(lines) {
27+
let inCodeBlock = false
28+
let codeLines = []
29+
let startLine = 0
30+
let endLine = 0
31+
let lang = ''
32+
for (const i in lines) {
33+
const line = lines[i]
34+
if (!inCodeBlock && line.startsWith('```')) {
35+
lang = line.slice(3)
36+
startLine = i
37+
codeLines = []
38+
inCodeBlock = true
39+
continue
40+
} else if (inCodeBlock && line.startsWith('```')) {
41+
endLine = i
42+
yield {code: codeLines, startLine, endLine, lang}
43+
inCodeBlock = false
44+
continue
45+
}
46+
if (inCodeBlock) {
47+
codeLines.push(line)
48+
}
49+
}
50+
}
51+
752
describe('smoke tests', () => {
8-
it('ensure all rules in lib/rules are included in index', () => {
9-
const exportedRules = new Set(Object.keys(config.rules))
10-
const files = new Set(fs.readdirSync('./lib/rules').map(f => path.basename(f, path.extname(f))))
11-
assert.deepEqual(files, exportedRules)
53+
it('has file for each exported rule and rule for each exported file', () => {
54+
assert.deepStrictEqual(
55+
Object.keys(config.rules),
56+
rulesFromDir('lib/rules'),
57+
'Expected lib/rules/*.js to be inside lib/index.js#rules'
58+
)
59+
})
60+
61+
it('has export for each config and config for each import', () => {
62+
assert.deepStrictEqual(
63+
Object.keys(config.configs),
64+
rulesFromDir('lib/configs'),
65+
'Expected lib/configs/*.js to be inside lib/index.js#configs'
66+
)
1267
})
1368

14-
it('exports every config in lib/config as .configs', () => {
15-
const exportedConfigs = new Set(Object.keys(config.configs))
16-
const files = new Set(fs.readdirSync('./lib/configs').map(f => path.basename(f, path.extname(f))))
17-
assert.deepEqual(files, exportedConfigs)
69+
for (const flavour in config.configs) {
70+
describe(`${flavour} config`, () => {
71+
it('exports valid rules', () => {
72+
const exportedRules = new Set(Object.keys(config.rules))
73+
const ceRules = Object.keys(config.configs[flavour].rules).filter(rule => rule.startsWith('custom-elements/'))
74+
const violations = ceRules.filter(rule => !exportedRules.has(rule.replace(/^custom-elements\//, '')))
75+
assert.deepStrictEqual(violations, [], 'All custom-elements/ rules should exist in lib/index.js#rules')
76+
})
77+
})
78+
}
79+
})
80+
81+
describe('test coverage', () => {
82+
it('has tests for each rule and rules for each test', () => {
83+
const tests = rulesFromDir('tests').filter(name => name !== 'check-rules')
84+
assert.deepStrictEqual(rulesFromDir('lib/rules'), tests, 'Expected lib/rules/*.js to have same files as tests/*.js')
1885
})
86+
})
1987

20-
it('exports valid rules in each config', () => {
21-
const exportedRules = new Set(Object.keys(config.rules))
22-
for (const flavour in config.configs) {
23-
for (const rule in config.configs[flavour].rules) {
24-
if (rule.startsWith('github/')) {
25-
assert(exportedRules.has(rule.replace(/^github\//, '')), `rule ${rule} is not a valid rule`)
88+
describe('documentation', () => {
89+
it('has rule for each doc file and doc file for each rule', () => {
90+
assert.deepStrictEqual(rulesFromDir('docs/rules'), rulesFromDir('lib/rules'))
91+
})
92+
93+
it('has readme link to each doc', () => {
94+
const contents = fs.readFileSync(`./README.md`, 'utf-8').split('\n')
95+
const i = contents.indexOf('### Rules')
96+
let n = contents.findIndex((line, index) => index > i && line.startsWith('#'))
97+
if (n < i) n = contents.length
98+
const ruleLinks = contents
99+
.slice(i + 1, n)
100+
.filter(Boolean)
101+
.map(x => x.trim())
102+
const desiredRuleLinks = rulesFromDir('docs/rules').map(rule => `- [${makeTitle(rule)}](./docs/rules/${rule}.md)`)
103+
assert.deepStrictEqual(desiredRuleLinks, ruleLinks, 'Expected each rule in docs/rules/*.md to have README link')
104+
})
105+
106+
for (const doc of rulesFromDir('docs/rules')) {
107+
it(`has correct headings in ${doc}.md`, () => {
108+
const contents = fs.readFileSync(`./docs/rules/${doc}.md`, 'utf-8').split('\n')
109+
let consume = true
110+
const headings = contents.filter(line => {
111+
// Discard lines that aren't headers or thumbs
112+
if (!(line.startsWith('#') || line.startsWith('\ud83d'))) return false
113+
// Ignore all sub headings/thumbs between `### Options` and `## When Not To Use It`
114+
if (line === '### Options') {
115+
consume = false
116+
return true
117+
} else if (line === '## When Not To Use It') {
118+
consume = true
119+
}
120+
return consume
121+
})
122+
const desiredHeadings = [
123+
`# ${makeTitle(doc)}`,
124+
'## Rule Details',
125+
'👎 Examples of **incorrect** code for this rule:',
126+
'👍 Examples of **correct** code for this rule:',
127+
config.rules?.[doc]?.schema?.length ? '### Options' : '',
128+
'## When Not To Use It',
129+
'## Version'
130+
].filter(Boolean)
131+
assert.deepStrictEqual(headings, desiredHeadings, 'Expected doc to have correct headings')
132+
})
133+
134+
it(`has working examples in ${doc}.md`, () => {
135+
const rules = {valid: [], invalid: []}
136+
const lines = fs.readFileSync(`./docs/rules/${doc}.md`, 'utf-8').split('\n')
137+
138+
for (const {code, startLine} of extractCodeblocks(lines)) {
139+
const validIndex = lines.lastIndexOf('👍 Examples of **correct** code for this rule:', startLine)
140+
const invalidIndex = lines.lastIndexOf('👎 Examples of **incorrect** code for this rule:', startLine)
141+
142+
if (validIndex === invalidIndex) {
143+
continue
144+
}
145+
146+
let filename = ''
147+
if (code[0].match(/\s*\/\/ .*\.[jt]s$/)) {
148+
filename = code[0].replace('// ', '').trim()
149+
}
150+
151+
if (validIndex > invalidIndex) {
152+
rules.valid.push({code: code.join('\n')})
153+
} else {
154+
rules.invalid.push({code: code.join('\n'), errors: 1, filename})
26155
}
27156
}
28-
}
29-
})
157+
158+
// eslint-disable-next-line import/no-dynamic-require
159+
const rule = require(`../lib/rules/${doc}`)
160+
ruleTester.run(doc, rule, rules)
161+
})
162+
163+
it(`has javascript examples in ${doc}.md`, () => {
164+
const lines = fs.readFileSync(`./docs/rules/${doc}.md`, 'utf-8').split('\n')
165+
assert(
166+
Array.from(extractCodeblocks(lines)).find(x => x.lang === 'js'),
167+
'Expected documentation to include a JavaScript codeblock'
168+
)
169+
})
170+
}
30171
})

0 commit comments

Comments
 (0)