Skip to content
Merged
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
3 changes: 2 additions & 1 deletion jest.e2e.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ module.exports = {
...baseConfig,
testMatch: ["<rootDir>/src/tests/ui/**/*.test.ts"],
testPathIgnorePatterns: ["/node_modules/", ".tui-test/"],
testTimeout: 30_000,
maxWorkers: 1,
testTimeout: 120_000,
};
39 changes: 8 additions & 31 deletions src/tests/ui/autocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,22 @@

import { jest } from "@jest/globals";
import { ShellUse } from "@microsoft/shell-use";
import { configs, returnChar, startSession } from "./helpers";
import { closeSession, configs, expectPrompt, returnChar, startSession } from "./helpers";

const accent = "#7d56f4";

jest.retryTimes(2);
jest.retryTimes(2, { logErrorsBeforeRetry: true });

describe("resize recovery", () => {
let terminal: ShellUse;
beforeEach(async () => {
terminal = await startSession({ label: "bash-resize", shell: "bash", env: { BASH_SILENCE_DEPRECATION_WARNING: "1" } }, ["-T", "-s", "bash"]);
});
afterEach(async () => {
await terminal.close();
await closeSession(terminal);
});

test("restores input and suggestions after becoming too narrow", async () => {
await terminal.expectText("> ");
await terminal.type("git st");
await terminal.expectText("stage");
await terminal.waitIdle();
Expand All @@ -40,7 +39,7 @@ describe("resize recovery", () => {
const restoredView = (await terminal.text()).trimEnd();
expect(restoredView).toMatchSnapshot("restored 80 column view");
expect(restoredView).toBe(initialView);
}, 30_000);
});
});

