Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ Drive a full-screen TUI the same way:
```sh
tui-test run vim file.txt
tui-test wait idle # let the screen settle
tui-test press i
tui-test key press i
tui-test type "some text"
tui-test press Escape : w q Enter
tui-test key press Escape : w q Enter
tui-test wait exit
```

Expand Down Expand Up @@ -219,12 +219,17 @@ print the screen bare.

| Command | Description |
| ------------------------------------------------------------- | ------------------------------------------------------------ |
| `type "text"` | Type literal text. |
| `submit ["text"]` | Type then press the shell return key. |
| `press <Key...>` | Named keys, e.g. `press Escape : w q Enter`, `press Ctrl+C`. |
| `keys "Control+a"` | A single key combo. |
| `mouse click X Y` / `mouse click --on-text "OK" [--clicks N]` | Click by coords or label. |
| `mouse move\|down\|up\|drag\|scroll ...` | Full mouse control. |
| `type "text"` | Type literal text. |
| `submit ["text"]` | Type then press the shell return key. |
| `key press <Key...>` | Send down then up, e.g. `key press Ctrl+C`. |
| `key down <Key...>` / `key up <Key...>` | Send explicit down and up events. |
| `key repeat <Key...>` | Send repeat events for keys that are currently down. |
| `mouse click X Y` / `mouse click --on-text "OK" [--clicks N]` | Click by coords or label. |
| `mouse move\|down\|up\|drag\|scroll ...` | Full mouse control. |

Key input follows the Kitty keyboard protocol negotiated by the child.
Modifiers are `Ctrl`, `Alt` / `Option`, `Shift`, `Super`, `Hyper`, and `Meta`;
the top-level `press` command remains a compatibility alias for `key press`.

### PTY

Expand Down
27 changes: 18 additions & 9 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,17 @@ without parsing text:
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `type "text"` | Type literal text (no return key). |
| `submit ["text"]` | Type text then press the shell's return key. Omit text to just submit. |
| `press <Key...>` | Named keys, e.g. `press Escape : w q Enter`, `press Ctrl+C`. |
| `keys "Ctrl+a"` | A single key combo. |
| `key press <Key...>` | Send down then up, e.g. `key press Ctrl+C`. |
| `key down <Key...>` / `key up <Key...>` | Send explicit down and up events. |
| `key repeat <Key...>` | Send repeat events for keys that are currently down. |
| `mouse click X Y` / `mouse click --on-text "OK" [--button N] [--clicks N]` | Click by coordinates or by visible label. |
| `mouse move\|down\|up\|drag\|scroll ...` | Full mouse control (`--button` default 0=left, `scroll --amount` default 3). |

Key input automatically follows the Kitty keyboard protocol flags negotiated by
the child application. Modifiers are `Ctrl`, `Alt` / `Option`, `Shift`, `Super`,
`Hyper`, and `Meta`. Top-level `press` remains a compatibility alias for
`key press`.

### PTY control

