Skip to content
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read
Expand Down Expand Up @@ -57,7 +56,7 @@ jobs:
- run: cargo fmt --all -- --check
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- run: cargo build
- run: cargo test --workspace
- run: cargo test --workspace -- --test-threads=1

- name: Install Python binding
run: python -m pip install --disable-pip-version-check -e ./bindings/python
Expand Down
58 changes: 22 additions & 36 deletions bindings/js/test/integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,17 @@ test("echo roundtrip drives a real session", async () => {
await su.submit("echo hello-sdk");
await su.waitCommand();
await su.expectText("hello-sdk", { strict: false });
await su.expectExitCode(0);

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);
const cursor = await su.getCursor();
assert.ok(
Number.isInteger(cursor.x) && cursor.x >= 0 && cursor.x < state.cols,
);
assert.ok(
Number.isInteger(cursor.y) && cursor.y >= 0 && cursor.y < state.rows,
);
assert.ok(state.cols > 0);
assert.match(await su.text(), /hello-sdk/);
assert.match(await su.getCommand(), /echo hello-sdk/);
assert.match(await su.getOutput(), /hello-sdk/);
assert.equal(await su.getExitCode(), 0);
assert.equal(typeof (await su.getCwd()), "string");
assert.deepEqual(await su.getSize(), { cols: state.cols, rows: state.rows });

Expand Down Expand Up @@ -107,32 +102,23 @@ test("a blocking native wait runs off the JS event loop", async () => {
await withTerminal({ program: [process.execPath, ...evalArgs] }, async (su) => {
await su.waitText("ready", { timeout: 2000 });

const intervalMs = 10;
const timeoutMs = 300;
let ticks = 0;
const heartbeat = setInterval(() => {
ticks += 1;
}, intervalMs);
const start = Date.now();
try {
await assert.rejects(
su.waitText("text-that-will-never-appear-xyz", { timeout: timeoutMs }),
(error) => error instanceof ExpectationError,
);
} finally {
clearInterval(heartbeat);
}
const elapsed = Date.now() - start;
assert.ok(
elapsed >= timeoutMs,
`expected the wait to run for at least ${timeoutMs}ms, took ${elapsed}ms`,
);
const expectedTicks = Math.floor(timeoutMs / intervalMs);
assert.ok(
ticks >= expectedTicks * 0.5,
`expected at least half of ~${expectedTicks} heartbeat ticks during the ` +
`blocking wait, got ${ticks}; the event loop appears to have stalled`,
const eventLoopTurn = new Promise((resolve) =>
setTimeout(() => resolve("event-loop"), 0),
);
const wait = su.waitText("text-that-will-never-appear-xyz", {
timeout: timeoutMs,
});
const first = await Promise.race([
eventLoopTurn,
wait.then(
() => "wait",
() => "wait",
),
]);

assert.equal(first, "event-loop", "the native wait blocked the event loop");
await assert.rejects(wait, (error) => error instanceof ExpectationError);
});
});

Expand Down
30 changes: 16 additions & 14 deletions crates/tui-test-cli/tests/session_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,10 @@ fn a_session_timeout_default_applies_to_later_commands() {
fn a_screenshot_and_an_assertion_agree_on_a_color() {
let sandbox = Sandbox::new("palette-agree");
// Printed lowercase so the match is the output, not the echoed command.
let print_red = r#"printf "\033[31m%s\033[0m\n" "$(echo QRSX | tr A-Z a-z)""#;
sandbox.ok(&["run", "--cols", "44", "--", "bash", "--norc"]);
sandbox.ok(&["submit", print_red]);
sandbox.ok(&["wait", "command"]);
let print_red = r#"printf "\033[31m%s\033[0m\n" "$(echo QRSX | tr A-Z a-z)"; sleep 30"#;
sandbox.ok(&[
"run", "--cols", "44", "--", "bash", "--norc", "-c", print_red,
]);

// The default profile is the VGA palette, so slot 1 is #800000.
sandbox.ok(&["expect", "text", "qrsx", "--fg", "#800000"]);
Expand All @@ -288,7 +288,7 @@ fn a_custom_profile_recolors_screenshots_and_assertions_together() {
std::fs::write(&config, "[profiles.neon.colors]\nred = \"#ff00ff\"\n").expect("write config");
let config_path = config.to_str().expect("utf-8 path");

let print_red = r#"printf "\033[31m%s\033[0m\n" "$(echo QRSX | tr A-Z a-z)""#;
let print_red = r#"printf "\033[31m%s\033[0m\n" "$(echo QRSX | tr A-Z a-z)"; sleep 30"#;
sandbox.ok(&[
"run",
"--config",
Expand All @@ -300,9 +300,9 @@ fn a_custom_profile_recolors_screenshots_and_assertions_together() {
"--",
"bash",
"--norc",
"-c",
print_red,
]);
sandbox.ok(&["submit", print_red]);
sandbox.ok(&["wait", "command"]);

sandbox.ok(&["expect", "text", "qrsx", "--fg", "#ff00ff"]);
let out = sandbox.run(&["expect", "text", "qrsx", "--fg", "#800000"]);
Expand Down Expand Up @@ -1130,23 +1130,25 @@ fn a_snapshot_records_the_title_only_when_asked() {
let sandbox = Sandbox::new("snap-title");
// Wide enough that the title is not truncated, so the assertion is about
// whether it was recorded at all rather than about how it was shortened.
sandbox.ok(&["run", "--cols", "40", "--", "bash", "--norc"]);
let set_title = r#"clear; printf '\033]2;tui-test-user@host: /some/path\007'; sleep 30"#;
sandbox.ok(&[
"submit",
r#"clear; printf '\033]2;ayman@host: /some/path\007'"#,
"run", "--cols", "40", "--", "bash", "--norc", "-c", set_title,
]);
sandbox.ok(&["expect", "title", "ayman@host", "--timeout", "5000"]);
sandbox.ok(&["expect", "title", "tui-test-user@host", "--timeout", "5000"]);

let plain = sandbox.ok(&["expect", "snapshot", "plain", "-u"]);
assert!(!plain.contains("ayman@host"), "default keeps the title out");
assert!(
!plain.contains("tui-test-user@host"),
"default keeps the title out"
);
let stored = std::fs::read_to_string(
std::env::current_dir()
.expect("cwd")
.join("__snapshots__/plain.snap"),
)
.expect("read snapshot");
assert!(
stored.starts_with("╭────") && !stored.contains("ayman@host"),
stored.starts_with("╭────") && !stored.contains("tui-test-user@host"),
"the border is plain, so a baseline is not tied to a machine: {stored}"
);

Expand All @@ -1158,7 +1160,7 @@ fn a_snapshot_records_the_title_only_when_asked() {
)
.expect("read snapshot");
assert!(
titled.contains("ayman@host: /some/path"),
titled.contains("tui-test-user@host: /some/path"),
"asking for it puts it in the border: {titled}"
);

Expand Down
1 change: 1 addition & 0 deletions crates/tui-test/tests/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tui_test::{
fn run_options(program: &str, args: &[&str]) -> RunOptions {
let defaults = OpenOptions::default();
RunOptions {
backend: defaults.backend,
program: program.to_string(),
args: args.iter().map(|arg| (*arg).to_string()).collect(),
profile: defaults.profile,
Expand Down
Loading