@tui-sandbox/library 12.3.1 → 12.4.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/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@tui-sandbox/library",
3
- "version": "12.3.1",
3
+ "version": "12.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/mikavilpas/tui-sandbox"
7
7
  },
8
8
  "license": "MIT",
9
9
  "type": "module",
10
+ "imports": {
11
+ "#tui-sandbox-template": "./src/server/cypress-support/tui-sandbox-template.ts"
12
+ },
10
13
  "exports": {
11
14
  ".": {
12
15
  "types": "./dist/src/client/index.d.ts",
@@ -33,12 +36,13 @@
33
36
  "tui": "dist/src/scripts/tui.js"
34
37
  },
35
38
  "files": [
36
- "dist"
39
+ "dist",
40
+ "src/server/cypress-support/tui-sandbox-template.ts"
37
41
  ],
38
42
  "dependencies": {
39
- "@catppuccin/palette": "1.7.1",
40
- "@trpc/client": "11.11.0",
41
- "@trpc/server": "11.11.0",
43
+ "@catppuccin/palette": "1.8.0",
44
+ "@trpc/client": "11.16.0",
45
+ "@trpc/server": "11.16.0",
42
46
  "@xterm/addon-clipboard": "0.2.0",
43
47
  "@xterm/addon-fit": "0.11.0",
44
48
  "@xterm/addon-unicode11": "0.9.0",
@@ -59,12 +63,12 @@
59
63
  "@types/command-exists": "1.2.3",
60
64
  "@types/cors": "2.8.19",
61
65
  "@types/express": "5.0.6",
62
- "@types/node": "24.11.0",
63
- "cypress": "15.11.0",
66
+ "@types/node": "24.12.0",
67
+ "cypress": "15.13.0",
64
68
  "nodemon": "3.1.14",
65
- "type-fest": "5.4.4",
66
- "vite": "8.0.0-beta.16",
67
- "vitest": "4.0.18"
69
+ "type-fest": "5.5.0",
70
+ "vite": "8.0.3",
71
+ "vitest": "4.1.2"
68
72
  },
69
73
  "peerDependencies": {
70
74
  "cypress": "^13 || ^14 || ^15",
@@ -0,0 +1,344 @@
1
+ /// <reference types="cypress" />
2
+ //
3
+ // This file is autogenerated by tui-sandbox. Do not edit it directly.
4
+ //
5
+ import type {
6
+ BrowserTerminalSettings,
7
+ GenericNeovimBrowserApi,
8
+ GenericTerminalBrowserApi,
9
+ } from "@tui-sandbox/library/browser/neovim-client.js"
10
+ import type { MyNeovimConfigModification } from "@tui-sandbox/library/client"
11
+ import { drawTextBox } from "@tui-sandbox/library/client"
12
+ import type {
13
+ AllKeys,
14
+ BlockingCommandClientInput,
15
+ BlockingShellCommandOutput,
16
+ ExCommandClientInput,
17
+ LuaCodeClientInput,
18
+ PollLuaCodeClientInput,
19
+ RunExCommandOutput,
20
+ RunLuaCodeOutput,
21
+ RunLuaFileClientInput,
22
+ StartNeovimGenericArguments,
23
+ StartTerminalGenericArguments,
24
+ TestDirectory,
25
+ } from "@tui-sandbox/library/server"
26
+ import type { OverrideProperties } from "type-fest"
27
+ import type { MyNeovimAppName, MyTestDirectory, MyTestDirectoryFile } from "../../MyTestDirectory"
28
+
29
+ export type TerminalTestApplicationContext = {
30
+ /** Types text into the terminal, making the terminal application receive the
31
+ * keystrokes as input. Requires the application to be running. */
32
+ typeIntoTerminal(text: string, options?: Partial<Cypress.TypeOptions>): void
33
+
34
+ /** Runs a shell command in a blocking manner, waiting for the command to
35
+ * finish before returning. Requires the terminal to be running. */
36
+ runBlockingShellCommand(input: MyBlockingCommandClientInput): Cypress.Chainable<BlockingShellCommandOutput>
37
+
38
+ /** The test directory, providing type-safe access to its file and directory structure */
39
+ dir: TestDirectory<MyTestDirectory>
40
+
41
+ /** Access to the clipboard of the terminal */
42
+ clipboard: {
43
+ system(): Cypress.Chainable<string>
44
+ primary(): Cypress.Chainable<string>
45
+ }
46
+
47
+ title: {
48
+ current(): Cypress.Chainable<string>
49
+ }
50
+ }
51
+
52
+ /** The api that can be used in tests after a Neovim instance has been started. */
53
+ export type NeovimContext = {
54
+ /** Types text into the terminal, making the terminal application receive
55
+ * the keystrokes as input. Requires neovim to be running. */
56
+ typeIntoTerminal(text: string, options?: Partial<Cypress.TypeOptions>): void
57
+
58
+ /** Runs a shell command in a blocking manner, waiting for the command to
59
+ * finish before returning. Requires neovim to be running. */
60
+ runBlockingShellCommand(input: MyBlockingCommandClientInput): Cypress.Chainable<BlockingShellCommandOutput>
61
+
62
+ /** Runs a shell command in a blocking manner, waiting for the command to
63
+ * finish before returning. Requires neovim to be running. */
64
+ runLuaCode(input: LuaCodeClientInput): Cypress.Chainable<RunLuaCodeOutput>
65
+
66
+ /** Runs a Lua file in neovim after it has started. Can be used to keep
67
+ * complex lua logic in a separate file, still being able to run it after
68
+ * startup. This way additional tools like lua LSP servers, linters, etc. can
69
+ * be used to ensure the code is correct.
70
+ */
71
+ doFile(input: MyRunLuaFileClientInput): Cypress.Chainable<RunExCommandOutput>
72
+
73
+ /**
74
+ * Like runLuaCode, but waits until the given code (maybe using lua's return
75
+ * assert()) does not raise an error, and returns the first successful result.
76
+ *
77
+ * Useful for waiting until Neovim's internal state has changed in a way that
78
+ * means the test can continue executing. This can avoid timing issues that are
79
+ * otherwise hard to catch.
80
+ */
81
+ waitForLuaCode(input: PollLuaCodeClientInput): Cypress.Chainable<RunLuaCodeOutput>
82
+
83
+ /** Run an ex command in neovim.
84
+ * @example "echo expand('%:.')" current file, relative to the cwd
85
+ */
86
+ runExCommand(input: ExCommandClientInput): Cypress.Chainable<RunExCommandOutput>
87
+
88
+ /** The test directory, providing type-safe access to its file and directory structure */
89
+ dir: TestDirectory<MyTestDirectory>
90
+
91
+ /** Access to the clipboard of the terminal */
92
+ clipboard: {
93
+ system(): Cypress.Chainable<string>
94
+ primary(): Cypress.Chainable<string>
95
+ }
96
+
97
+ title: {
98
+ current(): Cypress.Chainable<string>
99
+ }
100
+ }
101
+
102
+ /** Arguments for starting the neovim server. They are built based on your test
103
+ * environment in a type safe manner. */
104
+ export type MyStartNeovimServerArguments = OverrideProperties<
105
+ StartNeovimGenericArguments,
106
+ {
107
+ NVIM_APPNAME?: MyNeovimAppName
108
+ filename?: MyTestDirectoryFile | { openInVerticalSplits: MyTestDirectoryFile[] }
109
+ startupScriptModifications?: Array<MyNeovimConfigModification<MyTestDirectoryFile>>
110
+ }
111
+ >
112
+
113
+ export type MyRunLuaFileClientInput = OverrideProperties<RunLuaFileClientInput, { luaFile: MyTestDirectoryFile }>
114
+
115
+ Cypress.Commands.add("startNeovim", (startArguments?: MyStartNeovimServerArguments) => {
116
+ cy.window().then(async win => {
117
+ const underlyingNeovim: GenericNeovimBrowserApi = await win.startNeovim(
118
+ startArguments as StartNeovimGenericArguments
119
+ )
120
+ testNeovim = underlyingNeovim
121
+ testEnvironmentPath = underlyingNeovim.dir.testEnvironmentPath
122
+
123
+ // wrap everything so that Cypress can await all the commands
124
+ Cypress.Commands.addAll({
125
+ nvim_runBlockingShellCommand: underlyingNeovim.runBlockingShellCommand,
126
+ nvim_runExCommand: underlyingNeovim.runExCommand,
127
+ nvim_runLuaCode: underlyingNeovim.runLuaCode,
128
+ nvim_waitForLuaCode: underlyingNeovim.waitForLuaCode,
129
+ nvim_doFile: underlyingNeovim.doFile,
130
+ })
131
+
132
+ const api: NeovimContext = {
133
+ runBlockingShellCommand(input) {
134
+ return cy.nvim_runBlockingShellCommand(input)
135
+ },
136
+ runExCommand(input) {
137
+ return cy.nvim_runExCommand(input)
138
+ },
139
+ runLuaCode(input) {
140
+ return cy.nvim_runLuaCode(input)
141
+ },
142
+ doFile(input) {
143
+ return cy.nvim_doFile(input)
144
+ },
145
+ waitForLuaCode(input) {
146
+ return cy.nvim_waitForLuaCode(input)
147
+ },
148
+ typeIntoTerminal(text, options) {
149
+ cy.typeIntoTerminal(text, options)
150
+ },
151
+ dir: underlyingNeovim.dir as TestDirectory<MyTestDirectory>,
152
+
153
+ clipboard: {
154
+ primary() {
155
+ return cy.then(() => underlyingNeovim.clipboard.primary())
156
+ },
157
+ system() {
158
+ return cy.then(() => underlyingNeovim.clipboard.system())
159
+ },
160
+ },
161
+
162
+ title: {
163
+ current: () => cy.then(() => underlyingNeovim.title.get()),
164
+ },
165
+ }
166
+
167
+ return api
168
+ })
169
+ })
170
+
171
+ Cypress.Commands.add("nvim_isRunning", () => {
172
+ return cy.window().then(async _ => {
173
+ return !!testNeovim
174
+ })
175
+ })
176
+
177
+ Cypress.Commands.add("startTerminalApplication", (args: StartTerminalGenericArguments & BrowserTerminalSettings) => {
178
+ cy.window().then(async win => {
179
+ const terminal: GenericTerminalBrowserApi = await win.startTerminalApplication({
180
+ serverSettings: {
181
+ commandToRun: args.commandToRun,
182
+ additionalEnvironmentVariables: args.additionalEnvironmentVariables,
183
+ } satisfies AllKeys<StartTerminalGenericArguments>,
184
+ browserSettings: {
185
+ configureTerminal: args.configureTerminal,
186
+ } satisfies AllKeys<BrowserTerminalSettings>,
187
+ })
188
+
189
+ testEnvironmentPath = terminal.dir.testEnvironmentPath
190
+
191
+ Cypress.Commands.addAll({
192
+ terminal_runBlockingShellCommand: terminal.runBlockingShellCommand,
193
+ })
194
+
195
+ const api: TerminalTestApplicationContext = {
196
+ dir: terminal.dir as TestDirectory<MyTestDirectory>,
197
+ runBlockingShellCommand(input) {
198
+ return cy.terminal_runBlockingShellCommand(input)
199
+ },
200
+ typeIntoTerminal(text, options) {
201
+ cy.typeIntoTerminal(text, options)
202
+ },
203
+ clipboard: {
204
+ primary: () => cy.then(() => terminal.clipboard.primary()),
205
+ system: () => cy.then(() => terminal.clipboard.system()),
206
+ },
207
+ title: {
208
+ current: () => cy.then(() => terminal.title.get()),
209
+ },
210
+ }
211
+
212
+ return api
213
+ })
214
+ })
215
+
216
+ Cypress.Commands.add("typeIntoTerminal", (text: string, options?: Partial<Cypress.TypeOptions>) => {
217
+ // the syntax for keys is described here:
218
+ // https://docs.cypress.io/api/commands/type
219
+ cy.get("textarea").focus().type(text, options)
220
+ })
221
+
222
+ let testNeovim: GenericNeovimBrowserApi | undefined
223
+ let testEnvironmentPath: string | undefined
224
+
225
+ before(function () {
226
+ // disable Cypress's default behavior of logging all XMLHttpRequests and
227
+ // fetches to the Command Log
228
+ // https://gist.github.com/simenbrekken/3d2248f9e50c1143bf9dbe02e67f5399?permalink_comment_id=4615046#gistcomment-4615046
229
+ cy.intercept({ resourceType: /xhr|fetch/ }, { log: false })
230
+ })
231
+
232
+ export type MyBlockingCommandClientInput = OverrideProperties<
233
+ BlockingCommandClientInput,
234
+ { cwdRelative?: MyTestDirectoryFile | undefined }
235
+ >
236
+
237
+ declare global {
238
+ namespace Cypress {
239
+ interface Chainable {
240
+ startNeovim(args?: MyStartNeovimServerArguments): Chainable<NeovimContext>
241
+ startTerminalApplication(
242
+ args: StartTerminalGenericArguments & BrowserTerminalSettings
243
+ ): Chainable<TerminalTestApplicationContext>
244
+
245
+ /** Types text into the terminal, making the terminal application receive
246
+ * the keystrokes as input. Requires neovim to be running. */
247
+ typeIntoTerminal(text: string, options?: Partial<Cypress.TypeOptions>): Chainable<void>
248
+
249
+ /** Runs a shell command in a blocking manner, waiting for the command to
250
+ * finish before returning. Requires neovim to be running. */
251
+ nvim_runBlockingShellCommand(input: MyBlockingCommandClientInput): Chainable<BlockingShellCommandOutput>
252
+
253
+ nvim_runLuaCode(input: LuaCodeClientInput): Chainable<RunLuaCodeOutput>
254
+ nvim_doFile(input: MyRunLuaFileClientInput): Chainable<RunExCommandOutput>
255
+ nvim_waitForLuaCode(input: PollLuaCodeClientInput): Chainable<RunLuaCodeOutput>
256
+
257
+ /** Run an ex command in neovim.
258
+ * @example "echo expand('%:.')" current file, relative to the cwd
259
+ */
260
+ nvim_runExCommand(input: ExCommandClientInput): Chainable<RunExCommandOutput>
261
+
262
+ /** Returns true if neovim is running. Useful to conditionally run
263
+ * afterEach actions based on whether it's running. */
264
+ nvim_isRunning(): Chainable<boolean>
265
+
266
+ terminal_runBlockingShellCommand(input: MyBlockingCommandClientInput): Chainable<BlockingShellCommandOutput>
267
+ }
268
+ }
269
+ }
270
+
271
+ afterEach(function () {
272
+ testNeovim = undefined
273
+
274
+ if (testEnvironmentPath) {
275
+ const testErr = this.currentTest?.err
276
+ const terminalContent = getTerminalContent() ?? ""
277
+
278
+ const snapshot = {
279
+ testTitle: this.currentTest?.fullTitle() ?? "unknown",
280
+ testState: this.currentTest?.state ?? "unknown",
281
+ terminalContent,
282
+ error: testErr?.message ?? null,
283
+ timestamp: new Date().toISOString(),
284
+ }
285
+
286
+ // Write to testdirs/ which is already gitignored for all tui-sandbox consumers.
287
+ // YAML is used so that multiline terminal content is human/AI-readable
288
+ // without escape sequences.
289
+ const snapshotPath = `${testEnvironmentPath}/testdirs/.terminal-snapshot.yaml`
290
+ const indentedContent = snapshot.terminalContent
291
+ .split("\n")
292
+ .map(line => ` ${line}`)
293
+ .join("\n")
294
+ const testFile = this.currentTest?.file ?? "unknown"
295
+ const yaml = [
296
+ `testTitle: ${JSON.stringify(snapshot.testTitle)}`,
297
+ `testFile: ${JSON.stringify(testFile)}`,
298
+ `testState: ${snapshot.testState}`,
299
+ `error: ${snapshot.error ? JSON.stringify(snapshot.error) : "null"}`,
300
+ `timestamp: ${JSON.stringify(snapshot.timestamp)}`,
301
+ `terminalContent: |`,
302
+ indentedContent,
303
+ ].join("\n")
304
+ cy.writeFile(snapshotPath, yaml, { log: false })
305
+ }
306
+ })
307
+
308
+ /** Read the current terminal content from the xterm.js DOM. */
309
+ function getTerminalContent(): string | undefined {
310
+ const rows = Cypress.$("div.xterm-rows > div")
311
+ if (rows.length === 0) return undefined
312
+
313
+ const lines: string[] = []
314
+ rows.each((_, row) => {
315
+ lines.push(Cypress.$(row).text())
316
+ })
317
+
318
+ // Trim trailing empty lines for readability
319
+ while (lines.length > 0 && lines[lines.length - 1]?.trim() === "") {
320
+ lines.pop()
321
+ }
322
+
323
+ return lines.length > 0 ? lines.join("\n") : undefined
324
+ }
325
+
326
+ // On test failure in headless mode (cypress run / CI), append the terminal
327
+ // buffer contents to the error message so it appears in logs. In interactive
328
+ // mode (cypress open), the terminal is already visible on screen so this
329
+ // would just add noise.
330
+ //
331
+ // Registered via beforeEach + cy.on (per-test) so that tests can override
332
+ // with their own cy.on('fail') handler to swallow expected errors.
333
+ beforeEach(function () {
334
+ cy.on("fail", (err: Error) => {
335
+ if (!Cypress.config("isInteractive")) {
336
+ const content = getTerminalContent()
337
+ if (content) {
338
+ err.message += `\n\n${drawTextBox("Terminal content at failure", content)}`
339
+ }
340
+ }
341
+
342
+ throw err
343
+ })
344
+ })