| Command | Description |
Expand Down Expand Up @@ -140,18 +146,18 @@ tui-test expect exit-code 0 # assert the command succeeded
tui-test close
```

`submit` types text then presses Enter; `type` types without Enter; `press`
sends named keys (`press Escape : w q Enter`, `press Ctrl+C`); `keys` sends one
combo (`keys "Ctrl+a"`).
`submit` types text then presses Enter; `type` types without Enter; `key press`
sends complete key actions (`key press Escape : w q Enter`, `key press Ctrl+C`);
`key down` and `key up` model held keys, and `key repeat` sends repeat events.

## Workflow: drive a TUI program

```sh
tui-test run vim file.txt
tui-test wait idle # let the screen finish rendering
tui-test press i # enter insert mode
tui-test key press i # enter insert mode
tui-test type "some text"
tui-test press Escape : w q Enter # save and quit
tui-test key press Escape : w q Enter # save and quit
tui-test wait exit
```

Expand Down Expand Up @@ -321,7 +327,9 @@ The Rust crate exposes `Session` and `SessionRegistry` for terminal ownership,
plus the `Operation` and `OperationResult` enums for the command surface.

Python and JavaScript methods mirror the cli commands: `open` / `run`, `submit`
/ `type` / `write`, `press` / `keys`, `mouse.click|move|down|up|drag|scroll`,
/ `type` / `write`, `keyboard.press|down|repeat|up`, compatibility
`press`,
`mouse.click|move|down|up|drag|scroll`,
`resize`, `signal` / `kill`, `state`, `text`, `cells`, the dedicated
`get_command` / `get_output` / `get_exit_code` / `get_cwd` / `get_cursor` /
`get_size` / `get_title` methods,
Expand Down Expand Up @@ -413,7 +421,8 @@ cat ~/.tui-test/work.log
`git log` / `git diff`), a full-screen pager such as `less` is likely holding the
terminal, and `Ctrl+C` won't quit it. Confirm with `tui-test state`
(`"ready": false` and a stale last command). Quit the pager with
`tui-test press q`, or avoid it with `git --no-pager <cmd>` or `GIT_PAGER=cat`.
`tui-test key press q`, or avoid it with `git --no-pager <cmd>` or
`GIT_PAGER=cat`.

**Platform note.** On Windows ConPTY, `get output` and `get command` text can on some rare occasions be
unreliable due to screen repainting; grid-based checks (`expect text`,
Expand Down
6 changes: 5 additions & 1 deletion bindings/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ All derive from `TuiTestError` and carry `kind` and `exitCode`. `waitX` and `exp

## API

`new TuiTest(session?, { profile?, timeouts?, artifacts? })` mirrors the cli: `open` / `run`, `type` / `write`, `submit`, `press` / `keys`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `getCommand` / `getOutput` / `getExitCode` / `getCwd` / `getCursor` / `getSize` / `getTitle`, `screenshot`, `waitText` / `waitTitle` / `waitIdle` / `waitCommand` / `waitExit` / `waitReady`, `expectText` / `expectTitle` / `expectExitCode` / `expectOutput` / `expectSnapshot`, `close`, and `closeQuiet`.
`new TuiTest(session?, { profile?, timeouts?, artifacts? })` mirrors the cli: `open` / `run`, `type` / `write`, `submit`, `keyboard.press|down|repeat|up`, compatibility `press`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `getCommand` / `getOutput` / `getExitCode` / `getCwd` / `getCursor` / `getSize` / `getTitle`, `screenshot`, `waitText` / `waitTitle` / `waitIdle` / `waitCommand` / `waitExit` / `waitReady`, `expectText` / `expectTitle` / `expectExitCode` / `expectOutput` / `expectSnapshot`, `close`, and `closeQuiet`.

`keyboard.press()` sends down then up. Use `keyboard.down()`,
`keyboard.repeat()`, and `keyboard.up()` for explicit events. Top-level
`press()` remains a compatibility alias.

Module-level helpers: `sessions()`, `closeAll()`, `getRecording()`, `uniqueSession()`.

