@gtkx/testing 0.21.0 → 1.0.0-rc.2

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 (216) hide show
  1. package/README.md +137 -34
  2. package/dist/act.d.ts +19 -0
  3. package/dist/act.d.ts.map +1 -0
  4. package/dist/act.js +56 -0
  5. package/dist/act.js.map +1 -0
  6. package/dist/bound-queries.d.ts +33 -0
  7. package/dist/bound-queries.d.ts.map +1 -0
  8. package/dist/bound-queries.js +2 -0
  9. package/dist/bound-queries.js.map +1 -0
  10. package/dist/build-queries.d.ts +16 -0
  11. package/dist/build-queries.d.ts.map +1 -0
  12. package/dist/build-queries.js +115 -0
  13. package/dist/build-queries.js.map +1 -0
  14. package/dist/cleanup-registry.d.ts +5 -0
  15. package/dist/cleanup-registry.d.ts.map +1 -0
  16. package/dist/cleanup-registry.js +21 -0
  17. package/dist/cleanup-registry.js.map +1 -0
  18. package/dist/config.d.ts +20 -52
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +17 -46
  21. package/dist/config.js.map +1 -1
  22. package/dist/editable.d.ts +8 -0
  23. package/dist/editable.d.ts.map +1 -0
  24. package/dist/editable.js +26 -0
  25. package/dist/editable.js.map +1 -0
  26. package/dist/errors.d.ts +30 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +135 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fire-event.d.ts +8 -25
  31. package/dist/fire-event.d.ts.map +1 -1
  32. package/dist/fire-event.js +11 -30
  33. package/dist/fire-event.js.map +1 -1
  34. package/dist/frame-sync.d.ts +4 -0
  35. package/dist/frame-sync.d.ts.map +1 -0
  36. package/dist/frame-sync.js +37 -0
  37. package/dist/frame-sync.js.map +1 -0
  38. package/dist/index.d.ts +17 -12
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +11 -7
  41. package/dist/index.js.map +1 -1
  42. package/dist/matchers.d.ts +59 -0
  43. package/dist/matchers.d.ts.map +1 -0
  44. package/dist/matchers.js +117 -0
  45. package/dist/matchers.js.map +1 -0
  46. package/dist/pretty-widget.d.ts +24 -26
  47. package/dist/pretty-widget.d.ts.map +1 -1
  48. package/dist/pretty-widget.js +111 -117
  49. package/dist/pretty-widget.js.map +1 -1
  50. package/dist/queries.d.ts +190 -180
  51. package/dist/queries.d.ts.map +1 -1
  52. package/dist/queries.js +291 -320
  53. package/dist/queries.js.map +1 -1
  54. package/dist/register-test-hooks.d.ts +2 -0
  55. package/dist/register-test-hooks.d.ts.map +1 -0
  56. package/dist/register-test-hooks.js +16 -0
  57. package/dist/register-test-hooks.js.map +1 -0
  58. package/dist/render-hook.d.ts +15 -35
  59. package/dist/render-hook.d.ts.map +1 -1
  60. package/dist/render-hook.js +9 -45
  61. package/dist/render-hook.js.map +1 -1
  62. package/dist/render.d.ts +13 -42
  63. package/dist/render.d.ts.map +1 -1
  64. package/dist/render.js +138 -121
  65. package/dist/render.js.map +1 -1
  66. package/dist/role-helpers.d.ts +19 -53
  67. package/dist/role-helpers.d.ts.map +1 -1
  68. package/dist/role-helpers.js +48 -63
  69. package/dist/role-helpers.js.map +1 -1
  70. package/dist/screen.d.ts +7 -63
  71. package/dist/screen.d.ts.map +1 -1
  72. package/dist/screen.js +24 -110
  73. package/dist/screen.js.map +1 -1
  74. package/dist/screenshot.d.ts +21 -23
  75. package/dist/screenshot.d.ts.map +1 -1
  76. package/dist/screenshot.js +98 -50
  77. package/dist/screenshot.js.map +1 -1
  78. package/dist/suggestions.d.ts +27 -0
  79. package/dist/suggestions.d.ts.map +1 -0
  80. package/dist/suggestions.js +61 -0
  81. package/dist/suggestions.js.map +1 -0
  82. package/dist/traversal.d.ts +12 -10
  83. package/dist/traversal.d.ts.map +1 -1
  84. package/dist/traversal.js +36 -15
  85. package/dist/traversal.js.map +1 -1
  86. package/dist/types.d.ts +115 -169
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/user-event/adjustment.d.ts +10 -0
  90. package/dist/user-event/adjustment.d.ts.map +1 -0
  91. package/dist/user-event/adjustment.js +35 -0
  92. package/dist/user-event/adjustment.js.map +1 -0
  93. package/dist/user-event/click.d.ts +8 -0
  94. package/dist/user-event/click.d.ts.map +1 -0
  95. package/dist/user-event/click.js +73 -0
  96. package/dist/user-event/click.js.map +1 -0
  97. package/dist/user-event/controller.d.ts +8 -0
  98. package/dist/user-event/controller.d.ts.map +1 -0
  99. package/dist/user-event/controller.js +31 -0
  100. package/dist/user-event/controller.js.map +1 -0
  101. package/dist/user-event/dispatch.d.ts +7 -0
  102. package/dist/user-event/dispatch.d.ts.map +1 -0
  103. package/dist/user-event/dispatch.js +11 -0
  104. package/dist/user-event/dispatch.js.map +1 -0
  105. package/dist/user-event/event-wrapper.d.ts +4 -0
  106. package/dist/user-event/event-wrapper.d.ts.map +1 -0
  107. package/dist/user-event/event-wrapper.js +59 -0
  108. package/dist/user-event/event-wrapper.js.map +1 -0
  109. package/dist/user-event/gesture.d.ts +35 -0
  110. package/dist/user-event/gesture.d.ts.map +1 -0
  111. package/dist/user-event/gesture.js +138 -0
  112. package/dist/user-event/gesture.js.map +1 -0
  113. package/dist/user-event/index.d.ts +51 -0
  114. package/dist/user-event/index.d.ts.map +1 -0
  115. package/dist/user-event/index.js +42 -0
  116. package/dist/user-event/index.js.map +1 -0
  117. package/dist/user-event/keyboard.d.ts +10 -0
  118. package/dist/user-event/keyboard.d.ts.map +1 -0
  119. package/dist/user-event/keyboard.js +204 -0
  120. package/dist/user-event/keyboard.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +10 -0
  122. package/dist/user-event/pointer.d.ts.map +1 -0
  123. package/dist/user-event/pointer.js +30 -0
  124. package/dist/user-event/pointer.js.map +1 -0
  125. package/dist/user-event/selection.d.ts +5 -0
  126. package/dist/user-event/selection.d.ts.map +1 -0
  127. package/dist/user-event/selection.js +105 -0
  128. package/dist/user-event/selection.js.map +1 -0
  129. package/dist/user-event/state.d.ts +7 -0
  130. package/dist/user-event/state.d.ts.map +1 -0
  131. package/dist/user-event/state.js +3 -0
  132. package/dist/user-event/state.js.map +1 -0
  133. package/dist/user-event/text.d.ts +18 -0
  134. package/dist/user-event/text.d.ts.map +1 -0
  135. package/dist/user-event/text.js +113 -0
  136. package/dist/user-event/text.js.map +1 -0
  137. package/dist/wait-for.d.ts +16 -36
  138. package/dist/wait-for.d.ts.map +1 -1
  139. package/dist/wait-for.js +74 -70
  140. package/dist/wait-for.js.map +1 -1
  141. package/dist/widget-accessible-properties.d.ts +34 -0
  142. package/dist/widget-accessible-properties.d.ts.map +1 -0
  143. package/dist/widget-accessible-properties.js +260 -0
  144. package/dist/widget-accessible-properties.js.map +1 -0
  145. package/dist/within.d.ts +10 -26
  146. package/dist/within.d.ts.map +1 -1
  147. package/dist/within.js +16 -25
  148. package/dist/within.js.map +1 -1
  149. package/package.json +23 -12
  150. package/src/act.ts +77 -0
  151. package/src/bound-queries.ts +37 -0
  152. package/src/build-queries.ts +226 -0
  153. package/src/cleanup-registry.ts +27 -0
  154. package/src/config.ts +35 -62
  155. package/src/editable.ts +37 -0
  156. package/src/errors.ts +191 -0
  157. package/src/fire-event.ts +13 -38
  158. package/src/frame-sync.ts +51 -0
  159. package/src/index.ts +58 -15
  160. package/src/matchers.ts +251 -0
  161. package/src/pretty-widget.ts +163 -130
  162. package/src/queries.ts +485 -396
  163. package/src/register-test-hooks.ts +19 -0
  164. package/src/render-hook.tsx +38 -45
  165. package/src/render.tsx +192 -155
  166. package/src/role-helpers.ts +65 -72
  167. package/src/screen.ts +26 -114
  168. package/src/screenshot.ts +128 -56
  169. package/src/suggestions.ts +106 -0
  170. package/src/traversal.ts +56 -22
  171. package/src/types.ts +144 -172
  172. package/src/user-event/adjustment.ts +62 -0
  173. package/src/user-event/click.ts +99 -0
  174. package/src/user-event/controller.ts +58 -0
  175. package/src/user-event/dispatch.ts +25 -0
  176. package/src/user-event/event-wrapper.ts +80 -0
  177. package/src/user-event/gesture.ts +239 -0
  178. package/src/user-event/index.ts +81 -0
  179. package/src/user-event/keyboard.ts +290 -0
  180. package/src/user-event/pointer.ts +46 -0
  181. package/src/user-event/selection.ts +159 -0
  182. package/src/user-event/state.ts +8 -0
  183. package/src/user-event/text.ts +172 -0
  184. package/src/wait-for.ts +117 -78
  185. package/src/widget-accessible-properties.ts +388 -0
  186. package/src/within.ts +29 -27
  187. package/dist/bind-queries.d.ts +0 -14
  188. package/dist/bind-queries.d.ts.map +0 -1
  189. package/dist/bind-queries.js +0 -29
  190. package/dist/bind-queries.js.map +0 -1
  191. package/dist/error-builder.d.ts +0 -28
  192. package/dist/error-builder.d.ts.map +0 -1
  193. package/dist/error-builder.js +0 -81
  194. package/dist/error-builder.js.map +0 -1
  195. package/dist/timing.d.ts +0 -18
  196. package/dist/timing.d.ts.map +0 -1
  197. package/dist/timing.js +0 -18
  198. package/dist/timing.js.map +0 -1
  199. package/dist/user-event.d.ts +0 -138
  200. package/dist/user-event.d.ts.map +0 -1
  201. package/dist/user-event.js +0 -411
  202. package/dist/user-event.js.map +0 -1
  203. package/dist/widget-text.d.ts +0 -69
  204. package/dist/widget-text.d.ts.map +0 -1
  205. package/dist/widget-text.js +0 -177
  206. package/dist/widget-text.js.map +0 -1
  207. package/dist/widget.d.ts +0 -3
  208. package/dist/widget.d.ts.map +0 -1
  209. package/dist/widget.js +0 -13
  210. package/dist/widget.js.map +0 -1
  211. package/src/bind-queries.ts +0 -52
  212. package/src/error-builder.ts +0 -102
  213. package/src/timing.ts +0 -17
  214. package/src/user-event.ts +0 -492
  215. package/src/widget-text.ts +0 -202
  216. package/src/widget.ts +0 -15
