@sundaeswap/sprinkles 0.8.2 → 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 +3 -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 +474 -177
- 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 +3 -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 +477 -180
- 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 +1 -0
- package/src/Sprinkle/__tests__/show-menu.test.ts +132 -116
- package/src/Sprinkle/index.ts +546 -186
- 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",
|
|
@@ -588,6 +588,7 @@ describe("FillInStruct", () => {
|
|
|
588
588
|
);
|
|
589
589
|
expect(mockSelectCancellable.mock.calls[0][0].choices).toEqual([
|
|
590
590
|
{ name: "Enter existing private key", value: "existing" },
|
|
591
|
+
{ name: "Import from recovery phrase", value: "mnemonic" },
|
|
591
592
|
{ name: "Generate new wallet", value: "generate" },
|
|
592
593
|
]);
|
|
593
594
|
expect(result).toBe("my-private-key");
|
|
@@ -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
|
});
|