@ruakit/runtime 0.0.0-stage → 0.1.0-alpha.1

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +4 -0
  3. package/dist/action-feedback.d.ts +4 -0
  4. package/dist/action-feedback.d.ts.map +1 -0
  5. package/dist/action-feedback.js +3 -0
  6. package/dist/action-feedback.js.map +7 -0
  7. package/dist/dom.d.ts +17 -0
  8. package/dist/dom.d.ts.map +1 -0
  9. package/dist/dom.js +3 -0
  10. package/dist/dom.js.map +7 -0
  11. package/dist/errors.d.ts +6 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +3 -0
  14. package/dist/errors.js.map +7 -0
  15. package/dist/experimental/index.d.ts +24 -0
  16. package/dist/experimental/index.d.ts.map +1 -0
  17. package/dist/experimental/index.js +3 -0
  18. package/dist/experimental/index.js.map +7 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +3 -0
  22. package/dist/index.js.map +7 -0
  23. package/dist/operations.d.ts +5 -0
  24. package/dist/operations.d.ts.map +1 -0
  25. package/dist/operations.js +3 -0
  26. package/dist/operations.js.map +7 -0
  27. package/dist/runtime.d.ts +4 -0
  28. package/dist/runtime.d.ts.map +1 -0
  29. package/dist/runtime.js +3 -0
  30. package/dist/runtime.js.map +7 -0
  31. package/dist/screenshot.d.ts +3 -0
  32. package/dist/screenshot.d.ts.map +1 -0
  33. package/dist/screenshot.js +5 -0
  34. package/dist/screenshot.js.map +7 -0
  35. package/dist/targets.d.ts +15 -0
  36. package/dist/targets.d.ts.map +1 -0
  37. package/dist/targets.js +3 -0
  38. package/dist/targets.js.map +7 -0
  39. package/dist/types.d.ts +121 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +2 -0
  42. package/dist/types.js.map +7 -0
  43. package/package.json +37 -3
  44. package/src/action-feedback.ts +85 -0
  45. package/src/dom.ts +307 -0
  46. package/src/errors.ts +14 -0
  47. package/src/experimental/index.ts +192 -0
  48. package/src/index.ts +21 -0
  49. package/src/operations.ts +67 -0
  50. package/src/runtime.ts +541 -0
  51. package/src/screenshot.ts +172 -0
  52. package/src/targets.ts +141 -0
  53. package/src/types.ts +135 -0
  54. package/README.md +0 -3
