Skip to content

Commit 7272097

Browse files
committed
test: update stale rule counts and add publish readiness gate tests
- Fix plugin-structure.test.ts to verify all 36 rules (was 6) - Add formatter/formatter-progress file checks to build-verification - Add package-publish-readiness.test.ts (npm pack, CJS require, exports, version consistency) - Include new test in test:core / pre-check pipeline
1 parent d9963fa commit 7272097

5 files changed

Lines changed: 339 additions & 28 deletions

File tree

.claude/settings.local.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
"Bash(npm run test:*)",
77
"Bash(npm run type-check:*)",
88
"Bash(npm run lint:*)",
9-
"Bash(npm run pre-check:*)"
9+
"Bash(npm run pre-check:*)",
10+
"Bash(npm run test:coverage:*)",
11+
"Bash(npm pack:*)",
12+
"Bash(npm run test:core:*)",
13+
"Bash(npm run test:e2e:*)"
1014
]
1115
}
1216
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"test:coverage:report": "vitest run --coverage && open coverage/index.html",
4545
"lint": "eslint src/**/*.ts",
4646
"type-check": "tsc --noEmit --project tsconfig.typecheck.json",
47-
"test:core": "vitest run tests/vitest/unit/a11y-checker.test.ts tests/vitest/unit/linter/rules/rule-structure.test.ts tests/vitest/integration/build-verification.test.ts tests/vitest/integration/eslint-plugin-import.test.ts tests/vitest/unit/linter/rules/vue-rules.test.ts tests/vitest/unit/ArticleCard.test.ts tests/vitest/integration/typescript-react.test.ts",
47+
"test:core": "vitest run tests/vitest/unit/a11y-checker.test.ts tests/vitest/unit/linter/rules/rule-structure.test.ts tests/vitest/integration/build-verification.test.ts tests/vitest/integration/eslint-plugin-import.test.ts tests/vitest/unit/linter/rules/vue-rules.test.ts tests/vitest/unit/ArticleCard.test.ts tests/vitest/integration/typescript-react.test.ts tests/vitest/integration/package-publish-readiness.test.ts",
4848
"test:e2e": "vitest run tests/vitest/integration/eslint-cli-e2e.test.ts",
4949
"pre-check": "npm run build && npm run test:core && npm run test:e2e && npm run lint && npm run type-check",
5050
"verify": "npm run pre-check",

tests/vitest/integration/build-verification.test.ts

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { join } from 'path'
44

55
/**
66
* Build verification tests
7-
*
7+
*
88
* These tests verify that the build process correctly generates
99
* all required files and that package.json exports are correct.
1010
*/
@@ -43,53 +43,107 @@ describe('Build Verification', () => {
4343
const pluginTypesFile = join(distPath, 'linter/eslint-plugin/index.d.ts')
4444
expect(existsSync(pluginTypesFile)).toBe(true)
4545
})
46+
47+
it('should generate formatter (CJS)', () => {
48+
expect(existsSync(join(distPath, 'linter/eslint-plugin/formatter.js'))).toBe(true)
49+
})
50+
51+
it('should generate formatter (ESM)', () => {
52+
expect(existsSync(join(distPath, 'linter/eslint-plugin/formatter.mjs'))).toBe(true)
53+
})
54+
55+
it('should generate formatter TypeScript definitions', () => {
56+
expect(existsSync(join(distPath, 'linter/eslint-plugin/formatter.d.ts'))).toBe(true)
57+
})
58+
59+
it('should generate formatter-with-progress (CJS)', () => {
60+
expect(existsSync(join(distPath, 'linter/eslint-plugin/formatter-with-progress.js'))).toBe(true)
61+
})
62+
63+
it('should generate formatter-with-progress (ESM)', () => {
64+
expect(existsSync(join(distPath, 'linter/eslint-plugin/formatter-with-progress.mjs'))).toBe(true)
65+
})
66+
67+
it('should generate formatter-with-progress TypeScript definitions', () => {
68+
expect(existsSync(join(distPath, 'linter/eslint-plugin/formatter-with-progress.d.ts'))).toBe(true)
69+
})
4670
})
4771

