@tui-sandbox/library 9.0.1 → 9.1.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/CHANGELOG.md +7 -0
- package/dist/browser/assets/{index-DroaKLT0.js → index-pw1tOGNt.js} +6 -6
- package/dist/browser/index.html +1 -1
- package/dist/src/browser/neovim-client.d.ts +5 -0
- package/dist/src/browser/neovim-client.js +17 -6
- package/dist/src/client/index.d.ts +2 -1
- package/dist/src/client/index.js +2 -1
- package/dist/src/client/{terminal-client.d.ts → neovim-terminal-client.d.ts} +1 -1
- package/dist/src/client/{terminal-client.js → neovim-terminal-client.js} +3 -3
- package/dist/src/client/terminal-terminal-client.d.ts +14 -0
- package/dist/src/client/terminal-terminal-client.js +72 -0
- package/dist/src/server/cypress-support/contents.js +23 -1
- package/dist/src/server/dirtree/index.test.js +2 -0
- package/dist/src/server/neovim/NeovimApplication.d.ts +1 -1
- package/dist/src/server/neovim/environment/createTempDir.test.js +5 -5
- package/dist/src/server/server.d.ts +44 -4
- package/dist/src/server/server.js +24 -0
- package/dist/src/server/terminal/TerminalTestApplication.d.ts +22 -0
- package/dist/src/server/terminal/TerminalTestApplication.js +60 -0
- package/dist/src/server/terminal/index.d.ts +12 -0
- package/dist/src/server/terminal/index.js +40 -0
- package/dist/src/server/types.d.ts +5 -0
- package/dist/src/server/utilities/TerminalApplication.d.ts +1 -0
- package/dist/src/server/utilities/TerminalApplication.js +10 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/browser/neovim-client.ts +26 -6
- package/src/client/index.ts +2 -1
- package/src/client/{terminal-client.ts → neovim-terminal-client.ts} +3 -3
- package/src/client/terminal-terminal-client.ts +86 -0
- package/src/server/cypress-support/contents.ts +23 -1
- package/src/server/dirtree/index.test.ts +2 -0
- package/src/server/neovim/NeovimApplication.ts +3 -3
- package/src/server/neovim/environment/createTempDir.test.ts +5 -5
- package/src/server/server.ts +34 -0
- package/src/server/terminal/TerminalTestApplication.ts +98 -0
- package/src/server/terminal/index.ts +62 -0
- package/src/server/types.ts +13 -0
- package/src/server/utilities/TerminalApplication.ts +10 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tui-sandbox/library",
|
|
3
|
-
"version": "9.0
|
|
3
|
+
"version": "9.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@catppuccin/palette": "1.7.1",
|
|
11
|
-
"@trpc/client": "11.0.0-rc.
|
|
12
|
-
"@trpc/server": "11.0.0-rc.
|
|
11
|
+
"@trpc/client": "11.0.0-rc.768",
|
|
12
|
+
"@trpc/server": "11.0.0-rc.768",
|
|
13
13
|
"@xterm/addon-attach": "0.11.0",
|
|
14
14
|
"@xterm/addon-fit": "0.10.0",
|
|
15
15
|
"@xterm/xterm": "5.5.0",
|
|
@@ -20,21 +20,21 @@
|
|
|
20
20
|
"express": "4.21.2",
|
|
21
21
|
"neovim": "5.3.0",
|
|
22
22
|
"node-pty": "1.0.0",
|
|
23
|
-
"prettier": "3.
|
|
23
|
+
"prettier": "3.5.1",
|
|
24
24
|
"tsx": "4.19.2",
|
|
25
|
-
"type-fest": "4.
|
|
25
|
+
"type-fest": "4.34.1",
|
|
26
26
|
"winston": "3.17.0",
|
|
27
|
-
"zod": "3.24.
|
|
27
|
+
"zod": "3.24.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@runtyping/zod": "2.1.1",
|
|
31
31
|
"@types/command-exists": "1.2.3",
|
|
32
32
|
"@types/cors": "2.8.17",
|
|
33
33
|
"@types/express": "5.0.0",
|
|
34
|
-
"@types/node": "22.
|
|
34
|
+
"@types/node": "22.13.4",
|
|
35
35
|
"nodemon": "3.1.9",
|
|
36
|
-
"vite": "6.0
|
|
37
|
-
"vitest": "3.0.
|
|
36
|
+
"vite": "6.1.0",
|
|
37
|
+
"vitest": "3.0.5"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"cypress": "^13 || ^14",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { TerminalClient } from "../client/index.js"
|
|
1
|
+
import { TerminalClient as NeovimTerminalClient } from "../client/index.js"
|
|
2
|
+
import { TerminalTerminalClient } from "../client/terminal-terminal-client.js"
|
|
2
3
|
import type { BlockingCommandClientInput, ExCommandClientInput, LuaCodeClientInput } from "../server/server.js"
|
|
4
|
+
import type { StartTerminalGenericArguments } from "../server/terminal/TerminalTestApplication.js"
|
|
3
5
|
import type {
|
|
4
6
|
BlockingShellCommandOutput,
|
|
5
7
|
RunExCommandOutput,
|
|
@@ -7,13 +9,16 @@ import type {
|
|
|
7
9
|
StartNeovimGenericArguments,
|
|
8
10
|
TestDirectory,
|
|
9
11
|
} from "../server/types.js"
|
|
12
|
+
import { Lazy } from "../server/utilities/Lazy.js"
|
|
10
13
|
|
|
11
14
|
const app = document.querySelector<HTMLElement>("#app")
|
|
12
15
|
if (!app) {
|
|
13
16
|
throw new Error("No app element found")
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
// limitation: right now only one client can be used in the same test
|
|
20
|
+
const neovimClient = new Lazy(() => new NeovimTerminalClient(app))
|
|
21
|
+
const terminalClient = new Lazy(() => new TerminalTerminalClient(app))
|
|
17
22
|
|
|
18
23
|
export type GenericNeovimBrowserApi = {
|
|
19
24
|
runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput>
|
|
@@ -24,7 +29,8 @@ export type GenericNeovimBrowserApi = {
|
|
|
24
29
|
|
|
25
30
|
/** Entrypoint for the test runner (cypress) */
|
|
26
31
|
window.startNeovim = async function (startArgs?: StartNeovimGenericArguments): Promise<GenericNeovimBrowserApi> {
|
|
27
|
-
const
|
|
32
|
+
const neovim = neovimClient.get()
|
|
33
|
+
const testDirectory = await neovim.startNeovim({
|
|
28
34
|
additionalEnvironmentVariables: startArgs?.additionalEnvironmentVariables,
|
|
29
35
|
filename: startArgs?.filename ?? "initial-file.txt",
|
|
30
36
|
startupScriptModifications: startArgs?.startupScriptModifications ?? [],
|
|
@@ -32,13 +38,13 @@ window.startNeovim = async function (startArgs?: StartNeovimGenericArguments): P
|
|
|
32
38
|
|
|
33
39
|
const neovimBrowserApi: GenericNeovimBrowserApi = {
|
|
34
40
|
runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput> {
|
|
35
|
-
return
|
|
41
|
+
return neovim.runBlockingShellCommand(input)
|
|
36
42
|
},
|
|
37
43
|
runLuaCode(input) {
|
|
38
|
-
return
|
|
44
|
+
return neovim.runLuaCode(input)
|
|
39
45
|
},
|
|
40
46
|
runExCommand(input) {
|
|
41
|
-
return
|
|
47
|
+
return neovim.runExCommand(input)
|
|
42
48
|
},
|
|
43
49
|
dir: testDirectory,
|
|
44
50
|
}
|
|
@@ -49,5 +55,19 @@ window.startNeovim = async function (startArgs?: StartNeovimGenericArguments): P
|
|
|
49
55
|
declare global {
|
|
50
56
|
interface Window {
|
|
51
57
|
startNeovim(startArguments?: StartNeovimGenericArguments): Promise<GenericNeovimBrowserApi>
|
|
58
|
+
startTerminalApplication(args: StartTerminalGenericArguments): Promise<GenericTerminalBrowserApi>
|
|
52
59
|
}
|
|
53
60
|
}
|
|
61
|
+
|
|
62
|
+
export type GenericTerminalBrowserApi = {
|
|
63
|
+
dir: TestDirectory
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Entrypoint for the test runner (cypress) */
|
|
67
|
+
window.startTerminalApplication = async function (
|
|
68
|
+
args: StartTerminalGenericArguments
|
|
69
|
+
): Promise<GenericTerminalBrowserApi> {
|
|
70
|
+
const terminal = terminalClient.get()
|
|
71
|
+
const testDirectory = await terminal.startTerminalApplication(args)
|
|
72
|
+
return { dir: testDirectory }
|
|
73
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// This is the public client api. Semantic versioning will be applied to this.
|
|
2
2
|
|
|
3
3
|
export { rgbify } from "./color-utilities.js"
|
|
4
|
-
export { TerminalClient } from "./terminal-client.js"
|
|
4
|
+
export { NeovimTerminalClient as TerminalClient } from "./neovim-terminal-client.js"
|
|
5
|
+
export { TerminalTerminalClient } from "./terminal-terminal-client.js"
|
|
@@ -19,7 +19,7 @@ import { getTabId, startTerminal } from "./websocket-client.js"
|
|
|
19
19
|
|
|
20
20
|
/** Manages the terminal state in the browser as well as the (browser's)
|
|
21
21
|
* connection to the server side terminal application api. */
|
|
22
|
-
export class
|
|
22
|
+
export class NeovimTerminalClient {
|
|
23
23
|
private readonly ready: Promise<void>
|
|
24
24
|
private readonly tabId: { tabId: string }
|
|
25
25
|
private readonly terminal: Terminal
|
|
@@ -59,7 +59,7 @@ export class TerminalClient {
|
|
|
59
59
|
// start listening to Neovim stdout - this will take some (short) amount of
|
|
60
60
|
// time to complete
|
|
61
61
|
this.ready = new Promise<void>(resolve => {
|
|
62
|
-
console.log("Subscribing to
|
|
62
|
+
console.log("Subscribing to stdout")
|
|
63
63
|
trpc.neovim.onStdout.subscribe(
|
|
64
64
|
{ client: tabId },
|
|
65
65
|
{
|
|
@@ -70,7 +70,7 @@ export class TerminalClient {
|
|
|
70
70
|
terminal.write(data)
|
|
71
71
|
},
|
|
72
72
|
onError(err: unknown) {
|
|
73
|
-
console.error(`Error from
|
|
73
|
+
console.error(`Error from the application`, err)
|
|
74
74
|
},
|
|
75
75
|
}
|
|
76
76
|
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createTRPCClient, httpBatchLink, splitLink, unstable_httpSubscriptionLink } from "@trpc/client"
|
|
2
|
+
import type { Terminal } from "@xterm/xterm"
|
|
3
|
+
import "@xterm/xterm/css/xterm.css"
|
|
4
|
+
import type { AppRouter } from "../server/server.js"
|
|
5
|
+
import type { StartTerminalGenericArguments } from "../server/terminal/TerminalTestApplication.js"
|
|
6
|
+
import type { TestDirectory } from "../server/types.js"
|
|
7
|
+
import "./style.css"
|
|
8
|
+
import { getTabId, startTerminal } from "./websocket-client.js"
|
|
9
|
+
|
|
10
|
+
/** Manages the terminal state in the browser as well as the (browser's)
|
|
11
|
+
* connection to the server side terminal application api. */
|
|
12
|
+
export class TerminalTerminalClient {
|
|
13
|
+
private readonly ready: Promise<void>
|
|
14
|
+
private readonly tabId: { tabId: string }
|
|
15
|
+
private readonly terminal: Terminal
|
|
16
|
+
private readonly trpc: ReturnType<typeof createTRPCClient<AppRouter>>
|
|
17
|
+
|
|
18
|
+
constructor(app: HTMLElement) {
|
|
19
|
+
const trpc = createTRPCClient<AppRouter>({
|
|
20
|
+
links: [
|
|
21
|
+
splitLink({
|
|
22
|
+
condition: operation => operation.type === "subscription",
|
|
23
|
+
true: unstable_httpSubscriptionLink({
|
|
24
|
+
url: "/trpc",
|
|
25
|
+
}),
|
|
26
|
+
false: httpBatchLink({
|
|
27
|
+
url: "/trpc",
|
|
28
|
+
}),
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
})
|
|
32
|
+
this.trpc = trpc
|
|
33
|
+
|
|
34
|
+
this.tabId = getTabId()
|
|
35
|
+
const tabId = this.tabId
|
|
36
|
+
|
|
37
|
+
const terminal = startTerminal(app, {
|
|
38
|
+
onMouseEvent(data: string) {
|
|
39
|
+
void trpc.terminal.sendStdin.mutate({ tabId, data }).catch((error: unknown) => {
|
|
40
|
+
console.error(`Error sending mouse event`, error)
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
onKeyPress(event) {
|
|
44
|
+
void trpc.terminal.sendStdin.mutate({ tabId, data: event.key })
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
this.terminal = terminal
|
|
48
|
+
|
|
49
|
+
// start listening to Neovim stdout - this will take some (short) amount of
|
|
50
|
+
// time to complete
|
|
51
|
+
this.ready = new Promise<void>(resolve => {
|
|
52
|
+
console.log("Subscribing to stdout")
|
|
53
|
+
trpc.terminal.onStdout.subscribe(
|
|
54
|
+
{ client: tabId },
|
|
55
|
+
{
|
|
56
|
+
onStarted() {
|
|
57
|
+
resolve()
|
|
58
|
+
},
|
|
59
|
+
onData(data: string) {
|
|
60
|
+
terminal.write(data)
|
|
61
|
+
},
|
|
62
|
+
onError(err: unknown) {
|
|
63
|
+
console.error(`Error from the application`, err)
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async startTerminalApplication(args: StartTerminalGenericArguments): Promise<TestDirectory> {
|
|
71
|
+
await this.ready
|
|
72
|
+
const testDirectory = await this.trpc.terminal.start.mutate({
|
|
73
|
+
tabId: this.tabId,
|
|
74
|
+
startTerminalArguments: {
|
|
75
|
+
additionalEnvironmentVariables: args.additionalEnvironmentVariables,
|
|
76
|
+
commandToRun: args.commandToRun,
|
|
77
|
+
terminalDimensions: {
|
|
78
|
+
cols: this.terminal.cols,
|
|
79
|
+
rows: this.terminal.rows,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return testDirectory
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -10,7 +10,10 @@ export async function createCypressSupportFileContents(): Promise<string> {
|
|
|
10
10
|
//
|
|
11
11
|
// This file is autogenerated by tui-sandbox. Do not edit it directly.
|
|
12
12
|
//
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
GenericNeovimBrowserApi,
|
|
15
|
+
GenericTerminalBrowserApi,
|
|
16
|
+
} from "@tui-sandbox/library/dist/src/browser/neovim-client"
|
|
14
17
|
import type {
|
|
15
18
|
BlockingCommandClientInput,
|
|
16
19
|
ExCommandClientInput,
|
|
@@ -23,9 +26,19 @@ import type {
|
|
|
23
26
|
StartNeovimGenericArguments,
|
|
24
27
|
TestDirectory,
|
|
25
28
|
} from "@tui-sandbox/library/dist/src/server/types"
|
|
29
|
+
import type { StartTerminalGenericArguments } from "@tui-sandbox/library/src/server/terminal/TerminalTestApplication"
|
|
26
30
|
import type { OverrideProperties } from "type-fest"
|
|
27
31
|
import type { MyTestDirectory, MyTestDirectoryFile } from "../../MyTestDirectory"
|
|
28
32
|
|
|
33
|
+
export type TerminalTestApplicationContext = {
|
|
34
|
+
/** Types text into the terminal, making the terminal application receive the
|
|
35
|
+
* keystrokes as input. Requires the application to be running. */
|
|
36
|
+
typeIntoTerminal(text: string, options?: Partial<Cypress.TypeOptions>): void
|
|
37
|
+
|
|
38
|
+
/** The test directory, providing type-safe access to its file and directory structure */
|
|
39
|
+
dir: TestDirectory<MyTestDirectory>
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
/** The api that can be used in tests after a Neovim instance has been started. */
|
|
30
43
|
export type NeovimContext = {
|
|
31
44
|
/** Types text into the terminal, making the terminal application receive
|
|
@@ -93,6 +106,14 @@ Cypress.Commands.add("startNeovim", (startArguments?: MyStartNeovimServerArgumen
|
|
|
93
106
|
})
|
|
94
107
|
})
|
|
95
108
|
|
|
109
|
+
Cypress.Commands.add("startTerminalApplication", (args: StartTerminalGenericArguments) => {
|
|
110
|
+
cy.window().then(async win => {
|
|
111
|
+
const api: GenericTerminalBrowserApi = await win.startTerminalApplication(args)
|
|
112
|
+
|
|
113
|
+
return api
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
96
117
|
Cypress.Commands.add("typeIntoTerminal", (text: string, options?: Partial<Cypress.TypeOptions>) => {
|
|
97
118
|
// the syntax for keys is described here:
|
|
98
119
|
// https://docs.cypress.io/api/commands/type
|
|
@@ -112,6 +133,7 @@ declare global {
|
|
|
112
133
|
namespace Cypress {
|
|
113
134
|
interface Chainable {
|
|
114
135
|
startNeovim(args?: MyStartNeovimServerArguments): Chainable<NeovimContext>
|
|
136
|
+
startTerminalApplication(args: StartTerminalGenericArguments): Chainable<TerminalTestApplicationContext>
|
|
115
137
|
|
|
116
138
|
/** Types text into the terminal, making the terminal application receive
|
|
117
139
|
* the keystrokes as input. Requires neovim to be running. */
|
|
@@ -39,6 +39,7 @@ describe("dirtree", () => {
|
|
|
39
39
|
name: z.literal("test-environment/"),
|
|
40
40
|
type: z.literal("directory"),
|
|
41
41
|
contents: z.object({
|
|
42
|
+
".bashrc": z.object({ name: z.literal(".bashrc"), type: z.literal("file") }),
|
|
42
43
|
".config": z.object({
|
|
43
44
|
name: z.literal(".config/"),
|
|
44
45
|
type: z.literal("directory"),
|
|
@@ -127,6 +128,7 @@ describe("dirtree", () => {
|
|
|
127
128
|
export type MyDirectoryTree = MyDirectoryTreeContentsSchemaType["contents"]
|
|
128
129
|
|
|
129
130
|
export const testDirectoryFiles = z.enum([
|
|
131
|
+
".bashrc",
|
|
130
132
|
".config/.gitkeep",
|
|
131
133
|
".config/nvim/init.lua",
|
|
132
134
|
".config/nvim/prepare.lua",
|
|
@@ -5,7 +5,7 @@ import { access } from "fs/promises"
|
|
|
5
5
|
import type { NeovimClient as NeovimApiClient } from "neovim"
|
|
6
6
|
import { tmpdir } from "os"
|
|
7
7
|
import path, { join } from "path"
|
|
8
|
-
import type { TestDirectory } from "../types.js"
|
|
8
|
+
import type { TestDirectory, TestEnvironmentCommonEnvironmentVariables } from "../types.js"
|
|
9
9
|
import { DisposableSingleApplication } from "../utilities/DisposableSingleApplication.js"
|
|
10
10
|
import type { Lazy } from "../utilities/Lazy.js"
|
|
11
11
|
import { TerminalApplication } from "../utilities/TerminalApplication.js"
|
|
@@ -77,7 +77,7 @@ type ResettableState = {
|
|
|
77
77
|
client: Lazy<Promise<NeovimApiClient>>
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export class NeovimApplication {
|
|
80
|
+
export class NeovimApplication implements AsyncDisposable {
|
|
81
81
|
public state: ResettableState | undefined
|
|
82
82
|
public readonly events: EventEmitter
|
|
83
83
|
|
|
@@ -190,7 +190,7 @@ export class NeovimApplication {
|
|
|
190
190
|
XDG_DATA_HOME: join(testDirectory.testEnvironmentPath, ".repro", "data"),
|
|
191
191
|
|
|
192
192
|
...additionalEnvironmentVariables,
|
|
193
|
-
}
|
|
193
|
+
} satisfies TestEnvironmentCommonEnvironmentVariables
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
async [Symbol.asyncDispose](): Promise<void> {
|
|
@@ -13,12 +13,12 @@ class TempDirectory implements Disposable {
|
|
|
13
13
|
|
|
14
14
|
public static create(): TempDirectory {
|
|
15
15
|
const tmp = fs.mkdtempSync("test-temp-dir-" satisfies TestTempDirPrefix)
|
|
16
|
-
|
|
16
|
+
const absolutePath = nodePath.resolve(tmp)
|
|
17
|
+
return new TempDirectory(absolutePath)
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
[Symbol.dispose](): void {
|
|
20
|
-
|
|
21
|
-
fs.rm(this.path, { recursive: true }, () => {})
|
|
21
|
+
fs.rmdirSync(this.path, { recursive: true, maxRetries: 5 })
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -32,6 +32,6 @@ it("should create a temp dir with no contents", async () => {
|
|
|
32
32
|
|
|
33
33
|
expect(result.contents).toEqual({})
|
|
34
34
|
expect(result.testEnvironmentPath).toEqual(dir.path)
|
|
35
|
-
expect(result.testEnvironmentPath.
|
|
36
|
-
expect(result.testEnvironmentPathRelative.
|
|
35
|
+
expect(result.testEnvironmentPath.includes("test-temp-dir-" satisfies TestTempDirPrefix)).toBeTruthy()
|
|
36
|
+
expect(result.testEnvironmentPathRelative.includes("testdirs" satisfies TestDirsPath)).toBeTruthy()
|
|
37
37
|
})
|
package/src/server/server.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Except } from "type-fest"
|
|
|
4
4
|
import { z } from "zod"
|
|
5
5
|
import { trpc } from "./connection/trpc.js"
|
|
6
6
|
import * as neovim from "./neovim/index.js"
|
|
7
|
+
import * as terminal from "./terminal/index.js"
|
|
7
8
|
import { TestServer } from "./TestServer.js"
|
|
8
9
|
import type { DirectoriesConfig, TestServerConfig } from "./updateTestdirectorySchemaFile.js"
|
|
9
10
|
import { applicationAvailable } from "./utilities/applicationAvailable.js"
|
|
@@ -45,6 +46,39 @@ export async function createAppRouter(config: DirectoriesConfig) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const appRouter = trpc.router({
|
|
49
|
+
terminal: trpc.router({
|
|
50
|
+
onStdout: trpc.procedure.input(z.object({ client: tabIdSchema })).subscription(options => {
|
|
51
|
+
return terminal.initializeStdout(options.input, options.signal, config.testEnvironmentPath)
|
|
52
|
+
}),
|
|
53
|
+
|
|
54
|
+
start: trpc.procedure
|
|
55
|
+
.input(
|
|
56
|
+
z.object({
|
|
57
|
+
tabId: tabIdSchema,
|
|
58
|
+
startTerminalArguments: z.object({
|
|
59
|
+
commandToRun: z.array(z.string()),
|
|
60
|
+
additionalEnvironmentVariables: z.record(z.string()).optional(),
|
|
61
|
+
terminalDimensions: z.object({
|
|
62
|
+
cols: z.number(),
|
|
63
|
+
rows: z.number(),
|
|
64
|
+
}),
|
|
65
|
+
}),
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
.mutation(options => {
|
|
69
|
+
return terminal.start(
|
|
70
|
+
options.input.startTerminalArguments.terminalDimensions,
|
|
71
|
+
options.input.startTerminalArguments.commandToRun,
|
|
72
|
+
options.input.tabId,
|
|
73
|
+
config
|
|
74
|
+
)
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
sendStdin: trpc.procedure.input(z.object({ tabId: tabIdSchema, data: z.string() })).mutation(options => {
|
|
78
|
+
return terminal.sendStdin(options.input)
|
|
79
|
+
}),
|
|
80
|
+
}),
|
|
81
|
+
|
|
48
82
|
neovim: trpc.router({
|
|
49
83
|
start: trpc.procedure
|
|
50
84
|
.input(
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
import { exec } from "child_process"
|
|
3
|
+
import EventEmitter from "events"
|
|
4
|
+
import { join } from "path"
|
|
5
|
+
import type { StdoutOrStderrMessage, TerminalDimensions } from "../neovim/NeovimApplication.js"
|
|
6
|
+
import type { TestDirectory, TestEnvironmentCommonEnvironmentVariables } from "../types.js"
|
|
7
|
+
import { DisposableSingleApplication } from "../utilities/DisposableSingleApplication.js"
|
|
8
|
+
import { TerminalApplication } from "../utilities/TerminalApplication.js"
|
|
9
|
+
|
|
10
|
+
type ResettableState = {
|
|
11
|
+
testDirectory: TestDirectory
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type StartTerminalGenericArguments = {
|
|
15
|
+
commandToRun: string[]
|
|
16
|
+
additionalEnvironmentVariables?: Record<string, string> | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default class TerminalTestApplication implements AsyncDisposable {
|
|
20
|
+
public state: ResettableState | undefined
|
|
21
|
+
public readonly events: EventEmitter
|
|
22
|
+
|
|
23
|
+
public constructor(
|
|
24
|
+
private readonly testEnvironmentPath: string,
|
|
25
|
+
public readonly application: DisposableSingleApplication = new DisposableSingleApplication()
|
|
26
|
+
) {
|
|
27
|
+
this.events = new EventEmitter()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async startNextAndKillCurrent(
|
|
31
|
+
testDirectory: TestDirectory,
|
|
32
|
+
startArgs: StartTerminalGenericArguments,
|
|
33
|
+
terminalDimensions: TerminalDimensions
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
await this[Symbol.asyncDispose]()
|
|
36
|
+
assert(
|
|
37
|
+
this.state === undefined,
|
|
38
|
+
"TerminalTestApplication state should be undefined after disposing so that no previous state is reused."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const command = startArgs.commandToRun[0]
|
|
42
|
+
assert(command, "No command to run was provided.")
|
|
43
|
+
// TODO could check if the command is executable
|
|
44
|
+
const terminalArguments = startArgs.commandToRun.slice(1)
|
|
45
|
+
|
|
46
|
+
const stdout = this.events
|
|
47
|
+
|
|
48
|
+
await this.application.startNextAndKillCurrent(async () => {
|
|
49
|
+
const env = this.getEnvironmentVariables(testDirectory, startArgs.additionalEnvironmentVariables)
|
|
50
|
+
return TerminalApplication.start({
|
|
51
|
+
command,
|
|
52
|
+
args: terminalArguments,
|
|
53
|
+
|
|
54
|
+
cwd: this.testEnvironmentPath,
|
|
55
|
+
env: env,
|
|
56
|
+
dimensions: terminalDimensions,
|
|
57
|
+
|
|
58
|
+
onStdoutOrStderr(data) {
|
|
59
|
+
data satisfies string
|
|
60
|
+
stdout.emit("stdout" satisfies StdoutOrStderrMessage, data)
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const processId = this.application.processId()
|
|
66
|
+
assert(
|
|
67
|
+
processId !== undefined,
|
|
68
|
+
"TerminalApplication was started without a process ID. This is a bug - please open an issue."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
this.state = { testDirectory }
|
|
72
|
+
|
|
73
|
+
console.log(`🚀 Started Terminal instance ${processId}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public getEnvironmentVariables(
|
|
77
|
+
testDirectory: TestDirectory,
|
|
78
|
+
additionalEnvironmentVariables?: Record<string, string>
|
|
79
|
+
): NodeJS.ProcessEnv {
|
|
80
|
+
return {
|
|
81
|
+
...process.env,
|
|
82
|
+
HOME: testDirectory.rootPathAbsolute,
|
|
83
|
+
XDG_CONFIG_HOME: join(testDirectory.rootPathAbsolute, ".config"),
|
|
84
|
+
XDG_DATA_HOME: join(testDirectory.testEnvironmentPath, ".repro", "data"),
|
|
85
|
+
...additionalEnvironmentVariables,
|
|
86
|
+
} satisfies TestEnvironmentCommonEnvironmentVariables
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
90
|
+
await this.application[Symbol.asyncDispose]()
|
|
91
|
+
|
|
92
|
+
if (!this.state) return
|
|
93
|
+
|
|
94
|
+
exec(`rm -rf ${this.state.testDirectory.rootPathAbsolute}`)
|
|
95
|
+
|
|
96
|
+
this.state = undefined
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
import "core-js/proposals/async-explicit-resource-management.js"
|
|
3
|
+
import { prepareNewTestDirectory } from "../neovim/index.js"
|
|
4
|
+
import type { TerminalDimensions } from "../neovim/NeovimApplication.js"
|
|
5
|
+
import type { DirectoriesConfig } from "../updateTestdirectorySchemaFile.js"
|
|
6
|
+
import { convertEventEmitterToAsyncGenerator } from "../utilities/generator.js"
|
|
7
|
+
import { Lazy } from "../utilities/Lazy.js"
|
|
8
|
+
import type { TabId } from "../utilities/tabId.js"
|
|
9
|
+
import TerminalTestApplication from "./TerminalTestApplication.js"
|
|
10
|
+
|
|
11
|
+
const terminals = new Map<TabId["tabId"], TerminalTestApplication>()
|
|
12
|
+
const resources: Lazy<AsyncDisposableStack> = new Lazy(() => {
|
|
13
|
+
return new AsyncDisposableStack()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export async function start(
|
|
17
|
+
terminalDimensions: TerminalDimensions,
|
|
18
|
+
commandToRun: string[],
|
|
19
|
+
tabId: TabId,
|
|
20
|
+
config: DirectoriesConfig
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const app = terminals.get(tabId.tabId)
|
|
23
|
+
assert(app, `Terminal with tabId ${tabId.tabId} not found.`)
|
|
24
|
+
const testDirectory = await prepareNewTestDirectory(config)
|
|
25
|
+
await app.startNextAndKillCurrent(testDirectory, { commandToRun }, terminalDimensions)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function initializeStdout(
|
|
29
|
+
options: { client: TabId },
|
|
30
|
+
signal: AbortSignal | undefined,
|
|
31
|
+
testEnvironmentPath: string
|
|
32
|
+
): Promise<AsyncGenerator<string, void, unknown>> {
|
|
33
|
+
const tabId = options.client.tabId
|
|
34
|
+
const app = terminals.get(tabId) ?? new TerminalTestApplication(testEnvironmentPath)
|
|
35
|
+
if (terminals.get(tabId) === undefined) {
|
|
36
|
+
terminals.set(tabId, app)
|
|
37
|
+
resources.get().adopt(app, async a => {
|
|
38
|
+
await a[Symbol.asyncDispose]()
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const stdout = convertEventEmitterToAsyncGenerator(app.events, "stdout")
|
|
43
|
+
signal?.addEventListener("abort", () => {
|
|
44
|
+
void app[Symbol.asyncDispose]().finally(() => {
|
|
45
|
+
terminals.delete(tabId)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return stdout
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function sendStdin(options: { tabId: TabId; data: string }): Promise<void> {
|
|
53
|
+
const tabId = options.tabId.tabId
|
|
54
|
+
const app = terminals.get(tabId)
|
|
55
|
+
assert(app !== undefined, `Terminal instance for clientId not found - cannot send stdin. Maybe it's not started yet?`)
|
|
56
|
+
assert(
|
|
57
|
+
app.application,
|
|
58
|
+
`Terminal application not found for client id ${options.tabId.tabId}. Maybe it's not started yet?`
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
await app.application.write(options.data)
|
|
62
|
+
}
|
package/src/server/types.ts
CHANGED
|
@@ -31,6 +31,19 @@ export type TestDirectory<TContents extends object = object> = {
|
|
|
31
31
|
contents: TContents
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export type TestEnvironmentCommonEnvironmentVariables = {
|
|
35
|
+
HOME: string
|
|
36
|
+
|
|
37
|
+
// this is needed so that the application being tested can load its
|
|
38
|
+
// configuration, emulating a common setup real users have
|
|
39
|
+
XDG_CONFIG_HOME: string
|
|
40
|
+
|
|
41
|
+
// the data directory is where the application stores its data. To prevent
|
|
42
|
+
// downloading a new set of plugins/whatever for each test, share the data
|
|
43
|
+
// directory.
|
|
44
|
+
XDG_DATA_HOME: string
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
export type { StartNeovimGenericArguments } from "../server/neovim/NeovimApplication.js"
|
|
35
48
|
|
|
36
49
|
export type BlockingShellCommandOutput =
|
|
@@ -18,7 +18,8 @@ export class TerminalApplication implements StartableApplication {
|
|
|
18
18
|
private constructor(
|
|
19
19
|
private readonly subProcess: IPty,
|
|
20
20
|
public readonly onStdoutOrStderr: (data: string) => void,
|
|
21
|
-
public readonly untilExit: Promise<ExitInfo
|
|
21
|
+
public readonly untilExit: Promise<ExitInfo>,
|
|
22
|
+
public readonly name: string
|
|
22
23
|
) {
|
|
23
24
|
this.processId = subProcess.pid
|
|
24
25
|
|
|
@@ -30,12 +31,11 @@ export class TerminalApplication implements StartableApplication {
|
|
|
30
31
|
|
|
31
32
|
this.logger.debug(`started`)
|
|
32
33
|
|
|
33
|
-
subProcess.onData(this.onStdoutOrStderr)
|
|
34
|
-
|
|
35
34
|
subProcess.onExit(({ exitCode, signal }) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
35
|
+
signal satisfies number | undefined
|
|
36
|
+
const msg = `Child process ${this.processId} (${this.name}) exited with code ${String(exitCode)} and signal ${String(signal)}`
|
|
37
|
+
this.onStdoutOrStderr(msg)
|
|
38
|
+
this.logger.debug(msg)
|
|
39
39
|
})
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -55,7 +55,8 @@ export class TerminalApplication implements StartableApplication {
|
|
|
55
55
|
env?: NodeJS.ProcessEnv
|
|
56
56
|
dimensions: ITerminalDimensions
|
|
57
57
|
}): TerminalApplication {
|
|
58
|
-
console.log(`Starting '${command} ${args.join(" ")}' in cwd '${cwd}'`)
|
|
58
|
+
console.log(`Starting '${command}' with args '${args.join(" ")}' in cwd '${cwd}'`)
|
|
59
|
+
|
|
59
60
|
const ptyProcess = pty.spawn(command, args, {
|
|
60
61
|
name: "xterm-color",
|
|
61
62
|
cwd,
|
|
@@ -63,6 +64,7 @@ export class TerminalApplication implements StartableApplication {
|
|
|
63
64
|
cols: dimensions.cols,
|
|
64
65
|
rows: dimensions.rows,
|
|
65
66
|
})
|
|
67
|
+
ptyProcess.onData(onStdoutOrStderr)
|
|
66
68
|
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
67
69
|
console.log(`Child process exited with code ${exitCode} and signal ${signal}`)
|
|
68
70
|
})
|
|
@@ -79,7 +81,7 @@ export class TerminalApplication implements StartableApplication {
|
|
|
79
81
|
})
|
|
80
82
|
})
|
|
81
83
|
|
|
82
|
-
return new TerminalApplication(ptyProcess, onStdoutOrStderr, untilExit)
|
|
84
|
+
return new TerminalApplication(ptyProcess, onStdoutOrStderr, untilExit, ptyProcess.process satisfies string)
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
/** Write to the terminal's stdin. */
|