@tui-sandbox/library 9.5.1 → 10.0.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/browser/assets/{index-BZnWo448.js → index-Dm7lp9YI.js} +1 -1
  3. package/dist/browser/index.html +1 -1
  4. package/dist/src/browser/neovim-client.d.ts +2 -1
  5. package/dist/src/browser/neovim-client.js +3 -0
  6. package/dist/src/browser/neovim-client.js.map +1 -1
  7. package/dist/src/client/neovim-terminal-client.d.ts +2 -1
  8. package/dist/src/client/neovim-terminal-client.js +9 -0
  9. package/dist/src/client/neovim-terminal-client.js.map +1 -1
  10. package/dist/src/scripts/tui.js +9 -0
  11. package/dist/src/scripts/tui.js.map +1 -1
  12. package/dist/src/server/cypress-support/contents.js +20 -1
  13. package/dist/src/server/cypress-support/contents.js.map +1 -1
  14. package/dist/src/server/dirtree/index.test.js +5 -0
  15. package/dist/src/server/dirtree/index.test.js.map +1 -1
  16. package/dist/src/server/neovim/index.d.ts +5 -0
  17. package/dist/src/server/neovim/index.js +33 -0
  18. package/dist/src/server/neovim/index.js.map +1 -1
  19. package/dist/src/server/server.d.ts +35 -2
  20. package/dist/src/server/server.js +10 -10
  21. package/dist/src/server/server.js.map +1 -1
  22. package/dist/src/server/utilities/timeout.js +1 -1
  23. package/dist/src/server/utilities/timeout.js.map +1 -1
  24. package/dist/src/server/utilities/timeoutable.d.ts +1 -1
  25. package/dist/src/server/utilities/timeoutable.js +11 -9
  26. package/dist/src/server/utilities/timeoutable.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +9 -9
  29. package/src/browser/neovim-client.ts +5 -1
  30. package/src/client/neovim-terminal-client.ts +10 -1
  31. package/src/scripts/tui.ts +10 -0
  32. package/src/server/cypress-support/contents.ts +20 -1
  33. package/src/server/dirtree/index.test.ts +5 -0
  34. package/src/server/neovim/index.ts +56 -0
  35. package/src/server/server.ts +14 -14
  36. package/src/server/utilities/timeout.ts +1 -1
  37. package/src/server/utilities/timeoutable.ts +13 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tui-sandbox/library",
3
- "version": "9.5.1",
3
+ "version": "10.0.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,21 +8,21 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@catppuccin/palette": "1.7.1",
11
- "@trpc/client": "11.0.0-rc.819",
12
- "@trpc/server": "11.0.0-rc.819",
11
+ "@trpc/client": "11.0.0-rc.828",
12
+ "@trpc/server": "11.0.0-rc.828",
13
13
  "@xterm/addon-attach": "0.11.0",
14
14
  "@xterm/addon-fit": "0.10.0",
15
15
  "@xterm/xterm": "5.5.0",
16
16
  "command-exists": "1.2.9",
17
- "core-js": "3.40.0",
17
+ "core-js": "3.41.0",
18
18
  "cors": "2.8.5",
19
19
  "dree": "5.1.5",
20
20
  "express": "4.21.2",
21
21
  "neovim": "5.3.0",
22
22
  "node-pty": "1.0.0",
23
- "prettier": "3.5.2",
23
+ "prettier": "3.5.3",
24
24
  "tsx": "4.19.3",
25
- "type-fest": "4.35.0",
25
+ "type-fest": "4.37.0",
26
26
  "winston": "3.17.0",
27
27
  "zod": "3.24.2"
28
28
  },
@@ -31,10 +31,10 @@
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.13.7",
34
+ "@types/node": "22.13.10",
35
35
  "nodemon": "3.1.9",
36
- "vite": "6.2.0",
37
- "vitest": "3.0.7"
36
+ "vite": "6.2.1",
37
+ "vitest": "3.0.8"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "cypress": "^13 || ^14",
@@ -1,7 +1,7 @@
1
1
  import { TerminalClient as NeovimTerminalClient } from "../client/index.js"
