import { describe, expect, it } from "vitest"; import { FlagError, HELP_TEXT, parseFlags, validateProjectName, wantsHelp } from "../src/flags.js"; /** * `parseArgs` consumes argv from index 2 onward, mirroring `process.argv`. * Helper keeps tests readable. */ function argv(...args: string[]): string[] { return ["node", "create-emdash", ...args]; } describe("parseFlags — defaults", () => { it("returns no values when called with no args", () => { const flags = parseFlags(argv()); expect(flags).toEqual({ yes: false, force: false, help: false }); }); it("treats yes/force/help as false when omitted (not undefined)", () => { // Important: index.ts checks these directly, so they must be // boolean — never undefined. const flags = parseFlags(argv()); expect(flags.yes).toBe(false); expect(flags.force).toBe(false); expect(flags.help).toBe(false); }); }); describe("parseFlags — positional name", () => { it("captures the project name from the first positional", () => { const flags = parseFlags(argv("my-blog")); expect(flags.name).toBe("my-blog"); }); it('accepts "." for current directory', () => { const flags = parseFlags(argv(".")); expect(flags.name).toBe("."); }); it("does NOT validate the name itself — that's the resolver's job", () => { // Validation moved to validateProjectName so the prompt path and the // flag path use the same rule with the same error message. parseFlags // is purely structural; it stores whatever the user typed. The // resolver enforces PROJECT_NAME_PATTERN before scaffolding. const flags = parseFlags(argv("My-Site")); expect(flags.name).toBe("My-Site"); }); it("accepts a positional after flags", () => { const flags = parseFlags(argv("--yes", "my-blog")); expect(flags.name).toBe("my-blog"); expect(flags.yes).toBe(true); }); it("rejects extra positionals as a likely typo", () => { // `npm create emdash my blog` (space instead of hyphen) is the // killer case: without this check it parses as name="my", drops // "blog" silently, and creates a project literally named "my". expect(() => parseFlags(argv("my", "blog"))).toThrow(FlagError); expect(() => parseFlags(argv("my", "blog"))).toThrow(/Unexpected extra/); }); it("does not reject extra-position errors when the second token is a flag", () => { // argv ordering shouldn't trip the extra-positional check — flags // are removed by parseArgs before we count positionals. const flags = parseFlags(argv("my-blog", "--yes")); expect(flags.name).toBe("my-blog"); expect(flags.yes).toBe(true); }); }); describe("validateProjectName", () => { it("returns undefined for a valid name", () => { expect(validateProjectName("my-site")).toBeUndefined(); expect(validateProjectName("blog")).toBeUndefined(); expect(validateProjectName("a")).toBeUndefined(); expect(validateProjectName("123")).toBeUndefined(); }); it('returns undefined for "." (current-dir sentinel)', () => { expect(validateProjectName(".")).toBeUndefined(); }); it("returns an error message for invalid names", () => { expect(validateProjectName("My-Site")).toMatch(/lowercase letters/); expect(validateProjectName("my site")).toMatch(/lowercase letters/); expect(validateProjectName("my.site")).toMatch(/lowercase letters/); }); }); describe("parseFlags — --template", () => { it("accepts a bare template key", () => { expect(parseFlags(argv("--template", "blog")).template).toBe("blog"); expect(parseFlags(argv("--template", "starter")).template).toBe("starter"); expect(parseFlags(argv("--template", "marketing")).template).toBe("marketing"); expect(parseFlags(argv("--template", "portfolio")).template).toBe("portfolio"); }); it("rejects unknown template keys", () => { expect(() => parseFlags(argv("--template", "nope"))).toThrow(FlagError); expect(() => parseFlags(argv("--template", "nope"))).toThrow(/--template/); }); it("accepts the combined :