4872
describe('Package.json exports', () => {
4973
it('should have correct package name', () => {
5074
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
51-
75+
5276
expect(pkg.name).toBe('eslint-plugin-test-a11y-js')
5377
})
5478

5579
it('should have correct exports configuration', () => {
5680
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
57-
81+
5882
expect(pkg.exports).toBeDefined()
5983
expect(pkg.exports['.']).toBeDefined()
6084
expect(pkg.exports['./core']).toBeDefined()
85+
expect(pkg.exports['./formatter']).toBeDefined()
86+
expect(pkg.exports['./formatter-progress']).toBeDefined()
6187
})
6288

6389
it('should export main entry point as ESLint plugin', () => {
6490
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
65-
91+
6692
expect(pkg.exports['.'].import).toBe('./dist/linter/eslint-plugin/index.mjs')
6793
expect(pkg.exports['.'].require).toBe('./dist/linter/eslint-plugin/index.js')
6894
expect(pkg.exports['.'].types).toBe('./dist/linter/eslint-plugin/index.d.ts')
6995
})
7096

7197
it('should export core library at ./core', () => {
7298
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
73-
99+
74100
expect(pkg.exports['./core'].import).toBe('./dist/index.mjs')
75101
expect(pkg.exports['./core'].require).toBe('./dist/index.js')
76102
expect(pkg.exports['./core'].types).toBe('./dist/index.d.ts')
77103
})
78104

105+
it('should export formatter at ./formatter', () => {
106+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
107+
108+
expect(pkg.exports['./formatter'].import).toBe('./dist/linter/eslint-plugin/formatter.mjs')
109+
expect(pkg.exports['./formatter'].require).toBe('./dist/linter/eslint-plugin/formatter.js')
110+
expect(pkg.exports['./formatter'].types).toBe('./dist/linter/eslint-plugin/formatter.d.ts')
111+
})
112+
113+
it('should export formatter-progress at ./formatter-progress', () => {
114+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
115+
116+
expect(pkg.exports['./formatter-progress'].import).toBe('./dist/linter/eslint-plugin/formatter-with-progress.mjs')
117+
expect(pkg.exports['./formatter-progress'].require).toBe('./dist/linter/eslint-plugin/formatter-with-progress.js')
118+
expect(pkg.exports['./formatter-progress'].types).toBe('./dist/linter/eslint-plugin/formatter-with-progress.d.ts')
119+
})
120+
79121
it('should have correct main, module, and types fields (ESLint plugin)', () => {
80122
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
81-
123+
82124
expect(pkg.main).toBe('dist/linter/eslint-plugin/index.js')
83125
expect(pkg.module).toBe('dist/linter/eslint-plugin/index.mjs')
84126
expect(pkg.types).toBe('dist/linter/eslint-plugin/index.d.ts')
85127
})
128+
129+
it('every file referenced in exports should exist on disk', () => {
130+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
131+
const root = process.cwd()
132+
133+
for (const [exportPath, conditions] of Object.entries(pkg.exports)) {
134+
for (const [condition, filePath] of Object.entries(conditions as Record<string, string>)) {
135+
const fullPath = join(root, filePath)
136+
expect(existsSync(fullPath), `Export "${exportPath}" -> "${condition}": ${filePath} should exist`).toBe(true)
137+
}
138+
}
139+
})
86140
})
87141

