import { describe, test, expect, vi } from "vitest"; import pc from "picocolors"; import inquirer, { Answers, InputQuestionOptions } from "inquirer"; import Question from "./Question.js"; const MESSAGES = { skip: "(press enter to skip)", max: "upper %d chars", min: "%d chars at least", emptyWarning: "%s can not be empty", upperLimitWarning: "%s: %s over limit %d", lowerLimitWarning: "%s: %s below limit %d", }; const QUESTION_CONFIG = { title: "please input", messages: MESSAGES, }; const caseFn = (input: string | string[], delimiter?: string) => (Array.isArray(input) ? input : [input]) .map((segment) => segment[0].toUpperCase() + segment.slice(1)) .join(delimiter); describe("name", () => { test("should throw error when name is not a meaningful string", () => { expect( () => new Question("" as any, { ...QUESTION_CONFIG, }), ).toThrow(); expect( () => new Question( function () { return "scope"; } as any, { ...QUESTION_CONFIG, }, ), ).toThrow(); }); test("should set name when name is valid", () => { expect( new Question("test" as any, { ...QUESTION_CONFIG, }).question, ).toHaveProperty("name", "test"); }); }); describe("type", () => { test('should return "list" type when enumList is array and multipleSelectDefaultDelimiter is undefined', () => { const question = new Question("scope", { ...QUESTION_CONFIG, enumList: ["cli", "core"], }).question; expect(question).toHaveProperty("type", "list"); expect(question).toHaveProperty("choices", ["cli", "core"]); expect(question).not.toHaveProperty("transformer"); }); test('should return "checkbox" type when enumList is array and multipleSelectDefaultDelimiter is defined', () => { const question = new Question("scope", { ...QUESTION_CONFIG, enumList: ["cli", "core"], multipleSelectDefaultDelimiter: ",", }).question; expect(question).toHaveProperty("type", "checkbox"); expect(question).toHaveProperty("choices", ["cli", "core"]); expect(question).not.toHaveProperty("transformer"); }); test('should contain "skip" list item when enumList is array and skip is true', () => { const question = new Question("scope", { ...QUESTION_CONFIG, enumList: ["cli", "core"], skip: true, }).question; expect(question).toHaveProperty("type", "list"); expect(question).toHaveProperty("choices", [ "cli", "core", new inquirer.Separator(), { name: "empty", value: "", }, ]); expect(question).not.toHaveProperty("transformer"); }); test('should return "confirm" type when name is start with "is"', () => { const question = new Question("isSubmit" as any, { ...QUESTION_CONFIG, }).question; expect(question).toHaveProperty("type", "confirm"); expect(question).not.toHaveProperty("choices"); expect(question).not.toHaveProperty("transformer"); }); test('should return "input" type in other cases', () => { const question = new Question("body", { ...QUESTION_CONFIG, }).question; expect(question).toHaveProperty("type", "input"); expect(question).not.toHaveProperty("choices"); expect(question).toHaveProperty("transformer", expect.any(Function)); }); }); describe("message", () => { test("should display title when it is not input", () => { const question = new Question("body", { ...QUESTION_CONFIG, enumList: ["cli", "core"], }).question; expect(question).toHaveProperty("message", expect.any(Function)); expect((question.message as any)()).toBe("please input:"); }); test("should display skip hint when it is input and can skip", () => { const question = new Question("body" as any, { ...QUESTION_CONFIG, skip: true, }).question; expect(question).toHaveProperty("message", expect.any(Function)); expect((question.message as any)()).toBe( "please input (press enter to skip):\n", ); }); test("should not display skip hint when it is input and without skip string", () => { const question = new Question("scope", { ...QUESTION_CONFIG, messages: {}, skip: true, } as any).question; expect(question).toHaveProperty("message", expect.any(Function)); expect((question.message as any)()).toBe("please input:\n"); }); test("should display upper limit hint when it is input and has max length", () => { const question = new Question("scope", { ...QUESTION_CONFIG, maxLength: 80, } as any).question; expect(question).toHaveProperty("message", expect.any(Function)); expect((question.message as any)()).toBe("please input: upper 80 chars\n"); }); test("should display lower limit hint when it is input and has min length", () => { const question = new Question("scope", { ...QUESTION_CONFIG, minLength: 10, } as any).question; expect(question).toHaveProperty("message", expect.any(Function)); expect((question.message as any)()).toBe( "please input: 10 chars at least\n", ); }); test("should display hints with correct format", () => { const question = new Question("scope", { ...QUESTION_CONFIG, minLength: 10, maxLength: 80, skip: true, } as any).question; expect(question).toHaveProperty("message", expect.any(Function)); expect((question.message as any)()).toBe( "please input (press enter to skip): 10 chars at least, upper 80 chars\n", ); }); test("should execute function beforeQuestionStart when init message", () => { const mockFn = vi.fn(); class CustomQuestion extends Question { beforeQuestionStart(answers: Answers): void { mockFn(answers); } } const question = new CustomQuestion("body", { ...QUESTION_CONFIG, } as any).question; expect(question).toHaveProperty("message", expect.any(Function)); const answers = { header: "This is header", footer: "This is footer", }; (question.message as any)(answers); expect(mockFn).toHaveBeenCalledWith(answers); }); }); describe("filter", () => { test("should auto fix case and full-stop", () => { const question = new Question("body", { ...QUESTION_CONFIG, caseFn, fullStopFn: (input: string) => input + "!", }).question; expect(question.filter?.("xxxx", {})).toBe("Xxxx!"); }); test("should transform each item with same case when input is array", () => { const question = new Question("body", { ...QUESTION_CONFIG, caseFn, fullStopFn: (input: string) => input + "!", }).question; expect(question.filter?.(["xxxx", "yyyy"], {})).toBe("Xxxx,Yyyy!"); }); test("should concat items with multipleSelectDefaultDelimiter when input is array", () => { const question = new Question("body", { ...QUESTION_CONFIG, caseFn, fullStopFn: (input: string) => input + "!", multipleSelectDefaultDelimiter: "|", }).question; expect(question.filter?.(["xxxx", "yyyy"], {})).toBe("Xxxx|Yyyy!"); }); test("should split the string to items when multipleValueDelimiters is defined", () => { const question = new Question("body", { ...QUESTION_CONFIG, caseFn, fullStopFn: (input: string) => input + "!", multipleValueDelimiters: /,|\|/g, }).question; expect(question.filter?.("xxxx,yyyy|zzzz", {})).toBe("Xxxx,Yyyy|Zzzz!"); expect(question.filter?.("xxxx-yyyy-zzzz", {})).toBe("Xxxx-yyyy-zzzz!"); }); test("should works well when does not pass caseFn/fullStopFn", () => { const question = new Question("body", { ...QUESTION_CONFIG, }).question; expect(question.filter?.("xxxx", {})).toBe("xxxx"); }); }); describe("validate", () => { test("should display empty warning when can not skip but string is empty", () => { const question = new Question("body", { ...QUESTION_CONFIG, skip: false, }).question; expect(question.validate?.("")).toBe("body can not be empty"); }); test("should ignore empty validation when can skip", () => { const question = new Question("body", { ...QUESTION_CONFIG, skip: true, }).question; expect(question.validate?.("")).toBe(true); }); test("should display upper limit warning when char count is over upper limit", () => { const question = new Question("body", { ...QUESTION_CONFIG, maxLength: 5, }).question; expect(question.validate?.("xxxxxx")).toBe("body: body over limit 1"); }); test("should display lower limit warning when char count is less than lower limit", () => { const question = new Question("body", { ...QUESTION_CONFIG, minLength: 5, }).question; expect(question.validate?.("xxx")).toBe("body: body below limit 2"); }); test("should validate the final submit string", () => { const question = new Question("body", { ...QUESTION_CONFIG, caseFn: () => "", skip: false, }).question; expect(question.validate?.("xxxx")).not.toBe(true); }); }); describe("transformer", () => { test("should auto transform case and full-stop", () => { const question = new Question("body", { ...QUESTION_CONFIG, caseFn, fullStopFn: (input: string) => input + "!", }).question; expect( (question as InputQuestionOptions)?.transformer?.("xxxx", {}, {}), ).toBe("Xxxx!"); }); test("should char count with green color when in the limit range", () => { let question = new Question("body", { ...QUESTION_CONFIG, maxLength: 5, }).question; expect( (question as InputQuestionOptions)?.transformer?.("xxx", {}, {}), ).toEqual(pc.green(`(3) xxx`)); question = new Question("body", { ...QUESTION_CONFIG, minLength: 2, }).question; expect( (question as InputQuestionOptions)?.transformer?.("xxx", {}, {}), ).toEqual(pc.green(`(3) xxx`)); }); test("should char count with red color when over the limit range", () => { let question = new Question("body", { ...QUESTION_CONFIG, maxLength: 5, }).question; expect( (question as InputQuestionOptions)?.transformer?.("xxxxxx", {}, {}), ).toEqual(pc.red(`(6) xxxxxx`)); question = new Question("body", { ...QUESTION_CONFIG, minLength: 2, }).question; expect( (question as InputQuestionOptions)?.transformer?.("x", {}, {}), ).toEqual(pc.red(`(1) x`)); }); }); describe("inquirer question", () => { test('should pass "when" and "default" field to inquirer question', () => { const when = (answers: Answers) => !!answers.header; const question = new Question("body", { ...QUESTION_CONFIG, when, defaultValue: "update", }).question; expect(question).toHaveProperty("default", "update"); expect(question).toHaveProperty("when", when); }); });