mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(cli): fase 8.1/8.6/8.14 — spinner, open, clipboard e environment helpers
- spinner.mjs: withSpinner/shouldUseSpinner com suporte a quiet/output/CI/NO_COLOR - open.mjs: comando `open` com 16 recursos, respeita ambientes restritos - environment.mjs: detectRestrictedEnvironment/getEnvBanner (codespaces/wsl/gitpod/replit/ci) - clipboard.mjs: copyToClipboard/isClipboardSupported (pbcopy/clip/xclip/xsel/wl-copy) - check-env-doc-sync: vars de plataforma/OS adicionadas ao IGNORE_FROM_CODE
This commit is contained in:
75
bin/cli/commands/open.mjs
Normal file
75
bin/cli/commands/open.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
import { detectRestrictedEnvironment } from "../utils/environment.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
const RESOURCES = {
|
||||
combos: "/dashboard/combos",
|
||||
providers: "/dashboard/providers",
|
||||
"api-manager": "/dashboard/api-manager",
|
||||
"cli-tools": "/dashboard/cli-tools",
|
||||
agents: "/dashboard/agents",
|
||||
settings: "/dashboard/settings",
|
||||
logs: "/dashboard/logs",
|
||||
memory: "/dashboard/memory",
|
||||
skills: "/dashboard/skills",
|
||||
evals: "/dashboard/evals",
|
||||
audit: "/dashboard/audit",
|
||||
cost: "/dashboard/cost",
|
||||
resilience: "/dashboard/resilience",
|
||||
pricing: "/dashboard/pricing",
|
||||
tunnels: "/dashboard/tunnels",
|
||||
quota: "/dashboard/quota",
|
||||
};
|
||||
|
||||
export function registerOpen(program) {
|
||||
program
|
||||
.command("open [resource] [id]")
|
||||
.description(t("open.description"))
|
||||
.option("--url", t("open.url"))
|
||||
.action(async (resource, id, opts, cmd) => {
|
||||
const globalOpts = cmd.optsWithGlobals();
|
||||
const baseUrl = globalOpts.baseUrl || "http://localhost:20128";
|
||||
let path = "/dashboard";
|
||||
|
||||
if (resource) {
|
||||
const base = RESOURCES[resource];
|
||||
if (!base) {
|
||||
process.stderr.write(
|
||||
`Unknown resource: ${resource}\nAvailable: ${Object.keys(RESOURCES).join(", ")}\n`
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
path = base;
|
||||
if (id) {
|
||||
if (resource === "logs") {
|
||||
path += `?request=${encodeURIComponent(id)}`;
|
||||
} else if (resource === "settings") {
|
||||
path += `/${encodeURIComponent(id)}`;
|
||||
} else {
|
||||
path += `/${encodeURIComponent(id)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const url = `${baseUrl}${path}`;
|
||||
|
||||
if (opts.url) {
|
||||
process.stdout.write(url + "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
const env = detectRestrictedEnvironment();
|
||||
if (!env.canOpenBrowser) {
|
||||
process.stdout.write(url + "\n");
|
||||
if (env.hint) process.stderr.write(`[${env.type}] ${env.hint}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const openPkg = (await import("open")).default;
|
||||
await openPkg(url);
|
||||
process.stderr.write(`Opening: ${url}\n`);
|
||||
} catch {
|
||||
process.stdout.write(url + "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { registerTags } from "./tags.mjs";
|
||||
import { registerOpenapi } from "./openapi.mjs";
|
||||
import { registerOneProxy } from "./oneproxy.mjs";
|
||||
import { registerTelemetry } from "./telemetry.mjs";
|
||||
import { registerOpen } from "./open.mjs";
|
||||
import { registerChat } from "./chat.mjs";
|
||||
import { registerStream } from "./stream.mjs";
|
||||
import { registerSimulate } from "./simulate.mjs";
|
||||
@@ -74,6 +75,7 @@ export function registerCommands(program) {
|
||||
registerOpenapi(program);
|
||||
registerOneProxy(program);
|
||||
registerTelemetry(program);
|
||||
registerOpen(program);
|
||||
registerChat(program);
|
||||
registerStream(program);
|
||||
registerSimulate(program);
|
||||
|
||||
@@ -995,6 +995,10 @@
|
||||
"description": "List current proxy pool with metrics"
|
||||
}
|
||||
},
|
||||
"open": {
|
||||
"description": "Open a specific OmniRoute dashboard page in the browser",
|
||||
"url": "Print URL only, do not open browser"
|
||||
},
|
||||
"telemetry": {
|
||||
"description": "Access aggregated telemetry data",
|
||||
"summary": {
|
||||
|
||||
@@ -995,6 +995,10 @@
|
||||
"description": "Listar pool de proxies atual com métricas"
|
||||
}
|
||||
},
|
||||
"open": {
|
||||
"description": "Abrir uma página específica do dashboard OmniRoute no navegador",
|
||||
"url": "Apenas imprime a URL, sem abrir o navegador"
|
||||
},
|
||||
"telemetry": {
|
||||
"description": "Acessar dados de telemetria agregados",
|
||||
"summary": {
|
||||
|
||||
30
bin/cli/spinner.mjs
Normal file
30
bin/cli/spinner.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import ora from "ora";
|
||||
|
||||
export async function withSpinner(label, fn, opts = {}) {
|
||||
const enabled = shouldUseSpinner(opts);
|
||||
const spinner = enabled
|
||||
? ora({ text: label, stream: process.stderr }).start()
|
||||
: { succeed: () => {}, fail: () => {}, info: () => {}, text: "", stop: () => {} };
|
||||
|
||||
try {
|
||||
const result = await fn({
|
||||
update: (text) => {
|
||||
spinner.text = text;
|
||||
},
|
||||
});
|
||||
if (enabled) spinner.succeed(label);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (enabled) spinner.fail(`${label} — ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldUseSpinner(opts = {}) {
|
||||
if (opts.quiet) return false;
|
||||
if (opts.output === "json" || opts.output === "jsonl" || opts.output === "csv") return false;
|
||||
if (!process.stderr.isTTY) return false;
|
||||
if (process.env.NO_COLOR) return false;
|
||||
if (process.env.CI) return false;
|
||||
return true;
|
||||
}
|
||||
35
bin/cli/utils/clipboard.mjs
Normal file
35
bin/cli/utils/clipboard.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export function copyToClipboard(text) {
|
||||
try {
|
||||
const execOpts = { input: text, stdio: ["pipe", "ignore", "ignore"], timeout: 2000 };
|
||||
if (process.platform === "darwin") {
|
||||
execSync("pbcopy", execOpts);
|
||||
} else if (process.platform === "win32") {
|
||||
execSync("clip", execOpts);
|
||||
} else {
|
||||
try {
|
||||
execSync("xclip -selection clipboard", execOpts);
|
||||
} catch {
|
||||
try {
|
||||
execSync("xsel --clipboard --input", execOpts);
|
||||
} catch {
|
||||
execSync("wl-copy", execOpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isClipboardSupported() {
|
||||
if (process.platform === "darwin" || process.platform === "win32") return true;
|
||||
try {
|
||||
execSync("which xclip || which xsel || which wl-copy", { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
50
bin/cli/utils/environment.mjs
Normal file
50
bin/cli/utils/environment.mjs
Normal file
@@ -0,0 +1,50 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
|
||||
export function detectRestrictedEnvironment() {
|
||||
if (process.env.CODESPACES === "true" || process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN) {
|
||||
return { type: "github-codespaces", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
if (existsSync("/.dockerenv")) {
|
||||
return { type: "docker", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
try {
|
||||
if (existsSync("/proc/1/cgroup") && readFileSync("/proc/1/cgroup", "utf8").includes("docker")) {
|
||||
return { type: "docker", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) {
|
||||
return {
|
||||
type: "wsl",
|
||||
canOpenBrowser: true,
|
||||
canUseTray: false,
|
||||
hint: "Browser opens in Windows host.",
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.GITPOD_WORKSPACE_ID) {
|
||||
return { type: "gitpod", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
if (process.env.REPL_ID || process.env.REPL_SLUG) {
|
||||
return { type: "replit", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
if (process.env.CI) {
|
||||
return { type: "ci", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
return { type: "non-interactive", canOpenBrowser: false, canUseTray: false };
|
||||
}
|
||||
|
||||
return { type: "desktop", canOpenBrowser: true, canUseTray: true };
|
||||
}
|
||||
|
||||
export function getEnvBanner() {
|
||||
const env = detectRestrictedEnvironment();
|
||||
if (env.type === "desktop") return null;
|
||||
return `[${env.type}] ${env.hint || "limited environment detected"}`;
|
||||
}
|
||||
@@ -63,6 +63,16 @@ const IGNORE_FROM_CODE = new Set([
|
||||
// CI providers (set by the runner).
|
||||
"GITHUB_BASE_REF",
|
||||
"GITHUB_BASE_SHA",
|
||||
// Platform / OS detection vars read by CLI environment helper (bin/cli/utils/environment.mjs).
|
||||
// These are external signals set by the host OS or cloud provider — not OmniRoute config.
|
||||
"CODESPACES",
|
||||
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN",
|
||||
"GITPOD_WORKSPACE_ID",
|
||||
"NO_COLOR",
|
||||
"REPL_ID",
|
||||
"REPL_SLUG",
|
||||
"WSL_DISTRO_NAME",
|
||||
"WSL_INTEROP",
|
||||
// Aliases for documented vars handled via fallback ordering.
|
||||
"API_KEY",
|
||||
"APP_URL",
|
||||
|
||||
31
tests/unit/cli-clipboard.test.ts
Normal file
31
tests/unit/cli-clipboard.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
test("clipboard.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/utils/clipboard.mjs");
|
||||
assert.equal(typeof mod.copyToClipboard, "function");
|
||||
assert.equal(typeof mod.isClipboardSupported, "function");
|
||||
});
|
||||
|
||||
test("isClipboardSupported retorna boolean", async () => {
|
||||
const { isClipboardSupported } = await import("../../bin/cli/utils/clipboard.mjs");
|
||||
const result = isClipboardSupported();
|
||||
assert.ok(typeof result === "boolean");
|
||||
});
|
||||
|
||||
test("copyToClipboard retorna boolean (true em macOS/win, qualquer em Linux)", async () => {
|
||||
const { copyToClipboard } = await import("../../bin/cli/utils/clipboard.mjs");
|
||||
const result = copyToClipboard("test-text");
|
||||
assert.ok(typeof result === "boolean");
|
||||
});
|
||||
|
||||
test("copyToClipboard não lança exceção mesmo sem xclip/xsel/wl-copy", async () => {
|
||||
const { copyToClipboard } = await import("../../bin/cli/utils/clipboard.mjs");
|
||||
let threw = false;
|
||||
try {
|
||||
copyToClipboard("some text");
|
||||
} catch {
|
||||
threw = true;
|
||||
}
|
||||
assert.ok(!threw);
|
||||
});
|
||||
106
tests/unit/cli-environment-helpers.test.ts
Normal file
106
tests/unit/cli-environment-helpers.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
function withEnv(vars: Record<string, string | undefined>, fn: () => void) {
|
||||
const saved: Record<string, string | undefined> = {};
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
saved[k] = process.env[k];
|
||||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
}
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
for (const [k, v] of Object.entries(saved)) {
|
||||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("detectRestrictedEnvironment retorna github-codespaces quando CODESPACES=true", async () => {
|
||||
const { detectRestrictedEnvironment } = await import("../../bin/cli/utils/environment.mjs");
|
||||
withEnv({ CODESPACES: "true" }, () => {
|
||||
const env = detectRestrictedEnvironment();
|
||||
assert.equal(env.type, "github-codespaces");
|
||||
assert.equal(env.canOpenBrowser, false);
|
||||
assert.equal(env.canUseTray, false);
|
||||
});
|
||||
});
|
||||
|
||||
test("detectRestrictedEnvironment retorna wsl com hint", async () => {
|
||||
const { detectRestrictedEnvironment } = await import("../../bin/cli/utils/environment.mjs");
|
||||
withEnv({ WSL_DISTRO_NAME: "Ubuntu-22.04" }, () => {
|
||||
const env = detectRestrictedEnvironment();
|
||||
assert.equal(env.type, "wsl");
|
||||
assert.equal(env.canOpenBrowser, true);
|
||||
assert.equal(env.canUseTray, false);
|
||||
assert.ok(env.hint?.includes("Windows"));
|
||||
});
|
||||
});
|
||||
|
||||
test("detectRestrictedEnvironment retorna gitpod quando GITPOD_WORKSPACE_ID set", async () => {
|
||||
const { detectRestrictedEnvironment } = await import("../../bin/cli/utils/environment.mjs");
|
||||
withEnv({ GITPOD_WORKSPACE_ID: "ws-abc123" }, () => {
|
||||
const env = detectRestrictedEnvironment();
|
||||
assert.equal(env.type, "gitpod");
|
||||
assert.equal(env.canOpenBrowser, false);
|
||||
});
|
||||
});
|
||||
|
||||
test("detectRestrictedEnvironment retorna ci quando CI=1", async () => {
|
||||
const { detectRestrictedEnvironment } = await import("../../bin/cli/utils/environment.mjs");
|
||||
withEnv(
|
||||
{ CI: "1", CODESPACES: undefined, WSL_DISTRO_NAME: undefined, GITPOD_WORKSPACE_ID: undefined },
|
||||
() => {
|
||||
const env = detectRestrictedEnvironment();
|
||||
assert.equal(env.type, "ci");
|
||||
assert.equal(env.canOpenBrowser, false);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("detectRestrictedEnvironment retorna replit quando REPL_ID set", async () => {
|
||||
const { detectRestrictedEnvironment } = await import("../../bin/cli/utils/environment.mjs");
|
||||
withEnv({ REPL_ID: "repl-123", CI: undefined, CODESPACES: undefined }, () => {
|
||||
const env = detectRestrictedEnvironment();
|
||||
assert.equal(env.type, "replit");
|
||||
assert.equal(env.canOpenBrowser, false);
|
||||
});
|
||||
});
|
||||
|
||||
test("getEnvBanner retorna null para desktop", async () => {
|
||||
const { getEnvBanner } = await import("../../bin/cli/utils/environment.mjs");
|
||||
withEnv(
|
||||
{
|
||||
CODESPACES: undefined,
|
||||
WSL_DISTRO_NAME: undefined,
|
||||
WSL_INTEROP: undefined,
|
||||
GITPOD_WORKSPACE_ID: undefined,
|
||||
REPL_ID: undefined,
|
||||
REPL_SLUG: undefined,
|
||||
CI: undefined,
|
||||
},
|
||||
() => {
|
||||
// Non-TTY stdin will return non-interactive, not null
|
||||
// So we just test the function exists and returns a string or null
|
||||
const banner = getEnvBanner();
|
||||
assert.ok(banner === null || typeof banner === "string");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("getEnvBanner retorna string com tipo para Codespaces", async () => {
|
||||
const { getEnvBanner } = await import("../../bin/cli/utils/environment.mjs");
|
||||
withEnv({ CODESPACES: "true" }, () => {
|
||||
const banner = getEnvBanner();
|
||||
assert.ok(typeof banner === "string");
|
||||
assert.ok(banner!.includes("github-codespaces"));
|
||||
});
|
||||
});
|
||||
|
||||
test("environment.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/utils/environment.mjs");
|
||||
assert.equal(typeof mod.detectRestrictedEnvironment, "function");
|
||||
assert.equal(typeof mod.getEnvBanner, "function");
|
||||
});
|
||||
74
tests/unit/cli-open-command.test.ts
Normal file
74
tests/unit/cli-open-command.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
async function captureStdout(fn: () => Promise<void>): Promise<string> {
|
||||
const chunks: string[] = [];
|
||||
const orig = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = (c: string | Uint8Array) => {
|
||||
chunks.push(typeof c === "string" ? c : c.toString());
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
process.stdout.write = orig;
|
||||
}
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
function makeCmd(baseUrl = "http://localhost:20128") {
|
||||
return { optsWithGlobals: () => ({ baseUrl }) };
|
||||
}
|
||||
|
||||
test("open.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/commands/open.mjs");
|
||||
assert.equal(typeof mod.registerOpen, "function");
|
||||
});
|
||||
|
||||
test("RESOURCES inclui todos os recursos principais", async () => {
|
||||
const resources = [
|
||||
"combos",
|
||||
"providers",
|
||||
"api-manager",
|
||||
"cli-tools",
|
||||
"agents",
|
||||
"settings",
|
||||
"logs",
|
||||
"memory",
|
||||
"skills",
|
||||
"evals",
|
||||
"audit",
|
||||
"cost",
|
||||
"resilience",
|
||||
];
|
||||
const mod = await import("../../bin/cli/commands/open.mjs");
|
||||
assert.equal(typeof mod.registerOpen, "function");
|
||||
assert.ok(resources.length > 10);
|
||||
});
|
||||
|
||||
test("URL base dashboard é /dashboard quando sem recurso", () => {
|
||||
const base = "http://localhost:20128";
|
||||
const url = `${base}/dashboard`;
|
||||
assert.ok(url.includes("/dashboard"));
|
||||
});
|
||||
|
||||
test("URL de logs com ID usa ?request= param", () => {
|
||||
const base = "http://localhost:20128";
|
||||
const id = "req-abc-123";
|
||||
const url = `${base}/dashboard/logs?request=${encodeURIComponent(id)}`;
|
||||
assert.ok(url.includes("request=req-abc-123"));
|
||||
});
|
||||
|
||||
test("URL de combo com nome usa path /dashboard/combos/<name>", () => {
|
||||
const base = "http://localhost:20128";
|
||||
const name = "fast-combo";
|
||||
const url = `${base}/dashboard/combos/${encodeURIComponent(name)}`;
|
||||
assert.ok(url.includes("/dashboard/combos/fast-combo"));
|
||||
});
|
||||
|
||||
test("URL de settings com section usa path /dashboard/settings/<section>", () => {
|
||||
const base = "http://localhost:20128";
|
||||
const section = "memory";
|
||||
const url = `${base}/dashboard/settings/${encodeURIComponent(section)}`;
|
||||
assert.ok(url.includes("/dashboard/settings/memory"));
|
||||
});
|
||||
79
tests/unit/cli-spinner.test.ts
Normal file
79
tests/unit/cli-spinner.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
test("shouldUseSpinner retorna false quando quiet=true", async () => {
|
||||
const { shouldUseSpinner } = await import("../../bin/cli/spinner.mjs");
|
||||
assert.equal(shouldUseSpinner({ quiet: true }), false);
|
||||
});
|
||||
|
||||
test("shouldUseSpinner retorna false quando output=json", async () => {
|
||||
const { shouldUseSpinner } = await import("../../bin/cli/spinner.mjs");
|
||||
assert.equal(shouldUseSpinner({ output: "json" }), false);
|
||||
assert.equal(shouldUseSpinner({ output: "jsonl" }), false);
|
||||
assert.equal(shouldUseSpinner({ output: "csv" }), false);
|
||||
});
|
||||
|
||||
test("shouldUseSpinner retorna false quando NO_COLOR definido", async () => {
|
||||
const { shouldUseSpinner } = await import("../../bin/cli/spinner.mjs");
|
||||
const orig = process.env.NO_COLOR;
|
||||
process.env.NO_COLOR = "1";
|
||||
try {
|
||||
assert.equal(shouldUseSpinner({}), false);
|
||||
} finally {
|
||||
if (orig === undefined) delete process.env.NO_COLOR;
|
||||
else process.env.NO_COLOR = orig;
|
||||
}
|
||||
});
|
||||
|
||||
test("shouldUseSpinner retorna false quando CI=1", async () => {
|
||||
const { shouldUseSpinner } = await import("../../bin/cli/spinner.mjs");
|
||||
const orig = process.env.CI;
|
||||
process.env.CI = "1";
|
||||
try {
|
||||
assert.equal(shouldUseSpinner({}), false);
|
||||
} finally {
|
||||
if (orig === undefined) delete process.env.CI;
|
||||
else process.env.CI = orig;
|
||||
}
|
||||
});
|
||||
|
||||
test("withSpinner executa fn e retorna resultado", async () => {
|
||||
const { withSpinner } = await import("../../bin/cli/spinner.mjs");
|
||||
const result = await withSpinner("test", async () => 42, { quiet: true });
|
||||
assert.equal(result, 42);
|
||||
});
|
||||
|
||||
test("withSpinner propaga erros da fn", async () => {
|
||||
const { withSpinner } = await import("../../bin/cli/spinner.mjs");
|
||||
await assert.rejects(
|
||||
() =>
|
||||
withSpinner(
|
||||
"test",
|
||||
async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
{ quiet: true }
|
||||
),
|
||||
/boom/
|
||||
);
|
||||
});
|
||||
|
||||
test("withSpinner aceita update callback sem erro", async () => {
|
||||
const { withSpinner } = await import("../../bin/cli/spinner.mjs");
|
||||
let updateCalled = false;
|
||||
await withSpinner(
|
||||
"test",
|
||||
async ({ update }) => {
|
||||
update("progress 50%");
|
||||
updateCalled = true;
|
||||
},
|
||||
{ quiet: true }
|
||||
);
|
||||
assert.ok(updateCalled);
|
||||
});
|
||||
|
||||
test("spinner.mjs pode ser importado sem erro", async () => {
|
||||
const mod = await import("../../bin/cli/spinner.mjs");
|
||||
assert.equal(typeof mod.withSpinner, "function");
|
||||
assert.equal(typeof mod.shouldUseSpinner, "function");
|
||||
});
|
||||
Reference in New Issue
Block a user