@@ -0,0 +1,192 @@
1
+ // Copyright 2026 RuaKit contributors. SPDX-License-Identifier: MIT
2
+ import { pause, withDeadline } from '../operations.js';
3
+ import { RuntimeError } from '../errors.js';
4
+ import type { ActionOptions } from '../types.js';
5
+
6
+ /** CSS pixels relative to the surface's bounding rectangle, not canvas bitmap pixels. */
7
+ export interface Point {
8
+ x: number;
9
+ y: number;
10
+ }
11
+
12
+ export interface DragOptions extends ActionOptions {
13
+ steps?: number;
14
+ durationMs?: number;
15
+ }
16
+
17
+ export interface Pointer {
18
+ move(point: Point, options?: ActionOptions): Promise<void>;
19
+ down(point: Point, options?: ActionOptions): Promise<void>;
20
+ up(point: Point, options?: ActionOptions): Promise<void>;
21
+ drag(from: Point, to: Point, options?: DragOptions): Promise<void>;
22
+ cancel(options?: ActionOptions): Promise<void>;
23
+ }
24
+
25
+ /**
26
+ * Experimental synthetic mouse pointer bound to one surface (including a plain canvas).
27
+ * Events always target this element; this does not implement hit testing, native pointer
28
+ * capture, hover boundaries, browser default actions, or movement of the system cursor.
29
+ */
30
+ export function createPointer(surface: HTMLElement): Pointer {
31
+ const view = surface.ownerDocument.defaultView;
32
+ if (!view || !view.PointerEvent) {
33
+ throw new TypeError('createPointer requires a browser with PointerEvent support.');
34
+ }
35
+
36
+ let pressed = false;
37
+ let busy = false;
38
+ let position: Point = { x: 0, y: 0 };
39
+
40
+ function validate(point: Point): void {
41
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
42
+ throw new TypeError('Pointer coordinates must be finite numbers.');
43
+ }
44
+ }
45
+
46
+ function emit(type: string, point: Point): void {
47
+ position = { ...point };
48
+
49
+ const rect = surface.getBoundingClientRect();
50
+ const init: PointerEventInit = {
51
+ bubbles: true,
52
+ cancelable: true,
53
+ composed: true,
54
+ clientX: rect.left + point.x,
55
+ clientY: rect.top + point.y,
56
+ pointerId: 1,
57
+ pointerType: 'mouse',
58
+ isPrimary: true,
59
+ button: type === 'pointermove' || type === 'pointercancel' ? -1 : 0,
60
+ buttons: pressed ? 1 : 0,
61
+ pressure: pressed ? 0.5 : 0,
62
+ };
63
+
64
+ const accepted = surface.dispatchEvent(new view!.PointerEvent(type, init));
65
+
66
+ // Mouse compatibility events are explicit: dispatchEvent does not generate them.
67
+ if (accepted && type !== 'pointercancel') {
68
+ surface.dispatchEvent(
69
+ new view!.MouseEvent(type.replace('pointer', 'mouse'), {
70
+ ...init,
71
+ button: 0,
72
+ }),
73
+ );
74
+ }
75
+ }
76
+
77
+ function release(): void {
78
+ if (pressed) {
79
+ pressed = false;
80
+ emit('pointercancel', position);
81
+ }
82
+ }
83
+
84
+ async function run(
85
+ options: ActionOptions,
86
+ work: (signal: AbortSignal) => Promise<void>,
87
+ ): Promise<void> {
88
+ if (busy) {
89
+ throw new RuntimeError('BUSY', 'Another pointer operation is running.');
90
+ }
91
+
92
+ busy = true;
93
+ try {
94
+ await withDeadline(options.timeoutMs ?? 2000, options.signal, async (signal) => {
95
+ if (!surface.isConnected) {
96
+ throw new RuntimeError('STALE_TARGET', 'The pointer surface is disconnected.');
97
+ }
98
+
99
+ await work(signal);
100
+ await pause(signal);
101
+ });
102
+ } catch (error) {
103
+ release();
104
+ throw error;
105
+ } finally {
106
+ busy = false;
107
+ }
108
+ }
109
+
110
+ return {
111
+ move(point, options = {}) {
112
+ validate(point);
113
+
114
+ return run(options, async () => {
115
+ emit('pointermove', point);
116
+ });
117
+ },
118
+ down(point, options = {}) {
119
+ validate(point);
120
+
121
+ return run(options, async () => {
122
+ if (pressed) {
123
+ throw new Error('The pointer button is already down.');
124
+ }
125
+
126
+ pressed = true;
127
+ emit('pointerdown', point);
128
+ });
129
+ },
130
+ up(point, options = {}) {
131
+ validate(point);
132
+
133
+ return run(options, async () => {
134
+ if (!pressed) {
135
+ throw new Error('The pointer button is not down.');
136
+ }
137
+
138
+ pressed = false;
139
+ emit('pointerup', point);
140
+ });
141
+ },
142
+ drag(from, to, options = {}) {
143
+ validate(from);
144
+ validate(to);
145
+
146
+ const steps = options.steps ?? 12;
147
+ const duration = options.durationMs ?? 200;
148
+
149
+ if (
150
+ !Number.isInteger(steps) ||
151
+ steps < 1 ||
152
+ steps > 10000 ||
153
+ !Number.isFinite(duration) ||
154
+ duration < 0
155
+ ) {
156
+ throw new RangeError('Use 1–10000 integer steps and a finite, nonnegative durationMs.');
157
+ }
158
+
159
+ return run(options, async (signal) => {
160
+ if (pressed) {
161
+ throw new Error('Release the pointer button before dragging.');
162
+ }
163
+
164
+ emit('pointermove', from);
165
+ pressed = true;
166
+ emit('pointerdown', from);
167
+
168
+ for (let step = 1; step <= steps; step++) {
169
+ await pause(signal, duration / steps);
170
+ signal.throwIfAborted();
171
+
172
+ if (!surface.isConnected) {
173
+ throw new RuntimeError('STALE_TARGET', 'The pointer surface is disconnected.');
174
+ }
175
+
176
+ emit('pointermove', {
177
+ x: from.x + ((to.x - from.x) * step) / steps,
178
+ y: from.y + ((to.y - from.y) * step) / steps,
179
+ });
180
+ }
181
+
182
+ pressed = false;
183
+ emit('pointerup', to);
184
+ });
185
+ },
186
+ cancel(options = {}) {
187
+ return run(options, async () => {
188
+ release();
189
+ });
190
+ },
191
+ };
192
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Copyright 2026 RuaKit contributors. SPDX-License-Identifier: MIT
2
+ export { createRuntime } from './runtime.js';
3
+ export { RuntimeError } from './errors.js';
4
+ export { withActionFeedback } from './action-feedback.js';
5
+ export type { RuntimeErrorCode } from './errors.js';
6
+ export type {
7
+ ActionOptions,
8
+ ActionFeedbackOptions,
9
+ ActionRect,
10
+ ControlGroup,
11
+ ControlState,
12
+ KeyOptions,
13
+ PageState,
14
+ Runtime,
15
+ RuntimeOptions,
16
+ RuntimeAction,
17
+ RuntimeActionEvent,
18
+ Target,
19
+ Screenshot,
20
+ ScreenshotImage,
21
+ } from './types.js';
@@ -0,0 +1,67 @@
1
+ // Copyright 2026 RuaKit contributors. SPDX-License-Identifier: MIT
2
+ import { RuntimeError } from './errors.js';
3
+
4
+ export function validateTimeout(value: number): void {
5
+ if (!Number.isInteger(value) || value < 1 || value > 2147483647) {
6
+ throw new RangeError('timeoutMs must be an integer between 1 and 2147483647.');
7
+ }
8
+ }
9
+
10
+ /** Bound adapter work even when an adapter ignores cancellation. */
11
+ export async function withDeadline<T>(
12
+ timeoutMs: number,
13
+ caller: AbortSignal | undefined,
14
+ operation: (signal: AbortSignal) => Promise<T>,
15
+ ): Promise<T> {
16
+ validateTimeout(timeoutMs);
17
+
18
+ const controller = new AbortController();
19
+ const cancel = (): void => controller.abort(caller?.reason);
20
+
21
+ caller?.addEventListener('abort', cancel, { once: true });
22
+ if (caller?.aborted) {
23
+ cancel();
24
+ }
25
+
26
+ const timer = setTimeout(
27
+ () => controller.abort(new RuntimeError('TIMEOUT', 'The operation timed out.')),
28
+ timeoutMs,
29
+ );
30
+ const signal = controller.signal;
31
+
32
+ try {
33
+ signal.throwIfAborted();
34
+ return await new Promise<T>((resolve, reject) => {
35
+ const abort = (): void => reject(signal.reason);
36
+ signal.addEventListener('abort', abort, { once: true });
37
+ Promise.resolve()
38
+ .then(() => {
39
+ signal.throwIfAborted();
40
+ return operation(signal);
41
+ })
42
+ .then(resolve, reject)
43
+ .finally(() => signal.removeEventListener('abort', abort));
44
+ });
45
+ } finally {
46
+ clearTimeout(timer);
47
+ caller?.removeEventListener('abort', cancel);
48
+ }
49
+ }
50
+
51
+ export function pause(signal: AbortSignal, milliseconds = 0): Promise<void> {
52
+ signal.throwIfAborted();
53
+
54
+ return new Promise((resolve, reject) => {
55
+ const abort = (): void => {
56
+ clearTimeout(timer);
57
+ reject(signal.reason);
58
+ };
59
+
60
+ const timer = setTimeout(() => {
61
+ signal.removeEventListener('abort', abort);
62
+ resolve();
63
+ }, milliseconds);
64
+
65
+ signal.addEventListener('abort', abort, { once: true });
66
+ });
67
+ }