@effected/cli 0.10.0 → 0.12.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.
Files changed (92) hide show
  1. package/Cancelled.js +44 -0
  2. package/CliAudience.js +178 -0
  3. package/CliColor.js +13 -19
  4. package/CliEnv.js +89 -0
  5. package/CliExit.js +1 -1
  6. package/CliFailure.js +302 -0
  7. package/CliInteractive.js +71 -0
  8. package/CliLinks.js +154 -0
  9. package/CliLog.js +346 -0
  10. package/CliLogger.js +34 -33
  11. package/CliMessage.js +80 -0
  12. package/CliPrompt.js +104 -0
  13. package/CliRuntime.js +110 -54
  14. package/CliTest.js +16 -0
  15. package/CliTheme.js +141 -0
  16. package/ConfigIssueRenderer.js +14 -33
  17. package/Doc.js +536 -0
  18. package/Fmt.js +133 -0
  19. package/GithubAnnotation.js +40 -0
  20. package/Glyphs.js +83 -0
  21. package/NotInteractive.js +42 -0
  22. package/README.md +145 -131
  23. package/Render.js +255 -0
  24. package/SchemaIssueRenderer.js +7 -10
  25. package/Status.js +166 -0
  26. package/TestTerminal.js +80 -0
  27. package/Token.js +69 -0
  28. package/index.d.ts +3089 -169
  29. package/index.js +19 -1
  30. package/internal/ansi.js +230 -0
  31. package/internal/autoFormat.js +34 -0
  32. package/internal/canPrompt.js +15 -0
  33. package/internal/counts.js +84 -0
  34. package/internal/diagnostics.js +32 -0
  35. package/internal/displayWidth.js +35 -0
  36. package/internal/failureTarget.js +195 -0
  37. package/internal/fallbackAnswer.js +18 -0
  38. package/internal/fileSink.js +62 -0
  39. package/internal/format.js +62 -7
  40. package/internal/layout.js +250 -0
  41. package/internal/linkScheme.js +30 -0
  42. package/internal/linkTarget.js +50 -0
  43. package/internal/logSafety.js +46 -0
  44. package/internal/renderAnsi.js +52 -0
  45. package/internal/renderDoc.js +320 -0
  46. package/internal/renderGithubLog.js +46 -0
  47. package/internal/renderMarkdown.js +368 -0
  48. package/internal/renderPlain.js +50 -0
  49. package/internal/scanAudience.js +106 -0
  50. package/internal/splitFrame.js +56 -0
  51. package/internal/wizardGate.js +18 -0
  52. package/package.json +40 -5
  53. package/testing.d.ts +88 -2
  54. package/testing.js +2 -1
  55. package/ui/CliUi.js +432 -0
  56. package/ui/CliUiLive.js +446 -0
  57. package/ui/Confirm.js +245 -0
  58. package/ui/DocView.js +74 -0
  59. package/ui/KeyHelp.js +62 -0
  60. package/ui/KeyTable.js +199 -0
  61. package/ui/MultiSelect.js +260 -0
  62. package/ui/Select.js +230 -0
  63. package/ui/Tabs.js +202 -0
  64. package/ui/TextInput.js +290 -0
  65. package/ui/Toggle.js +32 -0
  66. package/ui/UiKey.js +44 -0
  67. package/ui/UiProvider.js +60 -0
  68. package/ui/UiStreams.js +18 -0
  69. package/ui/UiTheme.js +119 -0
  70. package/ui/Viewport.js +204 -0
  71. package/ui/internal/ErrorBoundary.js +30 -0
  72. package/ui/internal/Holder.js +74 -0
  73. package/ui/internal/ScreenContext.js +52 -0
  74. package/ui/internal/UiProviders.js +21 -0
  75. package/ui/internal/ink.js +122 -0
  76. package/ui/internal/inkChalk.js +58 -0
  77. package/ui/internal/inkConsole.js +146 -0
  78. package/ui/internal/lazyView.js +74 -0
  79. package/ui/internal/lineText.js +19 -0
  80. package/ui/internal/mountPermit.js +16 -0
  81. package/ui/internal/perfDrain.js +33 -0
  82. package/ui/internal/processStreams.js +19 -0
  83. package/ui/internal/renderOptions.js +13 -0
  84. package/ui/testing/CliUiTest.js +760 -0
  85. package/ui/testing/fakeStreams.js +79 -0
  86. package/ui/testing/terminalModel.js +59 -0
  87. package/ui-testing-serializer.d.ts +14 -0
  88. package/ui-testing-serializer.js +33 -0
  89. package/ui-testing.d.ts +527 -0
  90. package/ui-testing.js +3 -0
  91. package/ui.d.ts +1790 -0
  92. package/ui.js +17 -0
