mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat(pi-ask-user): support RPC UI dialogs
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { normalizeQuestions } from "../src/normalize.ts";
|
||||
import { runRpcQuestions, type RpcQuestionUI } from "../src/rpc.ts";
|
||||
|
||||
function mockUi(responses: {
|
||||
select?: Array<string | undefined>;
|
||||
input?: Array<string | undefined>;
|
||||
}): RpcQuestionUI & {
|
||||
calls: Array<{ kind: "select" | "input"; title: string; options?: string[] }>;
|
||||
notifications: Array<{ message: string; type?: "info" | "warning" | "error" }>;
|
||||
} {
|
||||
const selectResponses = [...(responses.select ?? [])];
|
||||
const inputResponses = [...(responses.input ?? [])];
|
||||
const calls: Array<{ kind: "select" | "input"; title: string; options?: string[] }> = [];
|
||||
const notifications: Array<{ message: string; type?: "info" | "warning" | "error" }> = [];
|
||||
return {
|
||||
calls,
|
||||
notifications,
|
||||
async select(title, options) {
|
||||
calls.push({ kind: "select", title, options });
|
||||
assert.ok(selectResponses.length > 0, "unexpected select dialog");
|
||||
return selectResponses.shift();
|
||||
},
|
||||
async input(title) {
|
||||
calls.push({ kind: "input", title });
|
||||
assert.ok(inputResponses.length > 0, "unexpected input dialog");
|
||||
return inputResponses.shift();
|
||||
},
|
||||
notify(message, type) {
|
||||
notifications.push({ message, type });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("asks RPC questions sequentially and preserves stable option values", async () => {
|
||||
const questions = normalizeQuestions([
|
||||
{
|
||||
id: "language",
|
||||
label: "Language",
|
||||
prompt: "Which language?",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "ts", label: "TypeScript", description: "Strict typing" },
|
||||
{ value: "js", label: "JavaScript" },
|
||||
],
|
||||
},
|
||||
{ id: "notes", prompt: "Anything else?", type: "text", required: false },
|
||||
]);
|
||||
const ui = mockUi({ select: ["2. JavaScript"], input: [" "] });
|
||||
|
||||
const result = await runRpcQuestions(ui, questions);
|
||||
|
||||
assert.equal(result.cancelled, false);
|
||||
assert.deepEqual(result.answers, [
|
||||
{
|
||||
id: "language",
|
||||
question: "Which language?",
|
||||
type: "select",
|
||||
answer: "JavaScript",
|
||||
value: "js",
|
||||
label: "JavaScript",
|
||||
custom: false,
|
||||
skipped: false,
|
||||
},
|
||||
{
|
||||
id: "notes",
|
||||
question: "Anything else?",
|
||||
type: "text",
|
||||
answer: "",
|
||||
value: "",
|
||||
label: "Skipped",
|
||||
custom: false,
|
||||
skipped: true,
|
||||
},
|
||||
]);
|
||||
assert.match(ui.calls[0]?.title ?? "", /^\[1\/2\] Language:/);
|
||||
assert.ok(ui.calls[0]?.options?.includes("1. TypeScript — Strict typing"));
|
||||
});
|
||||
|
||||
test("supports custom RPC answers and repeats required blank input", async () => {
|
||||
const questions = normalizeQuestions([{
|
||||
id: "other",
|
||||
prompt: "Choose or write one",
|
||||
type: "select",
|
||||
options: [{ value: "known", label: "Known" }],
|
||||
}]);
|
||||
const ui = mockUi({ select: ["Type something."], input: [" ", " Custom value "] });
|
||||
|
||||
const result = await runRpcQuestions(ui, questions);
|
||||
|
||||
assert.equal(result.cancelled, false);
|
||||
assert.equal(result.answers[0]?.answer, "Custom value");
|
||||
assert.equal(result.answers[0]?.custom, true);
|
||||
assert.deepEqual(ui.notifications, [{ message: "An answer is required.", type: "warning" }]);
|
||||
});
|
||||
|
||||
test("returns cancelled with partial answers when an RPC dialog is dismissed", async () => {
|
||||
const questions = normalizeQuestions([
|
||||
{ id: "name", prompt: "Name?", type: "text" },
|
||||
{ id: "confirm", prompt: "Continue?", type: "select", options: [{ value: "yes", label: "Yes" }] },
|
||||
]);
|
||||
const ui = mockUi({ input: ["Ada"], select: [undefined] });
|
||||
|
||||
const result = await runRpcQuestions(ui, questions);
|
||||
|
||||
assert.equal(result.cancelled, true);
|
||||
assert.equal(result.answers.length, 1);
|
||||
assert.equal(result.answers[0]?.answer, "Ada");
|
||||
});
|
||||
|
||||
test("does not open RPC dialogs after abort", async () => {
|
||||
const questions = normalizeQuestions([{ id: "name", prompt: "Name?", type: "text" }]);
|
||||
const ui = mockUi({});
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
const result = await runRpcQuestions(ui, questions, controller.signal);
|
||||
|
||||
assert.equal(result.cancelled, true);
|
||||
assert.deepEqual(result.answers, []);
|
||||
assert.deepEqual(ui.calls, []);
|
||||
});
|
||||
Reference in New Issue
Block a user