@textui/testing 0.1.0 → 0.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/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @textui/testing
2
2
 
3
- A headless harness for TextUI applications. No terminal, no timers, no
4
- snapshots of a screen nobody looked at.
3
+ [![npm](https://img.shields.io/npm/v/@textui/testing?label=%40textui%2Ftesting)](https://www.npmjs.com/package/@textui/testing) [![license](https://img.shields.io/npm/l/@textui/testing)](https://github.com/softov/textui/blob/main/LICENSE)
4
+
5
+ A headless harness for TextUI applications. No terminal, no timers, no snapshots of a screen nobody looked at.
5
6
 
6
7
  ```bash
7
8
  npm install --save-dev @textui/testing
@@ -20,9 +21,7 @@ expect(t.text()).toContain('Saved');
20
21
 
21
22
  ## Query by what it is, not where it is
22
23
 
23
- Every component declares a semantic role, so a test asks the way a person would
24
- - the Save button, the row labelled `api` - rather than by index into a tree
25
- that changes the moment the layout does.
24
+ Every component declares a semantic role, so a test asks the way a person would - the Save button, the row labelled `api` - rather than by index into a tree that changes the moment the layout does.
26
25
 
27
26
  | | |
28
27
  |---|---|
@@ -32,21 +31,17 @@ that changes the moment the layout does.
32
31
  | `text()`, `line(y)`, `lines()` | What is actually on the screen |
33
32
  | `tree()` | The instance tree, for when the pixels are not the question |
34
33
 
35
- An `Element` is a description - role, label, text, rect, focus, props - not a
36
- handle. You act on the application with keys, the way a person does.
34
+ An `Element` is a description - role, label, text, rect, focus, props - not a handle. You act on the application with keys, the way a person does.
37
35
 
38
36
  `get*` throws with the near misses listed; `query*` returns null.
39
37
 
40
38
  ## Drive it, then let it settle
41
39
 
42
- `press('ctrl+s')`, `pressAll('down', 'down', 'enter')`, `type('hello')`, and
43
- `resize(w, h)`. Effects and re-renders are asynchronous, so `await t.settle()`
44
- is what separates "I sent the key" from "the screen caught up".
40
+ `press('ctrl+s')`, `pressAll('down', 'down', 'enter')`, `type('hello')`, and `resize(w, h)`. Effects and re-renders are asynchronous, so `await t.settle()` is what separates "I sent the key" from "the screen caught up".
45
41
 
46
42
  ## Test at two sizes
47
43
 
48
- A single fixed size skips every breakpoint and hides the layout break you were
49
- trying to catch. `resize` is cheap, and a loop over two sizes costs one line:
44
+ A single fixed size skips every breakpoint and hides the layout break you were trying to catch. `resize` is cheap, and a loop over two sizes costs one line:
50
45
 
51
46
  ```ts
52
47
  for (const size of [{ width: 100, height: 30 }, { width: 60, height: 18 }]) {
@@ -56,9 +51,16 @@ for (const size of [{ width: 100, height: 30 }, { width: 60, height: 18 }]) {
56
51
 
57
52
  ## Runtime
58
53
 
59
- Depends on `@textui/core` and `@textui/terminal`. No `node:` imports - it runs
60
- on the virtual terminal, so there is no tty to have. Node 22+ and Bun.
54
+ Depends on [`@textui/core`](https://www.npmjs.com/package/@textui/core) and [`@textui/terminal`](https://www.npmjs.com/package/@textui/terminal). No `node:` imports - it runs on the virtual terminal, so there is no tty to have. Node 22+ and Bun.
61
55
 
62
56
  ## Documentation
63
57
 
64
58
  <https://softov.github.io/textui/>
59
+
60
+ <!-- family -->
61
+
62
+ ---
63
+
64
+ Part of **[TextUI](https://github.com/softov/textui)** - [documentation](https://softov.github.io/textui/) - [getting started](https://softov.github.io/textui/getting-started.html)
65
+
66
+ [`@textui/kit`](https://www.npmjs.com/package/@textui/kit) one install · [`@textui/core`](https://www.npmjs.com/package/@textui/core) the runtime · [`@textui/widgets`](https://www.npmjs.com/package/@textui/widgets) the catalog · [`@textui/terminal`](https://www.npmjs.com/package/@textui/terminal) adapters and input · **`@textui/testing`** the harness · [`@textui/cli`](https://www.npmjs.com/package/@textui/cli) the CLI
package/dist/index.d.ts CHANGED
@@ -105,6 +105,24 @@ export interface Harness {
105
105
  click(x: number, y: number, button?: 'left' | 'middle' | 'right'): void;
106
106
  /** Click the centre of an element. */
107
107
  clickOn(element: Element): void;
108
+ /**
109
+ * Click the same cell `times` in a row, close enough together to count as
110
+ * one gesture.
111
+ *
112
+ * `click` deliberately does not: it steps the clock past the repeat window
113
+ * each time, so two of them are two clicks and a test that means a double
114
+ * click has to say so.
115
+ */
116
+ clickRepeat(x: number, y: number, times: number): void;
117
+ /**
118
+ * Press at one point, drag through any points between, and release at the
119
+ * last one - the whole gesture, because that is the only part of it a
120
+ * handler can be wrong about.
121
+ *
122
+ * The drag goes to whoever took the press, wherever the points lead, so
123
+ * dragging off the element under test is a case worth writing.
124
+ */
125
+ drag(from: [number, number], ...to: [number, number][]): void;
108
126
  moveMouse(x: number, y: number): void;
109
127
  wheel(x: number, y: number, delta: number): void;
110
128
  focusTerminal(focused: boolean): void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACnB,aAAa,EACb,UAAU,EACV,aAAa,EACb,QAAQ,EACR,SAAS,EACT,aAAa,EACb,YAAY,EACZ,eAAe,EAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAyB,QAAQ,EAAoC,MAAM,cAAc,CAAC;AAEjG,OAAO,EAAuC,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAEpG;;;;;;;;GAQG;AAEH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACnC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,MAAM,CAAC,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,GAAG,UAAU,CAAC,CAAC;IACxE,8EAA8E;IAC9E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,sDAAsD;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAG1C,wDAAwD;IACxD,IAAI,IAAI,MAAM,CAAC;IACf,4BAA4B;IAC5B,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,+BAA+B;IAC/B,KAAK,IAAI,MAAM,EAAE,CAAC;IAClB,8DAA8D;IAC9D,MAAM,IAAI,MAAM,CAAC;IACjB,WAAW,IAAI,IAAI,CAAC;IACpB,oDAAoD;IACpD,IAAI,IAAI,aAAa,GAAG,IAAI,CAAC;IAG7B,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;IACnF,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,GAAG,IAAI,CAAC;IAC5F,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,EAAE,CAAC;IAC5C,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC;IAC3D,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,GAAG,IAAI,CAAC;IACpE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC;IACzD,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,GAAG,IAAI,CAAC;IAClE,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,EAAE,CAAC;IAC9D,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;IAC3C,wDAAwD;IACxD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAG/B,2DAA2D;IAC3D,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oCAAoC;IACpC,QAAQ,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACpC,wCAAwC;IACxC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IACxE,sCAAsC;IACtC,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACjD,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACtC,2DAA2D;IAC3D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAGzB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,eAAe,CAAC,SAAS,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACtD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAG3B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,yCAAyC;IACzC,KAAK,IAAI,IAAI,CAAC;IACd,+CAA+C;IAC/C,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAGxB,OAAO,IAAI,OAAO,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,IAAI,IAAI,CAAC;IACZ,QAAQ,IAAI,IAAI,CAAC;IAGjB,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjD,KAAK,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAE9D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,0CAA0C;AAC1C,wBAAsB,MAAM,CAC1B,IAAI,EAAE,aAAa,EACnB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAElB;AAED,oDAAoD;AACpD,wBAAsB,SAAS,CAC7B,OAAO,GAAE,aAAa,GAAG;IAAE,IAAI,CAAC,EAAE,aAAa,CAAA;CAAO,GACrD,OAAO,CAAC,OAAO,CAAC,CAElB;AAqSD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CA0BvD;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC;AAEpB;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAQpF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -69,6 +69,14 @@ async function mount(options) {
69
69
  }
70
70
  function createHarness(app, terminal, errors) {
71
71
  const flush = () => app.flush();
72
+ /**
73
+ * The clock mouse events are stamped from.
74
+ *
75
+ * A real terminal stamps them on arrival; here they are synthesised, so the
76
+ * harness owns the time - which is what lets "a double click" and "two
77
+ * clicks" be two different tests rather than a race.
78
+ */
79
+ let mouseClock = 0;
72
80
  const collect = (predicate) => {
73
81
  const tree = app.inspect();
74
82
  if (!tree)
@@ -172,8 +180,22 @@ function createHarness(app, terminal, errors) {
172
180
  flush();
173
181
  },
174
182
  click(x, y, button = 'left') {
175
- app.handleInput({ type: 'mouse', action: 'down', button, x, y, ctrl: false, alt: false, shift: false, handled: false });
176
- app.handleInput({ type: 'mouse', action: 'up', button, x, y, ctrl: false, alt: false, shift: false, handled: false });
183
+ // Past any repeat window, so consecutive clicks in a test are what they
184
+ // look like: separate ones.
185
+ mouseClock += 1000;
186
+ app.handleInput({ type: 'mouse', action: 'down', button, x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
187
+ app.handleInput({ type: 'mouse', action: 'up', button, x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
188
+ flush();
189
+ },
190
+ clickRepeat(x, y, times) {
191
+ mouseClock += 1000;
192
+ for (let i = 0; i < times; i++) {
193
+ // Inside the window, and on the same cell: the two things that make
194
+ // presses one gesture rather than several.
195
+ mouseClock += 60;
196
+ app.handleInput({ type: 'mouse', action: 'down', button: 'left', x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
197
+ app.handleInput({ type: 'mouse', action: 'up', button: 'left', x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
198
+ }
177
199
  flush();
178
200
  },
179
201
  clickOn(element) {
@@ -182,6 +204,19 @@ function createHarness(app, terminal, errors) {
182
204
  throw new Error(`[textui/testing] <${element.component}> has no bounds to click`);
183
205
  this.click(rect.x + Math.floor(rect.width / 2), rect.y + Math.floor(rect.height / 2));
184
206
  },
207
+ drag(from, ...to) {
208
+ mouseClock += 1000;
209
+ const send = (action, [x, y]) => {
210
+ mouseClock += 20;
211
+ app.handleInput({ type: 'mouse', action, button: 'left', x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
212
+ };
213
+ send('down', from);
214
+ const last = to[to.length - 1] ?? from;
215
+ for (const point of to)
216
+ send('drag', point);
217
+ send('up', last);
218
+ flush();
219
+ },
185
220
  moveMouse(x, y) {
186
221
  app.handleInput({ type: 'mouse', action: 'move', button: 'none', x, y, ctrl: false, alt: false, shift: false, handled: false });
187
222
  flush();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textui/testing",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Headless testing harness for TextUI - semantic queries, input, resizing, time",
5
5
  "keywords": [
6
6
  "terminal",
@@ -39,17 +39,17 @@
39
39
  "import": "./dist/index.js"
40
40
  }
41
41
  },
42
- "dependencies": {
43
- "@textui/widgets": "^0.1.0",
44
- "@textui/terminal": "^0.1.0",
45
- "@textui/core": "^0.1.0"
46
- },
47
- "publishConfig": {
48
- "access": "public"
49
- },
50
42
  "scripts": {
51
43
  "build": "tsc -p tsconfig.json",
52
44
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
53
45
  "test": "vitest run"
46
+ },
47
+ "dependencies": {
48
+ "@textui/core": "^0.2.0",
49
+ "@textui/terminal": "^0.2.0",
50
+ "@textui/widgets": "^0.2.0"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
54
  }
55
- }
55
+ }
package/src/index.ts CHANGED
@@ -119,6 +119,24 @@ export interface Harness {
119
119
  click(x: number, y: number, button?: 'left' | 'middle' | 'right'): void;
120
120
  /** Click the centre of an element. */
121
121
  clickOn(element: Element): void;
122
+ /**
123
+ * Click the same cell `times` in a row, close enough together to count as
124
+ * one gesture.
125
+ *
126
+ * `click` deliberately does not: it steps the clock past the repeat window
127
+ * each time, so two of them are two clicks and a test that means a double
128
+ * click has to say so.
129
+ */
130
+ clickRepeat(x: number, y: number, times: number): void;
131
+ /**
132
+ * Press at one point, drag through any points between, and release at the
133
+ * last one - the whole gesture, because that is the only part of it a
134
+ * handler can be wrong about.
135
+ *
136
+ * The drag goes to whoever took the press, wherever the points lead, so
137
+ * dragging off the element under test is a case worth writing.
138
+ */
139
+ drag(from: [number, number], ...to: [number, number][]): void;
122
140
  moveMouse(x: number, y: number): void;
123
141
  wheel(x: number, y: number, delta: number): void;
124
142
  focusTerminal(focused: boolean): void;
@@ -241,6 +259,15 @@ function createHarness(
241
259
  ): Harness {
242
260
  const flush = (): void => app.flush();
243
261
 
262
+ /**
263
+ * The clock mouse events are stamped from.
264
+ *
265
+ * A real terminal stamps them on arrival; here they are synthesised, so the
266
+ * harness owns the time - which is what lets "a double click" and "two
267
+ * clicks" be two different tests rather than a race.
268
+ */
269
+ let mouseClock = 0;
270
+
244
271
  const collect = (predicate: (node: InspectorNode) => boolean): Element[] => {
245
272
  const tree = app.inspect();
246
273
  if (!tree) return [];
@@ -354,8 +381,22 @@ function createHarness(
354
381
  flush();
355
382
  },
356
383
  click(x, y, button = 'left') {
357
- app.handleInput({ type: 'mouse', action: 'down', button, x, y, ctrl: false, alt: false, shift: false, handled: false });
358
- app.handleInput({ type: 'mouse', action: 'up', button, x, y, ctrl: false, alt: false, shift: false, handled: false });
384
+ // Past any repeat window, so consecutive clicks in a test are what they
385
+ // look like: separate ones.
386
+ mouseClock += 1000;
387
+ app.handleInput({ type: 'mouse', action: 'down', button, x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
388
+ app.handleInput({ type: 'mouse', action: 'up', button, x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
389
+ flush();
390
+ },
391
+ clickRepeat(x, y, times) {
392
+ mouseClock += 1000;
393
+ for (let i = 0; i < times; i++) {
394
+ // Inside the window, and on the same cell: the two things that make
395
+ // presses one gesture rather than several.
396
+ mouseClock += 60;
397
+ app.handleInput({ type: 'mouse', action: 'down', button: 'left', x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
398
+ app.handleInput({ type: 'mouse', action: 'up', button: 'left', x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
399
+ }
359
400
  flush();
360
401
  },
361
402
  clickOn(element) {
@@ -363,6 +404,18 @@ function createHarness(
363
404
  if (!rect) throw new Error(`[textui/testing] <${element.component}> has no bounds to click`);
364
405
  this.click(rect.x + Math.floor(rect.width / 2), rect.y + Math.floor(rect.height / 2));
365
406
  },
407
+ drag(from, ...to) {
408
+ mouseClock += 1000;
409
+ const send = (action: 'down' | 'drag' | 'up', [x, y]: [number, number]): void => {
410
+ mouseClock += 20;
411
+ app.handleInput({ type: 'mouse', action, button: 'left', x, y, ctrl: false, alt: false, shift: false, at: mouseClock, handled: false });
412
+ };
413
+ send('down', from);
414
+ const last = to[to.length - 1] ?? from;
415
+ for (const point of to) send('drag', point);
416
+ send('up', last);
417
+ flush();
418
+ },
366
419
  moveMouse(x, y) {
367
420
  app.handleInput({ type: 'mouse', action: 'move', button: 'none', x, y, ctrl: false, alt: false, shift: false, handled: false });
368
421
  flush();