88142
describe('Build output content', () => {
89143
it('should have valid JavaScript in main file', () => {
90144
const mainFile = join(distPath, 'index.js')
91145
const content = readFileSync(mainFile, 'utf-8')
92-
146+
93147
// Basic sanity checks
94148
expect(content.length).toBeGreaterThan(0)
95149
expect(content).toContain('A11yChecker')
@@ -98,12 +152,11 @@ describe('Build Verification', () => {
98152
it('should have valid JavaScript in plugin file', () => {
99153
const pluginFile = join(distPath, 'linter/eslint-plugin/index.js')
100154
const content = readFileSync(pluginFile, 'utf-8')
101-
155+
102156
// Basic sanity checks
103157
expect(content.length).toBeGreaterThan(0)
104158
// Plugin should export rules and configs
105159
expect(content).toMatch(/rules|configs/)
106160
})
107161
})
108162
})
109-
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { readFileSync, existsSync, statSync } from 'fs'
3+
import { join } from 'path'
4+
import { execSync } from 'child_process'
5+
import { createRequire } from 'module'
6+
7+
/**
8+
* Package publish readiness tests
9+
*
10+
* Comprehensive gate tests that catch issues before publishing.
11+
* These validate npm pack contents, CJS/ESM exports, rule loading,
12+
* and package.json correctness.
13+
*/
14+
15+
const root = process.cwd()
16+
const packageJsonPath = join(root, 'package.json')
17+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
18+
19+
describe('Package Publish Readiness', () => {
20+
describe('npm pack contents', () => {
21+
let packFiles: string[]
22+
23+
beforeAll(() => {
24+
// npm pack --json output gets contaminated by the prepare script's build output,
25+
// so we parse the human-readable dry-run output instead.
26+
const output = execSync('npm pack --dry-run 2>&1', {
27+
cwd: root,
28+
encoding: 'utf-8',
29+
})
30+
packFiles = output
31+
.split('\n')
32+
.filter(line => line.startsWith('npm notice') && /\d+(\.\d+)?[kKmMgG]?B\s/.test(line))
33+
.map(line => line.replace(/^npm notice\s+[\d.]+[kKmMgG]?B\s+/, '').trim())
34+
.filter(Boolean)
35+
})
36+
37+
it('should include all export-referenced files', () => {
38+
for (const [exportPath, conditions] of Object.entries(pkg.exports)) {
39+
for (const [condition, filePath] of Object.entries(conditions as Record<string, string>)) {
40+
const normalized = (filePath as string).replace(/^\.\//, '')
41+
expect(
42+
packFiles.includes(normalized),
43+
`Export "${exportPath}" -> "${condition}": ${normalized} should be in npm pack output`
44+
).toBe(true)
45+
}
46+
}
47+
})
48+
49+
it('should not leak source files', () => {
50+
const leakedSrc = packFiles.filter(f => f.startsWith('src/'))
51+
expect(leakedSrc, 'src/ files should not be in the tarball').toHaveLength(0)
52+
})
53+
54+
it('should not leak test files', () => {
55+
const leakedTests = packFiles.filter(f => f.startsWith('tests/'))
56+
expect(leakedTests, 'tests/ files should not be in the tarball').toHaveLength(0)
57+
})
58+
59+
it('should not leak config or env files', () => {
60+
const forbidden = ['.env', '.eslintrc', 'tsconfig.json', 'tsup.config.ts', 'vitest.config.ts']
61+
for (const name of forbidden) {
62+
expect(
63+
packFiles.includes(name),
64+
`${name} should not be in the tarball`
65+
).toBe(false)
66+
}
67+
})
68+
69+
it('should include README.md and LICENSE', () => {
70+
expect(packFiles.includes('README.md')).toBe(true)
71+
expect(packFiles.includes('LICENSE')).toBe(true)
72+
})
73+
74+
it('should include bin/eslint-with-progress.js', () => {
75+
expect(packFiles.includes('bin/eslint-with-progress.js')).toBe(true)
76+
})
77+
})
78+
79+
describe('CJS require works', () => {
80+
it('should be requireable and have rules and configs', () => {
81+
const require = createRequire(import.meta.url)
82+
const plugin = require(join(root, 'dist/linter/eslint-plugin/index.js'))
83+
84+
expect(plugin).toBeDefined()
85+
expect(plugin.rules).toBeDefined()
86+
expect(typeof plugin.rules).toBe('object')
87+
expect(plugin.configs).toBeDefined()
88+
expect(typeof plugin.configs).toBe('object')
89+
})
90+
})
91+
92+
describe('Export content validation', () => {
93+
it('. export should contain rules, configs, meta', () => {
94+
const content = readFileSync(join(root, 'dist/linter/eslint-plugin/index.js'), 'utf-8')
95+
expect(content.length).toBeGreaterThan(0)
96+
expect(content).toMatch(/rules/)
97+
expect(content).toMatch(/configs/)
98+
expect(content).toMatch(/meta/)
99+
})
100+
101+
it('./core export should contain A11yChecker', () => {
102+
const content = readFileSync(join(root, 'dist/index.js'), 'utf-8')
103+
expect(content.length).toBeGreaterThan(0)
104+
expect(content).toContain('A11yChecker')
105+
})
106+
107+
it('./formatter export should contain format function', () => {
108+
const content = readFileSync(join(root, 'dist/linter/eslint-plugin/formatter.js'), 'utf-8')
109+
expect(content.length).toBeGreaterThan(0)
110+
expect(content).toMatch(/format/)
111+
})
112+
113+
it('./formatter-progress export should contain format function', () => {
114+
const content = readFileSync(join(root, 'dist/linter/eslint-plugin/formatter-with-progress.js'), 'utf-8')
115+
expect(content.length).toBeGreaterThan(0)
116+
expect(content).toMatch(/format/)
117+
})
118+
119+
it('all built files referenced in exports should be non-empty', () => {
120+
for (const [exportPath, conditions] of Object.entries(pkg.exports)) {
121+
for (const [condition, filePath] of Object.entries(conditions as Record<string, string>)) {
122+
const fullPath = join(root, filePath as string)
123+
const stat = statSync(fullPath)
124+
expect(
125+
stat.size,
126+
`Export "${exportPath}" -> "${condition}": ${filePath} should be non-empty`
127+
).toBeGreaterThan(0)
128+
}
129+
}
130+
})
131+
})
132+
133+
describe('All 36 rules loadable', () => {
134+
it('every rule in the built plugin should have meta and create', () => {
135+
const require = createRequire(import.meta.url)
136+
const plugin = require(join(root, 'dist/linter/eslint-plugin/index.js'))
137+
const ruleNames = Object.keys(plugin.rules)
138+
139+
expect(ruleNames.length).toBe(36)
140+
141+
for (const name of ruleNames) {
142+
const rule = plugin.rules[name]
143+
expect(rule.meta, `Rule "${name}" should have meta`).toBeDefined()
144+
expect(typeof rule.create, `Rule "${name}" should have create function`).toBe('function')
145+
}
146+
})
147+
})
148+
149+
describe('Version consistency', () => {
150+
it('package.json version should match plugin meta version', () => {
151+
const require = createRequire(import.meta.url)
152+
const plugin = require(join(root, 'dist/linter/eslint-plugin/index.js'))
153+
154+
expect(plugin.meta.version).toBe(pkg.version)
155+
})
156+
})
157+
158+
describe('Peer dependencies', () => {
159+
it('should list eslint as a peer dependency', () => {
160+
expect(pkg.peerDependencies).toHaveProperty('eslint')
161+
})
162+
163+
it('should list vitest as a peer dependency', () => {
164+
expect(pkg.peerDependencies).toHaveProperty('vitest')
165+
})
166+
})
167+
168+
describe('Package.json required fields', () => {
169+
const requiredFields = [
170+
'name',
171+
'version',
172+
'description',
173+
'license',
174+
'repository',
175+
'main',
176+
'module',
177+
'types',
178+
'exports',
179+
]
180+
181+
for (const field of requiredFields) {
182+
it(`should have "${field}" field`, () => {
183+
expect(pkg[field], `Missing required field: ${field}`).toBeDefined()
184+
})
185+
}
186+
})
187+
})

0 commit comments

Comments
 (0)