@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.
- package/Cancelled.js +44 -0
- package/CliAudience.js +178 -0
- package/CliColor.js +13 -19
- package/CliEnv.js +89 -0
- package/CliExit.js +1 -1
- package/CliFailure.js +302 -0
- package/CliInteractive.js +71 -0
- package/CliLinks.js +154 -0
- package/CliLog.js +346 -0
- package/CliLogger.js +34 -33
- package/CliMessage.js +80 -0
- package/CliPrompt.js +104 -0
- package/CliRuntime.js +110 -54
- package/CliTest.js +16 -0
- package/CliTheme.js +141 -0
- package/ConfigIssueRenderer.js +14 -33
- package/Doc.js +536 -0
- package/Fmt.js +133 -0
- package/GithubAnnotation.js +40 -0
- package/Glyphs.js +83 -0
- package/NotInteractive.js +42 -0
- package/README.md +145 -131
- package/Render.js +255 -0
- package/SchemaIssueRenderer.js +7 -10
- package/Status.js +166 -0
- package/TestTerminal.js +80 -0
- package/Token.js +69 -0
- package/index.d.ts +3089 -169
- package/index.js +19 -1
- package/internal/ansi.js +230 -0
- package/internal/autoFormat.js +34 -0
- package/internal/canPrompt.js +15 -0
- package/internal/counts.js +84 -0
- package/internal/diagnostics.js +32 -0
- package/internal/displayWidth.js +35 -0
- package/internal/failureTarget.js +195 -0
- package/internal/fallbackAnswer.js +18 -0
- package/internal/fileSink.js +62 -0
- package/internal/format.js +62 -7
- package/internal/layout.js +250 -0
- package/internal/linkScheme.js +30 -0
- package/internal/linkTarget.js +50 -0
- package/internal/logSafety.js +46 -0
- package/internal/renderAnsi.js +52 -0
- package/internal/renderDoc.js +320 -0
- package/internal/renderGithubLog.js +46 -0
- package/internal/renderMarkdown.js +368 -0
- package/internal/renderPlain.js +50 -0
- package/internal/scanAudience.js +106 -0
- package/internal/splitFrame.js +56 -0
- package/internal/wizardGate.js +18 -0
- package/package.json +40 -5
- package/testing.d.ts +88 -2
- package/testing.js +2 -1
- package/ui/CliUi.js +432 -0
- package/ui/CliUiLive.js +446 -0
- package/ui/Confirm.js +245 -0
- package/ui/DocView.js +74 -0
- package/ui/KeyHelp.js +62 -0
- package/ui/KeyTable.js +199 -0
- package/ui/MultiSelect.js +260 -0
- package/ui/Select.js +230 -0
- package/ui/Tabs.js +202 -0
- package/ui/TextInput.js +290 -0
- package/ui/Toggle.js +32 -0
- package/ui/UiKey.js +44 -0
- package/ui/UiProvider.js +60 -0
- package/ui/UiStreams.js +18 -0
- package/ui/UiTheme.js +119 -0
- package/ui/Viewport.js +204 -0
- package/ui/internal/ErrorBoundary.js +30 -0
- package/ui/internal/Holder.js +74 -0
- package/ui/internal/ScreenContext.js +52 -0
- package/ui/internal/UiProviders.js +21 -0
- package/ui/internal/ink.js +122 -0
- package/ui/internal/inkChalk.js +58 -0
- package/ui/internal/inkConsole.js +146 -0
- package/ui/internal/lazyView.js +74 -0
- package/ui/internal/lineText.js +19 -0
- package/ui/internal/mountPermit.js +16 -0
- package/ui/internal/perfDrain.js +33 -0
- package/ui/internal/processStreams.js +19 -0
- package/ui/internal/renderOptions.js +13 -0
- package/ui/testing/CliUiTest.js +760 -0
- package/ui/testing/fakeStreams.js +79 -0
- package/ui/testing/terminalModel.js +59 -0
- package/ui-testing-serializer.d.ts +14 -0
- package/ui-testing-serializer.js +33 -0
- package/ui-testing.d.ts +527 -0
- package/ui-testing.js +3 -0
- package/ui.d.ts +1790 -0
- package/ui.js +17 -0
package/ui-testing.d.ts
ADDED
|
@@ -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