Expand Down
3 changes: 3 additions & 0 deletions bindings/js/native/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export declare class NativeSession {
type(text: string): Promise<void>
submit(data?: string | undefined | null): Promise<void>
press(keys: Array<string>): Promise<void>
keyDown(keys: Array<string>): Promise<void>
repeat(keys: Array<string>): Promise<void>
keyUp(keys: Array<string>): Promise<void>
mouseClick(options?: MouseClickOptions | undefined | null): Promise<void>
mouseMove(x: number, y: number): Promise<void>
mouseDown(x: number, y: number, button?: number | undefined | null): Promise<void>
Expand Down
47 changes: 45 additions & 2 deletions bindings/js/native/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tui_test::profile::{Profile as CoreProfile, Rgb};
use tui_test::shell::Shell as CoreShell;
use tui_test::{
global_registry, Cell as CoreCell, CellColor, Cursor as CoreCursor,
EffectiveTimeouts as CoreEffectiveTimeouts, ErrorKind, MouseAction,
EffectiveTimeouts as CoreEffectiveTimeouts, ErrorKind, KeyAction, MouseAction,
OpenOptions as CoreOpenOptions, OpenResult as CoreOpenResult, Operation, OperationResult,
RunOptions as CoreRunOptions, ScreenshotResult as CoreScreenshotResult, SessionHandle,
Size as CoreSize, SnapshotResult as CoreSnapshotResult, State as CoreState,
Expand Down Expand Up @@ -775,7 +775,50 @@ impl NativeSession {

#[napi]
pub async fn press(&self, keys: Vec<String>) -> Result<()> {
self.unit("press", Operation::Press { keys }).await
self.unit(
"press",
Operation::Key {
keys,
action: KeyAction::Press,
},
)
.await
}

#[napi]
pub async fn key_down(&self, keys: Vec<String>) -> Result<()> {
self.unit(
"keydown",
Operation::Key {
keys,
action: KeyAction::Down,
},
)
.await
}

#[napi]
pub async fn repeat(&self, keys: Vec<String>) -> Result<()> {
self.unit(
"repeat",
Operation::Key {
keys,
action: KeyAction::Repeat,
},
)
.await
}

#[napi]
pub async fn key_up(&self, keys: Vec<String>) -> Result<()> {
self.unit(
"keyup",
Operation::Key {
keys,
action: KeyAction::Up,
},
)
.await
}

#[napi]
Expand Down
32 changes: 27 additions & 5 deletions bindings/js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,30 @@ function optional<T>(value: T | null | undefined): T | undefined {
return value ?? undefined;
}

class Keyboard {
#runtime: NativeRuntime;

constructor(runtime: NativeRuntime) {
this.#runtime = runtime;
}

async press(...keys: string[]): Promise<void> {
await this.#runtime.press(keys);
}

async down(...keys: string[]): Promise<void> {
await this.#runtime.keyDown(keys);
}

async repeat(...keys: string[]): Promise<void> {
await this.#runtime.repeat(keys);
}

async up(...keys: string[]): Promise<void> {
await this.#runtime.keyUp(keys);
}
}

class Mouse {
#runtime: NativeRuntime;

Expand Down Expand Up @@ -132,6 +156,7 @@ class Mouse {

export class TuiTest {
readonly session: string;
readonly keyboard: Keyboard;
readonly mouse: Mouse;
#runtime: NativeRuntime;
#options: ClientOptions;
Expand All @@ -145,6 +170,7 @@ export class TuiTest {
profilePayload(opts.profile);
this.#options = opts;
this.#runtime = new NativeRuntime(this.session);
this.keyboard = new Keyboard(this.#runtime);
this.mouse = new Mouse(this.#runtime);
}

Expand Down Expand Up @@ -272,11 +298,7 @@ export class TuiTest {
}

async press(...keys: string[]): Promise<void> {
await this.#runtime.press(keys);
}

async keys(combo: string): Promise<void> {
await this.#runtime.press([combo]);
await this.keyboard.press(...keys);
}

async resize(cols: number, rows: number): Promise<void> {
Expand Down
12 changes: 12 additions & 0 deletions bindings/js/src/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,18 @@ export class NativeRuntime {
return this.#call((session) => session.press(keys));
}

keyDown(keys: string[]): Promise<void> {
return this.#call((session) => session.keyDown(keys));
}

repeat(keys: string[]): Promise<void> {
return this.#call((session) => session.repeat(keys));
}

keyUp(keys: string[]): Promise<void> {
return this.#call((session) => session.keyUp(keys));
}

mouseClick(options?: MouseClickOptions): Promise<void> {
return this.#call((session) => session.mouseClick(options));
}
Expand Down
9 changes: 8 additions & 1 deletion bindings/js/test/conformance.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const MAPPING = {
],
type: [["client", "type"]],
submit: [["client", "submit"]],
key: [["client", "keyboard"]],
press: [["client", "press"]],
keys: [["client", "keys"]],
mouse: [["client", "mouse"]],
resize: [["client", "resize"]],
write: [["client", "write"]],
Expand Down Expand Up @@ -70,6 +70,13 @@ test("every cli command is mapped or excluded", { skip: !schema }, () => {
}
});

test("keyboard exposes every key action", () => {
const keyboard = new TuiTest("keyboard-conformance").keyboard;
for (const method of ["press", "down", "repeat", "up"]) {
assert.equal(typeof keyboard[method], "function", method);
}
});

test("error exit codes match the taxonomy", () => {
assert.equal(new sdk.ExpectationError("x").exitCode, 1);
assert.equal(new sdk.UsageError("x").exitCode, 2);
Expand Down
15 changes: 6 additions & 9 deletions bindings/js/test/integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,11 @@ test("echo roundtrip drives a real session", async () => {
await su.expectText("hello-sdk", { strict: false });
await su.expectExitCode(0);

// A command finishes before the shell draws its next prompt. Wait for the
// prompt-end marker so these separate cursor reads cannot straddle that
// redraw and disagree by the prompt width.
await su.waitReady();
const state = await su.state();
// Read next to the snapshot it is compared against. The shell draws its
// next prompt after the command finishes, which moves the cursor, and
// these two calls read it separately: with other calls in between, the
// prompt lands between them and they disagree by its width.
//
// `waitIdle` is not the barrier it looks like here, since the screen is
// quiet *because* the prompt has not started, so idle arrives first.
assert.deepEqual(await su.getCursor(), state.cursor);
assert.ok(state.cols > 0);
assert.match(await su.text(), /hello-sdk/);
Expand All @@ -54,11 +51,11 @@ test("echo roundtrip drives a real session", async () => {
assert.match(await su.screenshot(), /hello-sdk/);

await su.write("echo typed-write");
await su.keys("Enter");
await su.keyboard.press("Enter");
await su.waitText("typed-write");
await su.waitCommand();
await su.type("echo typed-type");
await su.press("Enter");
await su.keyboard.press("Enter");
await su.waitText("typed-type");
await su.waitCommand();
});
Expand Down
3 changes: 3 additions & 0 deletions bindings/js/test/native.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ test("generated native declarations expose typed operations", async () => {
"type",
"submit",
"press",
"keyDown",
"repeat",
"keyUp",
"mouseClick",
"resize",
"signal",
Expand Down
6 changes: 5 additions & 1 deletion bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ All derive from `TuiTestError`. `wait_*` and `expect_*` raise `ExpectationError`

## API

`TuiTest(session="default", *, timeouts=None, profile=None, artifacts=None)` mirrors the cli: `open` / `run`, `type` / `write`, `submit`, `press` / `keys`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `get_command` / `get_output` / `get_exit_code` / `get_cwd` / `get_cursor` / `get_size` / `get_title`, `screenshot`, `wait_text` / `wait_title` / `wait_idle` / `wait_command` / `wait_exit` / `wait_ready`, `expect_text` / `expect_title` / `expect_exit_code` / `expect_output` / `expect_snapshot`, `close`, and `close_quiet`.
`TuiTest(session="default", *, timeouts=None, profile=None, artifacts=None)` mirrors the cli: `open` / `run`, `type` / `write`, `submit`, `keyboard.press|down|repeat|up`, compatibility `press`, `mouse.click|move|down|up|drag|scroll`, `resize`, `signal` / `kill`, `state`, `text`, `cells`, `get_command` / `get_output` / `get_exit_code` / `get_cwd` / `get_cursor` / `get_size` / `get_title`, `screenshot`, `wait_text` / `wait_title` / `wait_idle` / `wait_command` / `wait_exit` / `wait_ready`, `expect_text` / `expect_title` / `expect_exit_code` / `expect_output` / `expect_snapshot`, `close`, and `close_quiet`.

`keyboard.press()` sends down then up. Use `keyboard.down()`,
`keyboard.repeat()`, and `keyboard.up()` for explicit events. Top-level
`press()` remains a compatibility alias.

Module-level helpers: `sessions()`, `close_all()`, `get_recording()`, `unique_session()`.

Expand Down
Loading
Loading