@@ -4,27 +4,168 @@ const fs = require('fs')
44const assert = require ( 'assert' )
55const 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 ( T h e | A n ? | A n d | T o | I n | O n | W i t h ) \b / g, x => x . toLowerCase ( ) )
23+ . replace ( / \b ( D o m ) \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+
752describe ( '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 ( / ^ c u s t o m - e l e m e n t s \/ / , '' ) ) )
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 ( / ^ g i t h u b \/ / , '' ) ) , `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 * \/ \/ .* \. [ j t ] 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