| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import * as Effect from "effect/Effect" |
| 3 | import { spawn, spawnSync } from "node:child_process" |
| 4 | import { fileURLToPath } from "node:url" |
| 5 | |
| 6 | const fixture = fileURLToPath(new URL("fixtures/deno-terminal.ts", import.meta.url)) |
| 7 | |
| 8 | const runFixture = (mode: string, input: string) => |
| 9 | spawnSync(Deno.execPath(), [fixture, mode], { |
| 10 | encoding: "utf8", |
| 11 | input, |
| 12 | timeout: 2_000 |
| 13 | }) |
| 14 | |
| 15 | const assertResult = (mode: string, input: string, expected: string) => { |
| 16 | const result = runFixture(mode, input) |
| 17 | assert.isUndefined(result.error) |
| 18 | assert.strictEqual(result.status, 0, result.stderr) |
| 19 | assert.isTrue(result.stderr.includes(`RESULT ${expected}`), result.stderr) |
| 20 | } |
| 21 | |
| 22 | const assertOpenResult = (mode: string, input: string, expected: string) => |
| 23 | Effect.callback<void>((resume) => { |
| 24 | const child = spawn(Deno.execPath(), [fixture, mode]) |
| 25 | let stderr = "" |
| 26 | child.stderr.setEncoding("utf8") |
| 27 | child.stderr.on("data", (data) => { |
| 28 | stderr += data |
| 29 | if (stderr.includes("RESULT ")) { |
| 30 | child.stdin.end() |
| 31 | } |
| 32 | }) |
| 33 | child.on("exit", (code) => { |
| 34 | resume( |
| 35 | code === 0 && stderr.includes(`RESULT ${expected}`) |
| 36 | ? Effect.void |
| 37 | : Effect.die(new Error(stderr)) |
| 38 | ) |
| 39 | }) |
| 40 | child.stdin.write(input) |
| 41 | return Effect.sync(() => child.kill()) |
| 42 | }) |
| 43 | |
| 44 | const assertInteropResult = Effect.callback<void>((resume) => { |
| 45 | const child = spawn(Deno.execPath(), [fixture, "interop"]) |
| 46 | let stderr = "" |
| 47 | let sentTerminalInput = false |
| 48 | child.stderr.setEncoding("utf8") |
| 49 | child.stderr.on("data", (data) => { |
| 50 | stderr += data |
| 51 | if (!sentTerminalInput && stderr.includes("READY")) { |
| 52 | sentTerminalInput = true |
| 53 | child.stdin.end("terminal\n") |
| 54 | } |
| 55 | }) |
| 56 | child.on("exit", (code) => { |
| 57 | resume( |
| 58 | code === 0 && stderr.includes("RESULT {\"stdio\":\"stdio\",\"terminal\":\"terminal\"}") |
| 59 | ? Effect.void |
| 60 | : Effect.die(new Error(stderr)) |
| 61 | ) |
| 62 | }) |
| 63 | child.stdin.write("stdio\n") |
| 64 | return Effect.sync(() => child.kill()) |
| 65 | }) |
| 66 | |
| 67 | // spawnSync blocks the Vitest worker; concurrent tests share a running timeout |
| 68 | // while waiting for other fixture processes to finish. |
| 69 | describe("DenoTerminal", { concurrent: false }, () => { |
| 70 | it("does not install a readline interface until the terminal is used", () => { |
| 71 | assertResult("unused", "", "{\"dataListeners\":0}") |
| 72 | }) |
| 73 | |
| 74 | it("fails a prompt with QuitError after piped input is exhausted", () => { |
| 75 | assertResult("prompts", "y\n", "{\"first\":true,\"second\":\"QuitError\"}") |
| 76 | }) |
| 77 | |
| 78 | it("delivers buffered keypresses before ending the input queue", () => { |
| 79 | assertResult("read-input", "yn", "{\"first\":\"y\",\"second\":\"n\",\"ended\":true}") |
| 80 | }) |
| 81 | |
| 82 | it("flushes an unterminated line before failing readLine with QuitError at EOF", () => { |
| 83 | assertResult("read-line", "last line", "{\"first\":\"last line\",\"second\":\"QuitError\"}") |
| 84 | }) |
| 85 | |
| 86 | it("preserves lines buffered between sequential readLine calls", () => { |
| 87 | assertResult("read-lines", "first\nsecond\n", "{\"first\":\"first\",\"second\":\"second\"}") |
| 88 | }) |
| 89 | |
| 90 | it("fails readLine with QuitError when stdin ended before initialization", () => { |
| 91 | assertResult("read-line-after-end", "", "\"QuitError\"") |
| 92 | }) |
| 93 | |
| 94 | it.effect("disposes readline after its idle TTL", () => |
| 95 | assertOpenResult( |
| 96 | "read-line-disposed", |
| 97 | "line\n", |
| 98 | "{\"line\":\"line\",\"duringTtl\":1,\"dataListeners\":0}" |
| 99 | )) |
| 100 | |
| 101 | it.effect("interoperates with DenoStdio on stdin", () => assertInteropResult) |
| 102 | }) |