Initial Commit
This commit is contained in:
905
packages/devtools/tooling/electrobun/src/cli/cli.test.ts
Normal file
905
packages/devtools/tooling/electrobun/src/cli/cli.test.ts
Normal file
@@ -0,0 +1,905 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
ELECTROBUN_CODES,
|
||||
ElectrobunError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import type {
|
||||
CommandInput,
|
||||
CommandResult,
|
||||
CommandRunner,
|
||||
Prompter,
|
||||
} from "../types/electrobun.types.js";
|
||||
import {
|
||||
buildCommand,
|
||||
createCommand,
|
||||
devCommand,
|
||||
packageCommand,
|
||||
runCli,
|
||||
signCommand,
|
||||
} from "./cli.js";
|
||||
|
||||
interface Recording {
|
||||
runner: CommandRunner;
|
||||
calls: CommandInput[];
|
||||
}
|
||||
|
||||
function recordingRunner(
|
||||
results: ReadonlyArray<Partial<CommandResult>>
|
||||
): Recording {
|
||||
const calls: CommandInput[] = [];
|
||||
let i = 0;
|
||||
const fn: CommandRunner = async (input) => {
|
||||
calls.push(input);
|
||||
const r = results[i] ?? results[results.length - 1] ?? {};
|
||||
i += 1;
|
||||
return {
|
||||
exitCode: r.exitCode ?? 0,
|
||||
stdout: r.stdout ?? "1.0.0",
|
||||
stderr: r.stderr ?? "",
|
||||
durationMs: r.durationMs ?? 1,
|
||||
};
|
||||
};
|
||||
return { runner: fn, calls };
|
||||
}
|
||||
|
||||
function probeOk(stdout = "1.0.0"): Partial<CommandResult> {
|
||||
return { exitCode: 0, stdout };
|
||||
}
|
||||
|
||||
function silentPrompter(): Prompter {
|
||||
return {
|
||||
text: async ({ default: d }) => d ?? "demo",
|
||||
confirm: async () => true,
|
||||
select: async <T extends string>({
|
||||
default: d,
|
||||
choices,
|
||||
}: {
|
||||
default?: T;
|
||||
choices: ReadonlyArray<{ value: T }>;
|
||||
}): Promise<T> => (d ?? choices[0]!.value) as T,
|
||||
multiselect: async <T extends string>(): Promise<readonly T[]> =>
|
||||
[] as readonly T[],
|
||||
};
|
||||
}
|
||||
|
||||
describe("devCommand / buildCommand / packageCommand / signCommand", () => {
|
||||
it("devCommand validates options", { timeout: 5000 }, async () => {
|
||||
try {
|
||||
await devCommand({ cwd: "" });
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as ElectrobunError).code).toBe(
|
||||
ELECTROBUN_CODES.INVALID_OPTIONS
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("buildCommand passes --update when options.update is true", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
await buildCommand(
|
||||
{ cwd: "/tmp", target: "mac", update: true },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
// first call = probe, second = build
|
||||
const build = r.calls[1];
|
||||
expect(build?.args).toContain("--update");
|
||||
});
|
||||
|
||||
it("buildCommand omits --update by default", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
await buildCommand(
|
||||
{ cwd: "/tmp", target: "mac" },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
const build = r.calls[1];
|
||||
expect(build?.args.includes("--update")).toBe(false);
|
||||
});
|
||||
|
||||
it("packageCommand emits the expected argv", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
await packageCommand(
|
||||
{ cwd: "/tmp", target: "linux", extraArgs: ["--verbose"] },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
expect(r.calls[1]?.args).toEqual([
|
||||
"electrobun",
|
||||
"package",
|
||||
"--target",
|
||||
"linux",
|
||||
"--verbose",
|
||||
]);
|
||||
});
|
||||
|
||||
it("signCommand forwards identity, entitlements, and notarize", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
await signCommand(
|
||||
{
|
||||
cwd: "/tmp",
|
||||
identity: "Dev ID",
|
||||
entitlements: "/e.plist",
|
||||
notarize: true,
|
||||
},
|
||||
{ runner: r.runner }
|
||||
);
|
||||
const args = r.calls[1]?.args ?? [];
|
||||
expect(args).toContain("--identity");
|
||||
expect(args).toContain("Dev ID");
|
||||
expect(args).toContain("--entitlements");
|
||||
expect(args).toContain("/e.plist");
|
||||
expect(args).toContain("--notarize");
|
||||
});
|
||||
|
||||
it("subcommands use nodeCommandRunner default when no runner supplied", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
// No deps.runner → real spawn path. Outcome depends on whether
|
||||
// bunx electrobun resolves on this host; either branch (success
|
||||
// or ElectrobunError) is acceptable — the test only exercises
|
||||
// the default-runner fallback.
|
||||
const settle = async <T>(p: Promise<T>): Promise<unknown> => {
|
||||
try {
|
||||
return await p;
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
};
|
||||
await settle(signCommand({ cwd: "/tmp", identity: "Dev ID" }));
|
||||
await settle(devCommand({ cwd: "/tmp" }));
|
||||
await settle(buildCommand({ cwd: "/tmp", target: "mac" }));
|
||||
await settle(packageCommand({ cwd: "/tmp", target: "mac" }));
|
||||
});
|
||||
|
||||
it("signCommand forwards AbortSignal to runner", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
const ctl = new AbortController();
|
||||
await signCommand(
|
||||
{ cwd: "/tmp", identity: "Dev ID", signal: ctl.signal },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
expect(r.calls[1]?.signal).toBe(ctl.signal);
|
||||
});
|
||||
|
||||
it("buildCommand forwards AbortSignal to runner", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
const ctl = new AbortController();
|
||||
await buildCommand(
|
||||
{ cwd: "/tmp", target: "mac", signal: ctl.signal },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
expect(r.calls[1]?.signal).toBe(ctl.signal);
|
||||
});
|
||||
|
||||
it("packageCommand forwards AbortSignal to runner", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
const ctl = new AbortController();
|
||||
await packageCommand(
|
||||
{ cwd: "/tmp", target: "mac", signal: ctl.signal },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
expect(r.calls[1]?.signal).toBe(ctl.signal);
|
||||
});
|
||||
|
||||
it("devCommand forwards AbortSignal to runner", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
const ctl = new AbortController();
|
||||
await devCommand(
|
||||
{ cwd: "/tmp", signal: ctl.signal },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
expect(r.calls[1]?.signal).toBe(ctl.signal);
|
||||
});
|
||||
|
||||
it("signCommand without entitlements does not pass the flag", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
await signCommand(
|
||||
{ cwd: "/tmp", identity: "Dev ID" },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
const args = r.calls[1]?.args ?? [];
|
||||
expect(args.includes("--entitlements")).toBe(false);
|
||||
});
|
||||
|
||||
it("non-zero exit raises RUNNER_FAILED", { timeout: 5000 }, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 2, stderr: "fail" }]);
|
||||
try {
|
||||
await devCommand({ cwd: "/tmp" }, { runner: r.runner });
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as ElectrobunError).code).toBe(
|
||||
ELECTROBUN_CODES.RUNNER_FAILED
|
||||
);
|
||||
expect((err as ElectrobunError).details["exitCode"]).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
it("missing binary raises BINARY_UNAVAILABLE", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([{ exitCode: 127 }]);
|
||||
try {
|
||||
await buildCommand(
|
||||
{ cwd: "/tmp", target: "mac" },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as ElectrobunError).code).toBe(
|
||||
ELECTROBUN_CODES.BINARY_UNAVAILABLE
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("devCommand forwards extraArgs", { timeout: 5000 }, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
await devCommand(
|
||||
{ cwd: "/tmp", extraArgs: ["--watch"] },
|
||||
{ runner: r.runner }
|
||||
);
|
||||
expect(r.calls[1]?.args).toContain("--watch");
|
||||
});
|
||||
|
||||
it("devCommand records duration even on failure", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 5, stderr: "boom" }]);
|
||||
await expect(
|
||||
devCommand({ cwd: "/tmp" }, { runner: r.runner })
|
||||
).rejects.toBeInstanceOf(ElectrobunError);
|
||||
});
|
||||
|
||||
it("non-Electrobun error inside timeRunner is re-thrown", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r: CommandRunner = async () => {
|
||||
throw new Error("network down");
|
||||
};
|
||||
await expect(
|
||||
devCommand({ cwd: "/tmp" }, { runner: r })
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runCli", () => {
|
||||
it("--help returns code 0", { timeout: 5000 }, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["--help"], {
|
||||
out: (l) => captured.push(l),
|
||||
});
|
||||
expect(result).toEqual({ code: 0 });
|
||||
expect(captured[0]).toContain("Usage");
|
||||
});
|
||||
|
||||
it("runCli with default `out` argument still resolves", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
// Capture the console.log fallback by stubbing it for one call.
|
||||
const orig = console.log;
|
||||
const captured: string[] = [];
|
||||
console.log = (line: string) => captured.push(String(line));
|
||||
try {
|
||||
const result = await runCli(["--help"]);
|
||||
expect(result.code).toBe(0);
|
||||
expect(captured.join("\n")).toContain("Usage");
|
||||
} finally {
|
||||
console.log = orig;
|
||||
}
|
||||
});
|
||||
|
||||
it("-h short flag returns code 0", { timeout: 5000 }, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["-h"], {
|
||||
out: (l) => captured.push(l),
|
||||
});
|
||||
expect(result).toEqual({ code: 0 });
|
||||
});
|
||||
|
||||
it("no subcommand prints help and returns code 0", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli([], {
|
||||
out: (l) => captured.push(l),
|
||||
});
|
||||
expect(result).toEqual({ code: 0 });
|
||||
expect(captured[0]).toContain("Usage");
|
||||
});
|
||||
|
||||
it("unknown subcommand raises UNKNOWN_FLAG", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["zzz"], {
|
||||
out: (l) => captured.push(l),
|
||||
});
|
||||
expect(result).toEqual({ code: 1 });
|
||||
expect(captured.join("\n")).toContain("ELECTROBUN.UNKNOWN_FLAG");
|
||||
});
|
||||
|
||||
it("unknown long flag raises UNKNOWN_FLAG", { timeout: 5000 }, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["dev", "--zzz"], {
|
||||
out: (l) => captured.push(l),
|
||||
});
|
||||
expect(result).toEqual({ code: 1 });
|
||||
expect(captured.join("\n")).toContain("ELECTROBUN.UNKNOWN_FLAG");
|
||||
});
|
||||
|
||||
it("unknown short flag raises UNKNOWN_FLAG", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["dev", "-z"], {
|
||||
out: (l) => captured.push(l),
|
||||
});
|
||||
expect(result).toEqual({ code: 1 });
|
||||
});
|
||||
|
||||
it("dev subcommand dispatches and reports", { timeout: 5000 }, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["dev", "--cwd", "/tmp"], {
|
||||
out: (l) => captured.push(l),
|
||||
runner: r.runner,
|
||||
});
|
||||
expect(result).toEqual({ code: 0 });
|
||||
expect(captured.join("\n")).toContain("dev exited 0");
|
||||
});
|
||||
|
||||
it("build subcommand parses --target= and --update", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(
|
||||
["build", "--cwd=/tmp", "--target=mac", "--update"],
|
||||
{ out: (l) => captured.push(l), runner: r.runner }
|
||||
);
|
||||
expect(result).toEqual({ code: 0 });
|
||||
expect(r.calls[1]?.args).toContain("--update");
|
||||
});
|
||||
|
||||
it("package subcommand dispatches", { timeout: 5000 }, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
const result = await runCli(
|
||||
["package", "--cwd", "/tmp", "--target", "windows"],
|
||||
{ runner: r.runner, out: () => undefined }
|
||||
);
|
||||
expect(result.code).toBe(0);
|
||||
expect(r.calls[1]?.args).toContain("windows");
|
||||
});
|
||||
|
||||
it("sign subcommand dispatches with entitlements", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
const result = await runCli(
|
||||
[
|
||||
"sign",
|
||||
"--cwd",
|
||||
"/tmp",
|
||||
"--identity",
|
||||
"Dev ID",
|
||||
"--entitlements",
|
||||
"/e.plist",
|
||||
"--notarize",
|
||||
],
|
||||
{ runner: r.runner, out: () => undefined }
|
||||
);
|
||||
expect(result.code).toBe(0);
|
||||
});
|
||||
|
||||
it("missing --cwd raises INVALID_OPTIONS", { timeout: 5000 }, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["dev"], {
|
||||
out: (l) => captured.push(l),
|
||||
runner: recordingRunner([probeOk()]).runner,
|
||||
});
|
||||
expect(result.code).toBe(1);
|
||||
expect(captured.join("\n")).toContain("ELECTROBUN.INVALID_OPTIONS");
|
||||
});
|
||||
|
||||
it("build without --target raises INVALID_OPTIONS", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["build", "--cwd", "/tmp"], {
|
||||
out: (l) => captured.push(l),
|
||||
runner: recordingRunner([probeOk()]).runner,
|
||||
});
|
||||
expect(result.code).toBe(1);
|
||||
});
|
||||
|
||||
it("create requires a prompter when not skipping prompts", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["create", "/tmp"], {
|
||||
out: (l) => captured.push(l),
|
||||
});
|
||||
expect(result.code).toBe(1);
|
||||
expect(captured.join("\n")).toContain("ELECTROBUN.INVALID_OPTIONS");
|
||||
});
|
||||
|
||||
it("create subcommand dispatches with skip-prompts + overrides", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const writes: string[] = [];
|
||||
const fs = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile(path: string) {
|
||||
writes.push(path);
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(
|
||||
[
|
||||
"create",
|
||||
"/scratch/dest",
|
||||
"--skip-prompts",
|
||||
"--name",
|
||||
"x",
|
||||
"--id",
|
||||
"com.x.y",
|
||||
"--app-version",
|
||||
"1.0.0",
|
||||
],
|
||||
{
|
||||
out: (l) => captured.push(l),
|
||||
prompter: silentPrompter(),
|
||||
fs,
|
||||
}
|
||||
);
|
||||
expect(result.code).toBe(0);
|
||||
expect(writes.length).toBeGreaterThan(0);
|
||||
expect(captured.join("\n")).toContain("Created project");
|
||||
});
|
||||
|
||||
it("create with --icon forwards iconOverride", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fs = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile() {
|
||||
return undefined;
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const renderer = {
|
||||
async render() {
|
||||
return { files: ["app.icns"] };
|
||||
},
|
||||
};
|
||||
const result = await runCli(
|
||||
[
|
||||
"create",
|
||||
"/scratch/dest",
|
||||
"--skip-prompts",
|
||||
"--name",
|
||||
"x",
|
||||
"--id",
|
||||
"com.x.y",
|
||||
"--icon",
|
||||
"/icons/source.png",
|
||||
],
|
||||
{
|
||||
out: () => undefined,
|
||||
prompter: silentPrompter(),
|
||||
fs,
|
||||
renderer,
|
||||
fileExists: () => true,
|
||||
}
|
||||
);
|
||||
expect(result.code).toBe(0);
|
||||
});
|
||||
|
||||
it("runCli catches ElectrobunError from a subcommand and folds to code 1", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 9, stderr: "boom" }]);
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["dev", "--cwd", "/tmp"], {
|
||||
out: (l) => captured.push(l),
|
||||
runner: r.runner,
|
||||
});
|
||||
expect(result.code).toBe(1);
|
||||
expect(captured.join("\n")).toContain("ELECTROBUN.RUNNER_FAILED");
|
||||
});
|
||||
|
||||
it("parseArgs surfaces UNKNOWN_FLAG out to stdout", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const captured: string[] = [];
|
||||
const result = await runCli(["create", "/tmp", "--zzz"], {
|
||||
out: (l) => captured.push(l),
|
||||
});
|
||||
expect(result.code).toBe(1);
|
||||
expect(captured.join("\n")).toContain("ELECTROBUN.UNKNOWN_FLAG");
|
||||
});
|
||||
|
||||
it("create without positional directory uses defaults", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fs = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile() {
|
||||
return undefined;
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const result = await runCli(["create", "--skip-prompts"], {
|
||||
out: () => undefined,
|
||||
prompter: silentPrompter(),
|
||||
fs,
|
||||
});
|
||||
expect(result.code).toBe(0);
|
||||
});
|
||||
|
||||
it("create through runCli without fs override succeeds", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
// Real fs path — write into a real tmp dir.
|
||||
const real = mkdtempSync(join(tmpdir(), "electrobun-runcli-create-"));
|
||||
try {
|
||||
const result = await runCli(
|
||||
[
|
||||
"create",
|
||||
join(real, "out"),
|
||||
"--skip-prompts",
|
||||
"--name",
|
||||
"x",
|
||||
"--id",
|
||||
"com.x.y",
|
||||
],
|
||||
{
|
||||
out: () => undefined,
|
||||
prompter: silentPrompter(),
|
||||
}
|
||||
);
|
||||
expect(result.code).toBe(0);
|
||||
} finally {
|
||||
rmSync(real, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("sign with notarize-only (no entitlements) covers branch", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = recordingRunner([probeOk(), { exitCode: 0 }]);
|
||||
const result = await runCli(
|
||||
["sign", "--cwd", "/tmp", "--identity", "Dev ID", "--notarize"],
|
||||
{ runner: r.runner, out: () => undefined }
|
||||
);
|
||||
expect(result.code).toBe(0);
|
||||
});
|
||||
|
||||
it("runCli re-throws non-ElectrobunError from dispatch", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const failingRunner: CommandRunner = async () => {
|
||||
throw new Error("network");
|
||||
};
|
||||
await expect(
|
||||
runCli(["dev", "--cwd", "/tmp"], {
|
||||
runner: failingRunner,
|
||||
out: () => undefined,
|
||||
})
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createCommand", () => {
|
||||
it("skip-prompts uses overrides and defaults", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const written: string[] = [];
|
||||
const fs = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile(path: string) {
|
||||
written.push(path);
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const renderer = {
|
||||
async render() {
|
||||
return { files: ["app.icns", "app.ico"] };
|
||||
},
|
||||
};
|
||||
const report = await createCommand(
|
||||
{
|
||||
directory: "/scratch/dest",
|
||||
skipPrompts: true,
|
||||
nameOverride: "skip-app",
|
||||
identifierOverride: "com.skip.app",
|
||||
iconOverride: "/icons/skip.png",
|
||||
versionOverride: "2.3.4",
|
||||
},
|
||||
{
|
||||
fs,
|
||||
prompter: silentPrompter(),
|
||||
renderer,
|
||||
fileExists: () => true,
|
||||
}
|
||||
);
|
||||
expect(report.outputDir).toBe("/scratch/dest");
|
||||
expect(written.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("interactive path runs the prompt flow", { timeout: 5000 }, async () => {
|
||||
const fs = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile() {
|
||||
return undefined;
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const report = await createCommand(
|
||||
{
|
||||
directory: "/scratch/dest",
|
||||
nameOverride: "x",
|
||||
identifierOverride: "com.x.y",
|
||||
versionOverride: "1.1.1",
|
||||
},
|
||||
{ fs, prompter: silentPrompter() }
|
||||
);
|
||||
expect(report.outputDir).toBeDefined();
|
||||
});
|
||||
|
||||
it("skip-prompts honours a config file with full set of fields", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fsAdapter = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile() {
|
||||
return undefined;
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const cfg = {
|
||||
name: "cfg-name",
|
||||
identifier: "com.cfg.id",
|
||||
description: "cfg-desc",
|
||||
version: "9.9.9",
|
||||
features: ["i18n"],
|
||||
networkingComplexity: "full",
|
||||
r3fModules: ["camera"],
|
||||
};
|
||||
const discovery = {
|
||||
cwd: () => "/cwd",
|
||||
homedir: () => "/home",
|
||||
exists: (p: string) => p === "/cwd/.suaveplan-electrobun.json",
|
||||
readFile: () => JSON.stringify(cfg),
|
||||
};
|
||||
const report = await createCommand(
|
||||
{ skipPrompts: true, discovery },
|
||||
{ fs: fsAdapter, prompter: silentPrompter() }
|
||||
);
|
||||
expect(report.outputDir).toBe("cfg-name");
|
||||
});
|
||||
|
||||
it("interactive path with all overrides set", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fs = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile() {
|
||||
return undefined;
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const renderer = {
|
||||
async render() {
|
||||
return { files: ["app.icns"] };
|
||||
},
|
||||
};
|
||||
const report = await createCommand(
|
||||
{
|
||||
directory: "/scratch/y",
|
||||
nameOverride: "n",
|
||||
identifierOverride: "com.n.y",
|
||||
iconOverride: "/icons/x.png",
|
||||
versionOverride: "3.2.1",
|
||||
},
|
||||
{
|
||||
fs,
|
||||
prompter: silentPrompter(),
|
||||
renderer,
|
||||
fileExists: () => true,
|
||||
}
|
||||
);
|
||||
expect(report.outputDir).toBeDefined();
|
||||
});
|
||||
|
||||
it("interactive path with no overrides", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fs = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile() {
|
||||
return undefined;
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const report = await createCommand(
|
||||
{ directory: "/scratch/y" },
|
||||
{ fs, prompter: silentPrompter() }
|
||||
);
|
||||
expect(report.outputDir).toBeDefined();
|
||||
});
|
||||
|
||||
it("skip-prompts with a full config carries every field through ?? lefts", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fs = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile() {
|
||||
return undefined;
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const cfg = {
|
||||
name: "cfg-skip-name",
|
||||
identifier: "com.cfg.skip",
|
||||
description: "cfg-skip-desc",
|
||||
version: "8.8.8",
|
||||
iconSource: "/cfg/icon.png",
|
||||
features: ["i18n"],
|
||||
networkingComplexity: "full",
|
||||
r3fModules: ["camera"],
|
||||
};
|
||||
const discovery = {
|
||||
cwd: () => "/cwd",
|
||||
homedir: () => "/home",
|
||||
exists: (p: string) => p === "/cwd/.suaveplan-electrobun.json",
|
||||
readFile: () => JSON.stringify(cfg),
|
||||
};
|
||||
const renderer = {
|
||||
async render() {
|
||||
return { files: ["app.icns"] };
|
||||
},
|
||||
};
|
||||
const report = await createCommand(
|
||||
{ skipPrompts: true, discovery },
|
||||
{ fs, prompter: silentPrompter(), renderer, fileExists: () => true }
|
||||
);
|
||||
expect(report.outputDir).toBe("cfg-skip-name");
|
||||
});
|
||||
|
||||
it("skip-prompts uses bare default outputDir when none supplied", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const fs = {
|
||||
async readFile() {
|
||||
return "";
|
||||
},
|
||||
async writeFile() {
|
||||
return undefined;
|
||||
},
|
||||
async mkdir() {
|
||||
return undefined;
|
||||
},
|
||||
async stat() {
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
async readdir() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const report = await createCommand(
|
||||
{ skipPrompts: true },
|
||||
{ fs, prompter: silentPrompter() }
|
||||
);
|
||||
expect(report.outputDir).toBe("my-electrobun-app");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user