mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-17 17:28:09 +00:00
test: add comprehensive tests
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import mockFs from "mock-fs"
|
||||
import FileSystem from "mock-fs/lib/filesystem"
|
||||
import { Console } from "console"
|
||||
import { LogLevel, ScaffoldCmdConfig } from "../src/types"
|
||||
import { LogLevel, ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
|
||||
import * as config from "../src/config"
|
||||
import { resolve } from "../src/utils"
|
||||
import configFile from "./test-config"
|
||||
import { findConfigFile } from "../src/config"
|
||||
import { findConfigFile, getOptionValueForFile } from "../src/config"
|
||||
import { registerHelpers } from "../src/parser"
|
||||
import path from "path"
|
||||
|
||||
jest.mock("../src/git", () => {
|
||||
@@ -55,6 +56,39 @@ describe("config", () => {
|
||||
test("works with quotes", () => {
|
||||
expect(parseAppendData('key="value test"', blankCliConf)).toEqual({ key: "value test", name: "test" })
|
||||
})
|
||||
|
||||
test("handles JSON array values with :=", () => {
|
||||
expect(parseAppendData('items:=["a","b"]', blankCliConf)).toEqual({ items: ["a", "b"], name: "test" })
|
||||
})
|
||||
|
||||
test("handles JSON boolean with :=", () => {
|
||||
expect(parseAppendData("flag:=true", blankCliConf)).toEqual({ flag: true, name: "test" })
|
||||
expect(parseAppendData("flag:=false", blankCliConf)).toEqual({ flag: false, name: "test" })
|
||||
})
|
||||
|
||||
test("handles JSON null with :=", () => {
|
||||
expect(parseAppendData("val:=null", blankCliConf)).toEqual({ val: null, name: "test" })
|
||||
})
|
||||
|
||||
test("handles JSON object with :=", () => {
|
||||
expect(parseAppendData('obj:={"a":1}', blankCliConf)).toEqual({ obj: { a: 1 }, name: "test" })
|
||||
})
|
||||
|
||||
test("handles single quoted values", () => {
|
||||
expect(parseAppendData("key='value test'", blankCliConf)).toEqual({ key: "value test", name: "test" })
|
||||
})
|
||||
|
||||
test("handles empty string value", () => {
|
||||
expect(parseAppendData("key=", blankCliConf)).toEqual({ key: "", name: "test" })
|
||||
})
|
||||
|
||||
test("handles negative number with :=", () => {
|
||||
expect(parseAppendData("num:=-42", blankCliConf)).toEqual({ num: -42, name: "test" })
|
||||
})
|
||||
|
||||
test("handles float with :=", () => {
|
||||
expect(parseAppendData("num:=3.14", blankCliConf)).toEqual({ num: 3.14, name: "test" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("githubPartToUrl", () => {
|
||||
@@ -64,6 +98,14 @@ describe("config", () => {
|
||||
"https://github.com/chenasraf/simple-scaffold.git",
|
||||
)
|
||||
})
|
||||
|
||||
test("handles organization repos", () => {
|
||||
expect(githubPartToUrl("org/sub-repo")).toEqual("https://github.com/org/sub-repo.git")
|
||||
})
|
||||
|
||||
test("handles repos with dots in name", () => {
|
||||
expect(githubPartToUrl("user/my.repo")).toEqual("https://github.com/user/my.repo.git")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseConfigFile", () => {
|
||||
@@ -116,6 +158,111 @@ describe("config", () => {
|
||||
expect(result.output).toEqual("examples/test-output/override")
|
||||
})
|
||||
})
|
||||
|
||||
test("throws when name is missing", async () => {
|
||||
await expect(
|
||||
parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "",
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
}),
|
||||
).rejects.toThrow("Missing required option: name")
|
||||
})
|
||||
|
||||
test("preserves dryRun setting", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
dryRun: true,
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.dryRun).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves subdir setting", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
subdir: true,
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.subdir).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves overwrite setting", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
overwrite: true,
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.overwrite).toBe(true)
|
||||
})
|
||||
|
||||
test("merges data from config and appendData", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
data: { key1: "val1" },
|
||||
appendData: { key2: "val2" },
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.data).toEqual({ key1: "val1", key2: "val2" })
|
||||
})
|
||||
|
||||
test("appendData overrides data", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
data: { key: "original" },
|
||||
appendData: { key: "overridden" },
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.data?.key).toEqual("overridden")
|
||||
})
|
||||
|
||||
test("sets subdirHelper from config", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
subdirHelper: "pascalCase",
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.subdirHelper).toEqual("pascalCase")
|
||||
})
|
||||
|
||||
test("handles empty templates array", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
templates: [],
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.templates).toEqual([])
|
||||
})
|
||||
|
||||
test("throws when config key not found", async () => {
|
||||
await expect(
|
||||
parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
config: path.resolve(__dirname, "test-config.js"),
|
||||
key: "nonexistent",
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
}),
|
||||
).rejects.toThrow('Template "nonexistent" not found')
|
||||
})
|
||||
|
||||
test("uses default key when key not specified", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "MyComponent",
|
||||
templates: undefined as any,
|
||||
config: path.resolve(__dirname, "test-config.js"),
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.templates.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getConfig", () => {
|
||||
@@ -139,6 +286,62 @@ describe("config", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRemoteConfig", () => {
|
||||
test("throws for unsupported protocol", async () => {
|
||||
await expect(
|
||||
config.getRemoteConfig({
|
||||
git: "ftp://example.com/repo.git",
|
||||
logLevel: LogLevel.none,
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
}),
|
||||
).rejects.toThrow("Unsupported protocol")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOptionValueForFile", () => {
|
||||
const conf: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "output",
|
||||
templates: [],
|
||||
logLevel: LogLevel.none,
|
||||
data: { name: "test" },
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
registerHelpers(conf)
|
||||
})
|
||||
|
||||
test("returns static string value", () => {
|
||||
expect(getOptionValueForFile(conf, "/some/path", "static-value")).toEqual("static-value")
|
||||
})
|
||||
|
||||
test("returns static boolean value", () => {
|
||||
expect(getOptionValueForFile(conf, "/some/path", true)).toBe(true)
|
||||
expect(getOptionValueForFile(conf, "/some/path", false)).toBe(false)
|
||||
})
|
||||
|
||||
test("calls function with file path info", () => {
|
||||
const fn = jest.fn().mockReturnValue("custom-output")
|
||||
const result = getOptionValueForFile(conf, "/home/user/file.txt", fn)
|
||||
expect(result).toEqual("custom-output")
|
||||
expect(fn).toHaveBeenCalledWith(
|
||||
"/home/user/file.txt",
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
)
|
||||
})
|
||||
|
||||
test("returns default value when fn is not a function and no value", () => {
|
||||
expect(getOptionValueForFile(conf, "/some/path", undefined as any, "default")).toEqual("default")
|
||||
})
|
||||
|
||||
test("function receives parsed basename", () => {
|
||||
const fn = (_fullPath: string, _basedir: string, basename: string) => basename
|
||||
const result = getOptionValueForFile(conf, "/home/user/{{name}}.txt", fn)
|
||||
expect(result).toEqual("test.txt")
|
||||
})
|
||||
})
|
||||
|
||||
describe("findConfigFile", () => {
|
||||
const struct1 = {
|
||||
"scaffold.config.js": `module.exports = '${JSON.stringify(blankConfig)}'`,
|
||||
@@ -182,5 +385,90 @@ describe("config", () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe(
|
||||
"finds .mjs config file",
|
||||
withMock({ "scaffold.config.mjs": "export default {}" }, () => {
|
||||
test("finds scaffold.config.mjs", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.config.mjs")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"priority order",
|
||||
withMock(
|
||||
{
|
||||
"scaffold.config.js": "module.exports = {}",
|
||||
"scaffold.js": "module.exports = {}",
|
||||
},
|
||||
() => {
|
||||
test("prefers scaffold.config.js over scaffold.js", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.config.js")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"throws when no config found",
|
||||
withMock({ "unrelated-file.txt": "content" }, () => {
|
||||
test("throws error when no config file exists", async () => {
|
||||
await expect(findConfigFile(process.cwd())).rejects.toThrow("Could not find config file")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.config.cjs",
|
||||
withMock({ "scaffold.config.cjs": "module.exports = {}" }, () => {
|
||||
test("finds .cjs config file", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.config.cjs")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.config.json",
|
||||
withMock({ "scaffold.config.json": "{}" }, () => {
|
||||
test("finds .json config file", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.config.json")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.mjs",
|
||||
withMock({ "scaffold.mjs": "export default {}" }, () => {
|
||||
test("finds scaffold.mjs", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.mjs")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.cjs",
|
||||
withMock({ "scaffold.cjs": "module.exports = {}" }, () => {
|
||||
test("finds scaffold.cjs", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.cjs")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.json",
|
||||
withMock({ "scaffold.json": "{}" }, () => {
|
||||
test("finds scaffold.json", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.json")
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
455
tests/file.test.ts
Normal file
455
tests/file.test.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
import mockFs from "mock-fs"
|
||||
import FileSystem from "mock-fs/lib/filesystem"
|
||||
import { Console } from "console"
|
||||
import path from "node:path"
|
||||
import {
|
||||
removeGlob,
|
||||
makeRelativePath,
|
||||
getBasePath,
|
||||
getOutputDir,
|
||||
getUniqueTmpPath,
|
||||
pathExists,
|
||||
createDirIfNotExists,
|
||||
getTemplateGlobInfo,
|
||||
getTemplateFileInfo,
|
||||
copyFileTransformed,
|
||||
handleTemplateFile,
|
||||
getFileList,
|
||||
} from "../src/file"
|
||||
import { ScaffoldConfig, LogLevel } from "../src/types"
|
||||
import { registerHelpers } from "../src/parser"
|
||||
import { readFileSync } from "fs"
|
||||
|
||||
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
|
||||
return () => {
|
||||
beforeEach(() => {
|
||||
console = new Console(process.stdout, process.stderr)
|
||||
mockFs(fileStruct)
|
||||
})
|
||||
testFn()
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig: ScaffoldConfig = {
|
||||
name: "test_app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: LogLevel.none,
|
||||
data: { name: "test_app" },
|
||||
tmpDir: ".",
|
||||
}
|
||||
|
||||
describe("file utilities", () => {
|
||||
describe("removeGlob", () => {
|
||||
test("removes single wildcard", () => {
|
||||
expect(removeGlob("input/*")).toEqual(path.normalize("input/"))
|
||||
})
|
||||
|
||||
test("removes double wildcard", () => {
|
||||
expect(removeGlob("input/**/*")).toEqual(path.normalize("input///"))
|
||||
})
|
||||
|
||||
test("returns path unchanged when no glob", () => {
|
||||
expect(removeGlob("input/file.txt")).toEqual(path.normalize("input/file.txt"))
|
||||
})
|
||||
|
||||
test("removes wildcards from nested path", () => {
|
||||
expect(removeGlob("a/b/*/c/**")).toEqual(path.normalize("a/b//c//"))
|
||||
})
|
||||
|
||||
test("handles empty string", () => {
|
||||
expect(removeGlob("")).toEqual(".")
|
||||
})
|
||||
})
|
||||
|
||||
describe("makeRelativePath", () => {
|
||||
test("removes leading separator", () => {
|
||||
expect(makeRelativePath(path.sep + "some/path")).toEqual("some/path")
|
||||
})
|
||||
|
||||
test("returns path unchanged if no leading separator", () => {
|
||||
expect(makeRelativePath("some/path")).toEqual("some/path")
|
||||
})
|
||||
|
||||
test("handles empty string", () => {
|
||||
expect(makeRelativePath("")).toEqual("")
|
||||
})
|
||||
|
||||
test("removes only the first separator", () => {
|
||||
expect(makeRelativePath(path.sep + "a" + path.sep + "b")).toEqual("a" + path.sep + "b")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getBasePath", () => {
|
||||
test("resolves relative path against cwd", () => {
|
||||
const result = getBasePath("some/path")
|
||||
expect(result).toEqual("some/path")
|
||||
})
|
||||
|
||||
test("handles empty string", () => {
|
||||
const result = getBasePath("")
|
||||
expect(result).toEqual("")
|
||||
})
|
||||
|
||||
test("handles current directory", () => {
|
||||
const result = getBasePath(".")
|
||||
expect(result).toEqual("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOutputDir", () => {
|
||||
test("returns output path without subdir", () => {
|
||||
const config: ScaffoldConfig = { ...baseConfig, subdir: false }
|
||||
registerHelpers(config)
|
||||
const result = getOutputDir(config, "output", "")
|
||||
expect(result).toEqual(path.resolve(process.cwd(), "output"))
|
||||
})
|
||||
|
||||
test("returns output path with subdir", () => {
|
||||
const config: ScaffoldConfig = { ...baseConfig, subdir: true }
|
||||
registerHelpers(config)
|
||||
const result = getOutputDir(config, "output", "")
|
||||
expect(result).toEqual(path.resolve(process.cwd(), "output", "test_app"))
|
||||
})
|
||||
|
||||
test("applies subdirHelper to subdir name", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...baseConfig,
|
||||
subdir: true,
|
||||
subdirHelper: "pascalCase",
|
||||
}
|
||||
registerHelpers(config)
|
||||
const result = getOutputDir(config, "output", "")
|
||||
expect(result).toEqual(path.resolve(process.cwd(), "output", "TestApp"))
|
||||
})
|
||||
|
||||
test("includes basePath in output", () => {
|
||||
const config: ScaffoldConfig = { ...baseConfig, subdir: false }
|
||||
registerHelpers(config)
|
||||
const result = getOutputDir(config, "output", "nested/dir")
|
||||
expect(result).toEqual(path.resolve(process.cwd(), "output", "nested/dir"))
|
||||
})
|
||||
|
||||
test("combines output, basePath, and subdir", () => {
|
||||
const config: ScaffoldConfig = { ...baseConfig, subdir: true }
|
||||
registerHelpers(config)
|
||||
const result = getOutputDir(config, "output", "nested")
|
||||
expect(result).toEqual(path.resolve(process.cwd(), "output", "nested", "test_app"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("getUniqueTmpPath", () => {
|
||||
test("returns a path in os temp directory", () => {
|
||||
const result = getUniqueTmpPath()
|
||||
const os = require("os")
|
||||
expect(result.startsWith(os.tmpdir())).toBe(true)
|
||||
})
|
||||
|
||||
test("includes scaffold-config prefix", () => {
|
||||
const result = getUniqueTmpPath()
|
||||
expect(path.basename(result)).toMatch(/^scaffold-config-/)
|
||||
})
|
||||
|
||||
test("generates unique paths", () => {
|
||||
const a = getUniqueTmpPath()
|
||||
const b = getUniqueTmpPath()
|
||||
expect(a).not.toEqual(b)
|
||||
})
|
||||
})
|
||||
|
||||
describe(
|
||||
"pathExists",
|
||||
withMock(
|
||||
{
|
||||
"existing-file.txt": "content",
|
||||
"existing-dir": {},
|
||||
},
|
||||
() => {
|
||||
test("returns true for existing file", async () => {
|
||||
expect(await pathExists("existing-file.txt")).toBe(true)
|
||||
})
|
||||
|
||||
test("returns true for existing directory", async () => {
|
||||
expect(await pathExists("existing-dir")).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for non-existing path", async () => {
|
||||
expect(await pathExists("non-existing")).toBe(false)
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"createDirIfNotExists",
|
||||
withMock({}, () => {
|
||||
test("creates directory", async () => {
|
||||
await createDirIfNotExists("new-dir", { logLevel: LogLevel.none, dryRun: false })
|
||||
expect(await pathExists("new-dir")).toBe(true)
|
||||
})
|
||||
|
||||
test("creates nested directories recursively", async () => {
|
||||
await createDirIfNotExists("a/b/c", { logLevel: LogLevel.none, dryRun: false })
|
||||
expect(await pathExists("a/b/c")).toBe(true)
|
||||
})
|
||||
|
||||
test("does not create directory in dry run mode", async () => {
|
||||
await createDirIfNotExists("dry-dir", { logLevel: LogLevel.none, dryRun: true })
|
||||
expect(await pathExists("dry-dir")).toBe(false)
|
||||
})
|
||||
|
||||
test("does not throw if directory already exists", async () => {
|
||||
await createDirIfNotExists("existing", { logLevel: LogLevel.none, dryRun: false })
|
||||
await expect(
|
||||
createDirIfNotExists("existing", { logLevel: LogLevel.none, dryRun: false }),
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"getTemplateGlobInfo",
|
||||
withMock(
|
||||
{
|
||||
"template-dir": {
|
||||
"file1.txt": "content1",
|
||||
"file2.txt": "content2",
|
||||
},
|
||||
"single-file.txt": "content",
|
||||
},
|
||||
() => {
|
||||
test("detects directory template", async () => {
|
||||
const result = await getTemplateGlobInfo(baseConfig, "template-dir")
|
||||
expect(result.isDirOrGlob).toBe(true)
|
||||
expect(result.isGlob).toBe(false)
|
||||
expect(result.template).toEqual(path.join("template-dir", "**", "*"))
|
||||
})
|
||||
|
||||
test("detects glob template", async () => {
|
||||
const result = await getTemplateGlobInfo(baseConfig, "template-dir/**/*.txt")
|
||||
expect(result.isDirOrGlob).toBe(true)
|
||||
expect(result.isGlob).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves non-glob single file", async () => {
|
||||
const result = await getTemplateGlobInfo(baseConfig, "single-file.txt")
|
||||
expect(result.isDirOrGlob).toBe(false)
|
||||
expect(result.isGlob).toBe(false)
|
||||
expect(result.template).toEqual("single-file.txt")
|
||||
})
|
||||
|
||||
test("stores original template", async () => {
|
||||
const result = await getTemplateGlobInfo(baseConfig, "template-dir")
|
||||
expect(result.origTemplate).toEqual("template-dir")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"getFileList",
|
||||
withMock(
|
||||
{
|
||||
templates: {
|
||||
"file1.txt": "content1",
|
||||
"file2.js": "content2",
|
||||
".hidden": "hidden content",
|
||||
nested: {
|
||||
"file3.txt": "content3",
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
test("lists all files with glob", async () => {
|
||||
const files = await getFileList(baseConfig, ["templates/**/*"])
|
||||
expect(files.length).toBe(4)
|
||||
})
|
||||
|
||||
test("includes dotfiles", async () => {
|
||||
const files = await getFileList(baseConfig, ["templates/**/*"])
|
||||
expect(files.some((f) => f.includes(".hidden"))).toBe(true)
|
||||
})
|
||||
|
||||
test("filters by extension", async () => {
|
||||
const files = await getFileList(baseConfig, ["templates/**/*.txt"])
|
||||
expect(files.length).toBe(2)
|
||||
expect(files.every((f) => f.endsWith(".txt"))).toBe(true)
|
||||
})
|
||||
|
||||
test("supports exclusion patterns", async () => {
|
||||
const files = await getFileList(baseConfig, ["templates/**/*.txt"])
|
||||
expect(files.some((f) => f.includes(".hidden"))).toBe(false)
|
||||
expect(files.every((f) => f.endsWith(".txt"))).toBe(true)
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"getTemplateFileInfo",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
"{{name}}.txt": "Hello {{name}}",
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("calculates correct output path", async () => {
|
||||
const config: ScaffoldConfig = { ...baseConfig, tmpDir: "." }
|
||||
registerHelpers(config)
|
||||
const info = await getTemplateFileInfo(config, {
|
||||
templatePath: "input/{{name}}.txt",
|
||||
basePath: "",
|
||||
})
|
||||
expect(info.inputPath).toEqual(path.resolve(process.cwd(), "input/{{name}}.txt"))
|
||||
expect(info.outputPath).toContain("test_app.txt")
|
||||
expect(info.exists).toBe(false)
|
||||
})
|
||||
|
||||
test("detects existing output file", async () => {
|
||||
mockFs.restore()
|
||||
mockFs({
|
||||
input: { "{{name}}.txt": "Hello {{name}}" },
|
||||
output: { "test_app.txt": "existing" },
|
||||
})
|
||||
const config: ScaffoldConfig = { ...baseConfig, tmpDir: "." }
|
||||
registerHelpers(config)
|
||||
const info = await getTemplateFileInfo(config, {
|
||||
templatePath: "input/{{name}}.txt",
|
||||
basePath: "",
|
||||
})
|
||||
expect(info.exists).toBe(true)
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"copyFileTransformed",
|
||||
withMock(
|
||||
{
|
||||
"input.txt": "Hello {{name}}",
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("writes transformed content", async () => {
|
||||
const config: ScaffoldConfig = { ...baseConfig }
|
||||
registerHelpers(config)
|
||||
await createDirIfNotExists("output", { logLevel: LogLevel.none, dryRun: false })
|
||||
await copyFileTransformed(config, {
|
||||
exists: false,
|
||||
overwrite: false,
|
||||
outputPath: "output/result.txt",
|
||||
inputPath: "input.txt",
|
||||
})
|
||||
const content = readFileSync("output/result.txt").toString()
|
||||
expect(content).toEqual("Hello test_app")
|
||||
})
|
||||
|
||||
test("does not write in dry run mode", async () => {
|
||||
const config: ScaffoldConfig = { ...baseConfig, dryRun: true }
|
||||
registerHelpers(config)
|
||||
await copyFileTransformed(config, {
|
||||
exists: false,
|
||||
overwrite: false,
|
||||
outputPath: "output/result.txt",
|
||||
inputPath: "input.txt",
|
||||
})
|
||||
expect(await pathExists("output/result.txt")).toBe(false)
|
||||
})
|
||||
|
||||
test("skips existing file without overwrite", async () => {
|
||||
mockFs.restore()
|
||||
mockFs({
|
||||
"input.txt": "Hello {{name}}",
|
||||
output: { "result.txt": "original" },
|
||||
})
|
||||
const config: ScaffoldConfig = { ...baseConfig }
|
||||
registerHelpers(config)
|
||||
await copyFileTransformed(config, {
|
||||
exists: true,
|
||||
overwrite: false,
|
||||
outputPath: "output/result.txt",
|
||||
inputPath: "input.txt",
|
||||
})
|
||||
const content = readFileSync("output/result.txt").toString()
|
||||
expect(content).toEqual("original")
|
||||
})
|
||||
|
||||
test("overwrites existing file with overwrite flag", async () => {
|
||||
mockFs.restore()
|
||||
mockFs({
|
||||
"input.txt": "Hello {{name}}",
|
||||
output: { "result.txt": "original" },
|
||||
})
|
||||
const config: ScaffoldConfig = { ...baseConfig }
|
||||
registerHelpers(config)
|
||||
await copyFileTransformed(config, {
|
||||
exists: true,
|
||||
overwrite: true,
|
||||
outputPath: "output/result.txt",
|
||||
inputPath: "input.txt",
|
||||
})
|
||||
const content = readFileSync("output/result.txt").toString()
|
||||
expect(content).toEqual("Hello test_app")
|
||||
})
|
||||
|
||||
test("calls beforeWrite callback", async () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...baseConfig,
|
||||
beforeWrite: (content) => content.toString().toUpperCase(),
|
||||
}
|
||||
registerHelpers(config)
|
||||
await createDirIfNotExists("output", { logLevel: LogLevel.none, dryRun: false })
|
||||
await copyFileTransformed(config, {
|
||||
exists: false,
|
||||
overwrite: false,
|
||||
outputPath: "output/result.txt",
|
||||
inputPath: "input.txt",
|
||||
})
|
||||
const content = readFileSync("output/result.txt").toString()
|
||||
expect(content).toEqual("HELLO TEST_APP")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"handleTemplateFile",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
"{{name}}.txt": "Content for {{name}}",
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("processes template file end to end", async () => {
|
||||
const config: ScaffoldConfig = { ...baseConfig, tmpDir: "." }
|
||||
registerHelpers(config)
|
||||
await handleTemplateFile(config, {
|
||||
templatePath: "input/{{name}}.txt",
|
||||
basePath: "",
|
||||
})
|
||||
const content = readFileSync(path.join("output", "test_app.txt")).toString()
|
||||
expect(content).toEqual("Content for test_app")
|
||||
})
|
||||
|
||||
test("throws for non-existing template", async () => {
|
||||
const config: ScaffoldConfig = { ...baseConfig, tmpDir: "." }
|
||||
registerHelpers(config)
|
||||
await expect(
|
||||
handleTemplateFile(config, {
|
||||
templatePath: "non-existing.txt",
|
||||
basePath: "",
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
174
tests/logger.test.ts
Normal file
174
tests/logger.test.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { log, logInitStep, logInputFile } from "../src/logger"
|
||||
import { LogLevel, ScaffoldConfig } from "../src/types"
|
||||
|
||||
describe("logger", () => {
|
||||
let consoleSpy: {
|
||||
log: jest.SpyInstance
|
||||
warn: jest.SpyInstance
|
||||
error: jest.SpyInstance
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = {
|
||||
log: jest.spyOn(console, "log").mockImplementation(() => void 0),
|
||||
warn: jest.spyOn(console, "warn").mockImplementation(() => void 0),
|
||||
error: jest.spyOn(console, "error").mockImplementation(() => void 0),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.log.mockRestore()
|
||||
consoleSpy.warn.mockRestore()
|
||||
consoleSpy.error.mockRestore()
|
||||
})
|
||||
|
||||
describe("log", () => {
|
||||
test("does not log when logLevel is none", () => {
|
||||
log({ logLevel: LogLevel.none }, LogLevel.info, "test")
|
||||
expect(consoleSpy.log).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("logs info messages with console.log", () => {
|
||||
log({ logLevel: LogLevel.info }, LogLevel.info, "test message")
|
||||
expect(consoleSpy.log).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("logs warning messages with console.warn", () => {
|
||||
log({ logLevel: LogLevel.warning }, LogLevel.warning, "warning message")
|
||||
expect(consoleSpy.warn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("logs error messages with console.error", () => {
|
||||
log({ logLevel: LogLevel.error }, LogLevel.error, "error message")
|
||||
expect(consoleSpy.error).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("filters out messages below configured level", () => {
|
||||
log({ logLevel: LogLevel.warning }, LogLevel.info, "should be filtered")
|
||||
expect(consoleSpy.log).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("filters out debug messages when level is info", () => {
|
||||
log({ logLevel: LogLevel.info }, LogLevel.debug, "debug message")
|
||||
expect(consoleSpy.log).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("shows debug messages when level is debug", () => {
|
||||
log({ logLevel: LogLevel.debug }, LogLevel.debug, "debug message")
|
||||
expect(consoleSpy.log).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("shows all levels when configured as debug", () => {
|
||||
log({ logLevel: LogLevel.debug }, LogLevel.debug, "d")
|
||||
log({ logLevel: LogLevel.debug }, LogLevel.info, "i")
|
||||
log({ logLevel: LogLevel.debug }, LogLevel.warning, "w")
|
||||
log({ logLevel: LogLevel.debug }, LogLevel.error, "e")
|
||||
expect(consoleSpy.log).toHaveBeenCalledTimes(2) // debug + info
|
||||
expect(consoleSpy.warn).toHaveBeenCalledTimes(1)
|
||||
expect(consoleSpy.error).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("handles Error objects", () => {
|
||||
log({ logLevel: LogLevel.error }, LogLevel.error, new Error("test error"))
|
||||
expect(consoleSpy.error).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("handles objects with util.inspect", () => {
|
||||
log({ logLevel: LogLevel.info }, LogLevel.info, { key: "value" })
|
||||
expect(consoleSpy.log).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("handles multiple arguments", () => {
|
||||
log({ logLevel: LogLevel.info }, LogLevel.info, "a", "b", "c")
|
||||
expect(consoleSpy.log).toHaveBeenCalled()
|
||||
// First call, should have 3 arguments
|
||||
expect(consoleSpy.log.mock.calls[0].length).toBe(3)
|
||||
})
|
||||
|
||||
test("defaults to info when logLevel is undefined", () => {
|
||||
log({ logLevel: undefined as any }, LogLevel.info, "test")
|
||||
expect(consoleSpy.log).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("error level shows when logLevel is info", () => {
|
||||
log({ logLevel: LogLevel.info }, LogLevel.error, "error")
|
||||
expect(consoleSpy.error).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("warning level shows when logLevel is info", () => {
|
||||
log({ logLevel: LogLevel.info }, LogLevel.warning, "warning")
|
||||
expect(consoleSpy.warn).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("logInitStep", () => {
|
||||
test("logs config at debug level", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: LogLevel.debug,
|
||||
data: { name: "test" },
|
||||
}
|
||||
logInitStep(config)
|
||||
expect(consoleSpy.log).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("does not log config at info level (debug only)", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: LogLevel.info,
|
||||
data: { name: "test" },
|
||||
}
|
||||
logInitStep(config)
|
||||
// Should only log the "Data:" line at info, not the "Full config:" at debug
|
||||
expect(consoleSpy.log).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("logInputFile", () => {
|
||||
test("logs file info at debug level", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: LogLevel.debug,
|
||||
data: { name: "test" },
|
||||
}
|
||||
logInputFile(config, {
|
||||
originalTemplate: "input",
|
||||
relativePath: ".",
|
||||
parsedTemplate: "input/**/*",
|
||||
inputFilePath: "input/file.txt",
|
||||
nonGlobTemplate: "input",
|
||||
basePath: "",
|
||||
isDirOrGlob: true,
|
||||
isGlob: false,
|
||||
})
|
||||
expect(consoleSpy.log).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("does not log at info level", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: LogLevel.info,
|
||||
data: { name: "test" },
|
||||
}
|
||||
logInputFile(config, {
|
||||
originalTemplate: "input",
|
||||
relativePath: ".",
|
||||
parsedTemplate: "input/**/*",
|
||||
inputFilePath: "input/file.txt",
|
||||
nonGlobTemplate: "input",
|
||||
basePath: "",
|
||||
isDirOrGlob: true,
|
||||
isGlob: false,
|
||||
})
|
||||
expect(consoleSpy.log).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ScaffoldConfig } from "../src/types"
|
||||
import path from "node:path"
|
||||
import * as dateFns from "date-fns"
|
||||
import { dateHelper, defaultHelpers, handlebarsParse, nowHelper } from "../src/parser"
|
||||
import { dateHelper, defaultHelpers, handlebarsParse, nowHelper, registerHelpers } from "../src/parser"
|
||||
|
||||
const blankConf: ScaffoldConfig = {
|
||||
logLevel: "none",
|
||||
@@ -61,6 +61,88 @@ describe("parser", () => {
|
||||
),
|
||||
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
|
||||
})
|
||||
|
||||
test("should replace name token in content", () => {
|
||||
const result = handlebarsParse(blankConf, "Hello {{name}}")
|
||||
expect(result.toString()).toEqual("Hello test")
|
||||
})
|
||||
|
||||
test("should replace multiple tokens", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "app", version: "1.0" },
|
||||
}
|
||||
expect(handlebarsParse(config, "{{name}} v{{version}}").toString()).toEqual("app v1.0")
|
||||
})
|
||||
|
||||
test("should return Buffer", () => {
|
||||
expect(Buffer.isBuffer(handlebarsParse(blankConf, "test"))).toBe(true)
|
||||
})
|
||||
|
||||
test("should handle Buffer input", () => {
|
||||
expect(handlebarsParse(blankConf, Buffer.from("Hello {{name}}")).toString()).toEqual("Hello test")
|
||||
})
|
||||
|
||||
test("should return original content on handlebars error", () => {
|
||||
const result = handlebarsParse(blankConf, "{{#if}}invalid{{/unless}}")
|
||||
expect(Buffer.isBuffer(result)).toBe(true)
|
||||
expect(result.toString()).toEqual("{{#if}}invalid{{/unless}}")
|
||||
})
|
||||
|
||||
test("should handle empty template", () => {
|
||||
expect(handlebarsParse(blankConf, "").toString()).toEqual("")
|
||||
})
|
||||
|
||||
test("should handle template with no tokens", () => {
|
||||
expect(handlebarsParse(blankConf, "no tokens here").toString()).toEqual("no tokens here")
|
||||
})
|
||||
|
||||
test("should not escape HTML chars (noEscape)", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "<div>test</div>" },
|
||||
}
|
||||
expect(handlebarsParse(config, "{{name}}").toString()).toEqual("<div>test</div>")
|
||||
})
|
||||
|
||||
test("should handle nested data", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "test", nested: { key: "value" } },
|
||||
}
|
||||
expect(handlebarsParse(config, "{{nested.key}}").toString()).toEqual("value")
|
||||
})
|
||||
|
||||
test("should handle handlebars conditionals", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "test", showExtra: true },
|
||||
}
|
||||
registerHelpers(config)
|
||||
expect(handlebarsParse(config, "{{#if showExtra}}extra{{/if}} content").toString()).toEqual("extra content")
|
||||
})
|
||||
|
||||
test("should handle handlebars conditionals when false", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "test", showExtra: false },
|
||||
}
|
||||
registerHelpers(config)
|
||||
expect(handlebarsParse(config, "{{#if showExtra}}extra{{/if}}content").toString()).toEqual("content")
|
||||
})
|
||||
|
||||
test("should handle handlebars each loops", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "test", items: ["a", "b", "c"] },
|
||||
}
|
||||
registerHelpers(config)
|
||||
expect(handlebarsParse(config, "{{#each items}}{{this}},{{/each}}").toString()).toEqual("a,b,c,")
|
||||
})
|
||||
|
||||
test("should render empty for undefined data token", () => {
|
||||
expect(handlebarsParse(blankConf, "{{undefinedVar}}").toString()).toEqual("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Helpers", () => {
|
||||
@@ -111,6 +193,91 @@ describe("parser", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("string helpers edge cases", () => {
|
||||
test("camelCase single word", () => {
|
||||
expect(defaultHelpers.camelCase("hello")).toEqual("hello")
|
||||
})
|
||||
|
||||
test("camelCase empty string", () => {
|
||||
expect(defaultHelpers.camelCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("camelCase all uppercase", () => {
|
||||
expect(defaultHelpers.camelCase("HELLO WORLD")).toEqual("helloWorld")
|
||||
})
|
||||
|
||||
test("pascalCase single word", () => {
|
||||
expect(defaultHelpers.pascalCase("hello")).toEqual("Hello")
|
||||
})
|
||||
|
||||
test("pascalCase empty string", () => {
|
||||
expect(defaultHelpers.pascalCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("snakeCase single word", () => {
|
||||
expect(defaultHelpers.snakeCase("hello")).toEqual("hello")
|
||||
})
|
||||
|
||||
test("snakeCase empty string", () => {
|
||||
expect(defaultHelpers.snakeCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("kebabCase single word", () => {
|
||||
expect(defaultHelpers.kebabCase("hello")).toEqual("hello")
|
||||
})
|
||||
|
||||
test("kebabCase empty string", () => {
|
||||
expect(defaultHelpers.kebabCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("startCase single word", () => {
|
||||
expect(defaultHelpers.startCase("hello")).toEqual("Hello")
|
||||
})
|
||||
|
||||
test("startCase empty string", () => {
|
||||
expect(defaultHelpers.startCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("hyphenCase is same as kebabCase", () => {
|
||||
expect(defaultHelpers.hyphenCase("testString")).toEqual(defaultHelpers.kebabCase("testString"))
|
||||
expect(defaultHelpers.hyphenCase("test_string")).toEqual(defaultHelpers.kebabCase("test_string"))
|
||||
})
|
||||
|
||||
test("lowerCase lowercases everything", () => {
|
||||
expect(defaultHelpers.lowerCase("HELLO")).toEqual("hello")
|
||||
expect(defaultHelpers.lowerCase("Hello World")).toEqual("hello world")
|
||||
})
|
||||
|
||||
test("upperCase uppercases everything", () => {
|
||||
expect(defaultHelpers.upperCase("hello")).toEqual("HELLO")
|
||||
expect(defaultHelpers.upperCase("hello world")).toEqual("HELLO WORLD")
|
||||
})
|
||||
|
||||
test("camelCase handles numbers in string", () => {
|
||||
expect(defaultHelpers.camelCase("item1_name")).toEqual("item1Name")
|
||||
})
|
||||
|
||||
test("pascalCase handles multiple separators", () => {
|
||||
expect(defaultHelpers.pascalCase("a--b__c d")).toEqual("ABCD")
|
||||
})
|
||||
|
||||
test("snakeCase handles mixed separators", () => {
|
||||
expect(defaultHelpers.snakeCase("myApp-name_here")).toEqual("my_app_name_here")
|
||||
})
|
||||
|
||||
test("kebabCase handles mixed separators", () => {
|
||||
expect(defaultHelpers.kebabCase("myApp-name_here")).toEqual("my-app-name-here")
|
||||
})
|
||||
|
||||
test("single character inputs", () => {
|
||||
expect(defaultHelpers.camelCase("a")).toEqual("a")
|
||||
expect(defaultHelpers.pascalCase("a")).toEqual("A")
|
||||
expect(defaultHelpers.snakeCase("a")).toEqual("a")
|
||||
expect(defaultHelpers.kebabCase("a")).toEqual("a")
|
||||
expect(defaultHelpers.startCase("a")).toEqual("A")
|
||||
})
|
||||
})
|
||||
|
||||
describe("date helpers", () => {
|
||||
describe("now", () => {
|
||||
test("should work without extra params", () => {
|
||||
@@ -140,7 +307,122 @@ describe("parser", () => {
|
||||
dateFns.format(dateFns.add(now, { months: 1 }), fmt),
|
||||
)
|
||||
})
|
||||
|
||||
test("should work with years offset", () => {
|
||||
const dateStr = "2024-01-15T12:00:00.000Z"
|
||||
const date = dateFns.parseISO(dateStr)
|
||||
expect(dateHelper(dateStr, "yyyy", 1, "years")).toEqual(
|
||||
dateFns.format(dateFns.add(date, { years: 1 }), "yyyy"),
|
||||
)
|
||||
})
|
||||
|
||||
test("should work with weeks offset", () => {
|
||||
const dateStr = "2024-01-15T12:00:00.000Z"
|
||||
const date = dateFns.parseISO(dateStr)
|
||||
expect(dateHelper(dateStr, "yyyy-MM-dd", 2, "weeks")).toEqual(
|
||||
dateFns.format(dateFns.add(date, { weeks: 2 }), "yyyy-MM-dd"),
|
||||
)
|
||||
})
|
||||
|
||||
test("should work with minutes offset", () => {
|
||||
const dateStr = "2024-01-15T12:00:00.000Z"
|
||||
const date = dateFns.parseISO(dateStr)
|
||||
expect(dateHelper(dateStr, "HH:mm", 30, "minutes")).toEqual(
|
||||
dateFns.format(dateFns.add(date, { minutes: 30 }), "HH:mm"),
|
||||
)
|
||||
})
|
||||
|
||||
test("should work with seconds offset", () => {
|
||||
const dateStr = "2024-01-15T12:00:00.000Z"
|
||||
const date = dateFns.parseISO(dateStr)
|
||||
expect(dateHelper(dateStr, "HH:mm:ss", 45, "seconds")).toEqual(
|
||||
dateFns.format(dateFns.add(date, { seconds: 45 }), "HH:mm:ss"),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("now edge cases", () => {
|
||||
test("should work with different format tokens", () => {
|
||||
const now = new Date()
|
||||
expect(nowHelper("yyyy")).toEqual(dateFns.format(now, "yyyy"))
|
||||
expect(nowHelper("MM")).toEqual(dateFns.format(now, "MM"))
|
||||
expect(nowHelper("dd")).toEqual(dateFns.format(now, "dd"))
|
||||
})
|
||||
|
||||
test("should work with positive offset", () => {
|
||||
const now = new Date()
|
||||
const result = nowHelper("yyyy-MM-dd", 1, "days")
|
||||
const expected = dateFns.format(dateFns.add(now, { days: 1 }), "yyyy-MM-dd")
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
|
||||
test("should work with hours offset", () => {
|
||||
const now = new Date()
|
||||
const result = nowHelper("HH", 2, "hours")
|
||||
const expected = dateFns.format(dateFns.add(now, { hours: 2 }), "HH")
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("registerHelpers", () => {
|
||||
test("registers default helpers", () => {
|
||||
const config: ScaffoldConfig = { ...blankConf }
|
||||
registerHelpers(config)
|
||||
const result = handlebarsParse(
|
||||
{ ...config, data: { name: "hello_world" } },
|
||||
"{{camelCase name}}",
|
||||
)
|
||||
expect(result.toString()).toEqual("helloWorld")
|
||||
})
|
||||
|
||||
test("registers custom helpers", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
helpers: {
|
||||
reverse: (text: string) => text.split("").reverse().join(""),
|
||||
},
|
||||
}
|
||||
registerHelpers(config)
|
||||
const result = handlebarsParse(
|
||||
{ ...config, data: { name: "hello" } },
|
||||
"{{reverse name}}",
|
||||
)
|
||||
expect(result.toString()).toEqual("olleh")
|
||||
})
|
||||
|
||||
test("custom helpers override default helpers", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
helpers: {
|
||||
camelCase: () => "OVERRIDDEN",
|
||||
},
|
||||
}
|
||||
registerHelpers(config)
|
||||
const result = handlebarsParse(
|
||||
{ ...config, data: { name: "test" } },
|
||||
"{{camelCase name}}",
|
||||
)
|
||||
expect(result.toString()).toEqual("OVERRIDDEN")
|
||||
})
|
||||
})
|
||||
|
||||
describe("default helpers completeness", () => {
|
||||
test("all expected helpers are defined", () => {
|
||||
const expectedHelpers = [
|
||||
"camelCase", "snakeCase", "startCase", "kebabCase",
|
||||
"hyphenCase", "pascalCase", "lowerCase", "upperCase",
|
||||
"now", "date",
|
||||
]
|
||||
for (const helper of expectedHelpers) {
|
||||
expect(defaultHelpers).toHaveProperty(helper)
|
||||
expect(typeof defaultHelpers[helper as keyof typeof defaultHelpers]).toBe("function")
|
||||
}
|
||||
})
|
||||
|
||||
test("has exactly 10 helpers", () => {
|
||||
expect(Object.keys(defaultHelpers).length).toBe(10)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -524,4 +524,559 @@ describe("Scaffold", () => {
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"name is available in data",
|
||||
withMock(
|
||||
{
|
||||
input: { "file.txt": "Name: {{name}}" },
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("name is automatically injected into data", async () => {
|
||||
await Scaffold({
|
||||
name: "my_project",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
||||
expect(content).toEqual("Name: my_project")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"data overrides name in data",
|
||||
withMock(
|
||||
{
|
||||
input: { "file.txt": "Name: {{name}}" },
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("explicit data.name takes precedence", async () => {
|
||||
await Scaffold({
|
||||
name: "original_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
data: { name: "custom_name" },
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
||||
expect(content).toEqual("Name: custom_name")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"multiple templates",
|
||||
withMock(
|
||||
{
|
||||
template1: { "file1.txt": "From template 1: {{name}}" },
|
||||
template2: { "file2.txt": "From template 2: {{name}}" },
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("processes multiple template directories", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["template1", "template2"],
|
||||
logLevel: "none",
|
||||
})
|
||||
const file1 = readFileSync(join(process.cwd(), "output", "file1.txt")).toString()
|
||||
const file2 = readFileSync(join(process.cwd(), "output", "file2.txt")).toString()
|
||||
expect(file1).toEqual("From template 1: app")
|
||||
expect(file2).toEqual("From template 2: app")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"template with custom data",
|
||||
withMock(
|
||||
{
|
||||
input: { "{{name}}.txt": "Author: {{author}}, Version: {{version}}" },
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("uses custom data in content and filename", async () => {
|
||||
await Scaffold({
|
||||
name: "my_app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
data: { author: "John", version: "2.0" },
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "my_app.txt")).toString()
|
||||
expect(content).toEqual("Author: John, Version: 2.0")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"template with helpers in filenames",
|
||||
withMock(
|
||||
{
|
||||
input: { "{{pascalCase name}}.tsx": "component {{pascalCase name}}" },
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("applies helpers to filenames", async () => {
|
||||
await Scaffold({
|
||||
name: "my_component",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "MyComponent.tsx")).toString()
|
||||
expect(content).toEqual("component MyComponent")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"template with helpers in directory names",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
"{{kebabCase name}}": {
|
||||
"index.ts": "export from {{name}}",
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("applies helpers to directory names", async () => {
|
||||
await Scaffold({
|
||||
name: "MyComponent",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "my-component", "index.ts")).toString()
|
||||
expect(content).toEqual("export from MyComponent")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"deeply nested template structure",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
"root.txt": "root",
|
||||
level1: {
|
||||
"l1.txt": "level 1",
|
||||
level2: {
|
||||
"l2.txt": "level 2",
|
||||
level3: {
|
||||
"l3.txt": "level 3 {{name}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("preserves deep nesting", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
})
|
||||
expect(readFileSync(join(process.cwd(), "output", "root.txt")).toString()).toEqual("root")
|
||||
expect(readFileSync(join(process.cwd(), "output", "level1", "l1.txt")).toString()).toEqual("level 1")
|
||||
expect(
|
||||
readFileSync(join(process.cwd(), "output", "level1", "level2", "l2.txt")).toString(),
|
||||
).toEqual("level 2")
|
||||
expect(
|
||||
readFileSync(
|
||||
join(process.cwd(), "output", "level1", "level2", "level3", "l3.txt"),
|
||||
).toString(),
|
||||
).toEqual("level 3 app")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"overwrite as function",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
"keep.txt": "new keep",
|
||||
"replace.txt": "new replace",
|
||||
},
|
||||
output: {
|
||||
"keep.txt": "old keep",
|
||||
"replace.txt": "old replace",
|
||||
},
|
||||
},
|
||||
() => {
|
||||
test("per-file overwrite control", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
overwrite: (_fullPath, _basedir, basename) => basename === "replace.txt",
|
||||
})
|
||||
expect(readFileSync(join(process.cwd(), "output", "keep.txt")).toString()).toEqual("old keep")
|
||||
expect(readFileSync(join(process.cwd(), "output", "replace.txt")).toString()).toEqual("new replace")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"multiple custom helpers",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
"file.txt": "{{reverse name}} - {{repeat name}}",
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("multiple custom helpers work together", async () => {
|
||||
await Scaffold({
|
||||
name: "abc",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
helpers: {
|
||||
reverse: (text: string) => text.split("").reverse().join(""),
|
||||
repeat: (text: string) => text + text,
|
||||
},
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
||||
expect(content).toEqual("cba - abcabc")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"subdirHelper with different helpers",
|
||||
withMock(
|
||||
{
|
||||
input: { "file.txt": "content" },
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("subdirHelper camelCase", async () => {
|
||||
await Scaffold({
|
||||
name: "my_component",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
subdir: true,
|
||||
subdirHelper: "camelCase",
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "myComponent", "file.txt")).toString()
|
||||
expect(content).toEqual("content")
|
||||
})
|
||||
|
||||
test("subdirHelper kebabCase", async () => {
|
||||
await Scaffold({
|
||||
name: "MyComponent",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
subdir: true,
|
||||
subdirHelper: "kebabCase",
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "my-component", "file.txt")).toString()
|
||||
expect(content).toEqual("content")
|
||||
})
|
||||
|
||||
test("subdirHelper snakeCase", async () => {
|
||||
await Scaffold({
|
||||
name: "MyComponent",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
subdir: true,
|
||||
subdirHelper: "snakeCase",
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "my_component", "file.txt")).toString()
|
||||
expect(content).toEqual("content")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"empty template directory",
|
||||
withMock(
|
||||
{
|
||||
input: {},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("handles empty template dir gracefully", async () => {
|
||||
await expect(
|
||||
Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
}),
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"template with special characters in data",
|
||||
withMock(
|
||||
{
|
||||
input: { "file.txt": "Value: {{value}}" },
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("handles special characters in data values", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
data: { value: "hello & <world> \"test\"" },
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
||||
expect(content).toEqual("Value: hello & <world> \"test\"")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"beforeWrite with async callback",
|
||||
withMock(
|
||||
{
|
||||
input: { "file.txt": "Hello {{name}}" },
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("supports async beforeWrite", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
beforeWrite: async (content) => {
|
||||
return content.toString().replace("Hello", "Hi")
|
||||
},
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
||||
expect(content).toEqual("Hi app")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"beforeWrite receives all arguments",
|
||||
withMock(
|
||||
{
|
||||
input: { "{{name}}.txt": "Template: {{name}}" },
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("beforeWrite gets content, rawContent, and outputPath", async () => {
|
||||
const beforeWriteSpy = jest.fn().mockReturnValue(undefined)
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
beforeWrite: beforeWriteSpy,
|
||||
})
|
||||
expect(beforeWriteSpy).toHaveBeenCalledTimes(1)
|
||||
const [content, rawContent, outputPath] = beforeWriteSpy.mock.calls[0]
|
||||
expect(content.toString()).toEqual("Template: app")
|
||||
expect(rawContent.toString()).toEqual("Template: {{name}}")
|
||||
expect(outputPath).toContain("app.txt")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"multiple binary files",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
"img1.bin": crypto.randomBytes(5000),
|
||||
"img2.bin": crypto.randomBytes(8000),
|
||||
"text.txt": "regular text {{name}}",
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("handles mix of binary and text files", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
})
|
||||
const text = readFileSync(join(process.cwd(), "output", "text.txt")).toString()
|
||||
expect(text).toEqual("regular text app")
|
||||
const bin1 = readFileSync(join(process.cwd(), "output", "img1.bin"))
|
||||
const bin2 = readFileSync(join(process.cwd(), "output", "img2.bin"))
|
||||
expect(bin1.length).toBeGreaterThan(0)
|
||||
expect(bin2.length).toBeGreaterThan(0)
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"output with function returning dynamic path",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
"component.tsx": "component {{name}}",
|
||||
"style.css": "style for {{name}}",
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("output function can route files to different directories", async () => {
|
||||
await Scaffold({
|
||||
name: "Button",
|
||||
output: (_fullPath, _basedir, basename) => {
|
||||
if (basename.endsWith(".css")) return join("output", "styles")
|
||||
return join("output", "components")
|
||||
},
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
})
|
||||
const component = readFileSync(
|
||||
join(process.cwd(), "output", "components", "component.tsx"),
|
||||
).toString()
|
||||
const style = readFileSync(
|
||||
join(process.cwd(), "output", "styles", "style.css"),
|
||||
).toString()
|
||||
expect(component).toEqual("component Button")
|
||||
expect(style).toEqual("style for Button")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"handlebars block helpers",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
"file.txt": "{{#if showHeader}}Header\n{{/if}}Body for {{name}}",
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("supports handlebars block helpers in templates", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
data: { showHeader: true },
|
||||
})
|
||||
const content = readFileSync(join(process.cwd(), "output", "file.txt")).toString()
|
||||
expect(content).toEqual("Header\nBody for app")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"glob pattern as template",
|
||||
withMock(
|
||||
{
|
||||
src: {
|
||||
"file1.txt": "text 1 {{name}}",
|
||||
"file2.txt": "text 2 {{name}}",
|
||||
"file3.js": "js {{name}}",
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("glob pattern selects matching files only", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["src/*.txt"],
|
||||
logLevel: "none",
|
||||
})
|
||||
// glob templates maintain structure relative to the non-glob part
|
||||
const outputFiles = readdirSync(join(process.cwd(), "output", "src"))
|
||||
expect(outputFiles).toContain("file1.txt")
|
||||
expect(outputFiles).toContain("file2.txt")
|
||||
expect(outputFiles).not.toContain("file3.js")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"dotfiles in template",
|
||||
withMock(
|
||||
{
|
||||
input: {
|
||||
".gitignore": "node_modules",
|
||||
".env.example": "KEY={{name}}",
|
||||
},
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("includes dotfiles in output", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
})
|
||||
expect(readFileSync(join(process.cwd(), "output", ".gitignore")).toString()).toEqual("node_modules")
|
||||
expect(readFileSync(join(process.cwd(), "output", ".env.example")).toString()).toEqual("KEY=app")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"large number of files",
|
||||
withMock(
|
||||
{
|
||||
input: Object.fromEntries(
|
||||
Array.from({ length: 50 }, (_, i) => [`file${i}.txt`, `Content ${i} for {{name}}`]),
|
||||
),
|
||||
output: {},
|
||||
},
|
||||
() => {
|
||||
test("handles many files", async () => {
|
||||
await Scaffold({
|
||||
name: "app",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
logLevel: "none",
|
||||
})
|
||||
const files = readdirSync(join(process.cwd(), "output"))
|
||||
expect(files.length).toBe(50)
|
||||
expect(readFileSync(join(process.cwd(), "output", "file0.txt")).toString()).toEqual("Content 0 for app")
|
||||
expect(readFileSync(join(process.cwd(), "output", "file49.txt")).toString()).toEqual("Content 49 for app")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleErr, resolve, colorize, TermColor } from "../src/utils"
|
||||
import { handleErr, resolve, wrapNoopResolver, colorize, TermColor } from "../src/utils"
|
||||
describe("utils", () => {
|
||||
describe("resolve", () => {
|
||||
test("should resolve function", () => {
|
||||
@@ -10,6 +10,45 @@ describe("utils", () => {
|
||||
expect(resolve(1, null)).toBe(1)
|
||||
expect(resolve(2, 1)).toBe(2)
|
||||
})
|
||||
|
||||
test("should resolve function with argument transformation", () => {
|
||||
expect(resolve((x: number) => x * 2, 5)).toBe(10)
|
||||
})
|
||||
|
||||
test("should resolve static string", () => {
|
||||
expect(resolve("hello", null)).toBe("hello")
|
||||
})
|
||||
|
||||
test("should resolve static boolean", () => {
|
||||
expect(resolve(true, null)).toBe(true)
|
||||
expect(resolve(false, null)).toBe(false)
|
||||
})
|
||||
|
||||
test("should resolve static object", () => {
|
||||
const obj = { key: "value" }
|
||||
expect(resolve(obj, null)).toBe(obj)
|
||||
})
|
||||
|
||||
test("should resolve function returning object", () => {
|
||||
expect(resolve(() => ({ key: "value" }), null)).toEqual({ key: "value" })
|
||||
})
|
||||
|
||||
test("should pass argument to function", () => {
|
||||
const fn = (config: { name: string }) => config.name
|
||||
expect(resolve(fn, { name: "test" })).toBe("test")
|
||||
})
|
||||
|
||||
test("should resolve zero", () => {
|
||||
expect(resolve(0, null)).toBe(0)
|
||||
})
|
||||
|
||||
test("should resolve null", () => {
|
||||
expect(resolve(null, "anything")).toBe(null)
|
||||
})
|
||||
|
||||
test("should resolve undefined", () => {
|
||||
expect(resolve(undefined, "anything")).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe("handleErr", () => {
|
||||
@@ -17,6 +56,44 @@ describe("utils", () => {
|
||||
expect(() => handleErr({ name: "test", message: "test" })).toThrow()
|
||||
expect(() => handleErr(null as never)).not.toThrow()
|
||||
})
|
||||
|
||||
test("should throw the provided error", () => {
|
||||
const err = new Error("test error")
|
||||
expect(() => handleErr(err as unknown as NodeJS.ErrnoException)).toThrow("test error")
|
||||
})
|
||||
})
|
||||
|
||||
describe("wrapNoopResolver", () => {
|
||||
test("should wrap static value in function", () => {
|
||||
const wrapped = wrapNoopResolver("hello")
|
||||
expect(typeof wrapped).toBe("function")
|
||||
expect((wrapped as Function)("anything")).toBe("hello")
|
||||
})
|
||||
|
||||
test("should return function as-is", () => {
|
||||
const fn = (x: string) => x.toUpperCase()
|
||||
const wrapped = wrapNoopResolver(fn)
|
||||
expect(wrapped).toBe(fn)
|
||||
})
|
||||
|
||||
test("should wrap object value", () => {
|
||||
const obj = { key: "value" }
|
||||
const wrapped = wrapNoopResolver(obj)
|
||||
expect(typeof wrapped).toBe("function")
|
||||
expect((wrapped as Function)("anything")).toBe(obj)
|
||||
})
|
||||
|
||||
test("should wrap boolean value", () => {
|
||||
const wrapped = wrapNoopResolver(true)
|
||||
expect(typeof wrapped).toBe("function")
|
||||
expect((wrapped as Function)(null)).toBe(true)
|
||||
})
|
||||
|
||||
test("should wrap number value", () => {
|
||||
const wrapped = wrapNoopResolver(42)
|
||||
expect(typeof wrapped).toBe("function")
|
||||
expect((wrapped as Function)(null)).toBe(42)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -66,4 +143,64 @@ describe("colorize", () => {
|
||||
const result = colorize.blue`Hello ${"World"}`
|
||||
expect(result).toBe("\x1b[34mHello World\x1b[0m")
|
||||
})
|
||||
|
||||
test("should colorize with green", () => {
|
||||
expect(colorize("Hello", "green")).toBe("\x1b[32mHello\x1b[0m")
|
||||
})
|
||||
|
||||
test("should colorize with yellow", () => {
|
||||
expect(colorize("Hello", "yellow")).toBe("\x1b[33mHello\x1b[0m")
|
||||
})
|
||||
|
||||
test("should colorize with magenta", () => {
|
||||
expect(colorize("Hello", "magenta")).toBe("\x1b[35mHello\x1b[0m")
|
||||
})
|
||||
|
||||
test("should colorize with cyan", () => {
|
||||
expect(colorize("Hello", "cyan")).toBe("\x1b[36mHello\x1b[0m")
|
||||
})
|
||||
|
||||
test("should colorize with white", () => {
|
||||
expect(colorize("Hello", "white")).toBe("\x1b[37mHello\x1b[0m")
|
||||
})
|
||||
|
||||
test("should colorize with gray", () => {
|
||||
expect(colorize("Hello", "gray")).toBe("\x1b[90mHello\x1b[0m")
|
||||
})
|
||||
|
||||
test("should colorize with dim", () => {
|
||||
expect(colorize("Hello", "dim")).toBe("\x1b[2mHello\x1b[22m")
|
||||
})
|
||||
|
||||
test("should colorize with italic", () => {
|
||||
expect(colorize("Hello", "italic")).toBe("\x1b[3mHello\x1b[23m")
|
||||
})
|
||||
|
||||
test("should colorize with underline", () => {
|
||||
expect(colorize("Hello", "underline")).toBe("\x1b[4mHello\x1b[24m")
|
||||
})
|
||||
|
||||
test("color functions work as template strings", () => {
|
||||
const name = "World"
|
||||
expect(colorize.green`Hello ${name}`).toBe("\x1b[32mHello World\x1b[0m")
|
||||
})
|
||||
|
||||
test("color functions work with direct call", () => {
|
||||
expect(colorize.yellow("warning")).toBe("\x1b[33mwarning\x1b[0m")
|
||||
expect(colorize.cyan("info")).toBe("\x1b[36minfo\x1b[0m")
|
||||
})
|
||||
|
||||
test("handles empty string", () => {
|
||||
expect(colorize("", "red")).toBe("\x1b[31m\x1b[0m")
|
||||
})
|
||||
|
||||
test("handles special characters", () => {
|
||||
expect(colorize("hello\nworld", "blue")).toBe("\x1b[34mhello\nworld\x1b[0m")
|
||||
})
|
||||
|
||||
test("template string with multiple interpolations", () => {
|
||||
const a = "one"
|
||||
const b = "two"
|
||||
expect(colorize.red`${a} and ${b}`).toBe("\x1b[31mone and two\x1b[0m")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user