630 lines
20 KiB
TypeScript
630 lines
20 KiB
TypeScript
import { assert, describe, it } from "@effect/vitest"
|
|
import { Data, Effect, Fiber, FileSystem, Layer, Match, Path, Queue, Redacted } from "effect"
|
|
import { TestConsole } from "effect/testing"
|
|
import { Prompt } from "effect/unstable/cli"
|
|
import * as MockTerminal from "./services/MockTerminal.ts"
|
|
|
|
const ConsoleLayer = TestConsole.layer
|
|
const FileSystemLayer = FileSystem.layerNoop({})
|
|
const PathLayer = Path.layer
|
|
const TerminalLayer = MockTerminal.layer
|
|
|
|
const TestLayer = Layer.mergeAll(
|
|
ConsoleLayer,
|
|
FileSystemLayer,
|
|
PathLayer,
|
|
TerminalLayer
|
|
)
|
|
const Action = Data.taggedEnum<Prompt.ActionDefinition>()
|
|
|
|
const escape = String.fromCharCode(27)
|
|
const bell = String.fromCharCode(7)
|
|
|
|
const stripAnsi = (text: string) => {
|
|
let result = ""
|
|
let skipping = false
|
|
for (let index = 0; index < text.length; index++) {
|
|
const char = text[index]
|
|
if (skipping) {
|
|
if ((char >= "A" && char <= "Z") || (char >= "a" && char <= "z")) {
|
|
skipping = false
|
|
}
|
|
continue
|
|
}
|
|
if (char === escape) {
|
|
skipping = true
|
|
continue
|
|
}
|
|
result += char
|
|
}
|
|
return result
|
|
}
|
|
|
|
const toFrames = (lines: ReadonlyArray<unknown>) =>
|
|
lines
|
|
.map((line) => stripAnsi(String(line)))
|
|
.filter((line) => line.split(bell).join("").trim().length > 0)
|
|
|
|
const toRawFrames = (lines: ReadonlyArray<unknown>) =>
|
|
lines
|
|
.map((line) => String(line))
|
|
.filter((line) => stripAnsi(line).split(bell).join("").trim().length > 0)
|
|
|
|
const findFrame = (frames: ReadonlyArray<string>, text: string) => frames.find((frame) => frame.includes(text))
|
|
|
|
describe("Prompt.integer", () => {
|
|
it.effect("submits the default value", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.integer({ message: "Count", default: 42 })
|
|
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, 42)
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("starts from the default value so it can be edited", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.integer({ message: "Count", default: 4 })
|
|
|
|
yield* MockTerminal.inputText("2")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, 42)
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("clears the default value on ctrl-u", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.integer({ message: "Count", default: 42 })
|
|
|
|
yield* MockTerminal.inputKey("u", { ctrl: true })
|
|
yield* MockTerminal.inputText("7")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, 7)
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
})
|
|
|
|
describe("Prompt.float", () => {
|
|
it.effect("renders appended input without literal parsed", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.float({ message: "Rate" })
|
|
|
|
yield* MockTerminal.inputText("12.5")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, 12.5)
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toFrames(output)
|
|
const rendered = frames.join("\n")
|
|
|
|
assert.isTrue(rendered.includes("12.5"))
|
|
assert.isFalse(rendered.includes("parsed"))
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("clears the current input on ctrl-u", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.float({ message: "Rate" })
|
|
|
|
yield* MockTerminal.inputText("12.5")
|
|
yield* MockTerminal.inputKey("u", { ctrl: true })
|
|
yield* MockTerminal.inputText("7.25")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, 7.25)
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
})
|
|
|
|
describe("Prompt.text", () => {
|
|
it.effect("starts from the default value so it can be edited", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.text({
|
|
message: "Name",
|
|
default: "Jane"
|
|
})
|
|
|
|
yield* MockTerminal.inputText(" Doe")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "Jane Doe")
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("clears the current input on ctrl-u", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.text({
|
|
message: "Name",
|
|
default: "Jane"
|
|
})
|
|
|
|
yield* MockTerminal.inputText(" Doe")
|
|
yield* MockTerminal.inputKey("u", { ctrl: true })
|
|
yield* MockTerminal.inputText("John")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "John")
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("moves the cursor to the beginning on ctrl-a", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.text({
|
|
message: "Name",
|
|
default: "Jane"
|
|
})
|
|
|
|
yield* MockTerminal.inputText(" Doe")
|
|
yield* MockTerminal.inputKey("a", { ctrl: true })
|
|
yield* MockTerminal.inputText("Dr. ")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "Dr. Jane Doe")
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("moves the cursor to the end on ctrl-e", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.text({
|
|
message: "Name"
|
|
})
|
|
|
|
yield* MockTerminal.inputText("Jane")
|
|
yield* MockTerminal.inputKey("left")
|
|
yield* MockTerminal.inputKey("left")
|
|
yield* MockTerminal.inputKey("e", { ctrl: true })
|
|
yield* MockTerminal.inputText(" Doe")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "Jane Doe")
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("does not insert characters for unsupported ctrl key combinations", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.text({
|
|
message: "Name"
|
|
})
|
|
|
|
yield* MockTerminal.inputText("Ja")
|
|
yield* MockTerminal.inputKey("l", { ctrl: true })
|
|
yield* MockTerminal.inputText("ne")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "Jane")
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("does not render or submit the cleared default value", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.text({
|
|
message: "Name",
|
|
default: "Jane"
|
|
})
|
|
|
|
yield* MockTerminal.inputKey("u", { ctrl: true })
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "")
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toFrames(output)
|
|
const lastFrame = frames.at(-1)
|
|
|
|
assert.isTrue(lastFrame !== undefined)
|
|
assert.isFalse(lastFrame?.includes("Jane"))
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
})
|
|
|
|
describe("Prompt.password", () => {
|
|
it.effect("starts from the default value so it can be edited", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.password({
|
|
message: "Password",
|
|
default: "secret"
|
|
})
|
|
|
|
yield* MockTerminal.inputText("123")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(Redacted.value(result), "secret123")
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("does not submit the cleared default value", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.password({
|
|
message: "Password",
|
|
default: "secret"
|
|
})
|
|
|
|
yield* MockTerminal.inputKey("u", { ctrl: true })
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(Redacted.value(result), "")
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
})
|
|
|
|
describe("Prompt.autoComplete", () => {
|
|
it.effect("filters choices as you type", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.autoComplete({
|
|
message: "Pick fruit",
|
|
choices: [
|
|
{ title: "Apple", value: "apple" },
|
|
{ title: "Banana", value: "banana" },
|
|
{ title: "Cherry", value: "cherry" }
|
|
]
|
|
})
|
|
|
|
yield* MockTerminal.inputText("ban")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "banana")
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toFrames(output)
|
|
const filteredFrame = findFrame(frames, "[filter: ban]")
|
|
|
|
assert.isTrue(filteredFrame !== undefined)
|
|
assert.isTrue(filteredFrame?.includes("Banana"))
|
|
assert.isFalse(filteredFrame?.includes("Apple"))
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("removes the last character on backspace", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.autoComplete({
|
|
message: "Pick item",
|
|
choices: [
|
|
{ title: "Alpha", value: "alpha" },
|
|
{ title: "Beta", value: "beta" },
|
|
{ title: "Delta", value: "delta" }
|
|
]
|
|
})
|
|
|
|
yield* MockTerminal.inputText("al")
|
|
yield* MockTerminal.inputKey("backspace")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "alpha")
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toFrames(output)
|
|
const narrowedFrame = findFrame(frames, "[filter: al]")
|
|
const expandedFrame = findFrame(frames, "[filter: a]")
|
|
|
|
assert.isTrue(narrowedFrame !== undefined)
|
|
assert.isTrue(expandedFrame !== undefined)
|
|
assert.isFalse(narrowedFrame?.includes("Beta"))
|
|
assert.isTrue(expandedFrame?.includes("Beta"))
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("clears the filter input on ctrl-u", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.autoComplete({
|
|
message: "Pick item",
|
|
choices: [
|
|
{ title: "Alpha", value: "alpha" },
|
|
{ title: "Beta", value: "beta" },
|
|
{ title: "Delta", value: "delta" }
|
|
]
|
|
})
|
|
|
|
yield* MockTerminal.inputText("al")
|
|
yield* MockTerminal.inputKey("u", { ctrl: true })
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "alpha")
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toFrames(output)
|
|
const narrowedFrame = findFrame(frames, "[filter: al]")
|
|
const clearedFrame = findFrame(frames, "[filter: type to filter]")
|
|
|
|
assert.isTrue(narrowedFrame !== undefined)
|
|
assert.isTrue(clearedFrame !== undefined)
|
|
assert.isFalse(narrowedFrame?.includes("Beta"))
|
|
assert.isTrue(clearedFrame?.includes("Beta"))
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("renders empty message and beeps on submit with no matches", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.autoComplete({
|
|
message: "Pick pet",
|
|
choices: [
|
|
{ title: "Cat", value: "cat" },
|
|
{ title: "Dog", value: "dog" }
|
|
]
|
|
})
|
|
|
|
yield* MockTerminal.inputText("zzz")
|
|
yield* MockTerminal.inputKey("enter")
|
|
for (let i = 0; i < 3; i++) {
|
|
yield* MockTerminal.inputKey("backspace")
|
|
}
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "cat")
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toFrames(output)
|
|
|
|
assert.isTrue(output.some((line) => String(line).includes("\x07")))
|
|
assert.isTrue(findFrame(frames, "No matches") !== undefined)
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("beeps when submitting a disabled choice", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.autoComplete({
|
|
message: "Pick mode",
|
|
choices: [
|
|
{ title: "Slow", value: "slow", disabled: true },
|
|
{ title: "Fast", value: "fast" }
|
|
]
|
|
})
|
|
|
|
yield* MockTerminal.inputKey("enter")
|
|
yield* MockTerminal.inputKey("down")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "fast")
|
|
|
|
const output = yield* TestConsole.logLines
|
|
assert.isTrue(output.some((line) => String(line).includes("\x07")))
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("renders empty message with no choices", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.autoComplete({
|
|
message: "Pick option",
|
|
choices: []
|
|
})
|
|
|
|
yield* MockTerminal.inputKey("c", { ctrl: true })
|
|
const exit = yield* Prompt.run(prompt).pipe(Effect.exit)
|
|
assert.isTrue(exit._tag === "Failure")
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toFrames(output)
|
|
|
|
assert.isTrue(findFrame(frames, "No matches") !== undefined)
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
})
|
|
|
|
describe("Prompt.file", () => {
|
|
const FilePromptLayer = Layer.mergeAll(
|
|
ConsoleLayer,
|
|
FileSystem.layerNoop({
|
|
exists: () => Effect.succeed(true),
|
|
readDirectory: (directory) =>
|
|
Effect.succeed(
|
|
directory === "/workspace"
|
|
? ["alpha.txt", "banana.txt", "basket.txt"]
|
|
: []
|
|
),
|
|
stat: (path) =>
|
|
Effect.succeed(
|
|
path.endsWith(".txt")
|
|
? ({ type: "File" } as any)
|
|
: ({ type: "Directory" } as any)
|
|
)
|
|
}),
|
|
PathLayer,
|
|
TerminalLayer
|
|
)
|
|
|
|
it.effect("starts from the default value so it can be submitted", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.file({
|
|
message: "Pick file",
|
|
default: "/workspace/banana.txt"
|
|
})
|
|
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "/workspace/banana.txt")
|
|
}).pipe(Effect.provide(FilePromptLayer)))
|
|
|
|
it.effect("filters files as you type", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.file({
|
|
message: "Pick file",
|
|
startingPath: "/workspace"
|
|
})
|
|
|
|
yield* MockTerminal.inputText("ban")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "/workspace/banana.txt")
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toFrames(output)
|
|
const filteredFrame = findFrame(frames, "[filter: ban]")
|
|
|
|
assert.isTrue(filteredFrame !== undefined)
|
|
assert.isTrue(filteredFrame?.includes("banana.txt"))
|
|
assert.isFalse(filteredFrame?.includes("alpha.txt"))
|
|
assert.isFalse(filteredFrame?.includes("basket.txt"))
|
|
}).pipe(Effect.provide(FilePromptLayer)))
|
|
|
|
it.effect("removes the last character on backspace", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.file({
|
|
message: "Pick file",
|
|
startingPath: "/workspace"
|
|
})
|
|
|
|
yield* MockTerminal.inputText("ban")
|
|
yield* MockTerminal.inputKey("backspace")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "/workspace/banana.txt")
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toFrames(output)
|
|
const narrowedFrame = findFrame(frames, "[filter: ban]")
|
|
const expandedFrame = findFrame(frames, "[filter: ba]")
|
|
|
|
assert.isTrue(narrowedFrame !== undefined)
|
|
assert.isTrue(expandedFrame !== undefined)
|
|
assert.isFalse(narrowedFrame?.includes("basket.txt"))
|
|
assert.isTrue(expandedFrame?.includes("basket.txt"))
|
|
}).pipe(Effect.provide(FilePromptLayer)))
|
|
})
|
|
|
|
describe("Prompt.multiSelect", () => {
|
|
it.effect("underlines the active label", () =>
|
|
Effect.gen(function*() {
|
|
const prompt = Prompt.multiSelect({
|
|
message: "Pick items",
|
|
choices: [
|
|
{ title: "Alpha", value: "alpha" },
|
|
{ title: "Beta", value: "beta" },
|
|
{ title: "Gamma", value: "gamma" }
|
|
]
|
|
})
|
|
|
|
const fiber = yield* Prompt.run(prompt).pipe(Effect.forkChild)
|
|
|
|
yield* Effect.yieldNow
|
|
yield* MockTerminal.inputKey("down")
|
|
yield* MockTerminal.inputKey("down")
|
|
yield* MockTerminal.inputKey("down")
|
|
yield* Effect.yieldNow
|
|
|
|
const output = yield* TestConsole.logLines
|
|
const frames = toRawFrames(output)
|
|
const highlightedFrame = [...frames].reverse().find((frame) => frame.includes("Beta"))
|
|
|
|
assert.isTrue(highlightedFrame !== undefined)
|
|
assert.isTrue(highlightedFrame?.includes(`${escape}[4m${escape}[96mBeta${escape}[0m`))
|
|
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Fiber.join(fiber)
|
|
assert.deepStrictEqual(result, [])
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
})
|
|
|
|
describe("Prompt.custom", () => {
|
|
it.effect("receive handles events from external dequeue", () =>
|
|
Effect.gen(function*() {
|
|
const eventQueue = yield* Queue.make<string>()
|
|
|
|
const prompt = Prompt.custom(
|
|
{ count: 0 },
|
|
Queue.asDequeue(eventQueue),
|
|
{
|
|
render: (state) => Effect.succeed(`Count: ${state.count}`),
|
|
process: (input, state) =>
|
|
Match.value(input).pipe(
|
|
Match.tag("Input", () => Effect.succeed(Action.Submit({ value: state.count }))),
|
|
Match.tag("Event", ({ value }) =>
|
|
Effect.succeed(
|
|
Action.NextFrame({ state: { count: state.count + (value === "tick" ? 1 : 0) } })
|
|
)),
|
|
Match.exhaustive
|
|
),
|
|
clear: () => Effect.succeed("")
|
|
}
|
|
)
|
|
|
|
const fiber = yield* Prompt.run(prompt).pipe(Effect.forkChild)
|
|
|
|
// Give the prompt loop time to start and block on the race
|
|
yield* Effect.yieldNow
|
|
|
|
// Push two events
|
|
yield* Queue.offer(eventQueue, "tick")
|
|
yield* Effect.yieldNow
|
|
yield* Queue.offer(eventQueue, "tock")
|
|
yield* Effect.yieldNow
|
|
yield* Queue.offer(eventQueue, "tick")
|
|
yield* Effect.yieldNow
|
|
|
|
// Submit via keypress
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Fiber.join(fiber)
|
|
assert.strictEqual(result, 2)
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("falls back to process when no events are pending", () =>
|
|
Effect.gen(function*() {
|
|
const eventQueue = yield* Queue.make<string>()
|
|
|
|
const prompt = Prompt.custom(
|
|
{ keys: 0 },
|
|
Queue.asDequeue(eventQueue),
|
|
{
|
|
render: (state) => Effect.succeed(`Keys: ${state.keys}`),
|
|
process: (input, state) =>
|
|
Match.value(input).pipe(
|
|
Match.tag("Input", () => {
|
|
const next = state.keys + 1
|
|
return next >= 3
|
|
? Effect.succeed(Action.Submit({ value: next }))
|
|
: Effect.succeed(Action.NextFrame({ state: { keys: next } }))
|
|
}),
|
|
Match.tag("Event", () => Effect.succeed(Action.NextFrame({ state }))),
|
|
Match.exhaustive
|
|
),
|
|
clear: () => Effect.succeed("")
|
|
}
|
|
)
|
|
|
|
yield* MockTerminal.inputKey("a")
|
|
yield* MockTerminal.inputKey("b")
|
|
yield* MockTerminal.inputKey("c")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, 3)
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
|
|
it.effect("Input variant exposes the input field with UserInput", () =>
|
|
Effect.gen(function*() {
|
|
const eventQueue = yield* Queue.make<string>()
|
|
|
|
const prompt = Prompt.custom(
|
|
{ captured: "" },
|
|
Queue.asDequeue(eventQueue),
|
|
{
|
|
render: (state) => Effect.succeed(`Captured: ${state.captured}`),
|
|
process: (input, state) =>
|
|
Match.value(input).pipe(
|
|
Match.tag("Input", ({ input: userInput }) => {
|
|
const key = userInput.key.name
|
|
return key === "enter"
|
|
? Effect.succeed(Action.Submit({ value: state.captured }))
|
|
: Effect.succeed(Action.NextFrame({ state: { captured: state.captured + key } }))
|
|
}),
|
|
Match.tag("Event", () => Effect.succeed(Action.NextFrame({ state }))),
|
|
Match.exhaustive
|
|
),
|
|
clear: () => Effect.succeed("")
|
|
}
|
|
)
|
|
|
|
yield* MockTerminal.inputKey("x")
|
|
yield* MockTerminal.inputKey("y")
|
|
yield* MockTerminal.inputKey("enter")
|
|
|
|
const result = yield* Prompt.run(prompt)
|
|
assert.strictEqual(result, "xy")
|
|
}).pipe(Effect.provide(TestLayer)))
|
|
})
|