Add import template into tui. Fix tests that fail on macos. Fix some updates.
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
@@ -110,7 +111,13 @@ describe("mnemonic utilities", () => {
|
||||
"/nonexistent",
|
||||
"mnemonic-relative",
|
||||
);
|
||||
expect(resolved).toBe(path.join(tempDir, "mnemonic-relative"));
|
||||
|
||||
// Due to some weird MacOS behavior we need to use realpathSync to get the correct path
|
||||
// Basically, tmpDir() returns a symlink, but moving to that path puts us at `/private/${tmpDir()}`
|
||||
const expectedPath = realpathSync(path.join(tempDir, "mnemonic-relative"));
|
||||
|
||||
// Compare to the expected path
|
||||
expect(resolved).toBe(expectedPath);
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Node js tool for temp dir
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import { BlockchainMonitor, Engine } from "@xo-cash/engine";
|
||||
|
||||
import {
|
||||
@@ -16,6 +19,7 @@ import { InMemoryStorage } from "../../../src/services/storage";
|
||||
import { MockElectrumService } from "./electrum-service";
|
||||
import { MockRatesService } from "./rates-service";
|
||||
import { RatesService } from "../../../src/services/rates";
|
||||
import { SettingsService } from "../../../src/services/settings";
|
||||
|
||||
export const DEFAULT_SEED =
|
||||
"page pencil stock planet limb cluster assault speak off joke private pioneer";
|
||||
@@ -67,8 +71,6 @@ export const addFakeResource = async (
|
||||
status: UnspentOutputStatus.CONFIRMED,
|
||||
selectable: true,
|
||||
privacy: false,
|
||||
templateIdentifier: options.templateIdentifier ?? "test-template",
|
||||
outputIdentifier: options.outputIdentifier ?? "receiveOutput",
|
||||
outpointIndex: options.outpointIndex ?? 0,
|
||||
outpointTransactionHash: options.outpointTransactionHash ?? randomTxHash(),
|
||||
minedAtHeight: options.minedAtHeight ?? 800000,
|
||||
@@ -143,10 +145,7 @@ export const createMockEngine = async (seed: string) => {
|
||||
|
||||
// Create the in-memory blockchain provider.
|
||||
const blockchainProvider = new InMemoryBlockchainProvider();
|
||||
await blockchainProvider.initialize({
|
||||
applicationIdentifier: "xo-cli-tests",
|
||||
electrumOptions: {},
|
||||
});
|
||||
await blockchainProvider.initialize();
|
||||
|
||||
// Create the blockchain monitor instance.
|
||||
const blockchainMonitor = new BlockchainMonitor(state, blockchainProvider);
|
||||
@@ -160,10 +159,13 @@ export const createMockEngine = async (seed: string) => {
|
||||
};
|
||||
|
||||
export const createMockAppService = async (engine: Engine) => {
|
||||
const settings = new SettingsService(`${tmpdir()}/xo-cli-tests-settings.json`);
|
||||
settings.setCurrency("USD");
|
||||
|
||||
const storage = await InMemoryStorage.create();
|
||||
|
||||
const mockRates = new MockRatesService();
|
||||
const rates = new RatesService(mockRates);
|
||||
const rates = new RatesService(mockRates, settings);
|
||||
|
||||
const mockElectrum = new MockElectrumService();
|
||||
|
||||
@@ -176,5 +178,5 @@ export const createMockAppService = async (engine: Engine) => {
|
||||
invitationStoragePath: "test-invitations.db",
|
||||
};
|
||||
|
||||
return new AppService(engine, storage, config, mockElectrum, rates);
|
||||
return new AppService(engine, storage, config, mockElectrum, rates, settings);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test, describe, beforeEach, afterEach } from "vitest";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync, realpathSync } from "node:fs";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -93,7 +93,13 @@ describe("paths utilities", () => {
|
||||
try {
|
||||
writeFileSync(path.join(tempDir, "mnemonic-cwd-test"), "test");
|
||||
const resolved = resolveMnemonicFilePath("mnemonic-cwd-test");
|
||||
expect(resolved).toBe(path.join(tempDir, "mnemonic-cwd-test"));
|
||||
|
||||
// Due to some weird MacOS behavior we need to use realpathSync to get the correct path
|
||||
// Basically, tmpDir() returns a symlink, but moving to that path puts us at `/private/${tmpDir()}`
|
||||
const expectedPath = realpathSync(path.join(tempDir, "mnemonic-cwd-test"));
|
||||
|
||||
// Compare to the expected path
|
||||
expect(resolved).toBe(expectedPath);
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
|
||||
44
tests/tui/format-dialog-message.test.ts
Normal file
44
tests/tui/format-dialog-message.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
formatDialogMessageLines,
|
||||
getMessageContentWidth,
|
||||
getMessageDialogWidth,
|
||||
} from "../../src/tui/utils/format-dialog-message.js";
|
||||
|
||||
describe("formatDialogMessageLines", () => {
|
||||
test("drops empty lines from leading newlines", () => {
|
||||
const lines = formatDialogMessageLines("\n- first\n- second", 80);
|
||||
|
||||
expect(lines).toEqual(["- first", "- second"]);
|
||||
});
|
||||
|
||||
test("keeps short lines unchanged", () => {
|
||||
const lines = formatDialogMessageLines("- actions.receive: Invalid", 80);
|
||||
|
||||
expect(lines).toEqual(["- actions.receive: Invalid"]);
|
||||
});
|
||||
|
||||
test("breaks long dot-separated paths at segment boundaries", () => {
|
||||
const line =
|
||||
"- actions.requestFungibleTokens.roles.receiver.requirements: Unrecognized key: \"generate\"";
|
||||
const lines = formatDialogMessageLines(line, 56);
|
||||
|
||||
expect(lines.length).toBeGreaterThan(1);
|
||||
expect(lines.join("\n")).toContain("actions.requestFungibleTokens.");
|
||||
expect(lines.every((entry) => entry.length <= 58)).toBe(true);
|
||||
expect(lines[1]?.startsWith(" ")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dialog width helpers", () => {
|
||||
test("getMessageDialogWidth respects terminal bounds", () => {
|
||||
expect(getMessageDialogWidth(120)).toBe(100);
|
||||
expect(getMessageDialogWidth(80)).toBe(76);
|
||||
expect(getMessageDialogWidth(40)).toBe(60);
|
||||
});
|
||||
|
||||
test("getMessageContentWidth subtracts border and padding", () => {
|
||||
expect(getMessageContentWidth(76)).toBe(70);
|
||||
});
|
||||
});
|
||||
114
tests/tui/list-directory-entries.test.ts
Normal file
114
tests/tui/list-directory-entries.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { listDirectoryEntries } from "../../src/tui/utils/list-directory-entries.js";
|
||||
|
||||
describe("listDirectoryEntries", () => {
|
||||
let tempRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempRoot = mkdtempSync(path.join(tmpdir(), "xo-file-picker-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(tempRoot)) {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("includes parent entry and sorts directories before files", () => {
|
||||
mkdirSync(path.join(tempRoot, "beta-dir"));
|
||||
mkdirSync(path.join(tempRoot, "alpha-dir"));
|
||||
writeFileSync(path.join(tempRoot, "zebra.json"), "{}");
|
||||
writeFileSync(path.join(tempRoot, "apple.txt"), "x");
|
||||
|
||||
const childDir = path.join(tempRoot, "child");
|
||||
mkdirSync(childDir);
|
||||
writeFileSync(path.join(childDir, "nested.json"), "{}");
|
||||
|
||||
const rootResult = listDirectoryEntries(tempRoot);
|
||||
expect(rootResult.error).toBeUndefined();
|
||||
expect(rootResult.entries.map((entry) => entry.name)).toEqual([
|
||||
"..",
|
||||
"alpha-dir",
|
||||
"beta-dir",
|
||||
"child",
|
||||
"apple.txt",
|
||||
"zebra.json",
|
||||
]);
|
||||
|
||||
const childResult = listDirectoryEntries(childDir);
|
||||
expect(childResult.entries[0]).toMatchObject({
|
||||
name: "..",
|
||||
kind: "parent",
|
||||
absolutePath: tempRoot,
|
||||
});
|
||||
expect(childResult.entries.slice(1).map((entry) => entry.name)).toEqual([
|
||||
"nested.json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("filters files by extension when extensions are provided", () => {
|
||||
writeFileSync(path.join(tempRoot, "template.json"), "{}");
|
||||
writeFileSync(path.join(tempRoot, "readme.md"), "# hi");
|
||||
writeFileSync(path.join(tempRoot, "UPPER.JSON"), "{}");
|
||||
|
||||
const result = listDirectoryEntries(tempRoot, { extensions: ["json"] });
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.entries.map((entry) => entry.name)).toEqual([
|
||||
"..",
|
||||
"template.json",
|
||||
"UPPER.JSON",
|
||||
]);
|
||||
});
|
||||
|
||||
test("shows all files when extensions are omitted", () => {
|
||||
writeFileSync(path.join(tempRoot, "a.json"), "{}");
|
||||
writeFileSync(path.join(tempRoot, "b.txt"), "x");
|
||||
|
||||
const result = listDirectoryEntries(tempRoot);
|
||||
|
||||
expect(result.entries.map((entry) => entry.name)).toEqual([
|
||||
"..",
|
||||
"a.json",
|
||||
"b.txt",
|
||||
]);
|
||||
});
|
||||
|
||||
test("omits parent entry at filesystem root", () => {
|
||||
const rootResult = listDirectoryEntries(path.parse(tempRoot).root);
|
||||
|
||||
expect(rootResult.error).toBeUndefined();
|
||||
expect(rootResult.entries.some((entry) => entry.kind === "parent")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("returns error for missing directory without throwing", () => {
|
||||
const missingPath = path.join(tempRoot, "does-not-exist");
|
||||
|
||||
const result = listDirectoryEntries(missingPath);
|
||||
|
||||
expect(result.entries).toEqual([]);
|
||||
expect(result.error).toContain("Directory does not exist");
|
||||
});
|
||||
|
||||
test("returns error when path is a file", () => {
|
||||
const filePath = path.join(tempRoot, "file.txt");
|
||||
writeFileSync(filePath, "hello");
|
||||
|
||||
const result = listDirectoryEntries(filePath);
|
||||
|
||||
expect(result.entries).toEqual([]);
|
||||
expect(result.error).toContain("Not a directory");
|
||||
});
|
||||
});
|
||||
78
tests/utils/load-template-from-file.test.ts
Normal file
78
tests/utils/load-template-from-file.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import { parseTemplate } from "@xo-cash/utils";
|
||||
|
||||
import {
|
||||
loadTemplateFromFile,
|
||||
TemplateLoadError,
|
||||
} from "../../src/utils/load-template-from-file.js";
|
||||
import { p2pkhTemplate } from "../cli/mocks/template-p2pkh.js";
|
||||
|
||||
describe("loadTemplateFromFile", () => {
|
||||
let tempRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempRoot = mkdtempSync(path.join(tmpdir(), "xo-load-template-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(tempRoot)) {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("loads JSON templates directly", async () => {
|
||||
const jsonPath = path.join(tempRoot, "template.json");
|
||||
writeFileSync(jsonPath, JSON.stringify(p2pkhTemplate));
|
||||
|
||||
const contents = await loadTemplateFromFile(jsonPath);
|
||||
const parsed = parseTemplate(contents);
|
||||
|
||||
expect(parsed.name).toBe(p2pkhTemplate.name);
|
||||
});
|
||||
|
||||
test("loads TypeScript templates via child process", async () => {
|
||||
const tsTemplatePath = path.resolve(
|
||||
process.cwd(),
|
||||
"../templates/source/p2pkh.ts",
|
||||
);
|
||||
expect(existsSync(tsTemplatePath)).toBe(true);
|
||||
|
||||
const contents = await loadTemplateFromFile(tsTemplatePath);
|
||||
const parsed = parseTemplate(contents);
|
||||
|
||||
expect(parsed.name).toBe("Wallet (P2PKH)");
|
||||
});
|
||||
|
||||
test("loads JavaScript templates via child process", async () => {
|
||||
const jsPath = path.join(tempRoot, "template.mjs");
|
||||
writeFileSync(
|
||||
jsPath,
|
||||
`export default ${JSON.stringify(p2pkhTemplate)};\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const contents = await loadTemplateFromFile(jsPath);
|
||||
const parsed = parseTemplate(contents);
|
||||
|
||||
expect(parsed.name).toBe(p2pkhTemplate.name);
|
||||
});
|
||||
|
||||
test("throws TemplateLoadError for missing files", async () => {
|
||||
await expect(
|
||||
loadTemplateFromFile(path.join(tempRoot, "missing.json")),
|
||||
).rejects.toBeInstanceOf(TemplateLoadError);
|
||||
});
|
||||
|
||||
test("throws TemplateLoadError for unsupported extensions", async () => {
|
||||
const txtPath = path.join(tempRoot, "template.txt");
|
||||
writeFileSync(txtPath, "hello");
|
||||
|
||||
await expect(loadTemplateFromFile(txtPath)).rejects.toThrow(
|
||||
/Unsupported template file extension/,
|
||||
);
|
||||
});
|
||||
});
|
||||
57
tests/utils/pick-template-export.test.ts
Normal file
57
tests/utils/pick-template-export.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
isTemplateLike,
|
||||
pickTemplateExport,
|
||||
} from "../../src/utils/pick-template-export.js";
|
||||
|
||||
const sampleTemplate = {
|
||||
$schema: "https://libauth.org/schemas/wallet-template-v0.schema.json",
|
||||
name: "Sample",
|
||||
roles: { owner: { name: "Owner" } },
|
||||
};
|
||||
|
||||
describe("pickTemplateExport", () => {
|
||||
test("isTemplateLike accepts objects with schema, name, and roles", () => {
|
||||
expect(isTemplateLike(sampleTemplate)).toBe(true);
|
||||
expect(isTemplateLike(null)).toBe(false);
|
||||
expect(isTemplateLike({ name: "Missing schema" })).toBe(false);
|
||||
});
|
||||
|
||||
test("prefers default export when template-like", () => {
|
||||
const picked = pickTemplateExport({
|
||||
default: sampleTemplate,
|
||||
otherTemplate: {
|
||||
...sampleTemplate,
|
||||
name: "Other",
|
||||
},
|
||||
});
|
||||
|
||||
expect(picked).toBe(sampleTemplate);
|
||||
});
|
||||
|
||||
test("uses a single named export when no default export exists", () => {
|
||||
const picked = pickTemplateExport({
|
||||
p2pkhTemplate: sampleTemplate,
|
||||
});
|
||||
|
||||
expect(picked).toBe(sampleTemplate);
|
||||
});
|
||||
|
||||
test("throws when multiple template exports exist", () => {
|
||||
expect(() =>
|
||||
pickTemplateExport({
|
||||
firstTemplate: sampleTemplate,
|
||||
secondTemplate: { ...sampleTemplate, name: "Second" },
|
||||
}),
|
||||
).toThrow(/Multiple template exports found/);
|
||||
});
|
||||
|
||||
test("throws when no template export exists", () => {
|
||||
expect(() =>
|
||||
pickTemplateExport({
|
||||
notATemplate: { foo: "bar" },
|
||||
}),
|
||||
).toThrow(/No XOTemplate export found/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user