validate-readme-checklist.test.ts•24.9 kB
import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import { promises as fs } from "fs";
import * as path from "path";
import * as tmp from "tmp";
import {
validateReadmeChecklist,
ReadmeChecklistValidator,
ValidateReadmeChecklistSchema,
} from "../../src/tools/validate-readme-checklist";
describe("README Checklist Validator", () => {
let tempDir: string;
let validator: ReadmeChecklistValidator;
beforeEach(() => {
tempDir = tmp.dirSync({ unsafeCleanup: true }).name;
validator = new ReadmeChecklistValidator();
});
afterEach(async () => {
try {
await fs.rmdir(tempDir, { recursive: true });
} catch {
// Ignore cleanup errors
}
});
async function createTestReadme(
content: string,
filename = "README.md",
): Promise<string> {
const readmePath = path.join(tempDir, filename);
await fs.writeFile(readmePath, content, "utf-8");
return readmePath;
}
async function createProjectFile(
filename: string,
content = "",
): Promise<void> {
await fs.writeFile(path.join(tempDir, filename), content, "utf-8");
}
describe("Input Validation", () => {
it("should validate required fields", () => {
expect(() => ValidateReadmeChecklistSchema.parse({})).toThrow();
expect(() =>
ValidateReadmeChecklistSchema.parse({
readmePath: "",
}),
).toThrow();
});
it("should accept valid input with defaults", () => {
const input = ValidateReadmeChecklistSchema.parse({
readmePath: "/path/to/README.md",
});
expect(input.strict).toBe(false);
expect(input.outputFormat).toBe("console");
});
it("should validate output format options", () => {
const validFormats = ["json", "markdown", "console"];
for (const format of validFormats) {
expect(() =>
ValidateReadmeChecklistSchema.parse({
readmePath: "/test/README.md",
outputFormat: format,
}),
).not.toThrow();
}
expect(() =>
ValidateReadmeChecklistSchema.parse({
readmePath: "/test/README.md",
outputFormat: "invalid",
}),
).toThrow();
});
});
describe("Essential Sections Validation", () => {
it("should detect project title", async () => {
const goodReadme = await createTestReadme(
"# My Project\n\nDescription here",
"good-README.md",
);
const badReadme = await createTestReadme(
"## Not a main title\n\nNo main heading",
"bad-README.md",
);
const goodInput = ValidateReadmeChecklistSchema.parse({
readmePath: goodReadme,
});
const badInput = ValidateReadmeChecklistSchema.parse({
readmePath: badReadme,
});
const result = await validateReadmeChecklist(goodInput);
const result2 = await validateReadmeChecklist(badInput);
const titleCheck = result.categories["Essential Sections"].results.find(
(r) => r.item.id === "title",
);
const badTitleCheck = result2.categories[
"Essential Sections"
].results.find((r) => r.item.id === "title");
expect(titleCheck?.passed).toBe(true);
expect(badTitleCheck?.passed).toBe(false);
});
it("should detect project description", async () => {
const withSubtitle = await createTestReadme(
"# Project\n\n> A great project description",
"subtitle-README.md",
);
const withParagraph = await createTestReadme(
"# Project\n\nThis is a description paragraph",
"paragraph-README.md",
);
const withoutDesc = await createTestReadme(
"# Project\n\n## Installation",
"no-desc-README.md",
);
const subtitleResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: withSubtitle }),
);
const paragraphResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: withParagraph }),
);
const noDescResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: withoutDesc }),
);
const getDescCheck = (result: any) =>
result.categories["Essential Sections"].results.find(
(r: any) => r.item.id === "description",
);
expect(getDescCheck(subtitleResult)?.passed).toBe(true);
expect(getDescCheck(paragraphResult)?.passed).toBe(true);
expect(getDescCheck(noDescResult)?.passed).toBe(false);
});
it("should detect TL;DR section", async () => {
const withTldr = await createTestReadme(
"# Project\n\n## TL;DR\n\nQuick summary",
"tldr-README.md",
);
const withQuickStart = await createTestReadme(
"# Project\n\n## Quick Start\n\nQuick summary",
"quickstart-README.md",
);
const withoutTldr = await createTestReadme(
"# Project\n\n## Installation",
"no-tldr-README.md",
);
const tldrInput = ValidateReadmeChecklistSchema.parse({
readmePath: withTldr,
});
const quickStartInput = ValidateReadmeChecklistSchema.parse({
readmePath: withQuickStart,
});
const noTldrInput = ValidateReadmeChecklistSchema.parse({
readmePath: withoutTldr,
});
const result = await validateReadmeChecklist(tldrInput);
const result2 = await validateReadmeChecklist(quickStartInput);
const result3 = await validateReadmeChecklist(noTldrInput);
const getTldrCheck = (result: any) =>
result.categories["Essential Sections"].results.find(
(r: any) => r.item.id === "tldr",
);
expect(getTldrCheck(result)?.passed).toBe(true);
expect(getTldrCheck(result2)?.passed).toBe(true);
expect(getTldrCheck(result3)?.passed).toBe(false);
});
it("should detect installation instructions with code blocks", async () => {
const goodInstall = await createTestReadme(
`
# Project
## Installation
\`\`\`bash
npm install project
\`\`\`
`,
"good-install-README.md",
);
const noCodeBlocks = await createTestReadme(
`
# Project
## Installation
Just install it somehow
`,
"no-code-README.md",
);
const noInstallSection = await createTestReadme(
"# Project\n\nSome content",
"no-install-README.md",
);
const goodResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: goodInstall }),
);
const noCodeResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: noCodeBlocks }),
);
const noSectionResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: noInstallSection }),
);
const getInstallCheck = (result: any) =>
result.categories["Essential Sections"].results.find(
(r: any) => r.item.id === "installation",
);
expect(getInstallCheck(goodResult)?.passed).toBe(true);
expect(getInstallCheck(noCodeResult)?.passed).toBe(true); // This should pass because it has Installation section
expect(getInstallCheck(noSectionResult)?.passed).toBe(false);
});
it("should detect usage examples", async () => {
const goodUsage = await createTestReadme(
`
# Project
## Usage
\`\`\`javascript
const lib = require('lib');
lib.doSomething();
\`\`\`
`,
"good-usage-README.md",
);
const noUsage = await createTestReadme(
"# Project\n\nNo usage section",
"no-usage-README.md",
);
const goodResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: goodUsage }),
);
const noUsageResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: noUsage }),
);
const getUsageCheck = (result: any) =>
result.categories["Essential Sections"].results.find(
(r: any) => r.item.id === "usage",
);
expect(getUsageCheck(goodResult)?.passed).toBe(true);
expect(getUsageCheck(noUsageResult)?.passed).toBe(false);
});
it("should detect license information", async () => {
const readmeWithLicense = await createTestReadme(
"# Project\n\n## License\n\nMIT",
"license-README.md",
);
const readmeWithoutLicense = await createTestReadme(
"# Project\n\nNo license info",
"no-license-README.md",
);
// Test without LICENSE file first
const withLicenseResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readmeWithLicense,
projectPath: tempDir,
}),
);
const withoutLicenseResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readmeWithoutLicense,
projectPath: tempDir,
}),
);
// Test with LICENSE file
await createProjectFile("LICENSE", "MIT License...");
const readmeWithLicenseFile = await createTestReadme(
"# Project\n\nSome content",
"license-file-README.md",
);
const withLicenseFileResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readmeWithLicenseFile,
projectPath: tempDir,
}),
);
const getLicenseCheck = (result: any) =>
result.categories["Essential Sections"].results.find(
(r: any) => r.item.id === "license",
);
expect(getLicenseCheck(withLicenseResult)?.passed).toBe(true);
expect(getLicenseCheck(withoutLicenseResult)?.passed).toBe(false);
expect(getLicenseCheck(withLicenseFileResult)?.passed).toBe(true);
});
});
describe("Community Health Validation", () => {
it("should detect contributing guidelines", async () => {
const readmeWithContributing = await createTestReadme(
"# Project\n\n## Contributing\n\nSee CONTRIBUTING.md",
);
await createProjectFile("CONTRIBUTING.md", "Contributing guidelines...");
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readmeWithContributing,
projectPath: tempDir,
}),
);
const contributingCheck = result.categories[
"Community Health"
].results.find((r) => r.item.id === "contributing");
expect(contributingCheck?.passed).toBe(true);
});
it("should detect code of conduct", async () => {
await createProjectFile("CODE_OF_CONDUCT.md", "Code of conduct...");
const readme = await createTestReadme("# Project\n\nSome content");
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readme,
projectPath: tempDir,
}),
);
const cocCheck = result.categories["Community Health"].results.find(
(r) => r.item.id === "code-of-conduct",
);
expect(cocCheck?.passed).toBe(true);
});
it("should detect security policy", async () => {
await createProjectFile("SECURITY.md", "Security policy...");
const readme = await createTestReadme("# Project\n\nSome content");
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readme,
projectPath: tempDir,
}),
);
const securityCheck = result.categories["Community Health"].results.find(
(r) => r.item.id === "security",
);
expect(securityCheck?.passed).toBe(true);
});
});
describe("Visual Elements Validation", () => {
it("should detect status badges", async () => {
const withBadges = await createTestReadme(
`
# Project
[](https://travis-ci.org/user/repo)
[](https://badge.fury.io/js/package)
`,
"with-badges-README.md",
);
const withoutBadges = await createTestReadme(
"# Project\n\nNo badges here",
"no-badges-README.md",
);
const withBadgesResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: withBadges }),
);
const withoutBadgesResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: withoutBadges }),
);
const getBadgeCheck = (result: any) =>
result.categories["Visual Elements"].results.find(
(r: any) => r.item.id === "badges",
);
expect(getBadgeCheck(withBadgesResult)?.passed).toBe(true);
expect(getBadgeCheck(withoutBadgesResult)?.passed).toBe(false);
});
it("should detect screenshots and images", async () => {
const withScreenshots = await createTestReadme(
`
# Project


`,
"with-screenshots-README.md",
);
const withoutScreenshots = await createTestReadme(
"# Project\n\nNo images",
"no-screenshots-README.md",
);
const withScreenshotsResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: withScreenshots }),
);
const withoutScreenshotsResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: withoutScreenshots }),
);
const getScreenshotCheck = (result: any) =>
result.categories["Visual Elements"].results.find(
(r: any) => r.item.id === "screenshots",
);
expect(getScreenshotCheck(withScreenshotsResult)?.passed).toBe(true);
expect(getScreenshotCheck(withoutScreenshotsResult)?.passed).toBe(false);
});
it("should validate markdown formatting", async () => {
const goodFormatting = await createTestReadme(
`
# Main Title
## Section 1
### Subsection
## Section 2
`,
"good-formatting-README.md",
);
const poorFormatting = await createTestReadme(
`
# Title
#Another Title
##Poor Spacing
`,
"poor-formatting-README.md",
);
const goodResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: goodFormatting }),
);
const poorResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: poorFormatting }),
);
const getFormattingCheck = (result: any) =>
result.categories["Visual Elements"].results.find(
(r: any) => r.item.id === "formatting",
);
expect(getFormattingCheck(goodResult)?.passed).toBe(true);
expect(getFormattingCheck(poorResult)?.passed).toBe(false);
});
});
describe("Content Quality Validation", () => {
it("should detect working code examples", async () => {
const withCodeExamples = await createTestReadme(
`
# Project
\`\`\`javascript
const lib = require('lib');
lib.doSomething();
\`\`\`
\`\`\`bash
npm install lib
\`\`\`
`,
"with-code-README.md",
);
const withoutCodeExamples = await createTestReadme(
"# Project\n\nNo code examples",
"no-code-examples-README.md",
);
const withCodeResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: withCodeExamples }),
);
const withoutCodeResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: withoutCodeExamples,
}),
);
const getCodeCheck = (result: any) =>
result.categories["Content Quality"].results.find(
(r: any) => r.item.id === "working-examples",
);
expect(getCodeCheck(withCodeResult)?.passed).toBe(true);
expect(getCodeCheck(withoutCodeResult)?.passed).toBe(false);
});
it("should validate appropriate length", async () => {
const shortReadme = await createTestReadme(
"# Project\n\nShort content",
"short-README.md",
);
const longContent =
"# Project\n\n" + "Long line of content.\n".repeat(350);
const longReadme = await createTestReadme(longContent, "long-README.md");
const shortResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: shortReadme }),
);
const longResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: longReadme }),
);
const getLengthCheck = (result: any) =>
result.categories["Content Quality"].results.find(
(r: any) => r.item.id === "appropriate-length",
);
expect(getLengthCheck(shortResult)?.passed).toBe(true);
expect(getLengthCheck(longResult)?.passed).toBe(false);
});
it("should validate scannable structure", async () => {
const goodStructure = await createTestReadme(
`
# Main Title
## Section 1
### Subsection 1.1
- Item 1
- Item 2
### Subsection 1.2
## Section 2
### Subsection 2.1
- Another item
- Yet another item
`,
"good-structure-README.md",
);
const poorStructure = await createTestReadme(
`
# Title
#### Skipped levels
## Back to level 2
`,
"poor-structure-README.md",
);
const goodResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: goodStructure }),
);
const poorResult = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: poorStructure }),
);
const getStructureCheck = (result: any) =>
result.categories["Content Quality"].results.find(
(r: any) => r.item.id === "scannable-structure",
);
expect(getStructureCheck(goodResult)?.passed).toBe(true);
expect(getStructureCheck(poorResult)?.passed).toBe(false);
});
});
describe("Report Generation", () => {
it("should generate comprehensive report with all categories", async () => {
const readme = await createTestReadme(`
# Test Project
> A test project description
## TL;DR
Quick summary of the project.
## Quick Start
\`\`\`bash
npm install test-project
\`\`\`
## Usage
\`\`\`javascript
const test = require('test-project');
test.run();
\`\`\`
## License
MIT
`);
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: readme }),
);
expect(result.overallScore).toBeGreaterThan(0);
expect(result.totalItems).toBeGreaterThan(0);
expect(result.passedItems).toBeGreaterThan(0);
expect(result.categories).toHaveProperty("Essential Sections");
expect(result.categories).toHaveProperty("Community Health");
expect(result.categories).toHaveProperty("Visual Elements");
expect(result.categories).toHaveProperty("Content Quality");
expect(result.wordCount).toBeGreaterThan(0);
expect(result.estimatedReadTime).toBeGreaterThan(0);
});
it("should calculate scores correctly", async () => {
const perfectReadme = await createTestReadme(`
# Perfect Project
> An amazing project that does everything right
[](https://travis-ci.org/user/repo)
## TL;DR
This project is perfect and demonstrates all best practices.
## Quick Start
\`\`\`bash
npm install perfect-project
\`\`\`
## Usage
\`\`\`javascript
const perfect = require('perfect-project');
perfect.doSomething();
\`\`\`
## Contributing
See CONTRIBUTING.md for guidelines.
## License
MIT © Author
`);
await createProjectFile("CONTRIBUTING.md", "Guidelines...");
await createProjectFile("LICENSE", "MIT License...");
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: perfectReadme,
projectPath: tempDir,
}),
);
expect(result.overallScore).toBeGreaterThan(70);
expect(result.categories["Essential Sections"].score).toBeGreaterThan(80);
});
it("should provide helpful recommendations", async () => {
const poorReadme = await createTestReadme(
"# Poor Project\n\nMinimal content",
);
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: poorReadme }),
);
expect(result.recommendations.length).toBeGreaterThan(0);
expect(result.overallScore).toBeLessThan(50);
});
});
describe("Output Formatting", () => {
it("should format console output correctly", async () => {
const readme = await createTestReadme("# Test\n\nContent");
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readme,
outputFormat: "console",
}),
);
const formatted = validator.formatReport(result, "console");
expect(formatted).toContain("📋 README Checklist Report");
expect(formatted).toContain("Overall Score:");
expect(formatted).toContain("Essential Sections");
expect(formatted).toContain("✅");
expect(formatted).toContain("❌");
});
it("should format markdown output correctly", async () => {
const readme = await createTestReadme("# Test\n\nContent");
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readme,
outputFormat: "markdown",
}),
);
const formatted = validator.formatReport(result, "markdown");
expect(formatted).toContain("# README Checklist Report");
expect(formatted).toContain("## Overall Score:");
expect(formatted).toContain("### Essential Sections");
expect(formatted).toContain("- ✅");
expect(formatted).toContain("- ❌");
});
it("should format JSON output correctly", async () => {
const readme = await createTestReadme("# Test\n\nContent");
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readme,
outputFormat: "json",
}),
);
const formatted = validator.formatReport(result, "json");
const parsed = JSON.parse(formatted);
expect(parsed).toHaveProperty("overallScore");
expect(parsed).toHaveProperty("categories");
expect(parsed).toHaveProperty("recommendations");
});
});
describe("Error Handling", () => {
it("should handle non-existent README file", async () => {
const nonExistentPath = path.join(tempDir, "nonexistent.md");
await expect(
validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: nonExistentPath }),
),
).rejects.toThrow();
});
it("should handle invalid project path gracefully", async () => {
const readme = await createTestReadme("# Test\n\nContent");
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({
readmePath: readme,
projectPath: "/invalid/path",
}),
);
// Should still work, just without project file context
expect(result.overallScore).toBeGreaterThan(0);
});
it("should handle empty README file", async () => {
const emptyReadme = await createTestReadme("", "empty-README.md");
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: emptyReadme }),
);
// Empty README should pass length test (0 words <= 300) and external links test (no links to fail)
// but fail most other tests, resulting in a low overall score
expect(result.overallScore).toBeLessThan(20); // Very low score due to missing content
expect(result.passedItems).toBe(2); // Only length and external-links should pass
expect(result.failedItems).toBe(15); // Most checks should fail
});
});
describe("Suggestions Generation", () => {
it("should provide specific suggestions for failed checks", async () => {
const incompleteReadme = await createTestReadme(
"# Project\n\nMinimal content",
);
const result = await validateReadmeChecklist(
ValidateReadmeChecklistSchema.parse({ readmePath: incompleteReadme }),
);
const failedChecks = Object.values(result.categories)
.flatMap((cat) => cat.results)
.filter((r) => !r.passed && r.suggestions);
expect(failedChecks.length).toBeGreaterThan(0);
for (const check of failedChecks) {
expect(check.suggestions).toBeDefined();
expect(check.suggestions!.length).toBeGreaterThan(0);
}
});
});
});