2
2
  import { TerminalTerminalClient } from "../client/terminal-terminal-client.js"
3
3
  import type { BlockingCommandClientInput } from "../server/blockingCommandInputSchema.js"
4
- import type { ExCommandClientInput, LuaCodeClientInput } from "../server/server.js"
4
+ import type { ExCommandClientInput, LuaCodeClientInput, PollLuaCodeClientInput } from "../server/server.js"
5
5
  import type { StartTerminalGenericArguments } from "../server/terminal/TerminalTestApplication.js"
6
6
  import type {
7
7
  BlockingShellCommandOutput,
@@ -24,6 +24,7 @@ const terminalClient = new Lazy(() => new TerminalTerminalClient(app))
24
24
  export type GenericNeovimBrowserApi = {
25
25
  runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput>
26
26
  runLuaCode(input: LuaCodeClientInput): Promise<RunLuaCodeOutput>
27
+ waitForLuaCode(input: PollLuaCodeClientInput): Promise<RunLuaCodeOutput>
27
28
  runExCommand(input: ExCommandClientInput): Promise<RunExCommandOutput>
28
29
  dir: TestDirectory
29
30
  }
@@ -44,6 +45,9 @@ window.startNeovim = async function (startArgs?: StartNeovimGenericArguments): P
44
45
  runLuaCode(input) {
45
46
  return neovim.runLuaCode(input)
46
47
  },
48
+ waitForLuaCode(input) {
49
+ return neovim.waitForLuaCode(input)
50
+ },
47
51
  runExCommand(input) {
48
52
  return neovim.runExCommand(input)
49
53
  },
@@ -2,7 +2,7 @@ import { createTRPCClient, httpBatchLink, splitLink, unstable_httpSubscriptionLi
2
2
  import type { Terminal } from "@xterm/xterm"
3
3
  import "@xterm/xterm/css/xterm.css"
4
4
  import type { BlockingCommandClientInput } from "../server/blockingCommandInputSchema.js"
5
- import type { AppRouter, ExCommandClientInput, LuaCodeClientInput } from "../server/server.js"
5
+ import type { AppRouter, ExCommandClientInput, LuaCodeClientInput, PollLuaCodeClientInput } from "../server/server.js"
6
6
  import type {
7
7
  BlockingShellCommandOutput,
8
8
  RunExCommandOutput,
@@ -102,6 +102,15 @@ export class NeovimTerminalClient {
102
102
  return this.trpc.neovim.runLuaCode.mutate({ ...input, tabId: this.tabId })
103
103
  }
104
104
 
105
+ public async waitForLuaCode(input: PollLuaCodeClientInput): Promise<RunLuaCodeOutput> {
106
+ await this.ready
107
+ try {
108
+ return await this.trpc.neovim.waitForLuaCode.mutate({ ...input, tabId: this.tabId })
109
+ } catch (e) {
110
+ throw e
111
+ }
112
+ }
113
+
105
114
  public async runExCommand(input: ExCommandClientInput): Promise<RunExCommandOutput> {
106
115
  await this.ready
107
116
  return this.trpc.neovim.runExCommand.mutate({ ...input, tabId: this.tabId })
@@ -3,6 +3,7 @@ import path from "node:path"
3
3
  import { createCypressSupportFile } from "../server/cypress-support/createCypressSupportFile.js"
4
4
  import type { TestServerConfig } from "../server/index.js"
5
5
  import { startTestServer, updateTestdirectorySchemaFile } from "../server/index.js"
6
+ import { installDependencies } from "../server/neovim/index.js"
6
7
  import type { StdoutOrStderrMessage } from "../server/neovim/NeovimApplication.js"
7
8
  import { NeovimApplication } from "../server/neovim/NeovimApplication.js"
8
9
  import { prepareNewTestDirectory } from "../server/neovim/prepareNewTestDirectory.js"
@@ -28,6 +29,15 @@ const config = {
28
29
  const args = process.argv.slice(2)
29
30
 
30
31
  if (args[0] === "neovim") {
32
+ if (args[1] === "prepare" && args.length === 2) {
33
+ console.log("🚀 Installing neovim dependencies...")
34
+ await installDependencies(config.directories.testEnvironmentPath, config.directories).catch((err: unknown) => {
35
+ console.error("Error installing neovim dependencies", err)
36
+ process.exit(1)
37
+ })
38
+ process.exit(0)
39
+ }
40
+
31
41
  if (!(args[1] === "exec" && args.length === 3)) {
32
42
  showUsageAndExit()
33
43
  }
@@ -14,7 +14,11 @@ import type {
14
14
  GenericNeovimBrowserApi,
15
15
  GenericTerminalBrowserApi,
16
16
  } from "@tui-sandbox/library/dist/src/browser/neovim-client"
17
- import type { ExCommandClientInput, LuaCodeClientInput } from "@tui-sandbox/library/dist/src/server/server"
17
+ import type {
18
+ ExCommandClientInput,
19
+ LuaCodeClientInput,
20
+ PollLuaCodeClientInput,
21
+ } from "@tui-sandbox/library/dist/src/server/server"
18
22
  import type {
19
23
  BlockingShellCommandOutput,
20
24
  RunExCommandOutput,
@@ -54,6 +58,16 @@ export type NeovimContext = {
54
58
  * finish before returning. Requires neovim to be running. */
55
59
  runLuaCode(input: LuaCodeClientInput): Cypress.Chainable<RunLuaCodeOutput>
56
60
 
61
+ /**
62
+ * Like runLuaCode, but waits until the given code (maybe using lua's return
63
+ * assert()) does not raise an error, and returns the first successful result.
64
+ *
65
+ * Useful for waiting until Neovim's internal state has changed in a way that
66
+ * means the test can continue executing. This can avoid timing issues that are
67
+ * otherwise hard to catch.
68
+ */
69
+ waitForLuaCode(input: PollLuaCodeClientInput): Cypress.Chainable<RunLuaCodeOutput>
70
+
57
71
  /** Run an ex command in neovim.
58
72
  * @example "echo expand('%:.')" current file, relative to the cwd
59
73
  */
@@ -85,6 +99,7 @@ Cypress.Commands.add("startNeovim", (startArguments?: MyStartNeovimServerArgumen
85
99
  nvim_runBlockingShellCommand: underlyingNeovim.runBlockingShellCommand,
86
100
  nvim_runExCommand: underlyingNeovim.runExCommand,
87
101
  nvim_runLuaCode: underlyingNeovim.runLuaCode,
102
+ nvim_waitForLuaCode: underlyingNeovim.waitForLuaCode,
88
103
  })
89
104
 
90
105
  const api: NeovimContext = {
@@ -97,6 +112,9 @@ Cypress.Commands.add("startNeovim", (startArguments?: MyStartNeovimServerArgumen
97
112
  runLuaCode(input) {
98
113
  return cy.nvim_runLuaCode(input)
99
114
  },
115
+ waitForLuaCode(input) {
116
+ return cy.nvim_waitForLuaCode(input)
117
+ },
100
118
  typeIntoTerminal(text, options) {
101
119
  cy.typeIntoTerminal(text, options)
102
120
  },
@@ -164,6 +182,7 @@ declare global {
164
182
  nvim_runBlockingShellCommand(input: MyBlockingCommandClientInput): Chainable<BlockingShellCommandOutput>
165
183
 
166
184
  nvim_runLuaCode(input: LuaCodeClientInput): Chainable<RunLuaCodeOutput>
185
+ nvim_waitForLuaCode(input: PollLuaCodeClientInput): Chainable<RunLuaCodeOutput>
167
186
 
168
187
  /** Run an ex command in neovim.
169
188
  * @example "echo expand('%:.')" current file, relative to the cwd
@@ -63,6 +63,10 @@ describe("dirtree", () => {
63
63
  name: z.literal("add_command_to_count_open_buffers.lua"),
64
64
  type: z.literal("file"),
65
65
  }),
66
+ "add_command_to_update_buffer_after_timeout.lua": z.object({
67
+ name: z.literal("add_command_to_update_buffer_after_timeout.lua"),
68
+ type: z.literal("file"),
69
+ }),
66
70
  "don't_crash_when_modification_contains_unescaped_characters\\".lua": z.object({
67
71
  name: z.literal("don't_crash_when_modification_contains_unescaped_characters\\".lua"),
68
72
  type: z.literal("file"),
@@ -135,6 +139,7 @@ describe("dirtree", () => {
135
139
  ".config/nvim",
136
140
  ".config",
137
141
  "config-modifications/add_command_to_count_open_buffers.lua",
142
+ "config-modifications/add_command_to_update_buffer_after_timeout.lua",
138
143
  "config-modifications/don't_crash_when_modification_contains_unescaped_characters\\".lua",
139
144
  "config-modifications",
140
145
  "dir with spaces/file1.txt",
@@ -16,6 +16,7 @@ import type { DirectoriesConfig } from "../updateTestdirectorySchemaFile.js"
16
16
  import { convertEventEmitterToAsyncGenerator } from "../utilities/generator.js"
17
17
  import { Lazy } from "../utilities/Lazy.js"
18
18
  import type { TabId } from "../utilities/tabId.js"
19
+ import { timeout } from "../utilities/timeout.js"
19
20
  import type { StdoutOrStderrMessage, TerminalDimensions } from "./NeovimApplication.js"
20
21
  import { NeovimApplication } from "./NeovimApplication.js"
21
22
  import { prepareNewTestDirectory } from "./prepareNewTestDirectory.js"
@@ -154,6 +155,61 @@ export async function runLuaCode(options: LuaCodeInput): Promise<RunLuaCodeOutpu
154
155
  }
155
156
  }
156
157
 
158
+ export type PollLuaCodeInput = {
159
+ luaAssertion: string
160
+ tabId: TabId
161
+ }
162
+
163
+ export async function waitForLuaCode(
164
+ options: PollLuaCodeInput,
165
+ signal: AbortSignal | undefined
166
+ ): Promise<RunLuaCodeOutput> {
167
+ const neovim = neovims.get(options.tabId.tabId)
168
+ assert(
169
+ neovim !== undefined,
170
+ `Neovim instance for clientId not found - cannot pollLuaCode. Maybe neovim's not started yet?`
171
+ )
172
+ assert(
173
+ neovim.application,
174
+ `Neovim application not found for client id ${options.tabId.tabId}. Maybe it's not started yet?`
175
+ )
176
+
177
+ const api = await neovim.state?.client.get()
178
+ if (!api) {
179
+ throw new Error(`Neovim API not available for client id ${options.tabId.tabId}. Maybe it's not started yet?`)
180
+ }
181
+
182
+ console.log(`Neovim ${neovim.application.processId()} polling Lua code: ${options.luaAssertion}`)
183
+
184
+ let running: boolean = true
185
+ signal?.addEventListener("abort", () => {
186
+ console.log(`Polling Lua code: '${options.luaAssertion}' was aborted via signal`)
187
+ running = false
188
+ })
189
+
190
+ const maxIterations = 100
191
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
192
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
193
+ if (!running) {
194
+ throw new Error(`Polling Lua code: '${options.luaAssertion}' was aborted after ${iteration} iterations`)
195
+ }
196
+
197
+ try {
198
+ const value = await api.lua(options.luaAssertion)
199
+ console.log(`Lua code assertion passed: ${options.luaAssertion} (iteration ${iteration})`)
200
+
201
+ return { value }
202
+ } catch (e) {
203
+ console.error(`Caught error in iteration ${iteration}:`, e)
204
+ await timeout(100)
205
+ }
206
+ }
207
+
208
+ throw new Error(
209
+ `Polling Lua code: '${options.luaAssertion}' always raised an error after ${maxIterations} iterations`
210
+ )
211
+ }
212
+
157
213
  export async function runExCommand(options: ExCommandInput): Promise<RunExCommandOutput> {
158
214
  const neovim = neovims.get(options.tabId.tabId)
159
215
  assert(
@@ -15,6 +15,13 @@ const luaCodeInputSchema = z.object({ tabId: tabIdSchema, luaCode: z.string() })
15
15
  export type LuaCodeClientInput = Except<LuaCodeInput, "tabId">
16
16
  export type LuaCodeInput = z.infer<typeof luaCodeInputSchema>
17
17
 
18
+ const pollLuaCodeInputSchema = z.object({
19
+ tabId: tabIdSchema,
20
+ luaAssertion: z.string(),
21
+ timeoutMs: z.number().optional().default(10_000),
22
+ })
23
+ export type PollLuaCodeClientInput = Except<z.input<typeof pollLuaCodeInputSchema>, "tabId">
24
+
18
25
  const exCommandInputSchema = z.object({
19
26
  tabId: tabIdSchema,
20
27
  command: z.string(),
@@ -108,6 +115,11 @@ export async function createAppRouter(config: DirectoriesConfig) {
108
115
  return timeoutable(10_000, neovim.runLuaCode(options.input))
109
116
  }),
110
117
 
118
+ waitForLuaCode: trpc.procedure.input(pollLuaCodeInputSchema).mutation(async options => {
119
+ const result = await timeoutable(options.input.timeoutMs, neovim.waitForLuaCode(options.input, options.signal))
120
+ return result
121
+ }),
122
+
111
123
  runExCommand: trpc.procedure.input(exCommandInputSchema).mutation(options => {
112
124
  return timeoutable(10_000, neovim.runExCommand(options.input))
113
125
  }),
@@ -120,26 +132,14 @@ export async function createAppRouter(config: DirectoriesConfig) {
120
132
  export type AppRouter = Awaited<ReturnType<typeof createAppRouter>>
121
133
  export type RouterInput = inferRouterInputs<AppRouter>
122
134
 
123
- export async function startTestServer(config: TestServerConfig): Promise<TestServer> {
135
+ export async function startTestServer(config: TestServerConfig): Promise<void> {
124
136
  try {
125
137
  const testServer = new TestServer({
126
138
  port: config.port,
127
139
  })
128
140
  const appRouter = await createAppRouter(config.directories)
129
141
 
130
- const neovimTask: Promise<void> = neovim
131
- .installDependencies(config.directories.testEnvironmentPath, config.directories)
132
- .catch((err: unknown) => {
133
- console.error("Error installing neovim dependencies", err)
134
- // suppress the error because neovim is optional - other applications
135
- // can still be tested
136
- })
137
-
138
- const startServerTask = testServer.startAndRun(appRouter)
139
-
140
- await Promise.all([neovimTask, startServerTask])
141
-
142
- return testServer
142
+ await testServer.startAndRun(appRouter)
143
143
  } catch (err: unknown) {
144
144
  console.error("Error starting test server", err)
145
145
  throw err
@@ -1,3 +1,3 @@
1
1
  export function timeout(ms: number): Promise<unknown> {
2
- return new Promise((_, reject) => setTimeout(reject, ms))
2
+ return new Promise(resolve => setTimeout(resolve, ms))
3
3
  }
@@ -1,15 +1,19 @@
1
- export async function timeoutable<T>(ms: number, promise: Promise<T>): Promise<T> {
2
- let timeoutId: NodeJS.Timeout | undefined = undefined
3
- const timeout = new Promise<void>((_, reject) => {
4
- timeoutId = setTimeout(() => {
5
- reject(new Error(`Timeout after ${ms}ms`))
6
- }, ms)
1
+ import assert from "node:assert"
2
+
3
+ export async function timeoutable<T>(timeoutMs: number, promise: Promise<T>): Promise<T> {
4
+ let timeoutHandle: NodeJS.Timeout
5
+
6
+ const timeoutPromise = new Promise<T>((_, reject) => {
7
+ timeoutHandle = setTimeout(() => {
8
+ reject(new Error(`Operation timed out after ${timeoutMs}ms`))
9
+ }, timeoutMs)
7
10
  })
8
11
 
9
12
  try {
10
- await Promise.race([timeout, promise])
11
- return await promise
13
+ return await Promise.race([promise, timeoutPromise])
12
14
  } finally {
13
- clearTimeout(timeoutId) // Ensure the timeout is cleared
15
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
16
+ assert(timeoutHandle!)
17
+ clearTimeout(timeoutHandle)
14
18
  }
15
19
  }