ptywright 0.1.1 → 0.2.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/README.md +287 -1
- package/dist/agent.mjs +2 -0
- package/dist/bin/ptywright.mjs +6 -0
- package/dist/cli-DIUx2w6X.mjs +3587 -0
- package/dist/cli.mjs +2 -0
- package/{src/index.ts → dist/index.mjs} +7 -9
- package/dist/mcp.mjs +2 -0
- package/dist/pty-cassette.mjs +24 -0
- package/dist/pty_like-Cpkh_O9B.mjs +404 -0
- package/dist/runner-DzZlFrt1.mjs +1897 -0
- package/dist/runner-zApMYWZx.mjs +3257 -0
- package/dist/script.mjs +2 -0
- package/dist/server-VHuEWWj_.mjs +3068 -0
- package/dist/session.mjs +2 -0
- package/dist/terminal_session-DopC7Xg6.mjs +893 -0
- package/package.json +28 -21
- package/schemas/ptywright-agent-cassette.schema.json +57 -0
- package/schemas/ptywright-agent-check.schema.json +122 -0
- package/schemas/ptywright-agent-manifest.schema.json +107 -0
- package/schemas/ptywright-agent-promote.schema.json +146 -0
- package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
- package/schemas/ptywright-agent-run.schema.json +126 -0
- package/schemas/ptywright-agent.schema.json +182 -0
- package/schemas/ptywright-pty-cassette.schema.json +86 -0
- package/schemas/ptywright-script-manifest.schema.json +75 -0
- package/schemas/ptywright-script-run-summary.schema.json +114 -0
- package/schemas/ptywright-script.schema.json +55 -3
- package/bin/ptywright +0 -4
- package/src/cli.ts +0 -414
- package/src/generator/doc_parser.ts +0 -341
- package/src/generator/generate.ts +0 -161
- package/src/generator/index.ts +0 -10
- package/src/generator/script_generator.ts +0 -209
- package/src/generator/step_extractor.ts +0 -397
- package/src/mcp/http_server.ts +0 -174
- package/src/mcp/script_recording.ts +0 -238
- package/src/mcp/server.ts +0 -1348
- package/src/pty/bun_pty_adapter.ts +0 -34
- package/src/pty/bun_terminal_adapter.ts +0 -149
- package/src/pty/pty_adapter.ts +0 -31
- package/src/script/dsl.ts +0 -188
- package/src/script/module.ts +0 -43
- package/src/script/path.ts +0 -151
- package/src/script/run.ts +0 -108
- package/src/script/run_all.ts +0 -229
- package/src/script/runner.ts +0 -983
- package/src/script/schema.ts +0 -237
- package/src/script/steps/assert_snapshot_equals.ts +0 -21
- package/src/script/steps/index.ts +0 -2
- package/src/script/suite_report.ts +0 -626
- package/src/session/session_manager.ts +0 -145
- package/src/session/terminal_session.ts +0 -473
- package/src/terminal/ansi.ts +0 -142
- package/src/terminal/keys.ts +0 -180
- package/src/terminal/mask.ts +0 -70
- package/src/terminal/mouse.ts +0 -75
- package/src/terminal/snapshot.ts +0 -196
- package/src/terminal/style.ts +0 -121
- package/src/terminal/view.ts +0 -49
- package/src/trace/asciicast.ts +0 -20
- package/src/trace/asciinema_player_assets.ts +0 -44
- package/src/trace/cast_to_txt.ts +0 -116
- package/src/trace/recorder.ts +0 -110
- package/src/trace/report.ts +0 -2092
- package/src/types.ts +0 -86
- package/src/util/hash.ts +0 -8
- package/src/util/sleep.ts +0 -5
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { spawn } from "bun-pty";
|
|
2
|
-
import type { IPty, IPtyForkOptions } from "bun-pty";
|
|
3
|
-
|
|
4
|
-
import type { PtyAdapter, PtyProcess, PtySpawnOptions } from "./pty_adapter";
|
|
5
|
-
|
|
6
|
-
function toForkOptions(options: PtySpawnOptions): IPtyForkOptions {
|
|
7
|
-
return {
|
|
8
|
-
name: options.name,
|
|
9
|
-
cols: options.cols,
|
|
10
|
-
rows: options.rows,
|
|
11
|
-
cwd: options.cwd,
|
|
12
|
-
env: options.env,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function toPtyProcess(pty: IPty): PtyProcess {
|
|
17
|
-
return {
|
|
18
|
-
pid: pty.pid,
|
|
19
|
-
cols: pty.cols,
|
|
20
|
-
rows: pty.rows,
|
|
21
|
-
write: (data) => pty.write(data),
|
|
22
|
-
resize: (cols, rows) => pty.resize(cols, rows),
|
|
23
|
-
kill: (signal) => pty.kill(signal),
|
|
24
|
-
onData: (listener) => pty.onData(listener),
|
|
25
|
-
onExit: (listener) => pty.onExit(listener),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class BunPtyAdapter implements PtyAdapter {
|
|
30
|
-
spawn(command: string, args: string[], options: PtySpawnOptions): PtyProcess {
|
|
31
|
-
const pty = spawn(command, args, toForkOptions(options));
|
|
32
|
-
return toPtyProcess(pty);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type { Terminal, TerminalOptions } from "bun";
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
Disposable,
|
|
5
|
-
PtyAdapter,
|
|
6
|
-
PtyExitEvent,
|
|
7
|
-
PtyProcess,
|
|
8
|
-
PtySpawnOptions,
|
|
9
|
-
} from "./pty_adapter";
|
|
10
|
-
|
|
11
|
-
type Listener<T> = (arg: T) => void;
|
|
12
|
-
|
|
13
|
-
function createDisposable<T>(set: Set<Listener<T>>, listener: Listener<T>): Disposable {
|
|
14
|
-
return {
|
|
15
|
-
dispose: () => {
|
|
16
|
-
set.delete(listener);
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class BunTerminalAdapter implements PtyAdapter {
|
|
22
|
-
spawn(command: string, args: string[], options: PtySpawnOptions): PtyProcess {
|
|
23
|
-
if (process.platform === "win32") {
|
|
24
|
-
throw new Error("Bun.Terminal PTY is only available on POSIX systems (Linux/macOS)");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let cols = options.cols;
|
|
28
|
-
let rows = options.rows;
|
|
29
|
-
|
|
30
|
-
const decoder = new TextDecoder();
|
|
31
|
-
const dataListeners = new Set<Listener<string>>();
|
|
32
|
-
const exitListeners = new Set<Listener<PtyExitEvent>>();
|
|
33
|
-
|
|
34
|
-
const pendingData: string[] = [];
|
|
35
|
-
let pendingExit: PtyExitEvent | null = null;
|
|
36
|
-
|
|
37
|
-
const dispatchData = (chunk: string): void => {
|
|
38
|
-
if (!chunk) return;
|
|
39
|
-
if (dataListeners.size === 0) {
|
|
40
|
-
pendingData.push(chunk);
|
|
41
|
-
if (pendingData.length > 2000) {
|
|
42
|
-
pendingData.splice(0, pendingData.length - 2000);
|
|
43
|
-
}
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
for (const listener of dataListeners) listener(chunk);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const flushPendingDataTo = (listener: Listener<string>): void => {
|
|
50
|
-
if (pendingData.length === 0) return;
|
|
51
|
-
for (const chunk of pendingData) listener(chunk);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const dispatchExit = (event: PtyExitEvent): void => {
|
|
55
|
-
pendingExit = event;
|
|
56
|
-
for (const listener of exitListeners) listener(event);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const flushExitTo = (listener: Listener<PtyExitEvent>): void => {
|
|
60
|
-
if (!pendingExit) return;
|
|
61
|
-
listener(pendingExit);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const terminalOptions: TerminalOptions = {
|
|
65
|
-
cols,
|
|
66
|
-
rows,
|
|
67
|
-
name: options.name,
|
|
68
|
-
data: (_term, data) => {
|
|
69
|
-
const text = decoder.decode(data, { stream: true });
|
|
70
|
-
dispatchData(text);
|
|
71
|
-
},
|
|
72
|
-
exit: () => {
|
|
73
|
-
const tail = decoder.decode();
|
|
74
|
-
dispatchData(tail);
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
let terminal: Terminal | undefined;
|
|
79
|
-
let killed = false;
|
|
80
|
-
|
|
81
|
-
const proc = Bun.spawn([command, ...args], {
|
|
82
|
-
cwd: options.cwd,
|
|
83
|
-
env: options.env,
|
|
84
|
-
terminal: terminalOptions,
|
|
85
|
-
onExit(subprocess, exitCode, _signalCode) {
|
|
86
|
-
const tail = decoder.decode();
|
|
87
|
-
dispatchData(tail);
|
|
88
|
-
|
|
89
|
-
const signal = subprocess.signalCode ?? undefined;
|
|
90
|
-
dispatchExit({
|
|
91
|
-
exitCode: exitCode ?? (killed ? -1 : 0),
|
|
92
|
-
signal,
|
|
93
|
-
});
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
terminal = proc.terminal;
|
|
98
|
-
if (!terminal) {
|
|
99
|
-
throw new Error("expected Bun.spawn(..., { terminal }) to attach a PTY terminal");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Ensure input is passed through without CR->LF translation (needed for apps that
|
|
103
|
-
// distinguish Enter (CR) from Ctrl+J (LF), e.g. Codex CLI).
|
|
104
|
-
// Note: Bun.Terminal.setRawMode only affects PTY line discipline, not the child process.
|
|
105
|
-
terminal.setRawMode(true);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
pid: proc.pid,
|
|
109
|
-
get cols() {
|
|
110
|
-
return cols;
|
|
111
|
-
},
|
|
112
|
-
get rows() {
|
|
113
|
-
return rows;
|
|
114
|
-
},
|
|
115
|
-
write: (data) => {
|
|
116
|
-
terminal?.write(data);
|
|
117
|
-
},
|
|
118
|
-
resize: (nextCols, nextRows) => {
|
|
119
|
-
cols = nextCols;
|
|
120
|
-
rows = nextRows;
|
|
121
|
-
terminal?.resize(nextCols, nextRows);
|
|
122
|
-
},
|
|
123
|
-
kill: (signal) => {
|
|
124
|
-
killed = true;
|
|
125
|
-
if (signal) {
|
|
126
|
-
proc.kill(signal as unknown as NodeJS.Signals);
|
|
127
|
-
} else {
|
|
128
|
-
proc.kill();
|
|
129
|
-
}
|
|
130
|
-
terminal?.close();
|
|
131
|
-
},
|
|
132
|
-
onData: (listener) => {
|
|
133
|
-
dataListeners.add(listener);
|
|
134
|
-
flushPendingDataTo(listener);
|
|
135
|
-
if (dataListeners.size === 1) {
|
|
136
|
-
queueMicrotask(() => {
|
|
137
|
-
pendingData.length = 0;
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
return createDisposable(dataListeners, listener);
|
|
141
|
-
},
|
|
142
|
-
onExit: (listener) => {
|
|
143
|
-
exitListeners.add(listener);
|
|
144
|
-
flushExitTo(listener);
|
|
145
|
-
return createDisposable(exitListeners, listener);
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
}
|
package/src/pty/pty_adapter.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export type PtySpawnOptions = {
|
|
2
|
-
cols: number;
|
|
3
|
-
rows: number;
|
|
4
|
-
cwd: string;
|
|
5
|
-
env: Record<string, string>;
|
|
6
|
-
name: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export type PtyExitEvent = {
|
|
10
|
-
exitCode: number;
|
|
11
|
-
signal?: number | string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type Disposable = {
|
|
15
|
-
dispose(): void;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type PtyProcess = {
|
|
19
|
-
readonly pid: number;
|
|
20
|
-
readonly cols: number;
|
|
21
|
-
readonly rows: number;
|
|
22
|
-
write(data: string): void;
|
|
23
|
-
resize(cols: number, rows: number): void;
|
|
24
|
-
kill(signal?: string): void;
|
|
25
|
-
onData(listener: (data: string) => void): Disposable;
|
|
26
|
-
onExit(listener: (event: PtyExitEvent) => void): Disposable;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type PtyAdapter = {
|
|
30
|
-
spawn(command: string, args: string[], options: PtySpawnOptions): PtyProcess;
|
|
31
|
-
};
|
package/src/script/dsl.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { scriptSchema } from "./schema";
|
|
2
|
-
import type { Script, ScriptStep } from "./schema";
|
|
3
|
-
|
|
4
|
-
type SnapshotKey = string;
|
|
5
|
-
|
|
6
|
-
type SnapshotRef<K extends SnapshotKey> = K | "last";
|
|
7
|
-
|
|
8
|
-
type StepOf<T extends ScriptStep["type"]> = Extract<ScriptStep, { type: T }>;
|
|
9
|
-
|
|
10
|
-
type SnapshotStep = StepOf<"snapshot">;
|
|
11
|
-
type ExpectStep = StepOf<"expect">;
|
|
12
|
-
type ExpectGoldenStep = StepOf<"expectGolden">;
|
|
13
|
-
type ExpectMetaStep = StepOf<"expectMeta">;
|
|
14
|
-
type WaitForExitStep = StepOf<"waitForExit">;
|
|
15
|
-
type SendMouseStep = StepOf<"sendMouse">;
|
|
16
|
-
|
|
17
|
-
type CustomStepMap = Record<string, unknown>;
|
|
18
|
-
|
|
19
|
-
export class ScriptBuilder<K extends SnapshotKey = never, Steps extends CustomStepMap = {}> {
|
|
20
|
-
private readonly script: Script;
|
|
21
|
-
|
|
22
|
-
constructor(init: {
|
|
23
|
-
name?: string;
|
|
24
|
-
artifactsDir?: string;
|
|
25
|
-
launch: Script["launch"];
|
|
26
|
-
trace?: Script["trace"];
|
|
27
|
-
}) {
|
|
28
|
-
this.script = {
|
|
29
|
-
name: init.name,
|
|
30
|
-
artifactsDir: init.artifactsDir,
|
|
31
|
-
launch: init.launch,
|
|
32
|
-
trace: init.trace,
|
|
33
|
-
steps: [],
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
getName(): string | undefined {
|
|
38
|
-
return this.script.name;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
getLaunch(): Script["launch"] {
|
|
42
|
-
return this.script.launch;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
step(step: ScriptStep): this {
|
|
46
|
-
this.script.steps.push(step);
|
|
47
|
-
return this;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
use<NextK extends SnapshotKey, NextSteps extends CustomStepMap = Steps>(
|
|
51
|
-
fn: (s: ScriptBuilder<K, Steps>) => ScriptBuilder<NextK, NextSteps>,
|
|
52
|
-
): ScriptBuilder<NextK, NextSteps> {
|
|
53
|
-
return fn(this);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
custom<Name extends string>(
|
|
57
|
-
name: Name,
|
|
58
|
-
...args: Name extends keyof Steps & string
|
|
59
|
-
? undefined extends Steps[Name]
|
|
60
|
-
? [payload?: Steps[Name]]
|
|
61
|
-
: [payload: Steps[Name]]
|
|
62
|
-
: [payload?: unknown]
|
|
63
|
-
): this {
|
|
64
|
-
const payload = args[0] as unknown;
|
|
65
|
-
if (payload === undefined) {
|
|
66
|
-
return this.step({ type: "custom", name });
|
|
67
|
-
}
|
|
68
|
-
return this.step({ type: "custom", name, payload });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
sendText(text: string, options?: { enter?: boolean }): this {
|
|
72
|
-
return this.step({ type: "sendText", text, enter: options?.enter });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
pasteText(text: string, options?: { bracketed?: boolean }): this {
|
|
76
|
-
const bracketed = options?.bracketed ?? false;
|
|
77
|
-
const payload = bracketed ? `\x1b[200~${text}\x1b[201~` : text;
|
|
78
|
-
return this.sendText(payload);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
pressKey(key: string): this {
|
|
82
|
-
return this.step({ type: "pressKey", key });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
sendMouse(step: Omit<SendMouseStep, "type">): this {
|
|
86
|
-
return this.step({ type: "sendMouse", ...step });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
resize(cols: number, rows: number): this {
|
|
90
|
-
return this.step({ type: "resize", cols, rows });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
mark(label?: string): this {
|
|
94
|
-
return this.step({ type: "mark", label });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
sleep(ms: number): this {
|
|
98
|
-
return this.step({ type: "sleep", ms });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
waitForText(step: Omit<StepOf<"waitForText">, "type">): this {
|
|
102
|
-
return this.step({ type: "waitForText", ...step });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
waitForStableScreen(step: Omit<StepOf<"waitForStableScreen">, "type"> = {}): this {
|
|
106
|
-
return this.step({ type: "waitForStableScreen", ...step });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
waitForExit(step: Omit<WaitForExitStep, "type"> = {}): this {
|
|
110
|
-
return this.step({ type: "waitForExit", ...step });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
expectMeta(step: Omit<ExpectMetaStep, "type">): this {
|
|
114
|
-
return this.step({ type: "expectMeta", ...step });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
snapshot<K2 extends string>(
|
|
118
|
-
step: Omit<SnapshotStep, "type"> & { saveAs: K2 },
|
|
119
|
-
): ScriptBuilder<K | K2, Steps>;
|
|
120
|
-
snapshot(step: Omit<SnapshotStep, "type">): ScriptBuilder<K, Steps>;
|
|
121
|
-
snapshot(step: Omit<SnapshotStep, "type">): ScriptBuilder<any, Steps> {
|
|
122
|
-
this.step({ type: "snapshot", ...step });
|
|
123
|
-
return this as any;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
snapshotText<K2 extends string>(
|
|
127
|
-
step: Omit<SnapshotStep, "type" | "kind"> & { saveAs: K2 },
|
|
128
|
-
): ScriptBuilder<K | K2, Steps>;
|
|
129
|
-
snapshotText(step?: Omit<SnapshotStep, "type" | "kind">): ScriptBuilder<K, Steps>;
|
|
130
|
-
snapshotText(step: Omit<SnapshotStep, "type" | "kind"> = {}): ScriptBuilder<any, Steps> {
|
|
131
|
-
return this.snapshot({ ...step, kind: "text" } as Omit<SnapshotStep, "type">);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
snapshotView<K2 extends string>(
|
|
135
|
-
step: Omit<SnapshotStep, "type" | "kind"> & { saveAs: K2 },
|
|
136
|
-
): ScriptBuilder<K | K2, Steps>;
|
|
137
|
-
snapshotView(step?: Omit<SnapshotStep, "type" | "kind">): ScriptBuilder<K, Steps>;
|
|
138
|
-
snapshotView(step: Omit<SnapshotStep, "type" | "kind"> = {}): ScriptBuilder<any, Steps> {
|
|
139
|
-
return this.snapshot({ ...step, kind: "view" } as Omit<SnapshotStep, "type">);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
snapshotAnsi<K2 extends string>(
|
|
143
|
-
step: Omit<SnapshotStep, "type" | "kind"> & { saveAs: K2 },
|
|
144
|
-
): ScriptBuilder<K | K2, Steps>;
|
|
145
|
-
snapshotAnsi(step?: Omit<SnapshotStep, "type" | "kind">): ScriptBuilder<K, Steps>;
|
|
146
|
-
snapshotAnsi(step: Omit<SnapshotStep, "type" | "kind"> = {}): ScriptBuilder<any, Steps> {
|
|
147
|
-
return this.snapshot({ ...step, kind: "ansi" } as Omit<SnapshotStep, "type">);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
snapshotViewAnsi<K2 extends string>(
|
|
151
|
-
step: Omit<SnapshotStep, "type" | "kind"> & { saveAs: K2 },
|
|
152
|
-
): ScriptBuilder<K | K2, Steps>;
|
|
153
|
-
snapshotViewAnsi(step?: Omit<SnapshotStep, "type" | "kind">): ScriptBuilder<K, Steps>;
|
|
154
|
-
snapshotViewAnsi(step: Omit<SnapshotStep, "type" | "kind"> = {}): ScriptBuilder<any, Steps> {
|
|
155
|
-
return this.snapshot({ ...step, kind: "view_ansi" } as Omit<SnapshotStep, "type">);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
snapshotGrid<K2 extends string>(
|
|
159
|
-
step: Omit<SnapshotStep, "type" | "kind"> & { saveAs: K2 },
|
|
160
|
-
): ScriptBuilder<K | K2, Steps>;
|
|
161
|
-
snapshotGrid(step?: Omit<SnapshotStep, "type" | "kind">): ScriptBuilder<K, Steps>;
|
|
162
|
-
snapshotGrid(step: Omit<SnapshotStep, "type" | "kind"> = {}): ScriptBuilder<any, Steps> {
|
|
163
|
-
return this.snapshot({ ...step, kind: "grid" } as Omit<SnapshotStep, "type">);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
expect(step: Omit<ExpectStep, "type" | "from"> & { from?: SnapshotRef<K> }): this {
|
|
167
|
-
return this.step({ type: "expect", ...(step as Omit<ExpectStep, "type">) });
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
expectGolden(step: Omit<ExpectGoldenStep, "type" | "from"> & { from?: SnapshotRef<K> }): this {
|
|
171
|
-
return this.step({ type: "expectGolden", ...(step as Omit<ExpectGoldenStep, "type">) });
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
build(): Script {
|
|
175
|
-
return scriptSchema.parse(this.script) as Script;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function defineScript<K extends SnapshotKey, Steps extends CustomStepMap>(
|
|
180
|
-
build: () => ScriptBuilder<K, Steps> | Script,
|
|
181
|
-
): Script {
|
|
182
|
-
const value = build();
|
|
183
|
-
const script =
|
|
184
|
-
typeof value === "object" && value && "build" in value
|
|
185
|
-
? (value as ScriptBuilder<any, any>).build()
|
|
186
|
-
: value;
|
|
187
|
-
return scriptSchema.parse(script) as Script;
|
|
188
|
-
}
|
package/src/script/module.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
|
-
|
|
4
|
-
import type { CustomStepHandler } from "./runner";
|
|
5
|
-
|
|
6
|
-
function extractStepHandlers(
|
|
7
|
-
mod: Record<string, unknown>,
|
|
8
|
-
): Record<string, CustomStepHandler> | undefined {
|
|
9
|
-
return (mod.steps ?? mod.customSteps ?? mod.stepHandlers) as
|
|
10
|
-
| Record<string, CustomStepHandler>
|
|
11
|
-
| undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function loadScriptModule(modulePath: string): Promise<{
|
|
15
|
-
script: unknown;
|
|
16
|
-
steps?: Record<string, CustomStepHandler>;
|
|
17
|
-
}> {
|
|
18
|
-
const absPath = resolve(process.cwd(), modulePath);
|
|
19
|
-
const mod = (await import(pathToFileURL(absPath).href)) as Record<string, unknown>;
|
|
20
|
-
|
|
21
|
-
const script = mod.default ?? mod.script;
|
|
22
|
-
if (!script) {
|
|
23
|
-
throw new Error(`script module must export default or 'script': ${modulePath}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const steps = extractStepHandlers(mod);
|
|
27
|
-
|
|
28
|
-
return { script, steps };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function loadStepHandlersModule(modulePath: string): Promise<{
|
|
32
|
-
steps: Record<string, CustomStepHandler>;
|
|
33
|
-
}> {
|
|
34
|
-
const absPath = resolve(process.cwd(), modulePath);
|
|
35
|
-
const mod = (await import(pathToFileURL(absPath).href)) as Record<string, unknown>;
|
|
36
|
-
const steps = extractStepHandlers(mod);
|
|
37
|
-
if (!steps) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`steps module must export 'steps' (or customSteps/stepHandlers): ${modulePath}`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return { steps };
|
|
43
|
-
}
|
package/src/script/path.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { basename, extname, isAbsolute, resolve } from "node:path";
|
|
2
|
-
|
|
3
|
-
import { loadScriptModule, loadStepHandlersModule } from "./module";
|
|
4
|
-
import { runScript } from "./runner";
|
|
5
|
-
import type { CustomStepHandler } from "./runner";
|
|
6
|
-
import { scriptSchema } from "./schema";
|
|
7
|
-
import type { Script } from "./schema";
|
|
8
|
-
|
|
9
|
-
export type RunScriptPathOptions = {
|
|
10
|
-
artifactsDir?: string;
|
|
11
|
-
updateGoldens?: boolean;
|
|
12
|
-
stepsPath?: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type RunScriptPathSuccess = {
|
|
16
|
-
ok: true;
|
|
17
|
-
scriptName: string;
|
|
18
|
-
artifactsDir: string;
|
|
19
|
-
castPath?: string;
|
|
20
|
-
reportPath?: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type RunScriptPathFailure = {
|
|
24
|
-
ok: false;
|
|
25
|
-
error: string;
|
|
26
|
-
scriptName?: string;
|
|
27
|
-
artifactsDir?: string;
|
|
28
|
-
castPath?: string;
|
|
29
|
-
reportPath?: string;
|
|
30
|
-
failureArtifacts?: {
|
|
31
|
-
lastTextPath: string;
|
|
32
|
-
lastViewPath: string;
|
|
33
|
-
stepPath: string;
|
|
34
|
-
errorPath: string;
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type RunScriptPathResult = RunScriptPathSuccess | RunScriptPathFailure;
|
|
39
|
-
|
|
40
|
-
export async function runScriptPath(
|
|
41
|
-
scriptPath: string,
|
|
42
|
-
options?: RunScriptPathOptions,
|
|
43
|
-
): Promise<RunScriptPathResult> {
|
|
44
|
-
let scriptName: string | undefined;
|
|
45
|
-
let artifactsDir: string | undefined;
|
|
46
|
-
let castPath: string | undefined;
|
|
47
|
-
let reportPath: string | undefined;
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const ext = extname(scriptPath).toLowerCase();
|
|
51
|
-
const baseName = basename(scriptPath, extname(scriptPath));
|
|
52
|
-
|
|
53
|
-
const extraSteps = options?.stepsPath
|
|
54
|
-
? (await loadStepHandlersModule(options.stepsPath)).steps
|
|
55
|
-
: undefined;
|
|
56
|
-
|
|
57
|
-
const loaded = await loadScriptInput(scriptPath, ext);
|
|
58
|
-
const stepsFromModule = loaded.steps;
|
|
59
|
-
const mergedSteps =
|
|
60
|
-
stepsFromModule && extraSteps
|
|
61
|
-
? { ...stepsFromModule, ...extraSteps }
|
|
62
|
-
: (stepsFromModule ?? extraSteps);
|
|
63
|
-
|
|
64
|
-
const built = loaded.script;
|
|
65
|
-
const withName =
|
|
66
|
-
built && typeof built === "object" && !Array.isArray(built) && !("name" in built)
|
|
67
|
-
? { ...built, name: baseName }
|
|
68
|
-
: built;
|
|
69
|
-
|
|
70
|
-
const parsed = scriptSchema.parse(withName) as Script;
|
|
71
|
-
scriptName = parsed.name ?? baseName;
|
|
72
|
-
artifactsDir = resolveArtifactsDir(parsed, scriptName, options?.artifactsDir);
|
|
73
|
-
|
|
74
|
-
const trace = parsed.trace ?? {};
|
|
75
|
-
const saveCast = trace.saveCast ?? true;
|
|
76
|
-
const saveReport = trace.saveReport ?? true;
|
|
77
|
-
castPath = saveCast
|
|
78
|
-
? resolveArtifactPath(artifactsDir, trace.castPath ?? `${scriptName}.cast`)
|
|
79
|
-
: undefined;
|
|
80
|
-
reportPath = saveReport
|
|
81
|
-
? resolveArtifactPath(artifactsDir, trace.reportPath ?? `${scriptName}.report.html`)
|
|
82
|
-
: undefined;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
await runScript(parsed, {
|
|
86
|
-
artifactsDir,
|
|
87
|
-
updateGoldens: options?.updateGoldens,
|
|
88
|
-
steps: mergedSteps,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
return { ok: true, scriptName, artifactsDir, castPath, reportPath };
|
|
92
|
-
} catch (error) {
|
|
93
|
-
return {
|
|
94
|
-
ok: false,
|
|
95
|
-
error: (error as Error).message,
|
|
96
|
-
scriptName,
|
|
97
|
-
artifactsDir,
|
|
98
|
-
castPath,
|
|
99
|
-
reportPath,
|
|
100
|
-
failureArtifacts: {
|
|
101
|
-
lastTextPath: resolveArtifactPath(artifactsDir, "failure.last.txt"),
|
|
102
|
-
lastViewPath: resolveArtifactPath(artifactsDir, "failure.last.view.txt"),
|
|
103
|
-
stepPath: resolveArtifactPath(artifactsDir, "failure.step.json"),
|
|
104
|
-
errorPath: resolveArtifactPath(artifactsDir, "failure.error.txt"),
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
} catch (error) {
|
|
109
|
-
return {
|
|
110
|
-
ok: false,
|
|
111
|
-
error: (error as Error).message,
|
|
112
|
-
scriptName,
|
|
113
|
-
artifactsDir,
|
|
114
|
-
castPath,
|
|
115
|
-
reportPath,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function loadScriptInput(
|
|
121
|
-
scriptPath: string,
|
|
122
|
-
ext: string,
|
|
123
|
-
): Promise<{ script: unknown; steps?: Record<string, CustomStepHandler> }> {
|
|
124
|
-
if (ext === ".json") {
|
|
125
|
-
const raw = await Bun.file(scriptPath).text();
|
|
126
|
-
return { script: JSON.parse(raw) as unknown };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const loaded = await loadScriptModule(scriptPath);
|
|
130
|
-
const script = loaded.script;
|
|
131
|
-
if (
|
|
132
|
-
script &&
|
|
133
|
-
typeof script === "object" &&
|
|
134
|
-
"build" in script &&
|
|
135
|
-
typeof (script as { build?: unknown }).build === "function"
|
|
136
|
-
) {
|
|
137
|
-
return { script: (script as { build: () => unknown }).build(), steps: loaded.steps };
|
|
138
|
-
}
|
|
139
|
-
return { script, steps: loaded.steps };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function resolveArtifactsDir(script: Script, scriptName: string, override?: string): string {
|
|
143
|
-
if (override?.trim()) return resolve(override.trim());
|
|
144
|
-
if (script.artifactsDir?.trim()) return resolve(script.artifactsDir.trim());
|
|
145
|
-
return resolve(".tmp", "runs", scriptName);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function resolveArtifactPath(artifactsDir: string, path: string): string {
|
|
149
|
-
if (isAbsolute(path)) return path;
|
|
150
|
-
return resolve(artifactsDir, path);
|
|
151
|
-
}
|
package/src/script/run.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { runScriptPath } from "./path";
|
|
2
|
-
|
|
3
|
-
function parseArgs(argv: string[]): {
|
|
4
|
-
scriptPath: string;
|
|
5
|
-
artifactsDir?: string;
|
|
6
|
-
stepsPath?: string;
|
|
7
|
-
updateGoldens: boolean;
|
|
8
|
-
} {
|
|
9
|
-
const out: {
|
|
10
|
-
scriptPath?: string;
|
|
11
|
-
artifactsDir?: string;
|
|
12
|
-
stepsPath?: string;
|
|
13
|
-
updateGoldens: boolean;
|
|
14
|
-
} = {
|
|
15
|
-
updateGoldens: false,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
19
|
-
const arg = argv[i];
|
|
20
|
-
const next = argv[i + 1];
|
|
21
|
-
|
|
22
|
-
if (!out.scriptPath && arg && !arg.startsWith("-")) {
|
|
23
|
-
out.scriptPath = arg;
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (arg === "--artifacts-dir" && next) {
|
|
28
|
-
out.artifactsDir = next;
|
|
29
|
-
i += 1;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (arg === "--steps" && next) {
|
|
34
|
-
out.stepsPath = next;
|
|
35
|
-
i += 1;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (arg === "--update-goldens") {
|
|
40
|
-
out.updateGoldens = true;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
throw new Error(`unknown arg: ${arg ?? ""}`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!out.scriptPath) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
"Usage: bun run src/script/run.ts <file> [--artifacts-dir <dir>] [--steps <module.ts>] [--update-goldens]",
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return out as {
|
|
54
|
-
scriptPath: string;
|
|
55
|
-
artifactsDir?: string;
|
|
56
|
-
stepsPath?: string;
|
|
57
|
-
updateGoldens: boolean;
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function logLines(lines: Array<string | null | undefined>, stderr: boolean): void {
|
|
62
|
-
const filtered = lines.map((l) => l?.trim()).filter(Boolean) as string[];
|
|
63
|
-
for (const line of filtered) {
|
|
64
|
-
// eslint-disable-next-line no-console
|
|
65
|
-
(stderr ? console.error : console.log)(line);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (import.meta.main) {
|
|
70
|
-
try {
|
|
71
|
-
const args = parseArgs(process.argv.slice(2));
|
|
72
|
-
const result = await runScriptPath(args.scriptPath, {
|
|
73
|
-
artifactsDir: args.artifactsDir,
|
|
74
|
-
updateGoldens: args.updateGoldens,
|
|
75
|
-
stepsPath: args.stepsPath,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (!result.ok) {
|
|
79
|
-
logLines(
|
|
80
|
-
[
|
|
81
|
-
result.error,
|
|
82
|
-
result.artifactsDir ? `artifacts=${result.artifactsDir}` : null,
|
|
83
|
-
result.reportPath ? `report=${result.reportPath}` : null,
|
|
84
|
-
result.castPath ? `cast=${result.castPath}` : null,
|
|
85
|
-
result.failureArtifacts?.lastViewPath
|
|
86
|
-
? `last=${result.failureArtifacts.lastViewPath}`
|
|
87
|
-
: null,
|
|
88
|
-
result.failureArtifacts?.errorPath ? `error=${result.failureArtifacts.errorPath}` : null,
|
|
89
|
-
],
|
|
90
|
-
true,
|
|
91
|
-
);
|
|
92
|
-
process.exitCode = 1;
|
|
93
|
-
} else {
|
|
94
|
-
logLines(
|
|
95
|
-
[
|
|
96
|
-
`ok artifacts=${result.artifactsDir}`,
|
|
97
|
-
result.reportPath ? `report=${result.reportPath}` : null,
|
|
98
|
-
result.castPath ? `cast=${result.castPath}` : null,
|
|
99
|
-
],
|
|
100
|
-
false,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
} catch (error) {
|
|
104
|
-
// eslint-disable-next-line no-console
|
|
105
|
-
console.error((error as Error).message);
|
|
106
|
-
process.exitCode = 1;
|
|
107
|
-
}
|
|
108
|
-
}
|