@@ -0,0 +1,99 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { getOrCreateControllers, queryController } from "./controller.js";
3
+ import { wrapEvent } from "./event-wrapper.js";
4
+
5
+ const getPressPoint = (widget: Gtk.Widget): { x: number; y: number } => {
6
+ const width = widget.getWidth();
7
+ const height = widget.getHeight();
8
+
9
+ if (width > 0 && height > 0) {
10
+ return { x: width / 2, y: height / 2 };
11
+ }
12
+
13
+ return { x: 0, y: 0 };
14
+ };
15
+
16
+ const emitGesture = (widget: Gtk.Widget, controllers: Gtk.GestureClick[], nPress: number, signal: string): void => {
17
+ const { x, y } = getPressPoint(widget);
18
+
19
+ for (const controller of controllers) {
20
+ controller.emit(signal, nPress, x, y);
21
+ }
22
+ };
23
+
24
+ const emitPress = (widget: Gtk.Widget, controllers: Gtk.GestureClick[], nPress: number): void => {
25
+ emitGesture(widget, controllers, nPress, "pressed");
26
+ };
27
+
28
+ const emitRelease = (widget: Gtk.Widget, controllers: Gtk.GestureClick[], nPress: number): void => {
29
+ emitGesture(widget, controllers, nPress, "released");
30
+ };
31
+
32
+ const emitClickSequence = (widget: Gtk.Widget, target: Gtk.Widget, nPress: number): Promise<void> =>
33
+ wrapEvent(widget, () => {
34
+ const controllers = getOrCreateControllers(target, Gtk.GestureClick);
35
+
36
+ for (let i = 1; i <= nPress; i++) {
37
+ emitPress(target, controllers, i);
38
+ emitRelease(target, controllers, i);
39
+ }
40
+ });
41
+
42
+ const findClickableAncestor = (widget: Gtk.Widget): Gtk.Widget | null => {
43
+ let current = widget.getParent();
44
+
45
+ while (current) {
46
+ if (current instanceof Gtk.Button || queryController(current, Gtk.GestureClick) !== null) {
47
+ return current;
48
+ }
49
+
50
+ current = current.getParent();
51
+ }
52
+
53
+ return null;
54
+ };
55
+
56
+ const tryActivate = async (widget: Gtk.Widget): Promise<boolean> => {
57
+ if (widget.getAccessibleRole() === Gtk.AccessibleRole.LABEL) {
58
+ return false;
59
+ }
60
+
61
+ let isActivated = false;
62
+
63
+ await wrapEvent(widget, () => {
64
+ isActivated = widget.activate();
65
+ });
66
+
67
+ return isActivated;
68
+ };
69
+
70
+ const click = async (widget: Gtk.Widget): Promise<void> => {
71
+ if (widget instanceof Gtk.Button) {
72
+ await emitClickSequence(widget, widget, 1);
73
+
74
+ return;
75
+ }
76
+
77
+ if (widget instanceof Gtk.Switch) {
78
+ await wrapEvent(widget, () => {
79
+ widget.setActive(!widget.getActive());
80
+ });
81
+
82
+ return;
83
+ }
84
+
85
+ if (await tryActivate(widget)) {
86
+ return;
87
+ }
88
+
89
+ const target = findClickableAncestor(widget);
90
+
91
+ if (target) {
92
+ await emitClickSequence(widget, target, 1);
93
+ }
94
+ };
95
+
96
+ const dblClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 2);
97
+ const tripleClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 3);
98
+
99
+ export { emitPress, emitRelease, click, dblClick, tripleClick };
@@ -0,0 +1,58 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+
3
+ type ControllerConstructor<T extends Gtk.EventController> = new () => T;
4
+
5
+ const queryAllControllers = <T extends Gtk.EventController>(
6
+ widget: Gtk.Widget,
7
+ controllerType: ControllerConstructor<T>,
8
+ ): T[] => {
9
+ const controllers = widget.observeControllers();
10
+ const nItems = controllers.getNItems();
11
+ const matches: T[] = [];
12
+
13
+ for (let i = 0; i < nItems; i++) {
14
+ const controller = controllers.getItem(i);
15
+
16
+ if (controller instanceof controllerType) {
17
+ matches.push(controller);
18
+ }
19
+ }
20
+
21
+ return matches;
22
+ };
23
+
24
+ const queryController = <T extends Gtk.EventController>(
25
+ widget: Gtk.Widget,
26
+ controllerType: ControllerConstructor<T>,
27
+ ): T | null => queryAllControllers(widget, controllerType)[0] ?? null;
28
+
29
+ const getAllControllers = <T extends Gtk.EventController>(
30
+ widget: Gtk.Widget,
31
+ controllerType: ControllerConstructor<T>,
32
+ ): T[] => {
33
+ const controllers = queryAllControllers(widget, controllerType);
34
+
35
+ if (controllers.length === 0) {
36
+ throw new Error(`No ${controllerType.name} controller is attached to the widget`);
37
+ }
38
+
39
+ return controllers;
40
+ };
41
+
42
+ const getOrCreateControllers = <T extends Gtk.EventController>(
43
+ widget: Gtk.Widget,
44
+ controllerType: ControllerConstructor<T>,
45
+ ): T[] => {
46
+ const existing = queryAllControllers(widget, controllerType);
47
+
48
+ if (existing.length > 0) {
49
+ return existing;
50
+ }
51
+
52
+ const controller = new controllerType();
53
+ widget.addController(controller);
54
+
55
+ return [controller];
56
+ };
57
+
58
+ export { queryAllControllers, queryController, getAllControllers, getOrCreateControllers, type ControllerConstructor };
@@ -0,0 +1,25 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { type ControllerConstructor, getAllControllers, getOrCreateControllers } from "./controller.js";
3
+ import { wrapEvent } from "./event-wrapper.js";
4
+
5
+ type ControllerEmit<T extends Gtk.EventController> = (controller: T) => void | PromiseLike<void>;
6
+
7
+ const emitOnAll = async <T extends Gtk.EventController>(controllers: T[], emit: ControllerEmit<T>): Promise<void> => {
8
+ for (const controller of controllers) {
9
+ await emit(controller);
10
+ }
11
+ };
12
+
13
+ const dispatchOnOrCreateControllers = <T extends Gtk.EventController>(
14
+ widget: Gtk.Widget,
15
+ controllerType: ControllerConstructor<T>,
16
+ emit: ControllerEmit<T>,
17
+ ): Promise<void> => wrapEvent(widget, () => emitOnAll(getOrCreateControllers(widget, controllerType), emit));
18
+
19
+ const dispatchOnControllers = <T extends Gtk.EventController>(
20
+ widget: Gtk.Widget,
21
+ controllerType: ControllerConstructor<T>,
22
+ emit: ControllerEmit<T>,
23
+ ): Promise<void> => wrapEvent(widget, () => emitOnAll(getAllControllers(widget, controllerType), emit));
24
+
25
+ export { dispatchOnOrCreateControllers, dispatchOnControllers };
@@ -0,0 +1,80 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { runInAct } from "../act.js";
3
+ import { getConfig } from "../config.js";
4
+ import { formatRole } from "../role-helpers.js";
5
+
6
+ const NOT_SENSITIVE = "it is not sensitive (the widget or one of its ancestors is disabled)";
7
+ const WINDOW_NOT_ALLOCATED = "its window has not been allocated a size";
8
+ const NOT_MAPPED = "it is not mapped (it is not shown on screen, e.g. it is hidden or on a non-visible page)";
9
+ const WINDOW_NOT_ACTIVE = "its window never became active";
10
+
11
+ const actionableHop = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 1));
12
+
13
+ const canDisplayDeliverActivation = (window: Gtk.Window): boolean =>
14
+ window.getDisplay().getDefaultSeat() !== null;
15
+
16
+ const findWindowActionabilityFailure = (widget: Gtk.Widget, root: Gtk.Window): string | null => {
17
+ if (root.getAllocatedWidth() === 0) {
18
+ return WINDOW_NOT_ALLOCATED;
19
+ }
20
+
21
+ if (!widget.getMapped()) {
22
+ return NOT_MAPPED;
23
+ }
24
+
25
+ if (canDisplayDeliverActivation(root) && !root.isActive()) {
26
+ return WINDOW_NOT_ACTIVE;
27
+ }
28
+
29
+ return null;
30
+ };
31
+
32
+ const findActionabilityFailure = (widget: Gtk.Widget): string | null => {
33
+ if (!widget.isSensitive()) {
34
+ return NOT_SENSITIVE;
35
+ }
36
+
37
+ const root = widget.getRoot();
38
+
39
+ if (!(root instanceof Gtk.Window) || !root.getVisible()) {
40
+ return null;
41
+ }
42
+
43
+ return findWindowActionabilityFailure(widget, root);
44
+ };
45
+
46
+ const describeWidget = (widget: Gtk.Widget): string => {
47
+ const tag = widget.constructor.name;
48
+ const name = widget.getName();
49
+ const nameAttribute = name && !name.endsWith(tag) ? ` name="${name}"` : "";
50
+
51
+ return `<${tag}${nameAttribute} role="${formatRole(widget.getAccessibleRole())}">`;
52
+ };
53
+
54
+ const waitForActionable = async (widget: Gtk.Widget): Promise<void> => {
55
+ const timeout = getConfig().actionabilityTimeout;
56
+ const deadline = Date.now() + timeout;
57
+ let failure = findActionabilityFailure(widget);
58
+
59
+ while (failure !== null && Date.now() < deadline) {
60
+ await actionableHop();
61
+ failure = findActionabilityFailure(widget);
62
+ }
63
+
64
+ if (failure !== null) {
65
+ throw new Error(
66
+ `Cannot dispatch user event: ${describeWidget(widget)} did not become actionable ` +
67
+ `within ${String(timeout)}ms because ${failure}`,
68
+ );
69
+ }
70
+ };
71
+
72
+ const wrapEvent = async (widget: Gtk.Widget, body: () => void | PromiseLike<void>): Promise<void> => {
73
+ await waitForActionable(widget);
74
+
75
+ await runInAct(async () => {
76
+ await body();
77
+ });
78
+ };
79
+
80
+ export { wrapEvent };
@@ -0,0 +1,239 @@
1
+ import * as GObject from "@gtkx/gi/gobject";
2
+ import * as Gtk from "@gtkx/gi/gtk";
3
+ import { getAllControllers } from "./controller.js";
4
+ import { dispatchOnControllers, dispatchOnOrCreateControllers } from "./dispatch.js";
5
+ import { wrapEvent } from "./event-wrapper.js";
6
+
7
+ /** The value delivered to a drop target: a primitive (converted to a GObject.Value) or an explicit GObject.Value. */
8
+ type DropContent = string | number | boolean | GObject.Value;
9
+
10
+ /** Options for a drop: the drop coordinates within the target. */
11
+ type DropOptions = {
12
+ x?: number;
13
+ y?: number;
14
+ };
15
+
16
+ /** A drag offset relative to the drag start point. */
17
+ type DragOffset = {
18
+ x: number;
19
+ y: number;
20
+ };
21
+
22
+ /**
23
+ * Options for a drag gesture: the starting point, the number of interpolated drag updates, or
24
+ * explicit intermediate offsets emitted before the final one.
25
+ */
26
+ type DragOptions = {
27
+ startX?: number;
28
+ startY?: number;
29
+ steps?: number;
30
+ offsets?: DragOffset[];
31
+ };
32
+
33
+ type DragInstancePatch = {
34
+ getStartPoint?: Gtk.GestureDrag["getStartPoint"] | undefined;
35
+ getOffset?: Gtk.GestureDrag["getOffset"] | undefined;
36
+ };
37
+
38
+ type SavedDragState = {
39
+ instance: DragInstancePatch;
40
+ ownsStartPoint: boolean;
41
+ ownsOffset: boolean;
42
+ previousStartPoint: DragInstancePatch["getStartPoint"];
43
+ previousOffset: DragInstancePatch["getOffset"];
44
+ };
45
+
46
+ const buildDropValue = (content: DropContent): GObject.Value => {
47
+ if (content instanceof GObject.Value) {
48
+ return content;
49
+ }
50
+
51
+ if (typeof content === "string") {
52
+ return GObject.buildValue(GObject.TYPE_STRING, (v) => {
53
+ v.setString(content);
54
+ });
55
+ }
56
+
57
+ if (typeof content === "boolean") {
58
+ return GObject.buildValue(GObject.TYPE_BOOLEAN, (v) => {
59
+ v.setBoolean(content);
60
+ });
61
+ }
62
+
63
+ return GObject.buildValue(GObject.TYPE_DOUBLE, (v) => {
64
+ v.setDouble(content);
65
+ });
66
+ };
67
+
68
+ const hover = (widget: Gtk.Widget): Promise<void> =>
69
+ dispatchOnOrCreateControllers(widget, Gtk.EventControllerMotion, (controller) => {
70
+ controller.emit("enter", 0, 0);
71
+ });
72
+
73
+ const unhover = (widget: Gtk.Widget): Promise<void> =>
74
+ dispatchOnOrCreateControllers(widget, Gtk.EventControllerMotion, (controller) => {
75
+ controller.emit("leave");
76
+ });
77
+
78
+ const rotate = (widget: Gtk.Widget, angle: number, deltaAngle: number = angle): Promise<void> =>
79
+ dispatchOnControllers(widget, Gtk.GestureRotate, (controller) => {
80
+ controller.emit("angle-changed", angle, deltaAngle);
81
+ },
82
+ );
83
+
84
+ const zoom = (widget: Gtk.Widget, scale: number): Promise<void> =>
85
+ dispatchOnControllers(widget, Gtk.GestureZoom, (controller) => {
86
+ controller.emit("scale-changed", scale);
87
+ });
88
+
89
+ const swipe = (widget: Gtk.Widget, velocityX: number, velocityY: number): Promise<void> =>
90
+ dispatchOnControllers(widget, Gtk.GestureSwipe, (controller) => {
91
+ controller.emit("swipe", velocityX, velocityY);
92
+ });
93
+
94
+ const longPress = (widget: Gtk.Widget, x = 0, y = 0): Promise<void> =>
95
+ dispatchOnControllers(widget, Gtk.GestureLongPress, (controller) => {
96
+ controller.emit("pressed", x, y);
97
+ });
98
+
99
+ const restoreDragState = (saved: SavedDragState): void => {
100
+ const { instance } = saved;
101
+
102
+ if (saved.ownsStartPoint) {
103
+ instance.getStartPoint = saved.previousStartPoint;
104
+ } else {
105
+ delete instance.getStartPoint;
106
+ }
107
+
108
+ if (saved.ownsOffset) {
109
+ instance.getOffset = saved.previousOffset;
110
+ } else {
111
+ delete instance.getOffset;
112
+ }
113
+ };
114
+
115
+ const patchDragState = (controller: Gtk.GestureDrag, start: DragOffset, offset: () => DragOffset): (() => void) => {
116
+ const instance: DragInstancePatch = controller;
117
+
118
+ const saved: SavedDragState = {
119
+ instance,
120
+ ownsStartPoint: Object.hasOwn(instance, "getStartPoint"),
121
+ ownsOffset: Object.hasOwn(instance, "getOffset"),
122
+ previousStartPoint: instance.getStartPoint,
123
+ previousOffset: instance.getOffset,
124
+ };
125
+
126
+ instance.getStartPoint = () => [true, start.x, start.y];
127
+
128
+ instance.getOffset = () => {
129
+ const current = offset();
130
+
131
+ return [true, current.x, current.y];
132
+ };
133
+
134
+ return () => {
135
+ restoreDragState(saved);
136
+ };
137
+ };
138
+
139
+ const emitToControllers = (controllers: Gtk.GestureDrag[], signal: string, x: number, y: number): void => {
140
+ for (const controller of controllers) {
141
+ controller.emit(signal, x, y);
142
+ }
143
+ };
144
+
145
+ const runDragSequence = (controllers: Gtk.GestureDrag[], start: DragOffset, updates: DragOffset[]): void => {
146
+ let current: DragOffset = { x: 0, y: 0 };
147
+ const restores = controllers.map((controller) => patchDragState(controller, start, () => current));
148
+
149
+ try {
150
+ emitToControllers(controllers, "drag-begin", start.x, start.y);
151
+
152
+ for (const update of updates) {
153
+ current = update;
154
+ emitToControllers(controllers, "drag-update", update.x, update.y);
155
+ }
156
+
157
+ emitToControllers(controllers, "drag-end", current.x, current.y);
158
+ } finally {
159
+ for (const restore of restores) {
160
+ restore();
161
+ }
162
+ }
163
+ };
164
+
165
+ const resolveDragUpdates = (dx: number, dy: number, options: DragOptions): DragOffset[] => {
166
+ if (options.offsets) {
167
+ return [...options.offsets, { x: dx, y: dy }];
168
+ }
169
+
170
+ const steps = Math.max(1, Math.floor(options.steps ?? 2));
171
+ const updates: DragOffset[] = [];
172
+
173
+ for (let i = 1; i <= steps; i++) {
174
+ updates.push({ x: (dx * i) / steps, y: (dy * i) / steps });
175
+ }
176
+
177
+ return updates;
178
+ };
179
+
180
+ const drag = async (widget: Gtk.Widget, dx: number, dy: number, options: DragOptions = {}): Promise<void> => {
181
+ if (widget instanceof Gtk.Range) {
182
+ throw new TypeError(
183
+ "userEvent.drag cannot drive a Gtk.Range's built-in slider " +
184
+ "(its drag reads pointer coordinates from the display); " +
185
+ "use userEvent.slide(range, value) or userEvent.keyboard for sliders",
186
+ );
187
+ }
188
+
189
+ const start = { x: options.startX ?? 0, y: options.startY ?? 0 };
190
+ const updates = resolveDragUpdates(dx, dy, options);
191
+
192
+ await wrapEvent(widget, () => {
193
+ runDragSequence(getAllControllers(widget, Gtk.GestureDrag), start, updates);
194
+ });
195
+ };
196
+
197
+ const emitDrop = (target: Gtk.Widget, content: DropContent, options: DropOptions): void => {
198
+ const dropTargets = getAllControllers(target, Gtk.DropTarget);
199
+
200
+ for (const dropTarget of dropTargets) {
201
+ dropTarget.emit("drop", buildDropValue(content), options.x ?? 0, options.y ?? 0);
202
+ }
203
+ };
204
+
205
+ const drop = (widget: Gtk.Widget, content: DropContent, options: DropOptions = {}): Promise<void> =>
206
+ wrapEvent(widget, () => {
207
+ emitDrop(widget, content, options);
208
+ });
209
+
210
+ const dragAndDrop = async (
211
+ source: Gtk.Widget,
212
+ target: Gtk.Widget,
213
+ content: DropContent,
214
+ options: DropOptions = {},
215
+ ): Promise<void> => {
216
+ await wrapEvent(source, () => {
217
+ getAllControllers(source, Gtk.DragSource);
218
+ });
219
+
220
+ await wrapEvent(target, () => {
221
+ emitDrop(target, content, options);
222
+ });
223
+ };
224
+
225
+ export {
226
+ hover,
227
+ unhover,
228
+ rotate,
229
+ zoom,
230
+ swipe,
231
+ longPress,
232
+ drag,
233
+ drop,
234
+ dragAndDrop,
235
+ type DropContent,
236
+ type DropOptions,
237
+ type DragOffset,
238
+ type DragOptions,
239
+ };
@@ -0,0 +1,81 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { scroll, slide } from "./adjustment.js";
3
+ import { click, dblClick, tripleClick } from "./click.js";
4
+ import { drag, dragAndDrop, drop, hover, longPress, rotate, swipe, unhover, zoom } from "./gesture.js";
5
+ import { keyboard, tab } from "./keyboard.js";
6
+ import { pointer, type PointerInput } from "./pointer.js";
7
+ import { deselectOptions, selectOptions } from "./selection.js";
8
+ import { createInitialState } from "./state.js";
9
+ import { clear, copy, cut, paste, type } from "./text.js";
10
+
11
+ /**
12
+ * The set of user interaction helpers exposed by {@link userEvent}, covering clicks, typing,
13
+ * keyboard, pointer, gestures, selection, and scrolling.
14
+ */
15
+ type UserEvent = {
16
+ click: typeof click;
17
+ dblClick: typeof dblClick;
18
+ tripleClick: typeof tripleClick;
19
+ tab: typeof tab;
20
+ type: typeof type;
21
+ clear: typeof clear;
22
+ copy: typeof copy;
23
+ cut: typeof cut;
24
+ paste: typeof paste;
25
+ selectOptions: typeof selectOptions;
26
+ deselectOptions: typeof deselectOptions;
27
+ hover: typeof hover;
28
+ unhover: typeof unhover;
29
+ rotate: typeof rotate;
30
+ zoom: typeof zoom;
31
+ swipe: typeof swipe;
32
+ longPress: typeof longPress;
33
+ drag: typeof drag;
34
+ drop: typeof drop;
35
+ dragAndDrop: typeof dragAndDrop;
36
+ slide: typeof slide;
37
+ scroll: typeof scroll;
38
+ keyboard: (widget: Gtk.Widget, input: string) => Promise<void>;
39
+ pointer: (widget: Gtk.Widget, input: PointerInput) => Promise<void>;
40
+ };
41
+
42
+ const state = createInitialState();
43
+
44
+ /**
45
+ * High-level helpers that drive widgets by dispatching the same events and gestures GTK4 delivers
46
+ * when someone clicks, types, or drags.
47
+ */
48
+ const userEvent: UserEvent = {
49
+ click,
50
+ dblClick,
51
+ tripleClick,
52
+ tab,
53
+ type,
54
+ clear,
55
+ copy,
56
+ cut,
57
+ paste,
58
+ selectOptions,
59
+ deselectOptions,
60
+ hover,
61
+ unhover,
62
+ rotate,
63
+ zoom,
64
+ swipe,
65
+ longPress,
66
+ drag,
67
+ drop,
68
+ dragAndDrop,
69
+ slide,
70
+ scroll,
71
+ keyboard: (widget: Gtk.Widget, input: string): Promise<void> => keyboard(state, widget, input),
72
+ pointer: (widget: Gtk.Widget, input: PointerInput): Promise<void> => pointer(state, widget, input),
73
+ };
74
+
75
+ export type { ScrollDelta } from "./adjustment.js";
76
+ export type { DragOffset, DragOptions, DropContent, DropOptions } from "./gesture.js";
77
+ export type { TabOptions } from "./keyboard.js";
78
+ export type { PointerInput } from "./pointer.js";
79
+ export type { TypeOptions } from "./text.js";
80
+ export { resetClipboard } from "./text.js";
81
+ export { userEvent, type UserEvent };