@sundaeswap/sprinkles 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +19 -7
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +114 -0
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +109 -98
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/cjs/Sprinkle/index.js +480 -179
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/cjs/Sprinkle/types.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js +19 -7
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +114 -0
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js +110 -99
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/esm/Sprinkle/index.js +483 -182
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/types.js.map +1 -1
- package/dist/types/Sprinkle/index.d.ts +29 -0
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/types.d.ts +6 -0
- package/dist/types/Sprinkle/types.d.ts.map +1 -1
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Sprinkle/__tests__/enhancements.test.ts +20 -7
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +112 -1
- package/src/Sprinkle/__tests__/show-menu.test.ts +132 -116
- package/src/Sprinkle/index.ts +552 -188
- package/src/Sprinkle/types.ts +6 -0
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ const mockConfirmCancellable = mock();
|
|
|
11
11
|
const mockSearchCancellable = mock();
|
|
12
12
|
|
|
13
13
|
mock.module("@inquirer/prompts", () => ({
|
|
14
|
+
search: mockSearchCancellable,
|
|
14
15
|
select: mockSelect,
|
|
15
16
|
input: mockInput,
|
|
16
17
|
password: mockPassword,
|
|
@@ -28,24 +29,36 @@ mock.module("../prompts.js", () => ({
|
|
|
28
29
|
describe("beforeShow hook (2.2)", () => {
|
|
29
30
|
let sprinkle: Sprinkle<any>;
|
|
30
31
|
|
|
32
|
+
const pickExit = async (opts: any) => {
|
|
33
|
+
const choices = await opts.source(undefined);
|
|
34
|
+
return choices.find((c: any) => c.name === "Exit").value;
|
|
35
|
+
};
|
|
36
|
+
|
|
31
37
|
beforeEach(() => {
|
|
32
38
|
const schema = Type.Object({ name: Type.String() });
|
|
33
39
|
sprinkle = new Sprinkle(schema, "/tmp/test");
|
|
34
40
|
sprinkle.settings = { name: "test" } as any;
|
|
41
|
+
sprinkle.profileId = "test";
|
|
42
|
+
sprinkle.profileMeta = {
|
|
43
|
+
name: "Test",
|
|
44
|
+
createdAt: new Date().toISOString(),
|
|
45
|
+
updatedAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
mockSearchCancellable.mockClear();
|
|
35
48
|
mockSelect.mockClear();
|
|
36
49
|
mockSelectCancellable.mockClear();
|
|
37
50
|
});
|
|
38
51
|
|
|
39
|
-
test("calls beforeShow before rendering
|
|
52
|
+
test("calls root beforeShow before rendering the search prompt", async () => {
|
|
40
53
|
const callOrder: string[] = [];
|
|
41
54
|
|
|
42
55
|
const beforeShowFn = mock(async () => {
|
|
43
56
|
callOrder.push("beforeShow");
|
|
44
57
|
});
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
callOrder.push("
|
|
48
|
-
return
|
|
59
|
+
mockSearchCancellable.mockImplementation(async (opts: any) => {
|
|
60
|
+
callOrder.push("search");
|
|
61
|
+
return pickExit(opts);
|
|
49
62
|
});
|
|
50
63
|
|
|
51
64
|
const menu: IMenu<any> = {
|
|
@@ -58,13 +71,13 @@ describe("beforeShow hook (2.2)", () => {
|
|
|
58
71
|
|
|
59
72
|
expect(beforeShowFn).toHaveBeenCalledTimes(1);
|
|
60
73
|
expect(callOrder[0]).toBe("beforeShow");
|
|
61
|
-
expect(callOrder[1]).toBe("
|
|
74
|
+
expect(callOrder[1]).toBe("search");
|
|
62
75
|
});
|
|
63
76
|
|
|
64
77
|
test("passes sprinkle instance to beforeShow", async () => {
|
|
65
78
|
let receivedSprinkle: any;
|
|
66
79
|
|
|
67
|
-
|
|
80
|
+
mockSearchCancellable.mockImplementationOnce(pickExit);
|
|
68
81
|
|
|
69
82
|
const menu: IMenu<any> = {
|
|
70
83
|
title: "Test",
|
|
@@ -79,7 +92,7 @@ describe("beforeShow hook (2.2)", () => {
|
|
|
79
92
|
});
|
|
80
93
|
|
|
81
94
|
test("menu works without beforeShow", async () => {
|
|
82
|
-
|
|
95
|
+
mockSearchCancellable.mockImplementationOnce(pickExit);
|
|
83
96
|
|
|
84
97
|
const menu: IMenu<any> = {
|
|
85
98
|
title: "Test",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test, mock, beforeEach, spyOn } from "bun:test";
|
|
2
|
-
import { Sprinkle, Type, WalletSettingsSchema } from "../index.js";
|
|
2
|
+
import { Sprinkle, Type, WalletSettingsSchema, MultisigScriptModule } from "../index.js";
|
|
3
|
+
import { MultisigScript } from "../schemas.js";
|
|
3
4
|
import { UserCancelledError } from "../types.js";
|
|
4
5
|
|
|
5
6
|
// Mock @inquirer/prompts
|
|
@@ -587,6 +588,7 @@ describe("FillInStruct", () => {
|
|
|
587
588
|
);
|
|
588
589
|
expect(mockSelectCancellable.mock.calls[0][0].choices).toEqual([
|
|
589
590
|
{ name: "Enter existing private key", value: "existing" },
|
|
591
|
+
{ name: "Import from recovery phrase", value: "mnemonic" },
|
|
590
592
|
{ name: "Generate new wallet", value: "generate" },
|
|
591
593
|
]);
|
|
592
594
|
expect(result).toBe("my-private-key");
|
|
@@ -638,4 +640,113 @@ describe("FillInStruct", () => {
|
|
|
638
640
|
UserCancelledError,
|
|
639
641
|
);
|
|
640
642
|
});
|
|
643
|
+
|
|
644
|
+
// --- Type.Module / Type.Import ($ref resolution) ---
|
|
645
|
+
|
|
646
|
+
test("resolves MultisigScript TImport $ref (Signature variant)", async () => {
|
|
647
|
+
const hash = "a".repeat(56);
|
|
648
|
+
|
|
649
|
+
// Union variant selection: pick Signature
|
|
650
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
651
|
+
const sigVariant = opts.choices.find((c: any) =>
|
|
652
|
+
c.name === "Signature" || JSON.stringify(c.value).includes("Signature"),
|
|
653
|
+
);
|
|
654
|
+
return sigVariant?.value ?? opts.choices[0].value;
|
|
655
|
+
});
|
|
656
|
+
// Signature → single-field → key_hash → single-field → string prompt
|
|
657
|
+
mockInputCancellable.mockResolvedValueOnce(hash);
|
|
658
|
+
// "Save to addressbook?"
|
|
659
|
+
mockSelectCancellable.mockResolvedValueOnce(false);
|
|
660
|
+
|
|
661
|
+
const result = await sprinkle.FillInStruct(MultisigScript);
|
|
662
|
+
expect(result).toEqual({ Signature: { key_hash: hash } });
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("resolves MultisigScript $ref inside a wrapping schema", async () => {
|
|
666
|
+
const schema = Type.Object({
|
|
667
|
+
script: MultisigScript,
|
|
668
|
+
});
|
|
669
|
+
const hash = "b".repeat(56);
|
|
670
|
+
|
|
671
|
+
// Object has single required field "script" → skips menu
|
|
672
|
+
// Union variant selection: pick Signature
|
|
673
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
674
|
+
const sigVariant = opts.choices.find((c: any) =>
|
|
675
|
+
c.name === "Signature" || JSON.stringify(c.value).includes("Signature"),
|
|
676
|
+
);
|
|
677
|
+
return sigVariant?.value ?? opts.choices[0].value;
|
|
678
|
+
});
|
|
679
|
+
mockInputCancellable.mockResolvedValueOnce(hash);
|
|
680
|
+
// "Save to addressbook?"
|
|
681
|
+
mockSelectCancellable.mockResolvedValueOnce(false);
|
|
682
|
+
|
|
683
|
+
const result = await sprinkle.FillInStruct(schema);
|
|
684
|
+
expect(result).toEqual({ script: { Signature: { key_hash: hash } } });
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("resolves MultisigScript default matching when editing existing value", async () => {
|
|
688
|
+
// When editing an existing MultisigScript value, Value.Check is called
|
|
689
|
+
// to match the default against the selected variant. Variant schemas
|
|
690
|
+
// contain $ref which requires a references array to resolve.
|
|
691
|
+
const hash = "e".repeat(56);
|
|
692
|
+
const existingValue = {
|
|
693
|
+
AllOf: {
|
|
694
|
+
scripts: [{ Signature: { key_hash: hash } }],
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// Union variant selection: pick AllOf (matches the existing value)
|
|
699
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
700
|
+
const variant = opts.choices.find((c: any) =>
|
|
701
|
+
c.name === "AllOf" || (c.value?.properties && "AllOf" in c.value.properties),
|
|
702
|
+
);
|
|
703
|
+
return variant?.value ?? opts.choices[1].value;
|
|
704
|
+
});
|
|
705
|
+
// AllOf → single field → scripts array → items already populated from default
|
|
706
|
+
// Array menu: Done (keep existing items)
|
|
707
|
+
mockSelectCancellable.mockResolvedValueOnce("done");
|
|
708
|
+
// "Save to addressbook?"
|
|
709
|
+
mockSelectCancellable.mockResolvedValueOnce(false);
|
|
710
|
+
|
|
711
|
+
const result = await sprinkle.FillInStruct(MultisigScript, existingValue as any);
|
|
712
|
+
expect(result).toEqual(existingValue);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("resolves recursive $ref in nested MultisigScript (AllOf with nested Signature)", async () => {
|
|
716
|
+
const hash1 = "c".repeat(56);
|
|
717
|
+
|
|
718
|
+
// Top-level union: pick AllOf
|
|
719
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
720
|
+
const variant = opts.choices.find((c: any) =>
|
|
721
|
+
c.name === "AllOf" || (c.value?.properties && "AllOf" in c.value.properties),
|
|
722
|
+
);
|
|
723
|
+
return variant?.value ?? opts.choices[1].value;
|
|
724
|
+
});
|
|
725
|
+
// AllOf → single field → scripts array
|
|
726
|
+
// Array menu: Add item
|
|
727
|
+
mockSelectCancellable.mockResolvedValueOnce("add");
|
|
728
|
+
// Array item is MultisigScript ($ref) → resolves recursively → shows Union
|
|
729
|
+
// Union: pick Signature
|
|
730
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
731
|
+
const sigVariant = opts.choices.find((c: any) =>
|
|
732
|
+
c.name === "Signature" || (c.value?.properties && "Signature" in c.value.properties),
|
|
733
|
+
);
|
|
734
|
+
return sigVariant?.value ?? opts.choices[0].value;
|
|
735
|
+
});
|
|
736
|
+
// Fill key_hash
|
|
737
|
+
mockInputCancellable.mockResolvedValueOnce(hash1);
|
|
738
|
+
// "Save to addressbook?" for inner MultisigScript
|
|
739
|
+
mockSelectCancellable.mockResolvedValueOnce(false);
|
|
740
|
+
// Array menu: Done
|
|
741
|
+
mockSelectCancellable.mockResolvedValueOnce("done");
|
|
742
|
+
// "Save to addressbook?" for outer MultisigScript
|
|
743
|
+
mockSelectCancellable.mockResolvedValueOnce(false);
|
|
744
|
+
|
|
745
|
+
const result = await sprinkle.FillInStruct(MultisigScript);
|
|
746
|
+
expect(result).toEqual({
|
|
747
|
+
AllOf: {
|
|
748
|
+
scripts: [{ Signature: { key_hash: hash1 } }],
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
});
|
|
641
752
|
});
|
|
@@ -2,12 +2,14 @@ import { describe, expect, test, mock, beforeEach, spyOn } from "bun:test";
|
|
|
2
2
|
import { Sprinkle, Type, type IMenu } from "../index.js";
|
|
3
3
|
import { withProfile } from "./test-helpers.js";
|
|
4
4
|
|
|
5
|
+
const mockSearch = mock();
|
|
5
6
|
const mockSelect = mock();
|
|
6
7
|
const mockInput = mock();
|
|
7
8
|
const mockSelectCancellable = mock();
|
|
8
9
|
const mockInputCancellable = mock();
|
|
9
10
|
|
|
10
11
|
mock.module("@inquirer/prompts", () => ({
|
|
12
|
+
search: mockSearch,
|
|
11
13
|
select: mockSelect,
|
|
12
14
|
input: mockInput,
|
|
13
15
|
}));
|
|
@@ -17,104 +19,165 @@ mock.module("../prompts.js", () => ({
|
|
|
17
19
|
inputCancellable: mockInputCancellable,
|
|
18
20
|
passwordCancellable: mock(),
|
|
19
21
|
confirmCancellable: mock(),
|
|
20
|
-
searchCancellable:
|
|
22
|
+
searchCancellable: mockSearch,
|
|
21
23
|
select: mockSelectCancellable,
|
|
22
24
|
}));
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
type SearchSource = (
|
|
27
|
+
term: string | undefined,
|
|
28
|
+
) => Promise<{ name: string; value: any }[]>;
|
|
29
|
+
|
|
30
|
+
async function collectChoices(call: any): Promise<{ name: string; value: any }[]> {
|
|
31
|
+
const source: SearchSource = call[0].source;
|
|
32
|
+
return source(undefined);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pickByName(
|
|
36
|
+
choices: { name: string; value: any }[],
|
|
37
|
+
name: string,
|
|
38
|
+
): any {
|
|
39
|
+
const match = choices.find((c) => c.name === name);
|
|
40
|
+
if (!match) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`No choice named "${name}". Available: ${choices.map((c) => c.name).join(", ")}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return match.value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("showMenu (search-based)", () => {
|
|
25
49
|
let sprinkle: Sprinkle<any>;
|
|
26
50
|
|
|
27
51
|
beforeEach(() => {
|
|
28
52
|
const schema = Type.Object({ name: Type.String() });
|
|
29
53
|
sprinkle = withProfile(new Sprinkle(schema, "/tmp/test"));
|
|
30
54
|
sprinkle.settings = { name: "test" } as any;
|
|
55
|
+
mockSearch.mockClear();
|
|
31
56
|
mockSelect.mockClear();
|
|
32
57
|
mockSelectCancellable.mockClear();
|
|
33
58
|
mockInput.mockClear();
|
|
34
59
|
});
|
|
35
60
|
|
|
36
|
-
test("exits when Exit is selected
|
|
37
|
-
|
|
61
|
+
test("exits when the Exit leaf is selected", async () => {
|
|
62
|
+
mockSearch.mockImplementationOnce(async (opts: any) => {
|
|
63
|
+
const choices = await opts.source(undefined);
|
|
64
|
+
return pickByName(choices, "Exit");
|
|
65
|
+
});
|
|
38
66
|
|
|
39
67
|
const menu: IMenu<any> = {
|
|
40
68
|
title: "Test Menu",
|
|
41
|
-
items: [
|
|
42
|
-
{
|
|
43
|
-
title: "Action 1",
|
|
44
|
-
action: async () => {},
|
|
45
|
-
},
|
|
46
|
-
],
|
|
69
|
+
items: [{ title: "Action 1", action: async () => {} }],
|
|
47
70
|
};
|
|
48
71
|
|
|
49
72
|
await sprinkle.showMenu(menu);
|
|
73
|
+
expect(mockSearch).toHaveBeenCalledTimes(1);
|
|
50
74
|
});
|
|
51
75
|
|
|
52
|
-
test("executes action
|
|
76
|
+
test("executes a leaf action then loops back to the search prompt", async () => {
|
|
53
77
|
const actionFn = mock(async () => {});
|
|
54
78
|
|
|
55
|
-
|
|
79
|
+
mockSearch
|
|
80
|
+
.mockImplementationOnce(async (opts: any) => {
|
|
81
|
+
const choices = await opts.source(undefined);
|
|
82
|
+
return pickByName(choices, "My Action");
|
|
83
|
+
})
|
|
84
|
+
.mockImplementationOnce(async (opts: any) => {
|
|
85
|
+
const choices = await opts.source(undefined);
|
|
86
|
+
return pickByName(choices, "Exit");
|
|
87
|
+
});
|
|
56
88
|
|
|
57
89
|
const menu: IMenu<any> = {
|
|
58
90
|
title: "Test Menu",
|
|
59
|
-
items: [
|
|
60
|
-
{
|
|
61
|
-
title: "My Action",
|
|
62
|
-
action: actionFn,
|
|
63
|
-
},
|
|
64
|
-
],
|
|
91
|
+
items: [{ title: "My Action", action: actionFn }],
|
|
65
92
|
};
|
|
66
93
|
|
|
67
94
|
await sprinkle.showMenu(menu);
|
|
68
95
|
expect(actionFn).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(mockSearch).toHaveBeenCalledTimes(2);
|
|
69
97
|
});
|
|
70
98
|
|
|
71
|
-
test("
|
|
72
|
-
|
|
99
|
+
test("flattens submenus into breadcrumb leaves and surfaces Settings & Profiles + Exit", async () => {
|
|
100
|
+
mockSearch.mockImplementationOnce(async (opts: any) => {
|
|
101
|
+
const choices = await opts.source(undefined);
|
|
102
|
+
return pickByName(choices, "Exit");
|
|
103
|
+
});
|
|
73
104
|
|
|
74
105
|
const menu: IMenu<any> = {
|
|
75
|
-
title: "
|
|
76
|
-
items: [
|
|
106
|
+
title: "Main",
|
|
107
|
+
items: [
|
|
108
|
+
{
|
|
109
|
+
title: "Strategies",
|
|
110
|
+
items: [
|
|
111
|
+
{ title: "Create limit order", action: async () => {} },
|
|
112
|
+
{ title: "List strategies", action: async () => {} },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{ title: "Quit (custom)", action: async () => {} },
|
|
116
|
+
],
|
|
77
117
|
};
|
|
78
118
|
|
|
79
119
|
await sprinkle.showMenu(menu);
|
|
80
120
|
|
|
81
|
-
const choices =
|
|
82
|
-
const names = choices.map((c
|
|
83
|
-
expect(names).toContain("
|
|
121
|
+
const choices = await collectChoices(mockSearch.mock.calls[0]);
|
|
122
|
+
const names = choices.map((c) => c.name);
|
|
123
|
+
expect(names).toContain("Strategies › Create limit order");
|
|
124
|
+
expect(names).toContain("Strategies › List strategies");
|
|
125
|
+
expect(names).toContain("Quit (custom)");
|
|
126
|
+
expect(names).toContain("Settings & Profiles › Edit settings");
|
|
127
|
+
expect(names).toContain("Settings & Profiles › Import profile from file");
|
|
84
128
|
expect(names).toContain("Exit");
|
|
85
|
-
expect(names).not.toContain("Switch profile");
|
|
86
|
-
expect(names).not.toContain("Manage profiles");
|
|
87
|
-
expect(names).not.toContain("Edit settings");
|
|
88
129
|
});
|
|
89
130
|
|
|
90
|
-
test("
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.
|
|
131
|
+
test("filters leaves by typed term (case-insensitive, all tokens, leaf-title boost)", async () => {
|
|
132
|
+
let filtered: { name: string; value: any }[] = [];
|
|
133
|
+
|
|
134
|
+
mockSearch.mockImplementationOnce(async (opts: any) => {
|
|
135
|
+
filtered = await opts.source("strateg");
|
|
136
|
+
// pick Exit so the loop terminates
|
|
137
|
+
const exitChoice = (await opts.source(undefined)).find(
|
|
138
|
+
(c: any) => c.name === "Exit",
|
|
139
|
+
);
|
|
140
|
+
return exitChoice.value;
|
|
141
|
+
});
|
|
95
142
|
|
|
96
143
|
const menu: IMenu<any> = {
|
|
97
144
|
title: "Main",
|
|
98
145
|
items: [
|
|
99
146
|
{
|
|
100
|
-
title: "
|
|
101
|
-
items: [
|
|
147
|
+
title: "Strategies",
|
|
148
|
+
items: [
|
|
149
|
+
{ title: "Create limit order", action: async () => {} },
|
|
150
|
+
{ title: "Cancel strategy", action: async () => {} },
|
|
151
|
+
],
|
|
102
152
|
},
|
|
153
|
+
{ title: "Send payment", action: async () => {} },
|
|
103
154
|
],
|
|
104
155
|
};
|
|
105
156
|
|
|
106
157
|
await sprinkle.showMenu(menu);
|
|
107
158
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
expect(
|
|
111
|
-
expect(
|
|
159
|
+
const filteredNames = filtered.map((c) => c.name);
|
|
160
|
+
// Everything under "Strategies" matches via the breadcrumb token
|
|
161
|
+
expect(filteredNames).toContain("Strategies › Create limit order");
|
|
162
|
+
expect(filteredNames).toContain("Strategies › Cancel strategy");
|
|
163
|
+
// Direct leaf-title hit outranks ancestor-only match
|
|
164
|
+
expect(filteredNames[0]).toBe("Strategies › Cancel strategy");
|
|
165
|
+
// Unrelated items are filtered out
|
|
166
|
+
expect(filteredNames).not.toContain("Send payment");
|
|
112
167
|
});
|
|
113
168
|
|
|
114
|
-
test("action returning
|
|
169
|
+
test("action returning a Sprinkle persists updated settings", async () => {
|
|
115
170
|
sprinkle.settings = { name: "original" } as any;
|
|
116
171
|
|
|
117
|
-
|
|
172
|
+
mockSearch
|
|
173
|
+
.mockImplementationOnce(async (opts: any) => {
|
|
174
|
+
const choices = await opts.source(undefined);
|
|
175
|
+
return pickByName(choices, "Update");
|
|
176
|
+
})
|
|
177
|
+
.mockImplementationOnce(async (opts: any) => {
|
|
178
|
+
const choices = await opts.source(undefined);
|
|
179
|
+
return pickByName(choices, "Exit");
|
|
180
|
+
});
|
|
118
181
|
|
|
119
182
|
const menu: IMenu<any> = {
|
|
120
183
|
title: "Test",
|
|
@@ -133,85 +196,38 @@ describe("showMenu", () => {
|
|
|
133
196
|
expect(sprinkle.settings).toEqual({ name: "updated" });
|
|
134
197
|
});
|
|
135
198
|
|
|
136
|
-
test("
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("View settings displays masked settings", async () => {
|
|
157
|
-
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
158
|
-
|
|
159
|
-
// Select "Settings & Profiles" (-5), then "View settings" (0),
|
|
160
|
-
// then "Continue" for the press Enter prompt, then "Back" (-1), then "Exit" (-1)
|
|
161
|
-
mockSelectCancellable
|
|
162
|
-
.mockResolvedValueOnce(-5) // Settings & Profiles
|
|
163
|
-
.mockResolvedValueOnce(0) // View settings (first item)
|
|
164
|
-
.mockResolvedValueOnce("continue") // Press Enter to continue
|
|
165
|
-
.mockResolvedValueOnce(-1) // Back
|
|
166
|
-
.mockResolvedValueOnce(-1); // Exit
|
|
167
|
-
|
|
168
|
-
const menu: IMenu<any> = {
|
|
169
|
-
title: "Test",
|
|
170
|
-
items: [{ title: "Action", action: async () => {} }],
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
await sprinkle.showMenu(menu);
|
|
174
|
-
|
|
175
|
-
// Should have called console.log with the settings (find the JSON output among breadcrumbs)
|
|
176
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
177
|
-
const jsonOutput = consoleSpy.mock.calls.find((call: any[]) =>
|
|
178
|
-
call[0] && typeof call[0] === "string" && call[0].includes('"name"')
|
|
179
|
-
);
|
|
180
|
-
expect(jsonOutput).toBeDefined();
|
|
181
|
-
|
|
182
|
-
consoleSpy.mockRestore();
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("Create profile restores state when FillInStruct is cancelled", async () => {
|
|
186
|
-
// Capture original state
|
|
187
|
-
const originalProfileId = sprinkle.currentProfile?.id;
|
|
188
|
-
const originalSettings = { ...sprinkle.settings };
|
|
189
|
-
|
|
190
|
-
// Settings menu indices: 0=View, 1=Edit, 2=Switch, 3=Create new profile
|
|
191
|
-
mockSelectCancellable
|
|
192
|
-
.mockResolvedValueOnce(-5) // Settings & Profiles
|
|
193
|
-
.mockResolvedValueOnce(3); // Create new profile
|
|
194
|
-
|
|
195
|
-
// promptProfileMeta: name then description
|
|
196
|
-
mockInputCancellable
|
|
197
|
-
.mockResolvedValueOnce("New Profile") // Profile name
|
|
198
|
-
.mockResolvedValueOnce("") // Profile description
|
|
199
|
-
.mockResolvedValueOnce(null); // Cancel during FillInStruct (name field)
|
|
200
|
-
|
|
201
|
-
// After cancellation, back out of menus
|
|
202
|
-
mockSelectCancellable
|
|
203
|
-
.mockResolvedValueOnce(-1) // Back from settings
|
|
204
|
-
.mockResolvedValueOnce(-1); // Exit
|
|
199
|
+
test("submenu beforeShow fires only when a leaf under it is selected", async () => {
|
|
200
|
+
const order: string[] = [];
|
|
201
|
+
const subBeforeShow = mock(async () => {
|
|
202
|
+
order.push("sub.beforeShow");
|
|
203
|
+
});
|
|
204
|
+
const actionFn = mock(async () => {
|
|
205
|
+
order.push("action");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
mockSearch
|
|
209
|
+
.mockImplementationOnce(async (opts: any) => {
|
|
210
|
+
const choices = await opts.source(undefined);
|
|
211
|
+
return pickByName(choices, "Strategies › Create");
|
|
212
|
+
})
|
|
213
|
+
.mockImplementationOnce(async (opts: any) => {
|
|
214
|
+
const choices = await opts.source(undefined);
|
|
215
|
+
return pickByName(choices, "Exit");
|
|
216
|
+
});
|
|
205
217
|
|
|
206
218
|
const menu: IMenu<any> = {
|
|
207
|
-
title: "
|
|
208
|
-
items: [
|
|
219
|
+
title: "Main",
|
|
220
|
+
items: [
|
|
221
|
+
{
|
|
222
|
+
title: "Strategies",
|
|
223
|
+
beforeShow: subBeforeShow,
|
|
224
|
+
items: [{ title: "Create", action: actionFn }],
|
|
225
|
+
},
|
|
226
|
+
],
|
|
209
227
|
};
|
|
210
228
|
|
|
211
229
|
await sprinkle.showMenu(menu);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
expect(sprinkle.currentProfile?.id).toBe(originalProfileId);
|
|
215
|
-
expect(sprinkle.settings).toEqual(originalSettings);
|
|
230
|
+
expect(subBeforeShow).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(order).toEqual(["sub.beforeShow", "action"]);
|
|
216
232
|
});
|
|
217
233
|
});
|