diff --git a/packages/package.json b/packages/package.json index dbdeedf3..72ea26dc 100644 --- a/packages/package.json +++ b/packages/package.json @@ -24,6 +24,7 @@ "moduleNameMapper": { "^.+\\.(s?css|png|svg)$": "/../scripts/dummy.js", "@coder/ide/src/fill/evaluation": "/ide/src/fill/evaluation", + "@coder/ide/src/fill/client": "/ide/src/fill/client", "@coder/(.*)/test": "/$1/test", "@coder/(.*)": "/$1/src" }, diff --git a/packages/vscode/test/node-pty.test.ts b/packages/vscode/test/node-pty.test.ts new file mode 100644 index 00000000..d8d38df9 --- /dev/null +++ b/packages/vscode/test/node-pty.test.ts @@ -0,0 +1,99 @@ +import { IPty } from "node-pty"; +import { createClient } from "@coder/protocol/test"; + +const client = createClient(); +jest.mock("../../ide/src/fill/client", () => ({ client })); +const pty = require("../src/fill/node-pty") as typeof import("node-pty"); + +describe("node-pty", () => { + /** + * Returns a function that when called returns a promise that resolves with + * the next chunk of data from the process. + */ + const promisifyData = (proc: IPty): (() => Promise) => { + // Use a persistent callback instead of creating it in the promise since + // otherwise we could lose data that comes in while no promise is listening. + let onData: (() => void) | undefined; + let buffer: string | undefined; + proc.on("data", (data) => { + // Remove everything that isn't a letter, number, or $ to avoid issues + // with ANSI escape codes printing inside the test output. + buffer = (buffer || "") + data.toString().replace(/[^a-zA-Z0-9$]/g, ""); + if (onData) { + onData(); + } + }); + + return (): Promise => new Promise((resolve): void => { + onData = (): void => { + if (typeof buffer !== "undefined") { + const data = buffer; + buffer = undefined; + onData = undefined; + resolve(data); + } + }; + onData(); + }); + }; + + it("should create shell", async () => { + // Setting the config file to something that shouldn't exist so the test + // isn't affected by custom configuration. + const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], { + cols: 100, + rows: 10, + }); + + const getData = promisifyData(proc); + + // First it outputs @hostname:cwd + expect((await getData()).length).toBeGreaterThan(1); + + // Then it seems to overwrite that with a shorter prompt in the format of + // [hostname@user]$ + expect((await getData())).toContain("$"); + + proc.kill(); + + await new Promise((resolve): void => { + proc.on("exit", resolve); + }); + }); + + it("should resize", async () => { + // Requires the `tput lines` cmd to be available. + // Setting the config file to something that shouldn't exist so the test + // isn't affected by custom configuration. + const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], { + cols: 10, + rows: 10, + }); + + const getData = promisifyData(proc); + + // We've already tested these first two bits of output; see shell test. + await getData(); + await getData(); + + proc.write("tput lines\n"); + expect(await getData()).toContain("tput"); + + expect((await getData()).trim()).toContain("10"); + proc.resize(10, 50); + + // The prompt again. + await getData(); + await getData(); + + proc.write("tput lines\n"); + expect(await getData()).toContain("tput"); + + expect((await getData())).toContain("50"); + + proc.kill(); + await new Promise((resolve): void => { + proc.on("exit", resolve); + }); + }); +});