906 lines
27 KiB
TypeScript
906 lines
27 KiB
TypeScript
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");
|
|
});
|
|
});
|