@tui-sandbox/library 2.0.2 → 2.2.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 +14 -0
- package/dist/src/client/neovim-client.d.ts +2 -1
- package/dist/src/client/neovim-client.js +12 -3
- package/dist/src/server/TestServer.d.ts +1 -1
- package/dist/src/server/TestServer.js +15 -24
- package/dist/src/server/connection/trpc.d.ts +7 -15
- package/dist/src/server/connection/trpc.js +0 -3
- package/dist/src/server/neovim/NeovimApplication.d.ts +3 -2
- package/dist/src/server/neovim/NeovimApplication.js +21 -13
- package/dist/src/server/neovim/environment/createTempDir.test.js +1 -0
- package/dist/src/server/neovim/index.d.ts +1 -2
- package/dist/src/server/neovim/index.js +15 -21
- package/dist/src/server/server.d.ts +8 -7
- package/dist/src/server/server.js +9 -3
- package/dist/src/server/server.test.d.ts +1 -0
- package/dist/src/server/server.test.js +23 -0
- package/dist/src/server/updateTestdirectorySchemaFile.d.ts +2 -1
- package/dist/src/server/updateTestdirectorySchemaFile.js +4 -0
- package/dist/src/server/updateTestdirectorySchemaFile.test.d.ts +1 -0
- package/dist/src/server/updateTestdirectorySchemaFile.test.js +34 -0
- package/dist/src/server/utilities/DisposableSingleApplication.d.ts +6 -8
- package/dist/src/server/utilities/DisposableSingleApplication.js +9 -9
- package/dist/src/server/utilities/DisposableSingleApplication.test.d.ts +1 -0
- package/dist/src/server/utilities/DisposableSingleApplication.test.js +55 -0
- package/dist/src/server/utilities/TerminalApplication.d.ts +2 -1
- package/dist/src/server/utilities/applicationAvailable.d.ts +1 -0
- package/dist/src/server/utilities/applicationAvailable.js +4 -0
- package/dist/src/server/utilities/applicationAvailable.test.d.ts +1 -0
- package/dist/src/server/utilities/applicationAvailable.test.js +12 -0
- package/dist/src/server/utilities/generator.d.ts +2 -0
- package/dist/src/server/utilities/generator.js +8 -0
- package/dist/src/server/utilities/generator.test.d.ts +1 -0
- package/dist/src/server/utilities/generator.test.js +41 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +8 -0
- package/package.json +13 -10
- package/src/client/neovim-client.ts +14 -4
- package/src/server/TestServer.ts +17 -25
- package/src/server/connection/trpc.ts +1 -15
- package/src/server/neovim/NeovimApplication.ts +24 -14
- package/src/server/neovim/environment/createTempDir.test.ts +2 -0
- package/src/server/neovim/index.ts +25 -27
- package/src/server/server.test.ts +34 -0
- package/src/server/server.ts +11 -4
- package/src/server/updateTestdirectorySchemaFile.test.ts +43 -0
- package/src/server/updateTestdirectorySchemaFile.ts +7 -2
- package/src/server/utilities/DisposableSingleApplication.test.ts +70 -0
- package/src/server/utilities/DisposableSingleApplication.ts +17 -12
- package/src/server/utilities/TerminalApplication.ts +2 -1
- package/src/server/utilities/applicationAvailable.test.ts +14 -0
- package/src/server/utilities/applicationAvailable.ts +5 -0
- package/src/server/utilities/generator.test.ts +49 -0
- package/src/server/utilities/generator.ts +13 -0
- package/tsconfig.json +2 -2
- package/vitest.config.ts +9 -0
package/package.json
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tui-sandbox/library",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@catppuccin/palette": "1.
|
|
8
|
-
"@trpc/client": "11.0.0-rc.
|
|
9
|
-
"@trpc/server": "11.0.0-rc.
|
|
10
|
-
"@types/ws": "8.5.12",
|
|
7
|
+
"@catppuccin/palette": "1.5.0",
|
|
8
|
+
"@trpc/client": "11.0.0-rc.608",
|
|
9
|
+
"@trpc/server": "11.0.0-rc.608",
|
|
11
10
|
"@xterm/addon-attach": "0.11.0",
|
|
12
11
|
"@xterm/addon-fit": "0.10.0",
|
|
13
12
|
"@xterm/xterm": "5.5.0",
|
|
14
|
-
"
|
|
13
|
+
"command-exists": "1.2.9",
|
|
14
|
+
"core-js": "3.39.0",
|
|
15
|
+
"cors": "2.8.5",
|
|
15
16
|
"dree": "5.1.5",
|
|
16
17
|
"node-pty": "1.0.0",
|
|
17
18
|
"prettier": "3.3.3",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
19
|
+
"type-fest": "4.26.1",
|
|
20
|
+
"winston": "3.16.0",
|
|
20
21
|
"zod": "3.23.8"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@runtyping/zod": "2.1.1",
|
|
24
|
-
"@types/
|
|
25
|
+
"@types/command-exists": "1.2.3",
|
|
26
|
+
"@types/cors": "2.8.17",
|
|
27
|
+
"@types/node": "22.8.6",
|
|
25
28
|
"nodemon": "3.1.7",
|
|
26
|
-
"vitest": "2.1.
|
|
29
|
+
"vitest": "2.1.4"
|
|
27
30
|
},
|
|
28
31
|
"scripts": {
|
|
29
32
|
"build": "tsc",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createTRPCClient,
|
|
1
|
+
import { createTRPCClient, httpBatchLink, splitLink, unstable_httpSubscriptionLink } from "@trpc/client"
|
|
2
2
|
import type { Terminal } from "@xterm/xterm"
|
|
3
3
|
import "@xterm/xterm/css/xterm.css"
|
|
4
|
+
import type { Except } from "type-fest"
|
|
4
5
|
import type { StartNeovimGenericArguments } from "../server/neovim/NeovimApplication.ts"
|
|
5
6
|
import type { AppRouter } from "../server/server.ts"
|
|
6
7
|
import type { TestDirectory } from "../server/types.ts"
|
|
@@ -14,9 +15,18 @@ export class NeovimClient {
|
|
|
14
15
|
private readonly trpc: ReturnType<typeof createTRPCClient<AppRouter>>
|
|
15
16
|
|
|
16
17
|
constructor(app: HTMLElement) {
|
|
17
|
-
const wsClient = createWSClient({ url: `ws://localhost:3000`, WebSocket })
|
|
18
18
|
const trpc = createTRPCClient<AppRouter>({
|
|
19
|
-
links: [
|
|
19
|
+
links: [
|
|
20
|
+
splitLink({
|
|
21
|
+
condition: operation => operation.type === "subscription",
|
|
22
|
+
true: unstable_httpSubscriptionLink({
|
|
23
|
+
url: "http://localhost:3000",
|
|
24
|
+
}),
|
|
25
|
+
false: httpBatchLink({
|
|
26
|
+
url: "http://localhost:3000",
|
|
27
|
+
}),
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
20
30
|
})
|
|
21
31
|
this.trpc = trpc
|
|
22
32
|
|
|
@@ -56,7 +66,7 @@ export class NeovimClient {
|
|
|
56
66
|
})
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
public async startNeovim(args: StartNeovimGenericArguments): Promise<TestDirectory> {
|
|
69
|
+
public async startNeovim(args: Except<StartNeovimGenericArguments, "terminalDimensions">): Promise<TestDirectory> {
|
|
60
70
|
await this.ready
|
|
61
71
|
|
|
62
72
|
const neovim = await this.trpc.neovim.start.mutate({
|
package/src/server/TestServer.ts
CHANGED
|
@@ -1,53 +1,45 @@
|
|
|
1
1
|
import type { AnyTRPCRouter } from "@trpc/server"
|
|
2
|
-
import {
|
|
2
|
+
import { createHTTPServer } from "@trpc/server/adapters/standalone"
|
|
3
3
|
import "core-js/proposals/async-explicit-resource-management"
|
|
4
|
+
import cors from "cors"
|
|
4
5
|
import { once } from "events"
|
|
5
|
-
import { WebSocketServer } from "ws"
|
|
6
|
-
import { createContext } from "./connection/trpc"
|
|
7
6
|
import type { TestServerConfig } from "./updateTestdirectorySchemaFile"
|
|
8
7
|
import { updateTestdirectorySchemaFile } from "./updateTestdirectorySchemaFile"
|
|
9
8
|
|
|
10
9
|
export class TestServer {
|
|
11
10
|
public constructor(private readonly port: number) {}
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
public async startAndRun<TRouter extends AnyTRPCRouter>(appRouter: TRouter, config: TestServerConfig): Promise<void> {
|
|
12
|
+
public async startAndRun(appRouter: AnyTRPCRouter, config: TestServerConfig): Promise<void> {
|
|
15
13
|
console.log("🚀 Server starting")
|
|
16
14
|
|
|
17
15
|
await updateTestdirectorySchemaFile(config)
|
|
18
16
|
|
|
19
|
-
const
|
|
20
|
-
const handler = applyWSSHandler<TRouter>({
|
|
21
|
-
wss,
|
|
17
|
+
const server = createHTTPServer({
|
|
22
18
|
router: appRouter,
|
|
23
|
-
createContext,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// server ping message interval in milliseconds
|
|
28
|
-
pingMs: 30_000,
|
|
29
|
-
// connection is terminated if pong message is not received in this many milliseconds
|
|
30
|
-
pongWaitMs: 5000,
|
|
31
|
-
},
|
|
19
|
+
createContext: () => ({}),
|
|
20
|
+
middleware: cors({
|
|
21
|
+
origin: "*",
|
|
22
|
+
}),
|
|
32
23
|
})
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
server.listen(this.port)
|
|
26
|
+
server.on("connection", socket => {
|
|
27
|
+
console.log(`➕➕ Connection`)
|
|
36
28
|
socket.once("close", () => {
|
|
37
|
-
console.log(`➖➖ Connection
|
|
29
|
+
console.log(`➖➖ Connection`)
|
|
38
30
|
})
|
|
39
31
|
})
|
|
40
|
-
|
|
32
|
+
|
|
33
|
+
console.log(`✅ Server listening on ws://localhost:${this.port}`)
|
|
41
34
|
|
|
42
35
|
await Promise.race([once(process, "SIGTERM"), once(process, "SIGINT")])
|
|
43
36
|
console.log("Shutting down...")
|
|
44
|
-
|
|
45
|
-
wss.close(error => {
|
|
37
|
+
server.close(error => {
|
|
46
38
|
if (error) {
|
|
47
|
-
console.error("Error closing
|
|
39
|
+
console.error("Error closing server", error)
|
|
48
40
|
process.exit(1)
|
|
49
41
|
}
|
|
50
|
-
console.log("
|
|
42
|
+
console.log("Server closed")
|
|
51
43
|
process.exit(0)
|
|
52
44
|
})
|
|
53
45
|
}
|
|
@@ -1,17 +1,3 @@
|
|
|
1
1
|
import { initTRPC } from "@trpc/server"
|
|
2
|
-
import type { CreateWSSContextFnOptions } from "@trpc/server/adapters/ws"
|
|
3
|
-
import type { Socket } from "net"
|
|
4
|
-
import type { WebSocket } from "ws"
|
|
5
2
|
|
|
6
|
-
export
|
|
7
|
-
clientId: WebSocket
|
|
8
|
-
socket: Socket
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function createContext(opts: CreateWSSContextFnOptions): Connection {
|
|
12
|
-
return { clientId: opts.res, socket: opts.req.socket }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type Context = Awaited<ReturnType<typeof createContext>>
|
|
16
|
-
|
|
17
|
-
export const trpc = initTRPC.context<Context>().create()
|
|
3
|
+
export const trpc = initTRPC.context<object>().create()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import assert from "assert"
|
|
1
2
|
import { exec } from "child_process"
|
|
2
3
|
import EventEmitter from "events"
|
|
3
4
|
import { existsSync } from "fs"
|
|
@@ -59,12 +60,14 @@ export type StartNeovimGenericArguments = {
|
|
|
59
60
|
startupScriptModifications?: string[]
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
export class NeovimApplication
|
|
63
|
+
export class NeovimApplication {
|
|
63
64
|
private testDirectory: TestDirectory | undefined
|
|
64
65
|
public readonly events: EventEmitter
|
|
65
66
|
|
|
66
|
-
public constructor(
|
|
67
|
-
|
|
67
|
+
public constructor(
|
|
68
|
+
private readonly testEnvironmentPath: string,
|
|
69
|
+
public readonly application: DisposableSingleApplication = new DisposableSingleApplication()
|
|
70
|
+
) {
|
|
68
71
|
this.events = new EventEmitter()
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -105,22 +108,29 @@ export class NeovimApplication extends DisposableSingleApplication {
|
|
|
105
108
|
}
|
|
106
109
|
const stdout = this.events
|
|
107
110
|
|
|
108
|
-
this.application
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
await this.application.startNextAndKillCurrent(async () => {
|
|
112
|
+
return TerminalApplication.start({
|
|
113
|
+
command: "nvim",
|
|
114
|
+
args: neovimArguments,
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
cwd: this.testEnvironmentPath,
|
|
117
|
+
env: process.env,
|
|
118
|
+
dimensions: startArgs.terminalDimensions,
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
onStdoutOrStderr(data) {
|
|
121
|
+
data satisfies string
|
|
122
|
+
stdout.emit("stdout" satisfies StdoutMessage, data)
|
|
123
|
+
},
|
|
124
|
+
})
|
|
119
125
|
})
|
|
126
|
+
|
|
127
|
+
const processId = this.application.processId()
|
|
128
|
+
assert(processId !== undefined, "Neovim was started without a process ID. This is a bug - please open an issue.")
|
|
129
|
+
console.log(`🚀 Started Neovim instance ${processId}`)
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
|
|
123
|
-
await
|
|
132
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
133
|
+
await this.application[Symbol.asyncDispose]()
|
|
124
134
|
if (this.testDirectory) {
|
|
125
135
|
exec(`rm -rf ${this.testDirectory.rootPathAbsolute}`)
|
|
126
136
|
}
|
|
@@ -4,6 +4,8 @@ import { expect, it } from "vitest"
|
|
|
4
4
|
import type { TestDirsPath } from "./createTempDir"
|
|
5
5
|
import { createTempDir } from "./createTempDir"
|
|
6
6
|
|
|
7
|
+
vi.spyOn(console, "log").mockImplementation(vi.fn())
|
|
8
|
+
|
|
7
9
|
type TestTempDirPrefix = "test-temp-dir-"
|
|
8
10
|
|
|
9
11
|
class TempDirectory implements Disposable {
|
|
@@ -1,37 +1,35 @@
|
|
|
1
|
-
import type { Observable } from "@trpc/server/observable"
|
|
2
|
-
import { observable } from "@trpc/server/observable"
|
|
3
1
|
import assert from "assert"
|
|
4
2
|
import type { TestDirectory } from "../types"
|
|
5
3
|
import type { TestServerConfig } from "../updateTestdirectorySchemaFile"
|
|
4
|
+
import { convertEventEmitterToAsyncGenerator } from "../utilities/generator"
|
|
6
5
|
import type { TabId } from "../utilities/tabId"
|
|
7
6
|
import { createTempDir } from "./environment/createTempDir"
|
|
8
|
-
import type { StartNeovimGenericArguments
|
|
7
|
+
import type { StartNeovimGenericArguments } from "./NeovimApplication"
|
|
9
8
|
import { NeovimApplication } from "./NeovimApplication"
|
|
10
9
|
|
|
11
10
|
const neovims = new Map<TabId["tabId"], NeovimApplication>()
|
|
12
11
|
|
|
13
|
-
export function onStdout(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return () => {
|
|
29
|
-
neovim.events.off("stdout" satisfies StdoutMessage, send)
|
|
12
|
+
export async function onStdout(
|
|
13
|
+
options: { client: TabId },
|
|
14
|
+
signal: AbortSignal | undefined,
|
|
15
|
+
testEnvironmentPath: string
|
|
16
|
+
): Promise<AsyncGenerator<string, void, unknown>> {
|
|
17
|
+
const tabId = options.client.tabId
|
|
18
|
+
const neovim = neovims.get(tabId) ?? new NeovimApplication(testEnvironmentPath)
|
|
19
|
+
if (neovims.get(tabId) === undefined) {
|
|
20
|
+
neovims.set(tabId, neovim)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const stdout = convertEventEmitterToAsyncGenerator(neovim.events, "stdout")
|
|
24
|
+
if (signal) {
|
|
25
|
+
signal.addEventListener("abort", () => {
|
|
30
26
|
void neovim[Symbol.asyncDispose]().finally(() => {
|
|
31
27
|
neovims.delete(tabId)
|
|
32
28
|
})
|
|
33
|
-
}
|
|
34
|
-
}
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return stdout
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
export async function start(
|
|
@@ -45,10 +43,6 @@ export async function start(
|
|
|
45
43
|
const testDirectory = await createTempDir(config)
|
|
46
44
|
await neovim.startNextAndKillCurrent(testDirectory, options)
|
|
47
45
|
|
|
48
|
-
const processId = neovim.processId()
|
|
49
|
-
assert(processId !== undefined, "Neovim was started without a process ID. This is a bug - please open an issue.")
|
|
50
|
-
console.log(`🚀 Started Neovim instance ${processId}`)
|
|
51
|
-
|
|
52
46
|
return testDirectory
|
|
53
47
|
}
|
|
54
48
|
|
|
@@ -58,6 +52,10 @@ export async function sendStdin(options: { tabId: TabId; data: string }): Promis
|
|
|
58
52
|
neovim !== undefined,
|
|
59
53
|
`Neovim instance for clientId not found - cannot send stdin. Maybe it's not started yet?`
|
|
60
54
|
)
|
|
55
|
+
assert(
|
|
56
|
+
neovim.application,
|
|
57
|
+
`Neovim application not found for client id ${options.tabId.tabId}. Maybe it's not started yet?`
|
|
58
|
+
)
|
|
61
59
|
|
|
62
|
-
await neovim.write(options.data)
|
|
60
|
+
await neovim.application.write(options.data)
|
|
63
61
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createAppRouter } from "./server"
|
|
2
|
+
import { applicationAvailable } from "./utilities/applicationAvailable"
|
|
3
|
+
|
|
4
|
+
vi.mock("./utilities/applicationAvailable")
|
|
5
|
+
|
|
6
|
+
const mocked = {
|
|
7
|
+
applicationAvailable: vi.mocked(applicationAvailable),
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("Neovim server", () => {
|
|
11
|
+
it("complains when neovim is not installed", async () => {
|
|
12
|
+
await expect(
|
|
13
|
+
createAppRouter({
|
|
14
|
+
outputFilePath: "outputFilePath",
|
|
15
|
+
testEnvironmentPath: "testEnvironmentPath",
|
|
16
|
+
})
|
|
17
|
+
).rejects.toThrow("Neovim is not installed. Please install Neovim (nvim).")
|
|
18
|
+
|
|
19
|
+
expect(mocked.applicationAvailable).toHaveBeenCalledWith("nvim")
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("creates a router when neovim is installed", async () => {
|
|
23
|
+
mocked.applicationAvailable.mockResolvedValue("nvim")
|
|
24
|
+
|
|
25
|
+
await expect(
|
|
26
|
+
createAppRouter({
|
|
27
|
+
outputFilePath: "outputFilePath",
|
|
28
|
+
testEnvironmentPath: "testEnvironmentPath",
|
|
29
|
+
})
|
|
30
|
+
).resolves.toBeDefined()
|
|
31
|
+
|
|
32
|
+
expect(mocked.applicationAvailable).toHaveBeenCalledWith("nvim")
|
|
33
|
+
})
|
|
34
|
+
})
|
package/src/server/server.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { trpc } from "./connection/trpc"
|
|
|
4
4
|
import * as neovim from "./neovim"
|
|
5
5
|
import { TestServer } from "./TestServer"
|
|
6
6
|
import type { TestServerConfig } from "./updateTestdirectorySchemaFile"
|
|
7
|
+
import { applicationAvailable } from "./utilities/applicationAvailable"
|
|
7
8
|
import { tabIdSchema } from "./utilities/tabId"
|
|
8
9
|
|
|
9
10
|
/** Stack for managing resources that need to be disposed of when the server
|
|
@@ -14,7 +15,13 @@ autocleanup.defer(() => {
|
|
|
14
15
|
})
|
|
15
16
|
export { autocleanup }
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
/** @private */
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
20
|
+
export async function createAppRouter(config: TestServerConfig) {
|
|
21
|
+
if (!(await applicationAvailable("nvim"))) {
|
|
22
|
+
throw new Error("Neovim is not installed. Please install Neovim (nvim).")
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
const appRouter = trpc.router({
|
|
19
26
|
neovim: trpc.router({
|
|
20
27
|
start: trpc.procedure
|
|
@@ -40,7 +47,7 @@ function createAppRouter(config: TestServerConfig) {
|
|
|
40
47
|
return neovim.start(options.input.startNeovimArguments, options.input.tabId, config)
|
|
41
48
|
}),
|
|
42
49
|
onStdout: trpc.procedure.input(z.object({ client: tabIdSchema })).subscription(options => {
|
|
43
|
-
return neovim.onStdout(options.input, config.testEnvironmentPath)
|
|
50
|
+
return neovim.onStdout(options.input, options.signal, config.testEnvironmentPath)
|
|
44
51
|
}),
|
|
45
52
|
sendStdin: trpc.procedure.input(z.object({ tabId: tabIdSchema, data: z.string() })).mutation(options => {
|
|
46
53
|
return neovim.sendStdin(options.input)
|
|
@@ -51,12 +58,12 @@ function createAppRouter(config: TestServerConfig) {
|
|
|
51
58
|
return appRouter
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
export type AppRouter = ReturnType<typeof createAppRouter
|
|
61
|
+
export type AppRouter = Awaited<ReturnType<typeof createAppRouter>>
|
|
55
62
|
export type RouterInput = inferRouterInputs<AppRouter>
|
|
56
63
|
|
|
57
64
|
export async function startTestServer(config: TestServerConfig): Promise<TestServer> {
|
|
58
65
|
const testServer = new TestServer(3000)
|
|
59
|
-
const appRouter = createAppRouter(config)
|
|
66
|
+
const appRouter = await createAppRouter(config)
|
|
60
67
|
await testServer.startAndRun(appRouter, config)
|
|
61
68
|
|
|
62
69
|
return testServer
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "fs"
|
|
2
|
+
import { buildTestDirectorySchema } from "./dirtree"
|
|
3
|
+
import type { UpdateTestdirectorySchemaFileResult } from "./updateTestdirectorySchemaFile"
|
|
4
|
+
import { updateTestdirectorySchemaFile } from "./updateTestdirectorySchemaFile"
|
|
5
|
+
|
|
6
|
+
vi.mock("fs")
|
|
7
|
+
vi.mock("./dirtree")
|
|
8
|
+
vi.spyOn(console, "log").mockImplementation(vi.fn())
|
|
9
|
+
|
|
10
|
+
const mock = {
|
|
11
|
+
readFileSync: vi.mocked(readFileSync),
|
|
12
|
+
writeFileSync: vi.mocked(writeFileSync),
|
|
13
|
+
buildTestDirectorySchema: vi.mocked(buildTestDirectorySchema),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("when the schema has not changed", () => {
|
|
17
|
+
it("does not write the file", async () => {
|
|
18
|
+
mock.buildTestDirectorySchema.mockResolvedValue("schema")
|
|
19
|
+
mock.readFileSync.mockImplementation(() => "schema")
|
|
20
|
+
|
|
21
|
+
const result = await updateTestdirectorySchemaFile({
|
|
22
|
+
testEnvironmentPath: "path",
|
|
23
|
+
outputFilePath: "path",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(result).toBe("did-nothing" satisfies UpdateTestdirectorySchemaFileResult)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe("when the schema has changed", () => {
|
|
31
|
+
it("writes the file", async () => {
|
|
32
|
+
mock.buildTestDirectorySchema.mockResolvedValue("new schema")
|
|
33
|
+
mock.readFileSync.mockImplementation(() => "old schema")
|
|
34
|
+
|
|
35
|
+
const result = await updateTestdirectorySchemaFile({
|
|
36
|
+
testEnvironmentPath: "path",
|
|
37
|
+
outputFilePath: "path",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(result).toBe("updated" satisfies UpdateTestdirectorySchemaFileResult)
|
|
41
|
+
expect(mock.writeFileSync).toHaveBeenCalledWith("path", "new schema")
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -7,11 +7,13 @@ export type TestServerConfig = {
|
|
|
7
7
|
outputFilePath: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export type UpdateTestdirectorySchemaFileResult = "updated" | "did-nothing"
|
|
11
|
+
|
|
10
12
|
export async function updateTestdirectorySchemaFile({
|
|
11
13
|
testEnvironmentPath,
|
|
12
14
|
outputFilePath,
|
|
13
|
-
}: TestServerConfig): Promise<
|
|
14
|
-
const newSchema = await buildTestDirectorySchema(testEnvironmentPath)
|
|
15
|
+
}: TestServerConfig): Promise<UpdateTestdirectorySchemaFileResult> {
|
|
16
|
+
const newSchema: string = await buildTestDirectorySchema(testEnvironmentPath)
|
|
15
17
|
let oldSchema = ""
|
|
16
18
|
|
|
17
19
|
try {
|
|
@@ -25,5 +27,8 @@ export async function updateTestdirectorySchemaFile({
|
|
|
25
27
|
// because file watchers will trigger on file changes and we don't want to
|
|
26
28
|
// trigger a build if the schema hasn't changed
|
|
27
29
|
writeFileSync(outputFilePath, newSchema)
|
|
30
|
+
return "updated"
|
|
31
|
+
} else {
|
|
32
|
+
return "did-nothing"
|
|
28
33
|
}
|
|
29
34
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { StartableApplication } from "./DisposableSingleApplication"
|
|
2
|
+
import { DisposableSingleApplication } from "./DisposableSingleApplication"
|
|
3
|
+
|
|
4
|
+
vi.spyOn(console, "log").mockImplementation(vi.fn())
|
|
5
|
+
|
|
6
|
+
class TestDisposableSingleApplication extends DisposableSingleApplication {
|
|
7
|
+
public getApplication() {
|
|
8
|
+
return this.application
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const fakeApp: StartableApplication = {
|
|
13
|
+
processId: 123,
|
|
14
|
+
write: vi.fn(),
|
|
15
|
+
killAndWait: vi.fn(),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("DisposableSingleApplication", () => {
|
|
19
|
+
it("has no application when created", () => {
|
|
20
|
+
const app = new TestDisposableSingleApplication()
|
|
21
|
+
expect(app.processId()).toBeUndefined()
|
|
22
|
+
expect(app.getApplication()).toBeUndefined()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("can start an application", async () => {
|
|
26
|
+
const app = new TestDisposableSingleApplication()
|
|
27
|
+
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
28
|
+
expect(app.processId()).toBe(123)
|
|
29
|
+
expect(app.getApplication()).toBe(fakeApp)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("can write to the application", async () => {
|
|
33
|
+
const app = new TestDisposableSingleApplication()
|
|
34
|
+
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
35
|
+
await app.write("hello")
|
|
36
|
+
expect(fakeApp.write).toHaveBeenCalledWith("hello")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("fails to write if the application is not started", async () => {
|
|
40
|
+
// there is no need to support soft failing in the ui, so we do hard
|
|
41
|
+
// failing to make this error obvious
|
|
42
|
+
const app = new TestDisposableSingleApplication()
|
|
43
|
+
await expect(app.write("hello")).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
44
|
+
`[AssertionError: The application not started yet. It makes no sense to write to it, so this looks like a bug.]`
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe("disposing", () => {
|
|
49
|
+
it("disposes the application when disposed", async () => {
|
|
50
|
+
// it's important to make sure there are no dangling applications when
|
|
51
|
+
// starting new tests or ending the user session entirely and closing the
|
|
52
|
+
// application
|
|
53
|
+
const app = new TestDisposableSingleApplication()
|
|
54
|
+
|
|
55
|
+
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
56
|
+
expect(app.getApplication()).toBe(fakeApp)
|
|
57
|
+
|
|
58
|
+
await app[Symbol.asyncDispose]()
|
|
59
|
+
expect(fakeApp.killAndWait).toHaveBeenCalledOnce()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("does nothing if there is no application to dispose", async () => {
|
|
63
|
+
const app = new TestDisposableSingleApplication()
|
|
64
|
+
expect(app.getApplication()).toBeUndefined()
|
|
65
|
+
expect(app.processId()).toBeUndefined()
|
|
66
|
+
|
|
67
|
+
expect(() => app[Symbol.asyncDispose]()).not.toThrow()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -1,33 +1,38 @@
|
|
|
1
|
+
import assert from "assert"
|
|
1
2
|
import type { TerminalApplication } from "./TerminalApplication"
|
|
2
3
|
|
|
4
|
+
export type StartableApplication = Pick<TerminalApplication, "write" | "processId" | "killAndWait">
|
|
5
|
+
|
|
3
6
|
/** A testable application that can be started, killed, and given input. For a
|
|
4
7
|
* single instance of this interface, only a single instance can be running at
|
|
5
|
-
* a time
|
|
6
|
-
*
|
|
7
|
-
* @typeParam T The type of context the tests should have, e.g. information
|
|
8
|
-
* about a custom directory that the application is running in.
|
|
9
|
-
*
|
|
8
|
+
* a time.
|
|
10
9
|
*/
|
|
11
|
-
export
|
|
12
|
-
protected application:
|
|
10
|
+
export class DisposableSingleApplication implements AsyncDisposable {
|
|
11
|
+
protected application: StartableApplication | undefined
|
|
13
12
|
|
|
14
|
-
public async
|
|
15
|
-
await this.
|
|
13
|
+
public async startNextAndKillCurrent(startNext: () => Promise<StartableApplication>): Promise<void> {
|
|
14
|
+
await this[Symbol.asyncDispose]()
|
|
15
|
+
this.application = await startNext()
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
public async write(input: string): Promise<void> {
|
|
19
|
-
|
|
19
|
+
assert(
|
|
20
|
+
this.application,
|
|
21
|
+
"The application not started yet. It makes no sense to write to it, so this looks like a bug."
|
|
22
|
+
)
|
|
23
|
+
this.application.write(input)
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
public processId(): number | undefined {
|
|
23
27
|
return this.application?.processId
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
/** Kill the current application if it exists. */
|
|
31
|
+
public async [Symbol.asyncDispose](): Promise<void> {
|
|
27
32
|
if (this.processId() === undefined) {
|
|
28
33
|
return
|
|
29
34
|
}
|
|
30
35
|
console.log(`Killing current application ${this.processId()}...`)
|
|
31
|
-
await this.
|
|
36
|
+
await this.application?.killAndWait()
|
|
32
37
|
}
|
|
33
38
|
}
|
|
@@ -4,10 +4,11 @@ import { createLogger, format, transports } from "winston"
|
|
|
4
4
|
import type { ITerminalDimensions } from "@xterm/addon-fit"
|
|
5
5
|
import type { IPty } from "node-pty"
|
|
6
6
|
import pty from "node-pty"
|
|
7
|
+
import type { StartableApplication } from "./DisposableSingleApplication"
|
|
7
8
|
|
|
8
9
|
// NOTE separating stdout and stderr is not supported by node-pty
|
|
9
10
|
// https://github.com/microsoft/node-pty/issues/71
|
|
10
|
-
export class TerminalApplication {
|
|
11
|
+
export class TerminalApplication implements StartableApplication {
|
|
11
12
|
public readonly processId: number
|
|
12
13
|
|
|
13
14
|
public readonly logger: winston.Logger
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { applicationAvailable } from "./applicationAvailable"
|
|
2
|
+
|
|
3
|
+
describe("sanity checks for mocking", () => {
|
|
4
|
+
// because it makes no sense to mock the actual implementation if we don't
|
|
5
|
+
// know what it does in the current version, we better check what it's
|
|
6
|
+
// expected to do
|
|
7
|
+
it("can find neovim using the actual implementation", async () => {
|
|
8
|
+
await expect(applicationAvailable("nvim")).resolves.toBe("nvim")
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("complains when a nonexistent command is checked", async () => {
|
|
12
|
+
await expect(applicationAvailable("thisCommandDoesNotExist")).rejects.toBe(null)
|
|
13
|
+
})
|
|
14
|
+
})
|