project-line-counter
Version:
A simple CLI tool to count lines of code by language.
262 lines (261 loc) • 11.8 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import fs from "fs";
import path from "path";
import { select } from "@inquirer/prompts";
// Mock dependencies
jest.mock("fs");
jest.mock("path");
jest.mock("chalk", () => ({
red: jest.fn((text) => text),
blue: jest.fn((text) => text),
green: jest.fn((text) => text),
yellow: jest.fn((text) => text),
cyan: { bold: jest.fn((text) => text) },
gray: jest.fn((text) => text),
magenta: jest.fn((text) => text),
}));
jest.mock("@inquirer/prompts", () => ({
select: jest.fn(),
}));
// Import the code to test
import * as script from "../index";
describe("CLI Script Tests", () => {
let consoleLogSpy;
let consoleWarnSpy;
let consoleErrorSpy;
let exitSpy;
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
consoleLogSpy = jest.spyOn(console, "log").mockImplementation();
consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
exitSpy = jest.spyOn(process, "exit").mockImplementation(jest.fn());
// Setup default mock return values - ensure this always returns an array
fs.readdirSync.mockReturnValue([]);
fs.statSync.mockReturnValue({ isDirectory: () => false });
fs.readFileSync.mockReturnValue("");
path.extname.mockReturnValue(".js");
path.join.mockImplementation((...args) => args.join("/"));
process.cwd = jest.fn().mockReturnValue("/mock/directory");
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleWarnSpy.mockRestore();
consoleErrorSpy.mockRestore();
exitSpy.mockRestore();
});
describe("parseFlags", () => {
it("should parse --typescript flag correctly", () => {
process.argv = ["node", "script.js", "--typescript"];
const result = script.parseFlags();
expect(result).toBe("TypeScript");
});
it("should parse --javascript flag correctly", () => {
process.argv = ["node", "script.js", "--javascript"];
const result = script.parseFlags();
expect(result).toBe("JavaScript");
});
it("should parse --python flag correctly", () => {
process.argv = ["node", "script.js", "--python"];
const result = script.parseFlags();
expect(result).toBe("Python");
});
it("should parse --all flag correctly", () => {
process.argv = ["node", "script.js", "--all"];
const result = script.parseFlags();
expect(result).toBe("All");
});
it("should return null when no flags are provided", () => {
process.argv = ["node", "script.js"];
const result = script.parseFlags();
expect(result).toBeNull();
});
it("should exit with error for unknown flags", () => {
process.argv = ["node", "script.js", "--unknown"];
script.parseFlags();
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown flag"));
expect(exitSpy).toHaveBeenCalledWith(1);
});
});
describe("getAllFiles", () => {
it("should recursively gather files ignoring node_modules", () => {
fs.readdirSync.mockImplementation((dir) => {
if (dir === "/mock/directory") {
return ["file1.js", "node_modules", ".git", "subfolder"];
}
else if (dir === "/mock/directory/subfolder") {
return ["file2.js", "file3.py"];
}
return [];
});
fs.statSync.mockImplementation((path) => ({
isDirectory: () => path.includes("subfolder") ||
path.includes("node_modules") ||
path.includes(".git"),
}));
const files = script.getAllFiles("/mock/directory");
expect(files).toEqual([
"/mock/directory/file1.js",
"/mock/directory/subfolder/file2.js",
"/mock/directory/subfolder/file3.py",
]);
// Verify node_modules and .git were skipped
expect(files.find((f) => f.includes("node_modules"))).toBeUndefined();
expect(files.find((f) => f.includes(".git"))).toBeUndefined();
});
});
describe("countLines", () => {
it("should count lines in JavaScript files ignoring comments", () => {
const jsContent = `
// This is a comment
const a = 1;
/*
Multi-line comment
that should be ignored
*/
const b = 2;
// Another comment
`;
fs.readFileSync.mockReturnValue(jsContent);
const count = script.countLines("file.js", ".js");
// Should count 6 lines (empty lines + non-comment code lines)
expect(count).toBe(6);
});
it("should count lines in Python files ignoring comments", () => {
const pythonContent = `
# This is a comment
def function():
print("Hello")
# Another comment
return 42
`;
fs.readFileSync.mockReturnValue(pythonContent);
const count = script.countLines("file.py", ".py");
// Should count 5 lines (empty lines + non-comment code lines)
expect(count).toBe(5);
});
});
describe("performCount", () => {
beforeEach(() => {
// Add specific mock setup for each test in this describe block
fs.readdirSync.mockReturnValue(["file1.js"]);
});
it("should count lines for specific extensions", () => {
// Mock files in the directory - already setup in beforeEach
fs.readdirSync.mockReturnValue([
"file1.js",
"file2.py",
"file3.ts",
]);
// Mock file contents
fs.readFileSync.mockImplementation((file) => {
if (file.includes("file1.js"))
return "line1\nline2\nline3";
if (file.includes("file2.py"))
return "line1\nline2";
if (file.includes("file3.ts"))
return "line1\nline2\nline3\nline4";
return "";
});
// Only count JS files
path.extname.mockImplementation((file) => {
if (file.includes("file1"))
return ".js";
if (file.includes("file2"))
return ".py";
if (file.includes("file3"))
return ".ts";
return "";
});
script.performCount([".js", ".ts"]);
// Check that console output shows correct counts
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Files scanned: 2"));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Total lines of code: 7"));
});
it("should count all files when extensions is null", () => {
// Similar setup to previous test
fs.readdirSync.mockReturnValue(["file1.js", "file2.py"]);
fs.statSync.mockReturnValue({ isDirectory: () => false });
fs.readFileSync.mockImplementation((file) => {
if (file.includes("file1.js"))
return "line1\nline2";
if (file.includes("file2.py"))
return "line1\nline2\nline3";
return "";
});
path.extname.mockImplementation((file) => {
if (file.includes("file1"))
return ".js";
if (file.includes("file2"))
return ".py";
return "";
});
script.performCount(null);
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Files scanned: 2"));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Total lines of code: 5"));
});
it("should handle errors when reading files", () => {
fs.readdirSync.mockReturnValue(["file1.js", "file2.js"]);
fs.statSync.mockReturnValue({ isDirectory: () => false });
path.extname.mockReturnValue(".js");
// Mock readFileSync to throw error for file2.js
fs.readFileSync.mockImplementation((file) => {
if (file.includes("file2"))
throw new Error("Cannot read file");
return "line1\nline2";
});
script.performCount([".js"]);
// Should only count the first file
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Files scanned: 1"));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Total lines of code: 2"));
});
});
describe("interactiveFlow", () => {
it("should prompt user and perform count based on selection", () => __awaiter(void 0, void 0, void 0, function* () {
select.mockResolvedValue("JavaScript");
yield script.interactiveFlow();
expect(select).toHaveBeenCalledWith(expect.objectContaining({
message: expect.stringContaining("Select a language"),
choices: ["All", "TypeScript", "JavaScript", "Python"],
}));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("Counting JavaScript files"));
}));
});
describe("main function", () => {
beforeEach(() => {
// Mock interactiveFlow to prevent it from being called directly
jest.spyOn(script, "interactiveFlow").mockImplementation(jest.fn());
jest.spyOn(script, "performCount").mockImplementation(jest.fn());
});
it("should use flag language if provided", () => __awaiter(void 0, void 0, void 0, function* () {
// Mock parseFlags to return a language
jest.spyOn(script, "parseFlags").mockReturnValue("Python");
yield script.main();
expect(script.performCount).toHaveBeenCalledWith([".py"]);
expect(script.interactiveFlow).not.toHaveBeenCalled();
}));
it("should fall back to interactive flow if no flags", () => __awaiter(void 0, void 0, void 0, function* () {
jest.spyOn(script, "parseFlags").mockReturnValue(null);
jest.spyOn(script, "interactiveFlow").mockImplementation(jest.fn());
yield script.main();
expect(script.interactiveFlow).toHaveBeenCalled();
}));
it("should handle errors gracefully", () => __awaiter(void 0, void 0, void 0, function* () {
jest.spyOn(script, "parseFlags").mockImplementation(() => {
throw new Error("Test error");
});
yield script.main();
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Error:"), expect.any(Error));
expect(exitSpy).toHaveBeenCalledWith(1);
}));
});
});