@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.2.0](https://github.com/mikavilpas/tui-sandbox/compare/library-v2.1.0...library-v2.2.0) (2024-11-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* check that neovim is available before starting the server ([#107](https://github.com/mikavilpas/tui-sandbox/issues/107)) ([bcc817f](https://github.com/mikavilpas/tui-sandbox/commit/bcc817fe25a9811cdd7b1832f6811fa4f92974bd))
|
|
9
|
+
|
|
10
|
+
## [2.1.0](https://github.com/mikavilpas/tui-sandbox/compare/library-v2.0.2...library-v2.1.0) (2024-11-02)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* client connects to server using server-sent events ([e07765e](https://github.com/mikavilpas/tui-sandbox/commit/e07765e20dba6394538cf29b1b16463b62ba4b7a))
|
|
16
|
+
|
|
3
17
|
## [2.0.2](https://github.com/mikavilpas/tui-sandbox/compare/library-v2.0.1...library-v2.0.2) (2024-10-20)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "@xterm/xterm/css/xterm.css";
|
|
2
|
+
import type { Except } from "type-fest";
|
|
2
3
|
import type { StartNeovimGenericArguments } from "../server/neovim/NeovimApplication.ts";
|
|
3
4
|
import type { TestDirectory } from "../server/types.ts";
|
|
4
5
|
import "./style.css";
|
|
@@ -8,5 +9,5 @@ export declare class NeovimClient {
|
|
|
8
9
|
private readonly terminal;
|
|
9
10
|
private readonly trpc;
|
|
10
11
|
constructor(app: HTMLElement);
|
|
11
|
-
startNeovim(args: StartNeovimGenericArguments): Promise<TestDirectory>;
|
|
12
|
+
startNeovim(args: Except<StartNeovimGenericArguments, "terminalDimensions">): Promise<TestDirectory>;
|
|
12
13
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTRPCClient,
|
|
1
|
+
import { createTRPCClient, httpBatchLink, splitLink, unstable_httpSubscriptionLink } from "@trpc/client";
|
|
2
2
|
import "@xterm/xterm/css/xterm.css";
|
|
3
3
|
import "./style.css";
|
|
4
4
|
import { getTabId, startTerminal } from "./websocket-client.js";
|
|
@@ -8,9 +8,18 @@ export class NeovimClient {
|
|
|
8
8
|
terminal;
|
|
9
9
|
trpc;
|
|
10
10
|
constructor(app) {
|
|
11
|
-
const wsClient = createWSClient({ url: `ws://localhost:3000`, WebSocket });
|
|
12
11
|
const trpc = createTRPCClient({
|
|
13
|
-
links: [
|
|
12
|
+
links: [
|
|
13
|
+
splitLink({
|
|
14
|
+
condition: operation => operation.type === "subscription",
|
|
15
|
+
true: unstable_httpSubscriptionLink({
|
|
16
|
+
url: "http://localhost:3000",
|
|
17
|
+
}),
|
|
18
|
+
false: httpBatchLink({
|
|
19
|
+
url: "http://localhost:3000",
|
|
20
|
+
}),
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
14
23
|
});
|
|
15
24
|
this.trpc = trpc;
|
|
16
25
|
this.tabId = getTabId();
|
|
@@ -4,5 +4,5 @@ import type { TestServerConfig } from "./updateTestdirectorySchemaFile";
|
|
|
4
4
|
export declare class TestServer {
|
|
5
5
|
private readonly port;
|
|
6
6
|
constructor(port: number);
|
|
7
|
-
startAndRun
|
|
7
|
+
startAndRun(appRouter: AnyTRPCRouter, config: TestServerConfig): Promise<void>;
|
|
8
8
|
}
|
|
@@ -1,48 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHTTPServer } from "@trpc/server/adapters/standalone";
|
|
2
2
|
import "core-js/proposals/async-explicit-resource-management";
|
|
3
|
+
import cors from "cors";
|
|
3
4
|
import { once } from "events";
|
|
4
|
-
import { WebSocketServer } from "ws";
|
|
5
|
-
import { createContext } from "./connection/trpc";
|
|
6
5
|
import { updateTestdirectorySchemaFile } from "./updateTestdirectorySchemaFile";
|
|
7
6
|
export class TestServer {
|
|
8
7
|
port;
|
|
9
8
|
constructor(port) {
|
|
10
9
|
this.port = port;
|
|
11
10
|
}
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
13
11
|
async startAndRun(appRouter, config) {
|
|
14
12
|
console.log("🚀 Server starting");
|
|
15
13
|
await updateTestdirectorySchemaFile(config);
|
|
16
|
-
const
|
|
17
|
-
const handler = applyWSSHandler({
|
|
18
|
-
wss,
|
|
14
|
+
const server = createHTTPServer({
|
|
19
15
|
router: appRouter,
|
|
20
|
-
createContext,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// server ping message interval in milliseconds
|
|
25
|
-
pingMs: 30_000,
|
|
26
|
-
// connection is terminated if pong message is not received in this many milliseconds
|
|
27
|
-
pongWaitMs: 5000,
|
|
28
|
-
},
|
|
16
|
+
createContext: () => ({}),
|
|
17
|
+
middleware: cors({
|
|
18
|
+
origin: "*",
|
|
19
|
+
}),
|
|
29
20
|
});
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
server.listen(this.port);
|
|
22
|
+
server.on("connection", socket => {
|
|
23
|
+
console.log(`➕➕ Connection`);
|
|
32
24
|
socket.once("close", () => {
|
|
33
|
-
console.log(`➖➖ Connection
|
|
25
|
+
console.log(`➖➖ Connection`);
|
|
34
26
|
});
|
|
35
27
|
});
|
|
36
|
-
console.log(`✅
|
|
28
|
+
console.log(`✅ Server listening on ws://localhost:${this.port}`);
|
|
37
29
|
await Promise.race([once(process, "SIGTERM"), once(process, "SIGINT")]);
|
|
38
30
|
console.log("Shutting down...");
|
|
39
|
-
|
|
40
|
-
wss.close(error => {
|
|
31
|
+
server.close(error => {
|
|
41
32
|
if (error) {
|
|
42
|
-
console.error("Error closing
|
|
33
|
+
console.error("Error closing server", error);
|
|
43
34
|
process.exit(1);
|
|
44
35
|
}
|
|
45
|
-
console.log("
|
|
36
|
+
console.log("Server closed");
|
|
46
37
|
process.exit(0);
|
|
47
38
|
});
|
|
48
39
|
}
|
|
@@ -1,29 +1,21 @@
|
|
|
1
|
-
import type { CreateWSSContextFnOptions } from "@trpc/server/adapters/ws";
|
|
2
|
-
import type { Socket } from "net";
|
|
3
|
-
import type { WebSocket } from "ws";
|
|
4
|
-
export type Connection = {
|
|
5
|
-
clientId: WebSocket;
|
|
6
|
-
socket: Socket;
|
|
7
|
-
};
|
|
8
|
-
export declare function createContext(opts: CreateWSSContextFnOptions): Connection;
|
|
9
1
|
export declare const trpc: {
|
|
10
2
|
_config: import("@trpc/server/unstable-core-do-not-import").RootConfig<{
|
|
11
|
-
ctx:
|
|
3
|
+
ctx: object;
|
|
12
4
|
meta: object;
|
|
13
5
|
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
14
6
|
transformer: false;
|
|
15
7
|
}>;
|
|
16
|
-
procedure: import("@trpc/server/unstable-core-do-not-import").ProcedureBuilder<
|
|
17
|
-
middleware: <$ContextOverrides>(fn: import("@trpc/server/unstable-core-do-not-import").MiddlewareFunction<
|
|
8
|
+
procedure: import("@trpc/server/unstable-core-do-not-import").ProcedureBuilder<object, object, object, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, false>;
|
|
9
|
+
middleware: <$ContextOverrides>(fn: import("@trpc/server/unstable-core-do-not-import").MiddlewareFunction<object, object, object, $ContextOverrides, unknown>) => import("@trpc/server/unstable-core-do-not-import").MiddlewareBuilder<object, object, $ContextOverrides, unknown>;
|
|
18
10
|
router: {
|
|
19
11
|
<TInput extends import("@trpc/server").RouterRecord>(input: TInput): import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
|
|
20
|
-
ctx:
|
|
12
|
+
ctx: object;
|
|
21
13
|
meta: object;
|
|
22
14
|
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
23
15
|
transformer: false;
|
|
24
16
|
}, TInput>;
|
|
25
17
|
<TInput extends import("@trpc/server/unstable-core-do-not-import").CreateRouterOptions>(input: TInput): import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
|
|
26
|
-
ctx:
|
|
18
|
+
ctx: object;
|
|
27
19
|
meta: object;
|
|
28
20
|
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
29
21
|
transformer: false;
|
|
@@ -31,12 +23,12 @@ export declare const trpc: {
|
|
|
31
23
|
};
|
|
32
24
|
mergeRouters: typeof import("@trpc/server/unstable-core-do-not-import").mergeRouters;
|
|
33
25
|
createCallerFactory: <TRecord extends import("@trpc/server").RouterRecord>(router: Pick<import("@trpc/server/unstable-core-do-not-import").Router<{
|
|
34
|
-
ctx:
|
|
26
|
+
ctx: object;
|
|
35
27
|
meta: object;
|
|
36
28
|
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
37
29
|
transformer: false;
|
|
38
30
|
}, TRecord>, "_def">) => import("@trpc/server/unstable-core-do-not-import").RouterCaller<{
|
|
39
|
-
ctx:
|
|
31
|
+
ctx: object;
|
|
40
32
|
meta: object;
|
|
41
33
|
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
42
34
|
transformer: false;
|
|
@@ -12,11 +12,12 @@ export type StartNeovimGenericArguments = {
|
|
|
12
12
|
};
|
|
13
13
|
startupScriptModifications?: string[];
|
|
14
14
|
};
|
|
15
|
-
export declare class NeovimApplication
|
|
15
|
+
export declare class NeovimApplication {
|
|
16
16
|
private readonly testEnvironmentPath;
|
|
17
|
+
readonly application: DisposableSingleApplication;
|
|
17
18
|
private testDirectory;
|
|
18
19
|
readonly events: EventEmitter;
|
|
19
|
-
constructor(testEnvironmentPath: string);
|
|
20
|
+
constructor(testEnvironmentPath: string, application?: DisposableSingleApplication);
|
|
20
21
|
/**
|
|
21
22
|
* Kill the current application and start a new one with the given arguments.
|
|
22
23
|
*/
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import assert from "assert";
|
|
1
2
|
import { exec } from "child_process";
|
|
2
3
|
import EventEmitter from "events";
|
|
3
4
|
import { existsSync } from "fs";
|
|
4
5
|
import path from "path";
|
|
5
6
|
import { DisposableSingleApplication } from "../utilities/DisposableSingleApplication";
|
|
6
7
|
import { TerminalApplication } from "../utilities/TerminalApplication";
|
|
7
|
-
export class NeovimApplication
|
|
8
|
+
export class NeovimApplication {
|
|
8
9
|
testEnvironmentPath;
|
|
10
|
+
application;
|
|
9
11
|
testDirectory;
|
|
10
12
|
events;
|
|
11
|
-
constructor(testEnvironmentPath) {
|
|
12
|
-
super();
|
|
13
|
+
constructor(testEnvironmentPath, application = new DisposableSingleApplication()) {
|
|
13
14
|
this.testEnvironmentPath = testEnvironmentPath;
|
|
15
|
+
this.application = application;
|
|
14
16
|
this.events = new EventEmitter();
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
@@ -42,19 +44,25 @@ export class NeovimApplication extends DisposableSingleApplication {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
const stdout = this.events;
|
|
45
|
-
this.application
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
await this.application.startNextAndKillCurrent(async () => {
|
|
48
|
+
return TerminalApplication.start({
|
|
49
|
+
command: "nvim",
|
|
50
|
+
args: neovimArguments,
|
|
51
|
+
cwd: this.testEnvironmentPath,
|
|
52
|
+
env: process.env,
|
|
53
|
+
dimensions: startArgs.terminalDimensions,
|
|
54
|
+
onStdoutOrStderr(data) {
|
|
55
|
+
data;
|
|
56
|
+
stdout.emit("stdout", data);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
54
59
|
});
|
|
60
|
+
const processId = this.application.processId();
|
|
61
|
+
assert(processId !== undefined, "Neovim was started without a process ID. This is a bug - please open an issue.");
|
|
62
|
+
console.log(`🚀 Started Neovim instance ${processId}`);
|
|
55
63
|
}
|
|
56
64
|
async [Symbol.asyncDispose]() {
|
|
57
|
-
await
|
|
65
|
+
await this.application[Symbol.asyncDispose]();
|
|
58
66
|
if (this.testDirectory) {
|
|
59
67
|
exec(`rm -rf ${this.testDirectory.rootPathAbsolute}`);
|
|
60
68
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type { Observable } from "@trpc/server/observable";
|
|
2
1
|
import type { TestDirectory } from "../types";
|
|
3
2
|
import type { TestServerConfig } from "../updateTestdirectorySchemaFile";
|
|
4
3
|
import type { TabId } from "../utilities/tabId";
|
|
5
4
|
import type { StartNeovimGenericArguments } from "./NeovimApplication";
|
|
6
5
|
export declare function onStdout(options: {
|
|
7
6
|
client: TabId;
|
|
8
|
-
}, testEnvironmentPath: string):
|
|
7
|
+
}, signal: AbortSignal | undefined, testEnvironmentPath: string): Promise<AsyncGenerator<string, void, unknown>>;
|
|
9
8
|
export declare function start(options: StartNeovimGenericArguments, tabId: TabId, config: TestServerConfig): Promise<TestDirectory>;
|
|
10
9
|
export declare function sendStdin(options: {
|
|
11
10
|
tabId: TabId;
|
|
@@ -1,40 +1,34 @@
|
|
|
1
|
-
import { observable } from "@trpc/server/observable";
|
|
2
1
|
import assert from "assert";
|
|
2
|
+
import { convertEventEmitterToAsyncGenerator } from "../utilities/generator";
|
|
3
3
|
import { createTempDir } from "./environment/createTempDir";
|
|
4
4
|
import { NeovimApplication } from "./NeovimApplication";
|
|
5
5
|
const neovims = new Map();
|
|
6
|
-
export function onStdout(options, testEnvironmentPath) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
emit.next(data);
|
|
16
|
-
};
|
|
17
|
-
neovim.events.on("stdout", send);
|
|
18
|
-
return () => {
|
|
19
|
-
neovim.events.off("stdout", send);
|
|
6
|
+
export async function onStdout(options, signal, testEnvironmentPath) {
|
|
7
|
+
const tabId = options.client.tabId;
|
|
8
|
+
const neovim = neovims.get(tabId) ?? new NeovimApplication(testEnvironmentPath);
|
|
9
|
+
if (neovims.get(tabId) === undefined) {
|
|
10
|
+
neovims.set(tabId, neovim);
|
|
11
|
+
}
|
|
12
|
+
const stdout = convertEventEmitterToAsyncGenerator(neovim.events, "stdout");
|
|
13
|
+
if (signal) {
|
|
14
|
+
signal.addEventListener("abort", () => {
|
|
20
15
|
void neovim[Symbol.asyncDispose]().finally(() => {
|
|
21
16
|
neovims.delete(tabId);
|
|
22
17
|
});
|
|
23
|
-
};
|
|
24
|
-
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return stdout;
|
|
25
21
|
}
|
|
26
22
|
export async function start(options, tabId, config) {
|
|
27
23
|
const neovim = neovims.get(tabId.tabId);
|
|
28
24
|
assert(neovim, `Neovim instance not found for client id ${tabId.tabId}`);
|
|
29
25
|
const testDirectory = await createTempDir(config);
|
|
30
26
|
await neovim.startNextAndKillCurrent(testDirectory, options);
|
|
31
|
-
const processId = neovim.processId();
|
|
32
|
-
assert(processId !== undefined, "Neovim was started without a process ID. This is a bug - please open an issue.");
|
|
33
|
-
console.log(`🚀 Started Neovim instance ${processId}`);
|
|
34
27
|
return testDirectory;
|
|
35
28
|
}
|
|
36
29
|
export async function sendStdin(options) {
|
|
37
30
|
const neovim = neovims.get(options.tabId.tabId);
|
|
38
31
|
assert(neovim !== undefined, `Neovim instance for clientId not found - cannot send stdin. Maybe it's not started yet?`);
|
|
39
|
-
|
|
32
|
+
assert(neovim.application, `Neovim application not found for client id ${options.tabId.tabId}. Maybe it's not started yet?`);
|
|
33
|
+
await neovim.application.write(options.data);
|
|
40
34
|
}
|
|
@@ -5,14 +5,15 @@ import type { TestServerConfig } from "./updateTestdirectorySchemaFile";
|
|
|
5
5
|
* shuts down */
|
|
6
6
|
declare const autocleanup: AsyncDisposableStack;
|
|
7
7
|
export { autocleanup };
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** @private */
|
|
9
|
+
export declare function createAppRouter(config: TestServerConfig): Promise<import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
|
|
10
|
+
ctx: object;
|
|
10
11
|
meta: object;
|
|
11
12
|
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
12
13
|
transformer: false;
|
|
13
14
|
}, import("@trpc/server/unstable-core-do-not-import").DecorateCreateRouterOptions<{
|
|
14
15
|
neovim: import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
|
|
15
|
-
ctx:
|
|
16
|
+
ctx: object;
|
|
16
17
|
meta: object;
|
|
17
18
|
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
18
19
|
transformer: false;
|
|
@@ -35,13 +36,13 @@ declare function createAppRouter(config: TestServerConfig): import("@trpc/server
|
|
|
35
36
|
};
|
|
36
37
|
output: import("./types").TestDirectory;
|
|
37
38
|
}>;
|
|
38
|
-
onStdout: import("@trpc/server
|
|
39
|
+
onStdout: import("@trpc/server").TRPCSubscriptionProcedure<{
|
|
39
40
|
input: {
|
|
40
41
|
client: {
|
|
41
42
|
tabId: string;
|
|
42
43
|
};
|
|
43
44
|
};
|
|
44
|
-
output: string
|
|
45
|
+
output: AsyncGenerator<string, void, unknown>;
|
|
45
46
|
}>;
|
|
46
47
|
sendStdin: import("@trpc/server").TRPCMutationProcedure<{
|
|
47
48
|
input: {
|
|
@@ -53,7 +54,7 @@ declare function createAppRouter(config: TestServerConfig): import("@trpc/server
|
|
|
53
54
|
output: void;
|
|
54
55
|
}>;
|
|
55
56
|
}>;
|
|
56
|
-
}
|
|
57
|
-
export type AppRouter = ReturnType<typeof createAppRouter
|
|
57
|
+
}>>>;
|
|
58
|
+
export type AppRouter = Awaited<ReturnType<typeof createAppRouter>>;
|
|
58
59
|
export type RouterInput = inferRouterInputs<AppRouter>;
|
|
59
60
|
export declare function startTestServer(config: TestServerConfig): Promise<TestServer>;
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { trpc } from "./connection/trpc";
|
|
3
3
|
import * as neovim from "./neovim";
|
|
4
4
|
import { TestServer } from "./TestServer";
|
|
5
|
+
import { applicationAvailable } from "./utilities/applicationAvailable";
|
|
5
6
|
import { tabIdSchema } from "./utilities/tabId";
|
|
6
7
|
/** Stack for managing resources that need to be disposed of when the server
|
|
7
8
|
* shuts down */
|
|
@@ -10,7 +11,12 @@ autocleanup.defer(() => {
|
|
|
10
11
|
console.log("Closing any open test applications");
|
|
11
12
|
});
|
|
12
13
|
export { autocleanup };
|
|
13
|
-
|
|
14
|
+
/** @private */
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
16
|
+
export async function createAppRouter(config) {
|
|
17
|
+
if (!(await applicationAvailable("nvim"))) {
|
|
18
|
+
throw new Error("Neovim is not installed. Please install Neovim (nvim).");
|
|
19
|
+
}
|
|
14
20
|
const appRouter = trpc.router({
|
|
15
21
|
neovim: trpc.router({
|
|
16
22
|
start: trpc.procedure
|
|
@@ -34,7 +40,7 @@ function createAppRouter(config) {
|
|
|
34
40
|
return neovim.start(options.input.startNeovimArguments, options.input.tabId, config);
|
|
35
41
|
}),
|
|
36
42
|
onStdout: trpc.procedure.input(z.object({ client: tabIdSchema })).subscription(options => {
|
|
37
|
-
return neovim.onStdout(options.input, config.testEnvironmentPath);
|
|
43
|
+
return neovim.onStdout(options.input, options.signal, config.testEnvironmentPath);
|
|
38
44
|
}),
|
|
39
45
|
sendStdin: trpc.procedure.input(z.object({ tabId: tabIdSchema, data: z.string() })).mutation(options => {
|
|
40
46
|
return neovim.sendStdin(options.input);
|
|
@@ -45,7 +51,7 @@ function createAppRouter(config) {
|
|
|
45
51
|
}
|
|
46
52
|
export async function startTestServer(config) {
|
|
47
53
|
const testServer = new TestServer(3000);
|
|
48
|
-
const appRouter = createAppRouter(config);
|
|
54
|
+
const appRouter = await createAppRouter(config);
|
|
49
55
|
await testServer.startAndRun(appRouter, config);
|
|
50
56
|
return testServer;
|
|
51
57
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createAppRouter } from "./server";
|
|
2
|
+
import { applicationAvailable } from "./utilities/applicationAvailable";
|
|
3
|
+
vi.mock("./utilities/applicationAvailable");
|
|
4
|
+
const mocked = {
|
|
5
|
+
applicationAvailable: vi.mocked(applicationAvailable),
|
|
6
|
+
};
|
|
7
|
+
describe("Neovim server", () => {
|
|
8
|
+
it("complains when neovim is not installed", async () => {
|
|
9
|
+
await expect(createAppRouter({
|
|
10
|
+
outputFilePath: "outputFilePath",
|
|
11
|
+
testEnvironmentPath: "testEnvironmentPath",
|
|
12
|
+
})).rejects.toThrow("Neovim is not installed. Please install Neovim (nvim).");
|
|
13
|
+
expect(mocked.applicationAvailable).toHaveBeenCalledWith("nvim");
|
|
14
|
+
});
|
|
15
|
+
it("creates a router when neovim is installed", async () => {
|
|
16
|
+
mocked.applicationAvailable.mockResolvedValue("nvim");
|
|
17
|
+
await expect(createAppRouter({
|
|
18
|
+
outputFilePath: "outputFilePath",
|
|
19
|
+
testEnvironmentPath: "testEnvironmentPath",
|
|
20
|
+
})).resolves.toBeDefined();
|
|
21
|
+
expect(mocked.applicationAvailable).toHaveBeenCalledWith("nvim");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -3,4 +3,5 @@ export type TestServerConfig = {
|
|
|
3
3
|
testEnvironmentPath: string;
|
|
4
4
|
outputFilePath: string;
|
|
5
5
|
};
|
|
6
|
-
export
|
|
6
|
+
export type UpdateTestdirectorySchemaFileResult = "updated" | "did-nothing";
|
|
7
|
+
export declare function updateTestdirectorySchemaFile({ testEnvironmentPath, outputFilePath, }: TestServerConfig): Promise<UpdateTestdirectorySchemaFileResult>;
|
|
@@ -15,5 +15,9 @@ export async function updateTestdirectorySchemaFile({ testEnvironmentPath, outpu
|
|
|
15
15
|
// because file watchers will trigger on file changes and we don't want to
|
|
16
16
|
// trigger a build if the schema hasn't changed
|
|
17
17
|
writeFileSync(outputFilePath, newSchema);
|
|
18
|
+
return "updated";
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return "did-nothing";
|
|
18
22
|
}
|
|
19
23
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { buildTestDirectorySchema } from "./dirtree";
|
|
3
|
+
import { updateTestdirectorySchemaFile } from "./updateTestdirectorySchemaFile";
|
|
4
|
+
vi.mock("fs");
|
|
5
|
+
vi.mock("./dirtree");
|
|
6
|
+
vi.spyOn(console, "log").mockImplementation(vi.fn());
|
|
7
|
+
const mock = {
|
|
8
|
+
readFileSync: vi.mocked(readFileSync),
|
|
9
|
+
writeFileSync: vi.mocked(writeFileSync),
|
|
10
|
+
buildTestDirectorySchema: vi.mocked(buildTestDirectorySchema),
|
|
11
|
+
};
|
|
12
|
+
describe("when the schema has not changed", () => {
|
|
13
|
+
it("does not write the file", async () => {
|
|
14
|
+
mock.buildTestDirectorySchema.mockResolvedValue("schema");
|
|
15
|
+
mock.readFileSync.mockImplementation(() => "schema");
|
|
16
|
+
const result = await updateTestdirectorySchemaFile({
|
|
17
|
+
testEnvironmentPath: "path",
|
|
18
|
+
outputFilePath: "path",
|
|
19
|
+
});
|
|
20
|
+
expect(result).toBe("did-nothing");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe("when the schema has changed", () => {
|
|
24
|
+
it("writes the file", async () => {
|
|
25
|
+
mock.buildTestDirectorySchema.mockResolvedValue("new schema");
|
|
26
|
+
mock.readFileSync.mockImplementation(() => "old schema");
|
|
27
|
+
const result = await updateTestdirectorySchemaFile({
|
|
28
|
+
testEnvironmentPath: "path",
|
|
29
|
+
outputFilePath: "path",
|
|
30
|
+
});
|
|
31
|
+
expect(result).toBe("updated");
|
|
32
|
+
expect(mock.writeFileSync).toHaveBeenCalledWith("path", "new schema");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import type { TerminalApplication } from "./TerminalApplication";
|
|
2
|
+
export type StartableApplication = Pick<TerminalApplication, "write" | "processId" | "killAndWait">;
|
|
2
3
|
/** A testable application that can be started, killed, and given input. For a
|
|
3
4
|
* single instance of this interface, only a single instance can be running at
|
|
4
|
-
* a time
|
|
5
|
-
*
|
|
6
|
-
* @typeParam T The type of context the tests should have, e.g. information
|
|
7
|
-
* about a custom directory that the application is running in.
|
|
8
|
-
*
|
|
5
|
+
* a time.
|
|
9
6
|
*/
|
|
10
|
-
export declare
|
|
11
|
-
protected application:
|
|
12
|
-
|
|
7
|
+
export declare class DisposableSingleApplication implements AsyncDisposable {
|
|
8
|
+
protected application: StartableApplication | undefined;
|
|
9
|
+
startNextAndKillCurrent(startNext: () => Promise<StartableApplication>): Promise<void>;
|
|
13
10
|
write(input: string): Promise<void>;
|
|
14
11
|
processId(): number | undefined;
|
|
12
|
+
/** Kill the current application if it exists. */
|
|
15
13
|
[Symbol.asyncDispose](): Promise<void>;
|
|
16
14
|
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
+
import assert from "assert";
|
|
1
2
|
/** A testable application that can be started, killed, and given input. For a
|
|
2
3
|
* single instance of this interface, only a single instance can be running at
|
|
3
|
-
* a time
|
|
4
|
-
*
|
|
5
|
-
* @typeParam T The type of context the tests should have, e.g. information
|
|
6
|
-
* about a custom directory that the application is running in.
|
|
7
|
-
*
|
|
4
|
+
* a time.
|
|
8
5
|
*/
|
|
9
6
|
export class DisposableSingleApplication {
|
|
10
7
|
application;
|
|
11
|
-
async
|
|
12
|
-
await this.
|
|
8
|
+
async startNextAndKillCurrent(startNext) {
|
|
9
|
+
await this[Symbol.asyncDispose]();
|
|
10
|
+
this.application = await startNext();
|
|
13
11
|
}
|
|
14
12
|
async write(input) {
|
|
15
|
-
|
|
13
|
+
assert(this.application, "The application not started yet. It makes no sense to write to it, so this looks like a bug.");
|
|
14
|
+
this.application.write(input);
|
|
16
15
|
}
|
|
17
16
|
processId() {
|
|
18
17
|
return this.application?.processId;
|
|
19
18
|
}
|
|
19
|
+
/** Kill the current application if it exists. */
|
|
20
20
|
async [Symbol.asyncDispose]() {
|
|
21
21
|
if (this.processId() === undefined) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
console.log(`Killing current application ${this.processId()}...`);
|
|
25
|
-
await this.
|
|
25
|
+
await this.application?.killAndWait();
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DisposableSingleApplication } from "./DisposableSingleApplication";
|
|
2
|
+
vi.spyOn(console, "log").mockImplementation(vi.fn());
|
|
3
|
+
class TestDisposableSingleApplication extends DisposableSingleApplication {
|
|
4
|
+
getApplication() {
|
|
5
|
+
return this.application;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const fakeApp = {
|
|
9
|
+
processId: 123,
|
|
10
|
+
write: vi.fn(),
|
|
11
|
+
killAndWait: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
describe("DisposableSingleApplication", () => {
|
|
14
|
+
it("has no application when created", () => {
|
|
15
|
+
const app = new TestDisposableSingleApplication();
|
|
16
|
+
expect(app.processId()).toBeUndefined();
|
|
17
|
+
expect(app.getApplication()).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
it("can start an application", async () => {
|
|
20
|
+
const app = new TestDisposableSingleApplication();
|
|
21
|
+
await app.startNextAndKillCurrent(async () => fakeApp);
|
|
22
|
+
expect(app.processId()).toBe(123);
|
|
23
|
+
expect(app.getApplication()).toBe(fakeApp);
|
|
24
|
+
});
|
|
25
|
+
it("can write to the application", async () => {
|
|
26
|
+
const app = new TestDisposableSingleApplication();
|
|
27
|
+
await app.startNextAndKillCurrent(async () => fakeApp);
|
|
28
|
+
await app.write("hello");
|
|
29
|
+
expect(fakeApp.write).toHaveBeenCalledWith("hello");
|
|
30
|
+
});
|
|
31
|
+
it("fails to write if the application is not started", async () => {
|
|
32
|
+
// there is no need to support soft failing in the ui, so we do hard
|
|
33
|
+
// failing to make this error obvious
|
|
34
|
+
const app = new TestDisposableSingleApplication();
|
|
35
|
+
await expect(app.write("hello")).rejects.toThrowErrorMatchingInlineSnapshot(`[AssertionError: The application not started yet. It makes no sense to write to it, so this looks like a bug.]`);
|
|
36
|
+
});
|
|
37
|
+
describe("disposing", () => {
|
|
38
|
+
it("disposes the application when disposed", async () => {
|
|
39
|
+
// it's important to make sure there are no dangling applications when
|
|
40
|
+
// starting new tests or ending the user session entirely and closing the
|
|
41
|
+
// application
|
|
42
|
+
const app = new TestDisposableSingleApplication();
|
|
43
|
+
await app.startNextAndKillCurrent(async () => fakeApp);
|
|
44
|
+
expect(app.getApplication()).toBe(fakeApp);
|
|
45
|
+
await app[Symbol.asyncDispose]();
|
|
46
|
+
expect(fakeApp.killAndWait).toHaveBeenCalledOnce();
|
|
47
|
+
});
|
|
48
|
+
it("does nothing if there is no application to dispose", async () => {
|
|
49
|
+
const app = new TestDisposableSingleApplication();
|
|
50
|
+
expect(app.getApplication()).toBeUndefined();
|
|
51
|
+
expect(app.processId()).toBeUndefined();
|
|
52
|
+
expect(() => app[Symbol.asyncDispose]()).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|