@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.11
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/README.md +499 -313
- package/THIRD_PARTY_NOTICES.md +98 -23
- package/dist/Text-DV9CuzAT.d.ts +452 -0
- package/dist/ansi.d.ts +217 -0
- package/dist/ansi.js +87 -0
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BAC9-TZX.js +592 -0
- package/dist/color-policy-CCxuHIdD.d.ts +22 -0
- package/dist/color-profile-CyeHnG1T.d.ts +97 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-B3dL4Q11.js +374 -0
- package/dist/detect-Db6GbKOm.d.ts +195 -0
- package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-D48vQhhe.d.ts +21 -0
- package/dist/index-DDVME65c.d.ts +919 -0
- package/dist/index.d.ts +1310 -0
- package/dist/index.js +3211 -0
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +227 -0
- package/dist/jsx-runtime-XtwIVFsg.js +255 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +8 -0
- package/dist/osc-BFKKSqpg.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-Cx-zC_sX.d.ts +81 -0
- package/dist/query-BNc2B8GD.d.ts +152 -0
- package/dist/react-CTZ_8dwh.js +1197 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +49 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +711 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-CgC2WlVM.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-DDQ5V300.js +723 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-C1P5fOUi.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +121 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-B5npZrJU.js +18736 -0
- package/dist/yoga-5jKhYCJC.js +3465 -0
- package/dist/yoga.d.ts +2 -0
- package/dist/yoga.js +2 -0
- package/package.json +89 -29
- package/src/ansi/chalk.ts +138 -0
- package/src/ansi/cursor.ts +46 -0
- package/src/ansi/east-asian-width.ts +259 -0
- package/src/ansi/escapes.ts +120 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +27 -0
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/sgr.ts +234 -0
- package/src/ansi/slice.ts +43 -0
- package/src/ansi/string-width.ts +121 -0
- package/src/ansi/strip.ts +13 -0
- package/src/ansi/tokenize.ts +380 -0
- package/src/ansi/truncate.ts +196 -0
- package/src/ansi/wrap.ts +765 -0
- package/src/ansi-tokenizer.ts +510 -0
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +608 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +679 -0
- package/src/capabilities/store.ts +394 -0
- package/src/code-excerpt.ts +39 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AccessibilityContext.ts +5 -0
- package/src/components/AnimationContext.ts +24 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +878 -0
- package/src/components/AppContext.ts +111 -0
- package/src/components/BackgroundContext.ts +7 -0
- package/src/components/Box.tsx +100 -0
- package/src/components/CursorContext.ts +19 -0
- package/src/components/ErrorBoundary.tsx +39 -0
- package/src/components/ErrorOverview.tsx +135 -0
- package/src/components/FocusContext.ts +30 -0
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/Newline.tsx +16 -0
- package/src/components/Spacer.tsx +11 -0
- package/src/components/Static.tsx +60 -0
- package/src/components/StderrContext.ts +24 -0
- package/src/components/StdinContext.ts +48 -0
- package/src/components/StdoutContext.ts +26 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +122 -0
- package/src/components/Transform.tsx +38 -0
- package/src/components/VirtualList.tsx +128 -0
- package/src/cursor-position.ts +103 -0
- package/src/devtools.ts +103 -0
- package/src/dom.ts +301 -0
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +11 -0
- package/src/global.d.ts +38 -0
- package/src/glyphs.ts +99 -0
- package/src/hooks/use-animation.ts +142 -0
- package/src/hooks/use-app.ts +8 -0
- package/src/hooks/use-box-metrics.ts +134 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +33 -0
- package/src/hooks/use-focus-manager.ts +62 -0
- package/src/hooks/use-focus.ts +82 -0
- package/src/hooks/use-input.ts +267 -0
- package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
- package/src/hooks/use-paste.ts +78 -0
- package/src/hooks/use-stderr.ts +8 -0
- package/src/hooks/use-stdin.ts +10 -0
- package/src/hooks/use-stdout.ts +8 -0
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/hooks/use-virtual-scroll.ts +84 -0
- package/src/hooks/use-window-size.ts +37 -0
- package/src/index.ts +96 -0
- package/src/ink.tsx +1452 -0
- package/src/input-parser.ts +303 -0
- package/src/instances.ts +9 -0
- package/src/jsx-dev-runtime.ts +3 -0
- package/src/jsx-runtime.ts +6 -0
- package/src/kitty-keyboard.ts +185 -0
- package/src/measure-element.ts +62 -0
- package/src/measure-text.ts +31 -0
- package/src/paint-tree.ts +220 -0
- package/src/parse-keypress.ts +515 -0
- package/src/parse-stack-line.ts +138 -0
- package/src/patch-console.ts +106 -0
- package/src/quick-lru.ts +85 -0
- package/src/react.ts +53 -0
- package/src/reconciler.ts +476 -0
- package/src/render-background.ts +59 -0
- package/src/render-border.ts +167 -0
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +146 -0
- package/src/render.ts +284 -0
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/sanitize-ansi.ts +33 -0
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +305 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/signal-exit.ts +106 -0
- package/src/squash-text-nodes.ts +37 -0
- package/src/stream.ts +28 -0
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +753 -0
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +92 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +242 -0
- package/src/terminal/session.ts +408 -0
- package/src/terminal-size.ts +58 -0
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +361 -0
- package/src/testing/vitest.ts +157 -0
- package/src/throttle.ts +73 -0
- package/src/transform-adapter.ts +14 -0
- package/src/types.ts +10 -0
- package/src/virtual-scroll.ts +133 -0
- package/src/wrap-text.ts +54 -0
- package/src/yoga/config.ts +57 -0
- package/src/yoga/core/absoluteLayout.ts +626 -0
- package/src/yoga/core/baseline.ts +66 -0
- package/src/yoga/core/cache.ts +136 -0
- package/src/yoga/core/calculateLayout.ts +2926 -0
- package/src/yoga/core/config.ts +104 -0
- package/src/yoga/core/flexLine.ts +177 -0
- package/src/yoga/core/helpers.ts +293 -0
- package/src/yoga/core/layoutResults.ts +167 -0
- package/src/yoga/core/node.ts +611 -0
- package/src/yoga/core/numeric.ts +44 -0
- package/src/yoga/core/pixelGrid.ts +151 -0
- package/src/yoga/core/style.ts +887 -0
- package/src/yoga/core/types.ts +224 -0
- package/src/yoga/generated/YGEnums.ts +263 -0
- package/src/yoga/index.ts +19 -0
- package/src/yoga/node.ts +1140 -0
- package/dist/index.d.mts +0 -2379
- package/dist/index.mjs +0 -10072
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// End-to-end terminal testing: launch an app in a real PTY wired to a real
|
|
2
|
+
// terminal emulator engine (Ghostty's VT core or xterm.js), then interact
|
|
3
|
+
// and assert Playwright-style — locators, auto-waiting, key presses.
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createEmulator,
|
|
8
|
+
type Emulator,
|
|
9
|
+
type EmulatorCell,
|
|
10
|
+
type EmulatorName,
|
|
11
|
+
} from "#/testing/emulators.ts";
|
|
12
|
+
import { keyToSequence } from "#/testing/keys.ts";
|
|
13
|
+
import { connectLiveClient, currentTestName } from "#/testing/live.ts";
|
|
14
|
+
|
|
15
|
+
export type LaunchOptions = {
|
|
16
|
+
/**
|
|
17
|
+
Which emulator engine answers the app's terminal queries and interprets
|
|
18
|
+
its output.
|
|
19
|
+
|
|
20
|
+
@default "ghostty"
|
|
21
|
+
*/
|
|
22
|
+
emulator?: EmulatorName;
|
|
23
|
+
|
|
24
|
+
columns?: number;
|
|
25
|
+
rows?: number;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
The emulated OS color scheme at startup (Ghostty engine only).
|
|
29
|
+
|
|
30
|
+
@default "dark"
|
|
31
|
+
*/
|
|
32
|
+
colorScheme?: "dark" | "light";
|
|
33
|
+
|
|
34
|
+
cwd?: string;
|
|
35
|
+
env?: Record<string, string>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type WaitForOptions = {
|
|
39
|
+
/**
|
|
40
|
+
@default 5000
|
|
41
|
+
*/
|
|
42
|
+
timeout?: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
Wait for the text to appear (`"visible"`, default) or disappear
|
|
46
|
+
(`"hidden"`).
|
|
47
|
+
*/
|
|
48
|
+
state?: "visible" | "hidden";
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type TerminalLocator = {
|
|
52
|
+
/**
|
|
53
|
+
Whether the text is on the visible screen right now.
|
|
54
|
+
*/
|
|
55
|
+
isVisible: () => boolean;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
The full screen line containing the match, if any.
|
|
59
|
+
*/
|
|
60
|
+
line: () => string | undefined;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
Waits (polling) until the text appears — or disappears with
|
|
64
|
+
`state: "hidden"`. Throws with a screen dump on timeout.
|
|
65
|
+
*/
|
|
66
|
+
waitFor: (options?: WaitForOptions) => Promise<void>;
|
|
67
|
+
|
|
68
|
+
description: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type TerminalApp = {
|
|
72
|
+
readonly emulator: EmulatorName;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
The visible screen as right-trimmed lines.
|
|
76
|
+
*/
|
|
77
|
+
lines: () => string[];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
The visible screen as one string, trailing blank lines removed.
|
|
81
|
+
*/
|
|
82
|
+
text: () => string;
|
|
83
|
+
|
|
84
|
+
getByText: (text: string | RegExp) => TerminalLocator;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
Polls until the predicate holds. Throws with a screen dump on timeout.
|
|
88
|
+
*/
|
|
89
|
+
waitFor: (predicate: () => boolean, options?: { timeout?: number }) => Promise<void>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
Presses named keys in order: `press("Tab", "Enter")`, `press("Ctrl+C")`.
|
|
93
|
+
Keys are spaced out slightly, like real typing, so each key's effect
|
|
94
|
+
(focus moves, state updates) lands before the next one.
|
|
95
|
+
*/
|
|
96
|
+
press: (...keys: string[]) => Promise<void>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
Types text verbatim.
|
|
100
|
+
*/
|
|
101
|
+
type: (text: string) => void;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
Resizes the PTY and the emulator. Real terminal apps never do both at
|
|
105
|
+
once: `emulatorLag` (ms) delays the emulator's rewrap behind the PTY
|
|
106
|
+
resize, simulating an app that reports the new size to the process
|
|
107
|
+
before its screen has been rewrapped.
|
|
108
|
+
*/
|
|
109
|
+
resize: (columns: number, rows: number, options?: { readonly emulatorLag?: number }) => void;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
Flips the emulated OS color scheme (Ghostty engine only) — the app
|
|
113
|
+
receives a real color scheme report if it enabled them.
|
|
114
|
+
*/
|
|
115
|
+
setColorScheme: (scheme: "dark" | "light") => void;
|
|
116
|
+
|
|
117
|
+
cellAt: (x: number, y: number) => EmulatorCell | undefined;
|
|
118
|
+
|
|
119
|
+
cursor: () => { x: number; y: number };
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
Everything the app has written, unprocessed.
|
|
123
|
+
*/
|
|
124
|
+
output: () => string;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
The exit code once the app has exited.
|
|
128
|
+
*/
|
|
129
|
+
exitCode: () => number | undefined;
|
|
130
|
+
|
|
131
|
+
waitForExit: (options?: { timeout?: number }) => Promise<number>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
Kills the app (if still running) and disposes the emulator.
|
|
135
|
+
*/
|
|
136
|
+
close: () => void;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const defaultTimeout = 5000;
|
|
140
|
+
const pollInterval = 25;
|
|
141
|
+
|
|
142
|
+
const screenDump = (emulator: Emulator): string =>
|
|
143
|
+
["", "── screen ──", ...emulator.lines().map((line) => `│${line}`), "────────────"].join("\n");
|
|
144
|
+
|
|
145
|
+
const poll = async (
|
|
146
|
+
check: () => boolean,
|
|
147
|
+
timeout: number,
|
|
148
|
+
onTimeout: () => Error,
|
|
149
|
+
): Promise<void> => {
|
|
150
|
+
const deadline = Date.now() + timeout;
|
|
151
|
+
for (;;) {
|
|
152
|
+
if (check()) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (Date.now() > deadline) {
|
|
157
|
+
throw onTimeout();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
Launches a command inside a PTY attached to a real terminal emulator engine.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const app = await launchTerminal(["node", "--import=tsx", "examples/router/index.ts"]);
|
|
169
|
+
await app.getByText("Home").waitFor();
|
|
170
|
+
app.press("Tab", "Enter");
|
|
171
|
+
await app.getByText("Users").waitFor();
|
|
172
|
+
app.close();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A string command runs through `sh -c`.
|
|
176
|
+
*/
|
|
177
|
+
export const launchTerminal = async (
|
|
178
|
+
command: string | string[],
|
|
179
|
+
{
|
|
180
|
+
emulator: emulatorName = "ghostty",
|
|
181
|
+
columns = 100,
|
|
182
|
+
rows = 30,
|
|
183
|
+
colorScheme = "dark",
|
|
184
|
+
cwd = process.cwd(),
|
|
185
|
+
env = {},
|
|
186
|
+
}: LaunchOptions = {},
|
|
187
|
+
): Promise<TerminalApp> => {
|
|
188
|
+
const zigpty = await import("zigpty").catch((error) => {
|
|
189
|
+
throw new Error(
|
|
190
|
+
'The "zigpty" package is required to launch terminal apps. Install it as a dev dependency: pnpm add -D zigpty',
|
|
191
|
+
{ cause: error },
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const emulator = await createEmulator(emulatorName, { columns, rows, colorScheme });
|
|
196
|
+
|
|
197
|
+
const argv = Array.isArray(command) ? command : ["/bin/sh", "-c", command];
|
|
198
|
+
// Tests should behave like a user's interactive truecolor session, even on
|
|
199
|
+
// CI — both emulator engines understand 24-bit SGR.
|
|
200
|
+
const childEnv: Record<string, string> = {
|
|
201
|
+
...(process.env as Record<string, string>),
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
203
|
+
COLORTERM: "truecolor",
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
205
|
+
NODE_NO_WARNINGS: "1",
|
|
206
|
+
...env,
|
|
207
|
+
};
|
|
208
|
+
delete childEnv["CI"];
|
|
209
|
+
const child = zigpty.spawn(argv[0], argv.slice(1), {
|
|
210
|
+
name: "xterm-256color",
|
|
211
|
+
cols: columns,
|
|
212
|
+
rows,
|
|
213
|
+
cwd,
|
|
214
|
+
env: childEnv,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
let output = "";
|
|
218
|
+
let exitCode: number | undefined;
|
|
219
|
+
let closed = false;
|
|
220
|
+
|
|
221
|
+
// Mirror the session to the live hub (explorer /live page) when one is
|
|
222
|
+
// configured, so the run can be watched in the browser as it happens.
|
|
223
|
+
const live = connectLiveClient();
|
|
224
|
+
const liveId = randomUUID();
|
|
225
|
+
if (live) {
|
|
226
|
+
const commandTitle = Array.isArray(command) ? command.join(" ") : command;
|
|
227
|
+
live.send({ type: "start", id: liveId, title: commandTitle, columns, rows });
|
|
228
|
+
// Inside Vitest, relabel with the test that launched this session.
|
|
229
|
+
void currentTestName().then((name) => {
|
|
230
|
+
if (name) {
|
|
231
|
+
live.send({ type: "title", id: liveId, title: name });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
child.onData((data: string | Buffer) => {
|
|
237
|
+
if (closed) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const chunk = typeof data === "string" ? data : data.toString("latin1");
|
|
242
|
+
output += chunk;
|
|
243
|
+
emulator.feed(chunk);
|
|
244
|
+
live?.send({ type: "data", id: liveId, data: chunk });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
emulator.onResponse((data) => {
|
|
248
|
+
if (exitCode === undefined) {
|
|
249
|
+
child.write(data);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
child.onExit(({ exitCode: code }: { exitCode: number }) => {
|
|
254
|
+
exitCode = code;
|
|
255
|
+
live?.send({ type: "end", id: liveId, code });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const lines = (): string[] => emulator.lines();
|
|
259
|
+
|
|
260
|
+
const getByText = (matcher: string | RegExp): TerminalLocator => {
|
|
261
|
+
const description = typeof matcher === "string" ? JSON.stringify(matcher) : String(matcher);
|
|
262
|
+
const findLine = (): string | undefined =>
|
|
263
|
+
lines().find((line) =>
|
|
264
|
+
typeof matcher === "string" ? line.includes(matcher) : matcher.test(line),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
description,
|
|
269
|
+
isVisible: () => findLine() !== undefined,
|
|
270
|
+
line: findLine,
|
|
271
|
+
waitFor: async ({ timeout = defaultTimeout, state = "visible" }: WaitForOptions = {}) => {
|
|
272
|
+
const check =
|
|
273
|
+
state === "visible" ? () => findLine() !== undefined : () => findLine() === undefined;
|
|
274
|
+
await poll(check, timeout, () =>
|
|
275
|
+
state === "visible"
|
|
276
|
+
? new Error(`Timed out waiting for ${description} to appear.${screenDump(emulator)}`)
|
|
277
|
+
: new Error(
|
|
278
|
+
`Timed out waiting for ${description} to disappear.${screenDump(emulator)}`,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
emulator: emulatorName,
|
|
287
|
+
lines,
|
|
288
|
+
text: () => lines().join("\n").replace(/\n+$/, ""),
|
|
289
|
+
getByText,
|
|
290
|
+
waitFor: async (predicate, { timeout = defaultTimeout } = {}) => {
|
|
291
|
+
await poll(
|
|
292
|
+
predicate,
|
|
293
|
+
timeout,
|
|
294
|
+
() => new Error(`Timed out waiting for condition.${screenDump(emulator)}`),
|
|
295
|
+
);
|
|
296
|
+
},
|
|
297
|
+
press: async (...keys) => {
|
|
298
|
+
for (const key of keys) {
|
|
299
|
+
const outputBefore = output.length;
|
|
300
|
+
child.write(keyToSequence(key));
|
|
301
|
+
// Wait for the key's effect to render before the next key — a fixed
|
|
302
|
+
// gap flakes under load when a re-render (e.g. a focus move) hasn't
|
|
303
|
+
// committed yet. Keys without visible effect give up after the cap.
|
|
304
|
+
const deadline = Date.now() + 500;
|
|
305
|
+
while (Date.now() < deadline && output.length === outputBefore) {
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
type: (text) => {
|
|
313
|
+
child.write(text);
|
|
314
|
+
},
|
|
315
|
+
resize: (nextColumns, nextRows, options = {}) => {
|
|
316
|
+
child.resize(nextColumns, nextRows);
|
|
317
|
+
if (options.emulatorLag === undefined) {
|
|
318
|
+
emulator.resize(nextColumns, nextRows);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
if (!closed) emulator.resize(nextColumns, nextRows);
|
|
323
|
+
}, options.emulatorLag);
|
|
324
|
+
},
|
|
325
|
+
setColorScheme: (scheme) => {
|
|
326
|
+
if (!emulator.setColorScheme) {
|
|
327
|
+
throw new Error(`The ${emulatorName} emulator cannot change color scheme.`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
emulator.setColorScheme(scheme);
|
|
331
|
+
},
|
|
332
|
+
cellAt: emulator.cellAt,
|
|
333
|
+
cursor: emulator.cursor,
|
|
334
|
+
output: () => output,
|
|
335
|
+
exitCode: () => exitCode,
|
|
336
|
+
waitForExit: async ({ timeout = defaultTimeout } = {}) => {
|
|
337
|
+
await poll(
|
|
338
|
+
() => exitCode !== undefined,
|
|
339
|
+
timeout,
|
|
340
|
+
() => new Error(`Timed out waiting for the app to exit.${screenDump(emulator)}`),
|
|
341
|
+
);
|
|
342
|
+
return exitCode!;
|
|
343
|
+
},
|
|
344
|
+
close: () => {
|
|
345
|
+
if (closed) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
closed = true;
|
|
350
|
+
if (exitCode === undefined) {
|
|
351
|
+
try {
|
|
352
|
+
child.kill();
|
|
353
|
+
} catch {}
|
|
354
|
+
live?.send({ type: "end", id: liveId, code: undefined });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
live?.close();
|
|
358
|
+
emulator.dispose();
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Vitest as an embedded engine: the explorer UI drives test discovery and
|
|
2
|
+
// runs through vitest's Node API and streams state changes out through a
|
|
3
|
+
// reporter, instead of shelling out to the CLI or the stock UI.
|
|
4
|
+
export type SerializedTask = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
type: "suite" | "test";
|
|
8
|
+
state: string | undefined;
|
|
9
|
+
duration: number | undefined;
|
|
10
|
+
errors: string[];
|
|
11
|
+
tasks: SerializedTask[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type TestEngine = {
|
|
15
|
+
/**
|
|
16
|
+
The current task tree (files → suites → tests) with results.
|
|
17
|
+
*/
|
|
18
|
+
tree: () => SerializedTask[];
|
|
19
|
+
|
|
20
|
+
running: () => boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
Runs everything under the repo's test dir. Fire-and-forget; progress
|
|
24
|
+
arrives via `onUpdate`.
|
|
25
|
+
*/
|
|
26
|
+
runAll: () => void;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
Reruns a single file, suite, or test by task id.
|
|
30
|
+
*/
|
|
31
|
+
runTask: (id: string) => void;
|
|
32
|
+
|
|
33
|
+
onUpdate: (listener: () => void) => () => void;
|
|
34
|
+
|
|
35
|
+
close: () => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type VitestTask = {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
type: string;
|
|
42
|
+
filepath?: string;
|
|
43
|
+
result?: { state?: string; duration?: number; errors?: Array<{ message?: string }> };
|
|
44
|
+
tasks?: VitestTask[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const serializeTask = (task: VitestTask): SerializedTask => ({
|
|
48
|
+
id: task.id,
|
|
49
|
+
name: task.name,
|
|
50
|
+
type: task.type === "test" ? "test" : "suite",
|
|
51
|
+
state: task.result?.state,
|
|
52
|
+
duration: task.result?.duration,
|
|
53
|
+
errors: (task.result?.errors ?? [])
|
|
54
|
+
.map((error) => error.message ?? "")
|
|
55
|
+
.filter((message) => message.length > 0),
|
|
56
|
+
tasks: (task.tasks ?? []).map(serializeTask),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
Creates a live Vitest instance scoped to the repo's own `test/` directory.
|
|
61
|
+
Set any environment the test workers should inherit (e.g. `SIGIL_LIVE_URL`
|
|
62
|
+
for live terminal mirroring) before calling this.
|
|
63
|
+
*/
|
|
64
|
+
export const createVitestEngine = async (): Promise<TestEngine> => {
|
|
65
|
+
const { createVitest } = await import("vitest/node").catch((error) => {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'The "vitest" package is required for the test engine. Install it as a dev dependency: pnpm add -D vitest',
|
|
68
|
+
{ cause: error },
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const listeners = new Set<() => void>();
|
|
73
|
+
let running = false;
|
|
74
|
+
let notifyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
75
|
+
|
|
76
|
+
const notify = (): void => {
|
|
77
|
+
// Reporter hooks fire densely during a run; coalesce.
|
|
78
|
+
notifyTimer ??= setTimeout(() => {
|
|
79
|
+
notifyTimer = undefined;
|
|
80
|
+
for (const listener of listeners) {
|
|
81
|
+
listener();
|
|
82
|
+
}
|
|
83
|
+
}, 80);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const reporter = {
|
|
87
|
+
onTestRunStart: () => {
|
|
88
|
+
running = true;
|
|
89
|
+
notify();
|
|
90
|
+
},
|
|
91
|
+
onTestRunEnd: () => {
|
|
92
|
+
running = false;
|
|
93
|
+
notify();
|
|
94
|
+
},
|
|
95
|
+
onTestModuleCollected: notify,
|
|
96
|
+
onTestModuleEnd: notify,
|
|
97
|
+
onTestSuiteResult: notify,
|
|
98
|
+
onTestCaseResult: notify,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const ctx = await createVitest("test", { watch: true, reporters: [reporter] });
|
|
102
|
+
|
|
103
|
+
const isRelevant = (moduleId: string): boolean =>
|
|
104
|
+
moduleId.includes("/test/") && !moduleId.includes(".vendor");
|
|
105
|
+
|
|
106
|
+
const relevantSpecifications = async () =>
|
|
107
|
+
(await ctx.globTestSpecifications()).filter((specification) =>
|
|
108
|
+
isRelevant(specification.moduleId),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Populate the tree up front without running anything.
|
|
112
|
+
await ctx.collect(["test/"]);
|
|
113
|
+
notify();
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
tree: () =>
|
|
117
|
+
ctx.state
|
|
118
|
+
.getFiles()
|
|
119
|
+
.filter((file) => isRelevant((file as VitestTask).filepath ?? ""))
|
|
120
|
+
.map((file) => serializeTask(file as VitestTask)),
|
|
121
|
+
running: () => running,
|
|
122
|
+
runAll: () => {
|
|
123
|
+
if (running) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
void relevantSpecifications()
|
|
128
|
+
.then((specifications) => ctx.runTestSpecifications(specifications, false))
|
|
129
|
+
.catch(() => {
|
|
130
|
+
running = false;
|
|
131
|
+
notify();
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
runTask: (id) => {
|
|
135
|
+
if (running) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Present at runtime (the stock UI uses it) but absent from the
|
|
140
|
+
// public Vitest type.
|
|
141
|
+
const runner = ctx as unknown as { rerunTask: (taskId: string) => Promise<unknown> };
|
|
142
|
+
void Promise.resolve(runner.rerunTask(id)).catch(() => {
|
|
143
|
+
running = false;
|
|
144
|
+
notify();
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
onUpdate: (listener) => {
|
|
148
|
+
listeners.add(listener);
|
|
149
|
+
return () => {
|
|
150
|
+
listeners.delete(listener);
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
close: async () => {
|
|
154
|
+
await ctx.close();
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
};
|
package/src/throttle.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Derived from `throttle` from `es-toolkit/compat` (lodash-style),
|
|
2
|
+
// covering the surface Ink uses: leading + trailing edges, cancel() and flush().
|
|
3
|
+
|
|
4
|
+
export type Throttled<Arguments extends unknown[]> = {
|
|
5
|
+
(...args: Arguments): void;
|
|
6
|
+
cancel: () => void;
|
|
7
|
+
flush: () => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
Invokes `fn` at most once per `wait` milliseconds.
|
|
12
|
+
|
|
13
|
+
The first call in a window fires immediately (leading edge). Calls made while
|
|
14
|
+
the window is open are coalesced into a single trailing invocation with the
|
|
15
|
+
latest arguments. Matches the lodash semantics Ink relied on: a single call
|
|
16
|
+
produces only a leading invocation, no trailing one.
|
|
17
|
+
*/
|
|
18
|
+
export const throttle = <Arguments extends unknown[]>(
|
|
19
|
+
fn: (...args: Arguments) => void,
|
|
20
|
+
wait = 0,
|
|
21
|
+
): Throttled<Arguments> => {
|
|
22
|
+
let timer: NodeJS.Timeout | undefined;
|
|
23
|
+
let pendingArgs: Arguments | undefined;
|
|
24
|
+
|
|
25
|
+
const invokePending = (): void => {
|
|
26
|
+
const args = pendingArgs!;
|
|
27
|
+
pendingArgs = undefined;
|
|
28
|
+
fn(...args);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const onTimer = (): void => {
|
|
32
|
+
if (pendingArgs) {
|
|
33
|
+
invokePending();
|
|
34
|
+
timer = setTimeout(onTimer, wait);
|
|
35
|
+
} else {
|
|
36
|
+
timer = undefined;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const throttled = (...args: Arguments): void => {
|
|
41
|
+
if (timer) {
|
|
42
|
+
pendingArgs = args;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn(...args);
|
|
47
|
+
timer = setTimeout(onTimer, wait);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
throttled.cancel = (): void => {
|
|
51
|
+
if (timer) {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
timer = undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pendingArgs = undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
throttled.flush = (): void => {
|
|
60
|
+
if (!pendingArgs) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (timer) {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
timer = undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
invokePending();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return throttled;
|
|
73
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { styledCharsFromTokens, type StyledChar, tokenize } from "#/ansi/tokenize.ts";
|
|
2
|
+
|
|
3
|
+
export type AnsiTransformer = (text: string, line: number) => string;
|
|
4
|
+
|
|
5
|
+
/** The single ANSI compatibility boundary used by Transform and external styled strings. */
|
|
6
|
+
export function transformAnsiLine(
|
|
7
|
+
serializedSubtree: string,
|
|
8
|
+
lineIndex: number,
|
|
9
|
+
transformers: readonly AnsiTransformer[],
|
|
10
|
+
): readonly StyledChar[] {
|
|
11
|
+
let transformed = serializedSubtree;
|
|
12
|
+
for (const transformer of transformers) transformed = transformer(transformed, lineIndex);
|
|
13
|
+
return styledCharsFromTokens(tokenize(transformed));
|
|
14
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The one `type-fest` type Ink used, inlined (MIT, Sindre Sorhus).
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Allows creating a union type by combining primitive types and literal types
|
|
5
|
+
without sacrificing auto-completion in IDEs for the literal type part of the
|
|
6
|
+
union.
|
|
7
|
+
*/
|
|
8
|
+
export type LiteralUnion<LiteralType, BaseType extends string | number> =
|
|
9
|
+
| LiteralType
|
|
10
|
+
| (BaseType & Record<never, never>);
|