configs.map((config) => {
Expand All @@ -52,19 +51,17 @@ configs.map((config) => {
terminal = await startSession(config, args);
});
afterEach(async () => {
await terminal.close();
await closeSession(terminal);
});

test("basic git suggestions", async () => {
await terminal.expectText("> ");
await terminal.type("git ");

await terminal.expectText("blame");
await terminal.expectText("archive", { strict: false, bg: accent });
});

test("cursor up when at top of list", async () => {
await terminal.expectText("> ");
await terminal.type("git ");

await terminal.expectText("archive", { strict: false, bg: accent });
Expand All @@ -73,17 +70,15 @@ configs.map((config) => {
});

test("move cursor backwards to hide suggestions", async () => {
await terminal.expectText("> ");
await terminal.type("git ");

await terminal.expectText("archive", { strict: false });

await terminal.press("Left");
await terminal.expectText("archive", { strict: false, not: true });
}, 15_000);
});

test("cursor down when at top of list", async () => {
await terminal.expectText("> ");
await terminal.type("git ");

await terminal.expectText("archive", { strict: false });
Expand All @@ -96,7 +91,6 @@ configs.map((config) => {
});

test("scroll down a full page when at top of list", async () => {
await terminal.expectText("> ");
await terminal.type("git ");

await terminal.expectText("archive", { strict: false });
Expand All @@ -109,14 +103,12 @@ configs.map((config) => {

// excluding cmd since it doesn't support CWD tracking
(config.shell !== "cmd" ? test : test.skip)("generator results lead suggestions", async () => {
await terminal.expectText("> ");
await terminal.type("ls ");

await terminal.expectText("📄", { strict: false });
});

test("tab completion", async () => {
await terminal.expectText("> ");
await terminal.type("git ");

await terminal.expectText("archive", { strict: false });
Expand All @@ -126,7 +118,6 @@ configs.map((config) => {
});

test("backspacing after accepting tab completion", async () => {
await terminal.expectText("> ");
await terminal.type("git ");

await terminal.expectText("archive", { strict: false });
Expand All @@ -139,7 +130,6 @@ configs.map((config) => {
});

test("suggestion cursor resets between views", async () => {
await terminal.expectText("> ");
await terminal.type("git ");

await terminal.expectText("archive", { strict: false });
Expand All @@ -157,29 +147,24 @@ configs.map((config) => {
});

test("ui on bottom of the screen", async () => {
await terminal.expectText("> ");
await terminal.resize(80, 10);
await terminal.write(rc.repeat(10));
await terminal.expectText("> ");
await expectPrompt(terminal);

await terminal.type("git ");
await terminal.expectText("archive", { strict: false });
});

test("command detection after command execution", async () => {
await terminal.expectText("> ");

await terminal.write(`echo "hello"${rc}`);
await terminal.expectText("hello", { strict: false });
await terminal.expectText("> ");
await expectPrompt(terminal);

await terminal.type("git ");
await terminal.expectText("archive", { strict: false });
});

test("suggestions clear after command execution", async () => {
await terminal.expectText("> ");

await terminal.type("git ");
await terminal.expectText("archive", { strict: false });

Expand All @@ -188,8 +173,6 @@ configs.map((config) => {
});

test("cursor-position reports are not treated as input", async () => {
await terminal.expectText("> ");

await terminal.write("\u001B[2;7R");
await terminal.waitIdle();
await terminal.expectText("[2;7R", { not: true });
Expand All @@ -199,8 +182,6 @@ configs.map((config) => {
});

test.skip("access history when no suggestions exist", async () => {
await terminal.expectText("> ");

await terminal.type("clear");
await terminal.expectText("clear");

Expand All @@ -212,15 +193,11 @@ configs.map((config) => {
});

test("proper overflow truncation in command", async () => {
await terminal.expectText("> ");

await terminal.type("dotnet add package Holoon.Newtonsoft");
await terminal.expectText("CanBeUndefi…│");
});

test.skip("command detection with suggestions", async () => {
await terminal.expectText("> ");

await terminal.write(`dotnet add item${rc}`);
await terminal.expectText("dotnet", { strict: false });

Expand Down
59 changes: 53 additions & 6 deletions src/tests/ui/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import path from "node:path";
import fs from "node:fs";
import url from "node:url";
import { ShellUse } from "@microsoft/shell-use";
import type { ExpectTextOptions } from "@microsoft/shell-use";
import type { ExpectTextOptions, Shell, WaitTextOptions } from "@microsoft/shell-use";

export type ShellConfig = {
label: string;
Expand All @@ -33,17 +33,64 @@ export const configs = os.platform() == "win32" ? windowsConfigs : unixConfigs;
export const returnChar = (shell: string) => (shell == "xonsh" ? "\n" : "\r");

const buildEntry = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "..", "build", "index.js");
const expectTextTimeout = 15_000;

const expectTextTimeout = 30_000;
const idleTimeout = 15_000;
const promptTimeout = 20_000;
const startupAttempts = 3;

class E2EShellUse extends ShellUse {
override expectText(text: string, opts: ExpectTextOptions = {}): Promise<void> {
return super.expectText(text, { timeout: expectTextTimeout, ...opts });
}

override waitText(text: string, opts: WaitTextOptions = {}): Promise<void> {
return super.waitText(text, { timeout: expectTextTimeout, ...opts });
}

override waitIdle(opts: { timeout?: number } = {}): Promise<void> {
return super.waitIdle({ timeout: idleTimeout, ...opts });
}
}

export const expectPrompt = async (terminal: ShellUse, timeout = promptTimeout): Promise<void> => {
await terminal.expectText("> ", { timeout });
await terminal.waitIdle();
};

export const closeSession = async (terminal: ShellUse | undefined): Promise<void> => {
try {
await terminal?.close();
} catch {
/*empty*/
}
};

let counter = 0;
export const startSession = async (config: ShellConfig, args: string[], cols = 80, rows = 30): Promise<ShellUse> => {
const su = new E2EShellUse(`is-e2e-${config.label}-${process.pid}-${counter++}`);
await su.run("node", [buildEntry, ...args], { cols, rows, ...(config.env ? { env: config.env } : {}) });
return su;
const nextSessionName = (label: string) => `is-e2e-${label}-${process.pid}-${counter++}`;
const baseEnv = { ISTERM: "0", ISTERM_TESTING: "0" };
const withStartupRetries = async (label: string, start: (terminal: ShellUse) => Promise<void>): Promise<ShellUse> => {
let lastError: unknown;
for (let attempt = 0; attempt < startupAttempts; attempt++) {
const terminal = new E2EShellUse(nextSessionName(label));
try {
await start(terminal);
return terminal;
} catch (e) {
lastError = e;
await closeSession(terminal);
}
}
throw lastError;
};

export const startSession = (config: ShellConfig, args: string[], cols = 80, rows = 30): Promise<ShellUse> =>
withStartupRetries(config.label, async (terminal) => {
await terminal.run("node", [buildEntry, ...args], { cols, rows, env: { ...baseEnv, ...config.env } });
await expectPrompt(terminal);
});

export const startShell = (label: string, shell: Shell): Promise<ShellUse> =>
withStartupRetries(label, async (terminal) => {
await terminal.open({ shell, env: baseEnv });
});
17 changes: 9 additions & 8 deletions src/tests/ui/status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
// Licensed under the MIT License.

import os from "node:os";
import { jest } from "@jest/globals";
import { ShellUse } from "@microsoft/shell-use";
import { startSession } from "./helpers";
import type { Shell } from "@microsoft/shell-use";
import { closeSession, startSession, startShell } from "./helpers";

const shell = os.platform() == "darwin" ? "zsh" : os.platform() == "linux" ? "bash" : "powershell";
const shell: Shell = os.platform() == "darwin" ? "zsh" : os.platform() == "linux" ? "bash" : "powershell";

jest.retryTimes(2, { logErrorsBeforeRetry: true });

describe("status checks", () => {
describe("inside inshellisense session", () => {
Expand All @@ -14,12 +18,10 @@ describe("status checks", () => {
terminal = await startSession({ label: "status", shell }, ["-T", "-s", shell]);
});
afterEach(async () => {
await terminal.close();
await closeSession(terminal);
});

test("current status", async () => {
await terminal.expectText("> ", { timeout: 30_000 });

await terminal.write("is -c\r");
await terminal.expectText("live", { fg: "2" });
});
Expand All @@ -28,11 +30,10 @@ describe("status checks", () => {
describe("outside inshellisense session", () => {
let terminal: ShellUse;
beforeEach(async () => {
terminal = new ShellUse(`is-e2e-status-outside-${process.pid}`);
await terminal.open({ shell });
terminal = await startShell("status-outside", shell);
});
afterEach(async () => {
await terminal.close();
await closeSession(terminal);
});

test("current status", async () => {
Expand Down
Loading