@@ -0,0 +1,527 @@
1
+ import * as Cli from "@effected/cli";
2
+ import { KeyName, LiveHandle, LiveOptions, Screen } from "@effected/cli/ui";
3
+ import { ColorLevel } from "@effected/env";
4
+ import { Cause, Duration, Effect, Exit, Layer, Option, Scope } from "effect";
5
+ import { ReactElement } from "react";
6
+ //#region src/ui/testing/CliUiTest.d.ts
7
+ /**
8
+ * Options for {@link CliUiTest.render}, {@link CliUiTest.view} and {@link CliUiTest.session}, and the terminal's
9
+ * half of {@link CliUiTest.live}'s.
10
+ *
11
+ * @public
12
+ */
13
+ interface CliUiTestOptions {
14
+ /** The terminal width the screen lays out at; 80 by default. */
15
+ readonly columns?: number;
16
+ /** The terminal height; 24 by default. */
17
+ readonly rows?: number;
18
+ /**
19
+ * The colour level of both streams; `"truecolor"` by default, so frames carry the marker palette that
20
+ * {@link CliUiTest.styled} decodes to token markup. `"none"` gives escape-free frames.
21
+ *
22
+ * @remarks
23
+ * Only truecolor keeps tokens apart. Below it, chalk maps every marker `#0000NN` to the same colour
24
+ * (`ansi256(16)` at `"256"`, black at `"basic"`), so token identity is lost; do not snapshot tokens from such a run.
25
+ */
26
+ readonly color?: ColorLevel;
27
+ /** The glyph set; Unicode by default. */
28
+ readonly glyphs?: "unicode" | "ascii";
29
+ /**
30
+ * Whether the run is interactive; `true` by default. `false` makes a screen fail with `NotInteractive`, and makes a
31
+ * live view print each run's final frame as a string instead of mounting it.
32
+ */
33
+ readonly interactive?: boolean;
34
+ }
35
+ /**
36
+ * Options for {@link CliUiTest.session}: the terminal's, and the render path its screens mount on.
37
+ *
38
+ * @public
39
+ */
40
+ interface CliUiTestSessionOptions extends CliUiTestOptions {
41
+ /**
42
+ * The path the session's screens render on. `"debug"`, the default, is `render`'s: Ink writes every frame whole and
43
+ * unthrottled, which is what `next`, `frame` and `frames` read best, and where `clear` does nothing. `"production"`
44
+ * renders screens as a real terminal does, erase moves and all, so `CliUi.run`'s `clear: true` is observable in
45
+ * {@link CliUiTestSession.transcript}: the cleared frame is gone from it. A live view mounted in a session always
46
+ * renders on the production path, whichever this is.
47
+ */
48
+ readonly renderPath?: "debug" | "production";
49
+ }
50
+ /**
51
+ * A screen under test: drive it with keys and read its frames.
52
+ *
53
+ * @public
54
+ */
55
+ interface CliUiTestScreen {
56
+ /**
57
+ * Press named keys, one after another. Each waits until the screen draws its next frame or 50 ms pass with
58
+ * nothing written; `"escape"` first waits a real 30 ms, because Ink holds a lone ESC for 20 ms before reporting
59
+ * it. Pressing, typing or chunking on a screen that has ended is a defect, not a key for whatever screen is mounted
60
+ * now.
61
+ *
62
+ * @remarks
63
+ * A `{ char }` item sends its text as typed, as `chunk` does, settling like a key: `press({ char: "n" }, "enter")`.
64
+ * A bare string that is not a key name (`press("n")`) is a defect naming `type("n")` and `{ char: "n" }`, since a
65
+ * letter is not a `KeyName`.
66
+ */
67
+ readonly press: (...keys: ReadonlyArray<KeyName | {
68
+ readonly char: string;
69
+ }>) => Effect.Effect<void>;
70
+ /** Type text, one character at a time, each settling like a key. */
71
+ readonly type: (text: string) => Effect.Effect<void>;
72
+ /**
73
+ * Press named keys together: all their bytes in ONE stdin write, then one settle, as a fast typist, a held key or
74
+ * a batching terminal delivers them.
75
+ *
76
+ * @remarks
77
+ * Ink dispatches every key of one read before React re-renders, so a handler that steps from state its render
78
+ * captured repeats the first key's move; {@link CliUiTestScreen.press}, which writes and settles key by key, can
79
+ * never show that. Use `chunk` to test a key handler against it. An `"escape"` inside a chunk joins the bytes after
80
+ * it, as on a real terminal; only a trailing one waits out Ink's ESC hold. A `{ char }` writes its text as typed:
81
+ * Ink hands text read in one go to `useInput` as one string, `"yy"` or `"y\r"`, which `useKeys` splits into keys.
82
+ */
83
+ readonly chunk: (...keys: ReadonlyArray<KeyName | {
84
+ readonly char: string;
85
+ }>) => Effect.Effect<void>;
86
+ /** Resize the terminal and emit `resize`, as a real one does, settling like a key. */
87
+ readonly resize: (columns: number, rows: number) => Effect.Effect<void>;
88
+ /** The latest frame as token markup ({@link CliUiTest.styled}), each line's trailing spaces trimmed. */
89
+ readonly frame: Effect.Effect<string>;
90
+ /** The latest frame as Ink wrote it, escapes included. */
91
+ readonly rawFrame: Effect.Effect<string>;
92
+ /** The latest frame as plain text: no escapes and no markup, each line's trailing spaces trimmed. */
93
+ readonly plainFrame: Effect.Effect<string>;
94
+ /** Every frame of this screen so far, oldest first, as token markup like {@link CliUiTestScreen.frame}. */
95
+ readonly frames: Effect.Effect<ReadonlyArray<string>>;
96
+ }
97
+ /**
98
+ * A screen mounted by {@link CliUiTest.render}: a {@link CliUiTestScreen} that can also be swapped and awaited.
99
+ *
100
+ * @public
101
+ */
102
+ interface CliUiTestHandle<A> extends CliUiTestScreen {
103
+ /**
104
+ * Show another screen in place of the current one, settling like a key.
105
+ *
106
+ * @remarks
107
+ * `screen` is called with the same `ScreenControl` the first screen got, so `result` still resolves through it,
108
+ * and only the screen's own subtree is swapped: the error boundary, root keys and colour hold stay mounted. Ink's
109
+ * own `rerender` is never used. A rerender after the screen has ended is a defect, not a no-op, because a test
110
+ * that does it has lost track of the screen; so is one on a screen that has not mounted within 2 s, such as a
111
+ * handle queued behind another mounted screen.
112
+ */
113
+ readonly rerender: (screen: Screen<A>) => Effect.Effect<void>;
114
+ /** How the screen ended: its value, or `Cancelled` or `NotInteractive`. Waits for it to end. */
115
+ readonly result: Effect.Effect<A, Cli.Cancelled | Cli.NotInteractive>;
116
+ }
117
+ /**
118
+ * A display-only element mounted by {@link CliUiTest.view}: a {@link CliUiTestScreen} that can be swapped for another
119
+ * element, with no result to wait for.
120
+ *
121
+ * @public
122
+ */
123
+ interface CliUiTestView extends CliUiTestScreen {
124
+ /**
125
+ * Show another element in place of the current one, settling like a key.
126
+ *
127
+ * @remarks
128
+ * Only the element's subtree is swapped: the kit's providers stay mounted. A rerender after the view has ended
129
+ * (Esc or Ctrl-C ends it, as on every screen) is a defect, not a no-op.
130
+ */
131
+ readonly rerender: (element: ReactElement) => Effect.Effect<void>;
132
+ }
133
+ /**
134
+ * Options for {@link CliUiTestSession.next}.
135
+ *
136
+ * @public
137
+ */
138
+ interface CliUiTestNextOptions {
139
+ /**
140
+ * Text the screen must have shown, in plain text, before `next` returns it: in any frame since its mount, not
141
+ * necessarily the latest one. Read `plainFrame` to check what it shows now.
142
+ */
143
+ readonly contains?: string;
144
+ }
145
+ /**
146
+ * A terminal a whole program runs its screens on, from {@link CliUiTest.session}.
147
+ *
148
+ * @public
149
+ */
150
+ interface CliUiTestSession {
151
+ /**
152
+ * Provide it around the program: in-memory terminal streams (`UiStreams`), the marker-palette `CliTheme`,
153
+ * `CliInteractive` from the session's options, frame capture, and a `Console` whose writes the session keeps. Its
154
+ * type names `CliTheme` alone because that is the only one of them a program can require: `UiStreams`,
155
+ * `CliInteractive`, the capture and `Console` are `Context.Reference`s with defaults, which never appear in a
156
+ * requirement, so a type cannot say that the layer supplies them. Their defaults are the real process streams, a
157
+ * run that is not interactive, and the real console, so a session provided where it is shadowed fails quietly
158
+ * rather than by a type error. Hence the ordering below.
159
+ *
160
+ * @remarks
161
+ * **Where you provide it decides what it supplies.** Anything the program provides closer to the screens wins, so:
162
+ *
163
+ * - Provided **outside** a layer that supplies the same services, it is shadowed there. Under `CliRuntime.main`
164
+ * with `env`, `CliEnv.layer` (provided inside `main`) supplies the theme and decides interactivity, as it does for
165
+ * real, and the session keeps only the streams, the capture and the console. That is the intended way to test the
166
+ * production wiring.
167
+ * - To have the session's own theme and `interactive` option win, provide it **inside** any presentation layer of
168
+ * your own (closest to the program), or drop that layer from the test.
169
+ *
170
+ * The streams, the capture and the console are the session's wherever it is provided, since nothing else in the kit
171
+ * provides them.
172
+ */
173
+ readonly layer: Layer.Layer<Cli.CliTheme>;
174
+ /**
175
+ * Wait for the next screen to mount and draw (and, with `contains`, to show that text), and return it.
176
+ *
177
+ * @remarks
178
+ * Each call takes the next mount in order, counting every mount since the session began, so a screen that mounted
179
+ * before the call is not missed. It waits at most 2 s, then dies naming the screen's number, the text it waited
180
+ * for and how many screens had mounted. The returned screen's frames start at its own mount. A screen that crashed
181
+ * makes `next` die with the crash instead.
182
+ */
183
+ readonly next: (options?: CliUiTestNextOptions) => Effect.Effect<CliUiTestScreen>;
184
+ /**
185
+ * How many screens have mounted so far: every run that started mounting, one whose thunk threw before Ink drew
186
+ * included.
187
+ */
188
+ readonly mounts: Effect.Effect<number>;
189
+ /** What the program wrote to stdout through `Console` (`log`, `info`, `debug`), one line per call. */
190
+ readonly stdout: Effect.Effect<string>;
191
+ /** What the program wrote to stderr through `Console` (`error`, `warn`, `trace`), one line per call. */
192
+ readonly stderr: Effect.Effect<string>;
193
+ /**
194
+ * What the terminal shows now, scrollback included, as plain text: what the session's screens and live views wrote to
195
+ * the terminal streams (`UiStreams`), with Ink's erases and clears applied, each line's trailing spaces trimmed and
196
+ * blank lines left out, as {@link CliUiTestLive.transcript} reads it.
197
+ *
198
+ * @remarks
199
+ * It holds the lines a live view writes above its frame through `handle.logConsole` (stdout's and stderr's, merged in the order
200
+ * written, as one terminal shows them; `stdoutWritten` and `stderrWritten` keep them apart), each run's committed frame, a frame printed as a string, and, with
201
+ * `renderPath: "production"`, each screen's frames as they stay on the terminal, so a screen run with `clear: true`
202
+ * leaves nothing. On the default debug path each screen frame is written whole, one after another, so the
203
+ * transcript shows every frame a screen drew rather than what a terminal would keep. What the program writes through
204
+ * `Console` is not here: that is `stdout` and `stderr`.
205
+ */
206
+ readonly transcript: Effect.Effect<string>;
207
+ /** Every byte written to the terminal streams, both of them in the order written, escapes included. */
208
+ readonly written: Effect.Effect<string>;
209
+ /**
210
+ * Every byte written to the terminal's stdout (`UiStreams.stdout`) alone, escapes included: with
211
+ * {@link CliUiTestSession.stderrWritten}, what tells a line on the wrong stream from one on the right, which the
212
+ * merged `transcript` and `written` cannot.
213
+ */
214
+ readonly stdoutWritten: Effect.Effect<string>;
215
+ /** Every byte written to the terminal's stderr (`UiStreams.stderr`) alone, escapes included. */
216
+ readonly stderrWritten: Effect.Effect<string>;
217
+ /**
218
+ * What the terminal's stdout alone shows now, as plain text: {@link CliUiTestSession.stdoutWritten} run through the
219
+ * same pipeline as {@link CliUiTestSession.transcript} (Ink's erases and clears applied, escapes stripped, trailing
220
+ * spaces trimmed, blank lines left out).
221
+ *
222
+ * @remarks
223
+ * Assert text per stream here rather than on the raw bytes, which split a painted line wherever the paint breaks
224
+ * (a counter's `Dry run` from its `0/2 repos`). A frame cleared on the production render path is absent from it
225
+ * and present in `stdoutWritten`.
226
+ */
227
+ readonly stdoutTranscript: Effect.Effect<string>;
228
+ /** What the terminal's stderr alone shows now, as plain text: {@link CliUiTestSession.stderrWritten} read as `stdoutTranscript` reads stdout. */
229
+ readonly stderrTranscript: Effect.Effect<string>;
230
+ }
231
+ /**
232
+ * A live view mounted by {@link CliUiTest.live}: its event stream to publish to, and its output on the production
233
+ * render path.
234
+ *
235
+ * @public
236
+ */
237
+ interface CliUiTestLive<E, S> {
238
+ /**
239
+ * Publish one event to the view's stream, then wait as a key press does: until the view draws its next frame and a
240
+ * short quiet follows, or 50 ms pass with nothing drawn.
241
+ */
242
+ readonly publish: (event: E) => Effect.Effect<void>;
243
+ /** End the event stream, then wait for the view to finish (`handle.done`): it dies with what the view died of. */
244
+ readonly end: Effect.Effect<void>;
245
+ /**
246
+ * Move the `TestClock` on by `duration`, which fires the view's tick, then wait as `publish` does. The test runs
247
+ * under `it.effect`, whose clock is a `TestClock`; under `it.live` there is no `TestClock` to move and `advance`
248
+ * dies, while the view's own tick runs on real time.
249
+ */
250
+ readonly advance: (duration: Duration.Input) => Effect.Effect<void>;
251
+ /** Resize the terminal, then wait as `publish` does. */
252
+ readonly resize: (columns: number, rows: number) => Effect.Effect<void>;
253
+ /**
254
+ * The last frame drawn, as token markup (see {@link CliUiTest.styled}); empty before the first.
255
+ *
256
+ * @remarks
257
+ * Frames are best-effort: each is the write Ink makes after a render, so a render whose output is unchanged, or
258
+ * empty, adds none, and a frame printed as a string (not interactive, or a degraded run) is not one. `transcript`
259
+ * and `written` are the authority on what reached the terminal.
260
+ */
261
+ readonly frame: Effect.Effect<string>;
262
+ /** The last frame drawn, as written: with its escape sequences. */
263
+ readonly rawFrame: Effect.Effect<string>;
264
+ /** The last frame drawn, as plain text. */
265
+ readonly plainFrame: Effect.Effect<string>;
266
+ /**
267
+ * The frames drawn, across every run, as token markup, oldest first: best-effort, as `frame` says (an unchanged or
268
+ * empty render adds none, and a printed string is not a frame); `transcript` and `written` are the authority.
269
+ */
270
+ readonly frames: Effect.Effect<ReadonlyArray<string>>;
271
+ /**
272
+ * What the terminal shows now, scrollback included, as plain text: every committed frame, every line logged above a
273
+ * frame, every frame printed as a string, and the frame drawn now, with Ink's erases and clears applied (a
274
+ * scrollback wipe shows as the loss of what was above the frame), each line's trailing spaces trimmed and blank
275
+ * lines left out. For assertions about what stays on the terminal. With `interactive: false`, the frames printed
276
+ * as strings show here and in `written`, and nowhere else.
277
+ */
278
+ readonly transcript: Effect.Effect<string>;
279
+ /**
280
+ * Every byte written to the terminal, escapes included: what to assert a sequence on, such as no `ESC[3J`
281
+ * (a scrollback wipe) anywhere in a run.
282
+ */
283
+ readonly written: Effect.Effect<string>;
284
+ /** The view's own handle: its `state`, its `logConsole`, `done` and `close`. */
285
+ readonly handle: LiveHandle<S>;
286
+ }
287
+ /**
288
+ * Drive and read Ink screens in tests: mount a screen on in-memory streams, press keys, and read its frames as token
289
+ * markup.
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * import { assert, it } from "@effect/vitest"
294
+ * import { Select } from "@effected/cli/ui"
295
+ * import { CliUiTest } from "@effected/cli/ui/testing"
296
+ * import { Effect } from "effect"
297
+ *
298
+ * it.effect("chooses the second option", () =>
299
+ * Effect.gen(function* () {
300
+ * const choices = [
301
+ * { label: "a", value: "a" },
302
+ * { label: "b", value: "b" },
303
+ * ]
304
+ * const handle = yield* CliUiTest.render(Select.screen({ message: "Pick one", choices }))
305
+ * yield* handle.press("down", "enter")
306
+ * assert.strictEqual(yield* handle.result, "b")
307
+ * }).pipe(Effect.scoped),
308
+ * )
309
+ * ```
310
+ *
311
+ * @public
312
+ */
313
+ export declare class CliUiTest {
314
+ private constructor();
315
+ /**
316
+ * Mount `screen` on in-memory terminal streams for the enclosing scope.
317
+ *
318
+ * @remarks
319
+ * The screen runs under a fixed environment: a `TerminalEnv` test layer with the given columns and colour, a
320
+ * `CliTheme` whose every token paints in its own marker colour (so frames do not depend on the real palette), and
321
+ * `CliInteractive` set from `interactive`. Ink renders in debug mode, writing every frame in full; the frames
322
+ * are taken from those writes. Closing the scope unmounts the screen. The returned handle is ready once the first
323
+ * frame is drawn or the screen has ended.
324
+ *
325
+ * Waiting is on real time, through native timers a `TestClock` cannot hold, so the harness's own waits work under
326
+ * `it.effect` and `it.live` alike, and never sleep longer than 50 ms past the last write (30 ms first, for Esc). A
327
+ * screen test that itself sleeps, times out or retries on a schedule needs `it.live` (or real timers): under
328
+ * `it.effect` those run on the `TestClock`, which nothing advances while a screen waits on real time.
329
+ *
330
+ * The first frame is awaited for at most 2 s: a screen that draws nothing for longer gives a handle whose `frames`
331
+ * is `[]`. Screens run one at a time process-wide (`CliUi.run`), so a second handle opened while another is
332
+ * still mounted waits for that mount: it returns at the 2 s cap with no frames, and its keys queue in its input until
333
+ * it mounts.
334
+ *
335
+ * Debug frames bypass Ink's erase-and-redraw path, so a harness frame says nothing about what Ink writes between
336
+ * frames on a real terminal (a screen clear, for instance), nor about the final frame left in the scrollback once
337
+ * the screen ends (answered screens stay); a test of that needs the production render path.
338
+ * For the same reason `CliUi.run`'s `clear` has no visible effect on a harness frame, since Ink's `clear` does
339
+ * nothing in debug mode: test `clear` with `session({ renderPath: "production" })` and its `transcript`.
340
+ * Unmounting is the scope's close; to draw a different screen, render it in a new scope.
341
+ *
342
+ * A crash is never swallowed. A screen thunk that throws (a classic-JSX `React is not defined` included) or a
343
+ * component that throws ends the run with a defect: `result` dies with it, and so does the next frame read, key,
344
+ * resize or rerender, so a crashed screen is never read as one that drew nothing. As with `view`, after a crash
345
+ * the frames drawn before it cannot be read. A run refused as not interactive is `result`'s `NotInteractive`, and
346
+ * its frames read as `[]`.
347
+ *
348
+ * @param screen - the screen to mount
349
+ * @param options - the terminal's size, colour and glyphs, and whether the run is interactive
350
+ */
351
+ static readonly render: <A>(screen: Screen<A>, options?: CliUiTestOptions) => Effect.Effect<CliUiTestHandle<A>, never, Scope.Scope>;
352
+ /**
353
+ * Mount a display-only element on in-memory terminal streams for the enclosing scope: a status line, a live view,
354
+ * a component a consumer mounts in an Ink tree of its own.
355
+ *
356
+ * @remarks
357
+ * The same harness as {@link CliUiTest.render}, with the same options: the marker-palette theme, the fake streams
358
+ * and the debug frames, and the element is drawn inside the kit's providers, so `useTheme`, `useGlyphs`,
359
+ * `useTerminalSize` and `Styled` work in it. The handle reads frames and sends keys as a rendered screen's does, and
360
+ * `rerender` swaps in another element, but it has no `result`: a display-only element never ends on its own, so a
361
+ * `render` of one would leave `result` waiting forever. Closing the scope unmounts it.
362
+ *
363
+ * The kit's root keys stay bound, as on every screen: Esc or Ctrl-C ends the view, after which a key or a rerender
364
+ * is a defect.
365
+ *
366
+ * An element that crashes, or a run that is refused (`interactive: false` ends it with `NotInteractive`), is never
367
+ * swallowed: `view` dies with that error when it happens before the first frame, and otherwise the next frame read,
368
+ * key, resize or rerender does. After a crash every read dies with it, `frames` included, so the frames drawn before
369
+ * the crash cannot be read: the crash is the signal a test needs. A deliberate end (Esc or Ctrl-C) is not a crash:
370
+ * the frames stay readable, and only a key, resize or rerender after it dies, saying the screen has ended.
371
+ *
372
+ * @param element - the element to mount
373
+ * @param options - the terminal's size, colour and glyphs, and whether the run is interactive
374
+ */
375
+ static readonly view: (element: ReactElement, options?: CliUiTestOptions) => Effect.Effect<CliUiTestView, never, Scope.Scope>;
376
+ /**
377
+ * A terminal for a whole program that runs screens of its own (a wizard, a handler calling `CliUi.prompt` several
378
+ * times): provide its `layer` around the program, then take each screen as it mounts with `next`.
379
+ *
380
+ * @remarks
381
+ * The same environment and the same waiting as {@link CliUiTest.render}, with the same options. The session also
382
+ * keeps what the program writes through `Console`, its own output beside the screens, as `stdout` and `stderr`.
383
+ *
384
+ * Run the program forked (`Effect.forkScoped`) and drive it from the test: `next` returns each screen once it has
385
+ * mounted and drawn, `press` and `type` settle as they do on a rendered screen, and joining the program's fiber
386
+ * gives its exit. Screens still run one at a time, process-wide, so `next` sees them in the order they mount.
387
+ *
388
+ * By default, as with `render`, a screen run with `clear` leaves its frames unchanged here: Ink renders screens in
389
+ * debug mode, where its `clear` does nothing. Pass `renderPath: "production"` to render them as a terminal does, and
390
+ * read `transcript` to see what stays on it: a cleared screen leaves nothing there. A live view a handler mounts
391
+ * (`CliUi.live`) always renders on the production path, and the lines it writes above its frame through
392
+ * `handle.logConsole` are in `transcript` and `written`.
393
+ * The waits are real time: a session test that itself sleeps or times out needs `it.live`. To assert that no screen
394
+ * mounted (a non-interactive run, a flag that skips a prompt), check that `mounts` is `0` once the program has
395
+ * finished.
396
+ *
397
+ * A screen that crashes (its thunk or a component throws) is never swallowed: `next` dies with the crash when it has
398
+ * happened by then, whatever `contains` waited for, and otherwise the screen's next frame read, key or resize does.
399
+ * The program's own fiber dies with it too.
400
+ *
401
+ * Driving a whole `Command` handler: provide `layer`, a fresh `CliExit.layer` if the handler records a code, a
402
+ * `ConfigProvider` that sandboxes what the handler reads (`HOME`, the XDG directories), and the platform core's
403
+ * runner needs, then fork the program and take each screen with `next`:
404
+ *
405
+ * ```ts
406
+ * const session = yield* CliUiTest.session()
407
+ * const program = Effect.gen(function* () {
408
+ * yield* Command.runWith(root, { version })(["init"])
409
+ * return MutableRef.get((yield* CliExit).code)
410
+ * }).pipe(
411
+ * Effect.provide(session.layer),
412
+ * Effect.provide(CliExit.layer),
413
+ * Effect.provide(NodeServices.layer),
414
+ * Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({ HOME: "/sandbox/home" })),
415
+ * )
416
+ * const fiber = yield* Effect.forkScoped(program)
417
+ * yield* (yield* session.next({ contains: "Profile" })).press("enter")
418
+ * const code = yield* Fiber.join(fiber)
419
+ * ```
420
+ *
421
+ * To exercise `CliRuntime.main` as well (its failure report and exit code), run the program through it with the
422
+ * session's layer provided around it instead; `main` provides its own `CliExit`.
423
+ *
424
+ * @param options - the terminal's size, colour and glyphs, whether the run is interactive, and the screens' render path
425
+ */
426
+ static readonly session: (options?: CliUiTestSessionOptions) => Effect.Effect<CliUiTestSession, never, Scope.Scope>;
427
+ /**
428
+ * Mount a live view (`CliUi.live`) on a fresh in-memory terminal for the enclosing scope, with an event stream the
429
+ * test publishes to, and read what it draws on the production render path.
430
+ *
431
+ * @remarks
432
+ * The options are `CliUi.live`'s without `events`, which the harness supplies, and the terminal's own (`columns`,
433
+ * `rows`, `color`, `glyphs`, `interactive`). The view runs as it does for real: Ink is interactive and not in debug
434
+ * mode, so frames are what Ink actually writes, committed frames stay on the terminal, and `transcript` shows what is
435
+ * left there, scrollback included, through a small terminal model (it does not wrap a line wider than the terminal).
436
+ * stderr is the same stream as stdout, as on a terminal, so a line logged through `handle.logConsole` lands in the
437
+ * transcript too.
438
+ *
439
+ * Write live tests with `it.effect`: the view's tick runs on the `TestClock`, so `advance` (or `TestClock.adjust`)
440
+ * drives it frame by frame, and the frame index is `floor(now / tickMillis)` from the clock's epoch. The waits after
441
+ * `publish`, `advance` and `resize` are real time, which the `TestClock` does not hold. Without `@effect/vitest`'s
442
+ * `it.effect`, provide the clock yourself: `Effect.provide(test, TestClock.layer())` (from `effect/testing`).
443
+ *
444
+ * @example
445
+ * ```ts
446
+ * import { assert, it } from "@effect/vitest"
447
+ * import { CliUiTest } from "@effected/cli/ui/testing"
448
+ * import { Effect } from "effect"
449
+ * import { Text } from "ink"
450
+ * import { createElement } from "react"
451
+ *
452
+ * type Event = { readonly _tag: "RunStarted" } | { readonly _tag: "RunEnded" }
453
+ *
454
+ * it.effect("turns the spinner on the tick", () =>
455
+ * Effect.gen(function* () {
456
+ * const view = yield* CliUiTest.live({
457
+ * initial: 0,
458
+ * reduce: (count: number, _event: Event) => count + 1,
459
+ * render: (_count, frame) => createElement(Text, null, `frame ${frame}`),
460
+ * isStart: (event) => event._tag === "RunStarted",
461
+ * isTerminal: (event) => event._tag === "RunEnded",
462
+ * })
463
+ * yield* view.publish({ _tag: "RunStarted" })
464
+ * // The clock starts at 0 and the tick is 80 ms, so 160 ms on is frame 2.
465
+ * yield* view.advance("160 millis")
466
+ * assert.include(yield* view.plainFrame, "frame 2")
467
+ * }).pipe(Effect.scoped),
468
+ * )
469
+ * ```
470
+ *
471
+ * @param options - the live view's options without `events`, and the terminal's size, colour, glyphs and
472
+ * interactivity
473
+ */
474
+ static readonly live: <E, S>(options: Omit<LiveOptions<E, S>, "events"> & CliUiTestOptions) => Effect.Effect<CliUiTestLive<E, S>, never, Scope.Scope>;
475
+ /**
476
+ * Why a screen or a program was cancelled, read from its `Exit` or `Cause`: `"escape"` or `"interrupt"` when it
477
+ * carries a `Cancelled`, as a typed failure or as a defect, else `None`.
478
+ *
479
+ * @remarks
480
+ * Pure. A screen's `result` fails with `Cancelled` in the typed channel; a prompt cancelled where the program
481
+ * declares no such error carries it as a defect. Either way a test asks this instead of walking `cause.reasons`:
482
+ *
483
+ * ```ts
484
+ * const exit = yield* Fiber.await(program)
485
+ * assert.deepStrictEqual(CliUiTest.cancelReason(exit), Option.some("escape"))
486
+ * ```
487
+ *
488
+ * @param exitOrCause - the exit of a screen's `result` or of a program, or a bare cause
489
+ */
490
+ static readonly cancelReason: (exitOrCause: Exit.Exit<unknown, unknown> | Cause.Cause<unknown>) => Option.Option<"escape" | "interrupt">;
491
+ /**
492
+ * Decode ANSI back to markup: a marker colour to its token (`[success]…[/success]`), any other foreground to
493
+ * `[fg:red]` or `[fg:#ff0000]` (backgrounds likewise as `[bg:…]`), bold to `[b]`, dim to `[dim]`, italic, underline,
494
+ * inverse and strikethrough to `[i]`, `[u]`, `[inverse]` and `[s]`. A reset closes everything open; every other
495
+ * escape (cursor, erase, hyperlinks) is dropped.
496
+ *
497
+ * @param ansi - text with escapes
498
+ */
499
+ static readonly styled: (ansi: string) => string;
500
+ /**
501
+ * A Vitest snapshot serializer. It claims a string carrying escapes, a token tag opened and closed, or a colour
502
+ * tag (a raw or styled frame, or a `Render.ansi` string), but not a log line's lone `[info]` prefix, nor one whose
503
+ * only brackets are style tags like `[b]`, which unrelated data uses too. It prints the string as token markup with
504
+ * each line's trailing spaces trimmed, so a snapshot reads without escapes and does not churn with the palette.
505
+ *
506
+ * @remarks
507
+ * Register it in either of two ways. Through the Vitest config, by the module whose default export it is, which
508
+ * needs no code in a test file:
509
+ *
510
+ * ```ts
511
+ * // vitest.config.ts
512
+ * export default defineConfig({ test: { snapshotSerializers: ["@effected/cli/ui/testing/serializer"] } })
513
+ * ```
514
+ *
515
+ * Or in a test file, with `expect.addSnapshotSerializer(CliUiTest.serializer)`.
516
+ *
517
+ * Snapshots are the one place a test needs `expect`: `assert` has no snapshot form, so a suite that asserts with
518
+ * `assert.*` everywhere else still writes `expect(frame).toMatchInlineSnapshot(...)` for a snapshot.
519
+ */
520
+ static readonly serializer: {
521
+ readonly test: (value: unknown) => boolean;
522
+ readonly serialize: (value: unknown) => string;
523
+ };
524
+ }
525
+ //#endregion
526
+ export type { CliUiTestHandle, CliUiTestLive, CliUiTestNextOptions, CliUiTestOptions, CliUiTestScreen, CliUiTestSession, CliUiTestSessionOptions, CliUiTestView };
527
+ //# sourceMappingURL=ui-testing.d.ts.map
package/ui-testing.js ADDED
@@ -0,0 +1,3 @@
1
+ import { CliUiTest } from "./ui/testing/CliUiTest.js";
2
+
3
+ export { CliUiTest };