@gtkx/testing 1.0.0-rc.4 → 1.1.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 (212) hide show
  1. package/README.md +5 -5
  2. package/dist/accessible-native.d.ts +16 -0
  3. package/dist/accessible-native.d.ts.map +1 -0
  4. package/dist/accessible-native.js +206 -0
  5. package/dist/accessible-native.js.map +1 -0
  6. package/dist/act.d.ts +1 -3
  7. package/dist/act.d.ts.map +1 -1
  8. package/dist/act.js +1 -1
  9. package/dist/act.js.map +1 -1
  10. package/dist/build-queries.d.ts +27 -10
  11. package/dist/build-queries.d.ts.map +1 -1
  12. package/dist/build-queries.js +22 -24
  13. package/dist/build-queries.js.map +1 -1
  14. package/dist/cleanup-registry.d.ts +1 -1
  15. package/dist/cleanup-registry.d.ts.map +1 -1
  16. package/dist/cleanup-registry.js.map +1 -1
  17. package/dist/config.d.ts +8 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +1 -0
  20. package/dist/config.js.map +1 -1
  21. package/dist/errors.d.ts +10 -1
  22. package/dist/errors.d.ts.map +1 -1
  23. package/dist/errors.js +10 -1
  24. package/dist/errors.js.map +1 -1
  25. package/dist/fire-event.d.ts +35 -6
  26. package/dist/fire-event.d.ts.map +1 -1
  27. package/dist/fire-event.js +23 -7
  28. package/dist/fire-event.js.map +1 -1
  29. package/dist/frame-sync.d.ts +3 -2
  30. package/dist/frame-sync.d.ts.map +1 -1
  31. package/dist/frame-sync.js +19 -9
  32. package/dist/frame-sync.js.map +1 -1
  33. package/dist/harness-window.d.ts +5 -0
  34. package/dist/harness-window.d.ts.map +1 -0
  35. package/dist/harness-window.js +17 -0
  36. package/dist/harness-window.js.map +1 -0
  37. package/dist/index.d.ts +14 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +9 -5
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal.d.ts +5 -0
  42. package/dist/internal.d.ts.map +1 -0
  43. package/dist/internal.js +5 -0
  44. package/dist/internal.js.map +1 -0
  45. package/dist/matchers.d.ts +88 -46
  46. package/dist/matchers.d.ts.map +1 -1
  47. package/dist/matchers.js +178 -20
  48. package/dist/matchers.js.map +1 -1
  49. package/dist/pretty-format.d.ts +2 -0
  50. package/dist/pretty-format.d.ts.map +1 -0
  51. package/dist/pretty-format.js +2 -0
  52. package/dist/pretty-format.js.map +1 -0
  53. package/dist/pretty-widget.d.ts +5 -2
  54. package/dist/pretty-widget.d.ts.map +1 -1
  55. package/dist/pretty-widget.js +75 -57
  56. package/dist/pretty-widget.js.map +1 -1
  57. package/dist/production.d.ts +8 -0
  58. package/dist/production.d.ts.map +1 -0
  59. package/dist/production.js +34 -0
  60. package/dist/production.js.map +1 -0
  61. package/dist/queries.d.ts +98 -56
  62. package/dist/queries.d.ts.map +1 -1
  63. package/dist/queries.js +123 -83
  64. package/dist/queries.js.map +1 -1
  65. package/dist/query-helpers.d.ts +44 -0
  66. package/dist/query-helpers.d.ts.map +1 -0
  67. package/dist/query-helpers.js +57 -0
  68. package/dist/query-helpers.js.map +1 -0
  69. package/dist/render-children.d.ts +9 -0
  70. package/dist/render-children.d.ts.map +1 -0
  71. package/dist/render-children.js +11 -0
  72. package/dist/render-children.js.map +1 -0
  73. package/dist/render.d.ts +12 -1
  74. package/dist/render.d.ts.map +1 -1
  75. package/dist/render.js +52 -16
  76. package/dist/render.js.map +1 -1
  77. package/dist/role-helpers.d.ts +12 -3
  78. package/dist/role-helpers.d.ts.map +1 -1
  79. package/dist/role-helpers.js +15 -5
  80. package/dist/role-helpers.js.map +1 -1
  81. package/dist/role-naming.d.ts +5 -0
  82. package/dist/role-naming.d.ts.map +1 -0
  83. package/dist/role-naming.js +96 -0
  84. package/dist/role-naming.js.map +1 -0
  85. package/dist/screenshot.d.ts +12 -20
  86. package/dist/screenshot.d.ts.map +1 -1
  87. package/dist/screenshot.js +179 -93
  88. package/dist/screenshot.js.map +1 -1
  89. package/dist/timers.d.ts +9 -0
  90. package/dist/timers.d.ts.map +1 -0
  91. package/dist/timers.js +21 -0
  92. package/dist/timers.js.map +1 -0
  93. package/dist/traversal.d.ts +7 -1
  94. package/dist/traversal.d.ts.map +1 -1
  95. package/dist/traversal.js +40 -6
  96. package/dist/traversal.js.map +1 -1
  97. package/dist/types.d.ts +32 -15
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js.map +1 -1
  100. package/dist/user-event/adjustment.d.ts +3 -0
  101. package/dist/user-event/adjustment.d.ts.map +1 -1
  102. package/dist/user-event/adjustment.js +25 -3
  103. package/dist/user-event/adjustment.js.map +1 -1
  104. package/dist/user-event/click.d.ts +44 -8
  105. package/dist/user-event/click.d.ts.map +1 -1
  106. package/dist/user-event/click.js +176 -44
  107. package/dist/user-event/click.js.map +1 -1
  108. package/dist/user-event/column-header-click.d.ts +4 -0
  109. package/dist/user-event/column-header-click.d.ts.map +1 -0
  110. package/dist/user-event/column-header-click.js +55 -0
  111. package/dist/user-event/column-header-click.js.map +1 -0
  112. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  113. package/dist/user-event/event-wrapper.js +34 -20
  114. package/dist/user-event/event-wrapper.js.map +1 -1
  115. package/dist/user-event/index.d.ts +80 -13
  116. package/dist/user-event/index.d.ts.map +1 -1
  117. package/dist/user-event/index.js +44 -26
  118. package/dist/user-event/index.js.map +1 -1
  119. package/dist/user-event/indexed-children.d.ts +10 -0
  120. package/dist/user-event/indexed-children.d.ts.map +1 -0
  121. package/dist/user-event/indexed-children.js +50 -0
  122. package/dist/user-event/indexed-children.js.map +1 -0
  123. package/dist/user-event/keyboard.d.ts.map +1 -1
  124. package/dist/user-event/keyboard.js +115 -31
  125. package/dist/user-event/keyboard.js.map +1 -1
  126. package/dist/user-event/list-row-click.d.ts +4 -0
  127. package/dist/user-event/list-row-click.d.ts.map +1 -0
  128. package/dist/user-event/list-row-click.js +38 -0
  129. package/dist/user-event/list-row-click.js.map +1 -0
  130. package/dist/user-event/native-click.d.ts +6 -0
  131. package/dist/user-event/native-click.d.ts.map +1 -0
  132. package/dist/user-event/native-click.js +36 -0
  133. package/dist/user-event/native-click.js.map +1 -0
  134. package/dist/user-event/notebook-tab-click.d.ts +5 -0
  135. package/dist/user-event/notebook-tab-click.d.ts.map +1 -0
  136. package/dist/user-event/notebook-tab-click.js +39 -0
  137. package/dist/user-event/notebook-tab-click.js.map +1 -0
  138. package/dist/user-event/pointer.d.ts +1 -1
  139. package/dist/user-event/pointer.d.ts.map +1 -1
  140. package/dist/user-event/pointer.js +33 -9
  141. package/dist/user-event/pointer.js.map +1 -1
  142. package/dist/user-event/selection.d.ts +11 -8
  143. package/dist/user-event/selection.d.ts.map +1 -1
  144. package/dist/user-event/selection.js +49 -38
  145. package/dist/user-event/selection.js.map +1 -1
  146. package/dist/user-event/text.d.ts +12 -5
  147. package/dist/user-event/text.d.ts.map +1 -1
  148. package/dist/user-event/text.js +151 -46
  149. package/dist/user-event/text.js.map +1 -1
  150. package/dist/wait-for.d.ts.map +1 -1
  151. package/dist/wait-for.js +4 -3
  152. package/dist/wait-for.js.map +1 -1
  153. package/dist/widget-accessible-properties.d.ts +14 -26
  154. package/dist/widget-accessible-properties.d.ts.map +1 -1
  155. package/dist/widget-accessible-properties.js +135 -190
  156. package/dist/widget-accessible-properties.js.map +1 -1
  157. package/dist/widget-getters.d.ts +11 -0
  158. package/dist/widget-getters.d.ts.map +1 -0
  159. package/dist/widget-getters.js +46 -0
  160. package/dist/widget-getters.js.map +1 -0
  161. package/dist/window-state.d.ts +10 -0
  162. package/dist/window-state.d.ts.map +1 -0
  163. package/dist/window-state.js +41 -0
  164. package/dist/window-state.js.map +1 -0
  165. package/dist/within.d.ts +3 -2
  166. package/dist/within.d.ts.map +1 -1
  167. package/dist/within.js +3 -2
  168. package/dist/within.js.map +1 -1
  169. package/package.json +21 -8
  170. package/src/accessible-native.ts +333 -0
  171. package/src/act.ts +1 -1
  172. package/src/build-queries.ts +47 -41
  173. package/src/cleanup-registry.ts +1 -1
  174. package/src/config.ts +10 -1
  175. package/src/errors.ts +12 -1
  176. package/src/fire-event.ts +61 -8
  177. package/src/frame-sync.ts +30 -9
  178. package/src/harness-window.ts +23 -0
  179. package/src/index.ts +18 -11
  180. package/src/internal.ts +12 -0
  181. package/src/matchers.ts +457 -72
  182. package/src/pretty-format.ts +1 -0
  183. package/src/pretty-widget.ts +98 -70
  184. package/src/production.ts +51 -0
  185. package/src/queries.ts +127 -89
  186. package/src/query-helpers.ts +97 -0
  187. package/src/render-children.ts +25 -0
  188. package/src/render.tsx +77 -17
  189. package/src/role-helpers.ts +16 -5
  190. package/src/role-naming.ts +99 -0
  191. package/src/screenshot.ts +266 -104
  192. package/src/timers.ts +31 -0
  193. package/src/traversal.ts +66 -6
  194. package/src/types.ts +34 -17
  195. package/src/user-event/adjustment.ts +31 -3
  196. package/src/user-event/click.ts +248 -52
  197. package/src/user-event/column-header-click.ts +76 -0
  198. package/src/user-event/event-wrapper.ts +41 -23
  199. package/src/user-event/index.ts +130 -40
  200. package/src/user-event/indexed-children.ts +75 -0
  201. package/src/user-event/keyboard.ts +161 -43
  202. package/src/user-event/list-row-click.ts +54 -0
  203. package/src/user-event/native-click.ts +51 -0
  204. package/src/user-event/notebook-tab-click.ts +57 -0
  205. package/src/user-event/pointer.ts +48 -9
  206. package/src/user-event/selection.ts +67 -41
  207. package/src/user-event/text.ts +192 -46
  208. package/src/wait-for.ts +4 -4
  209. package/src/widget-accessible-properties.ts +183 -230
  210. package/src/widget-getters.ts +85 -0
  211. package/src/window-state.ts +70 -0
  212. package/src/within.ts +3 -2
@@ -2,33 +2,38 @@ import * as Gtk from "@gtkx/gi/gtk";
2
2
  import { runInAct } from "../act.js";
3
3
  import { getConfig } from "../config.js";
4
4
  import { formatRole } from "../role-helpers.js";
5
+ import { delay, now } from "../timers.js";
6
+ import { getWidgetAccessibleName } from "../widget-accessible-properties.js";
7
+ import { getTypeTag, isDefaultWidgetName } from "../widget-getters.js";
8
+ import { isWindowAllocated, isWindowBlockedByModal } from "../window-state.js";
5
9
 
6
10
  const NOT_SENSITIVE = "it is not sensitive (the widget or one of its ancestors is disabled)";
11
+ const NOT_ROOTED = "it is not inside a toplevel (it was removed from the widget tree, or was never added to one)";
12
+ const WINDOW_NOT_VISIBLE = "its window is not visible (it was hidden, or it was never shown)";
7
13
  const WINDOW_NOT_ALLOCATED = "its window has not been allocated a size";
8
14
  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";
15
+ const BLOCKED_BY_MODAL = "its window is blocked by a modal window holding the grab";
16
+ const ACTIONABLE_HOP_MS = 1;
10
17
 
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;
18
+ const findWindowActionabilityFailure = (window: Gtk.Window): string | null => {
19
+ if (!window.getVisible()) {
20
+ return WINDOW_NOT_VISIBLE;
19
21
  }
20
22
 
21
- if (!widget.getMapped()) {
22
- return NOT_MAPPED;
23
+ if (!isWindowAllocated(window)) {
24
+ return WINDOW_NOT_ALLOCATED;
23
25
  }
24
26
 
25
- if (canDisplayDeliverActivation(root) && !root.isActive()) {
26
- return WINDOW_NOT_ACTIVE;
27
+ if (isWindowBlockedByModal(window)) {
28
+ return BLOCKED_BY_MODAL;
27
29
  }
28
30
 
29
31
  return null;
30
32
  };
31
33
 
34
+ const findRootActionabilityFailure = (root: Gtk.Root): string | null =>
35
+ root instanceof Gtk.Window ? findWindowActionabilityFailure(root) : null;
36
+
32
37
  const findActionabilityFailure = (widget: Gtk.Widget): string | null => {
33
38
  if (!widget.isSensitive()) {
34
39
  return NOT_SENSITIVE;
@@ -36,28 +41,41 @@ const findActionabilityFailure = (widget: Gtk.Widget): string | null => {
36
41
 
37
42
  const root = widget.getRoot();
38
43
 
39
- if (!(root instanceof Gtk.Window) || !root.getVisible()) {
40
- return null;
44
+ if (root === null) {
45
+ return NOT_ROOTED;
46
+ }
47
+
48
+ const rootFailure = findRootActionabilityFailure(root);
49
+
50
+ if (rootFailure !== null) {
51
+ return rootFailure;
41
52
  }
42
53
 
43
- return findWindowActionabilityFailure(widget, root);
54
+ return widget.getMapped() ? null : NOT_MAPPED;
44
55
  };
45
56
 
46
- const describeWidget = (widget: Gtk.Widget): string => {
47
- const tag = widget.constructor.name;
57
+ const attribute = (key: string, value: string | null): string => (value === null ? "" : ` ${key}="${value}"`);
58
+
59
+ const widgetNameAttribute = (widget: Gtk.Widget): string => {
48
60
  const name = widget.getName();
49
- const nameAttribute = name && !name.endsWith(tag) ? ` name="${name}"` : "";
50
61
 
51
- return `<${tag}${nameAttribute} role="${formatRole(widget.getAccessibleRole())}">`;
62
+ return attribute("name", isDefaultWidgetName(widget, name) ? null : name);
63
+ };
64
+
65
+ const describeWidget = (widget: Gtk.Widget): string => {
66
+ const accessibleName = attribute("accessible-name", getWidgetAccessibleName(widget));
67
+ const role = attribute("role", formatRole(widget.getAccessibleRole()));
68
+
69
+ return `<${getTypeTag(widget)}${accessibleName}${widgetNameAttribute(widget)}${role}>`;
52
70
  };
53
71
 
54
72
  const waitForActionable = async (widget: Gtk.Widget): Promise<void> => {
55
73
  const timeout = getConfig().actionabilityTimeout;
56
- const deadline = Date.now() + timeout;
74
+ const deadline = now() + timeout;
57
75
  let failure = findActionabilityFailure(widget);
58
76
 
59
- while (failure !== null && Date.now() < deadline) {
60
- await actionableHop();
77
+ while (failure !== null && now() < deadline) {
78
+ await delay(ACTIONABLE_HOP_MS);
61
79
  failure = findActionabilityFailure(widget);
62
80
  }
63
81
 
@@ -1,42 +1,87 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { delay } from "../timers.js";
2
3
  import { scroll, slide } from "./adjustment.js";
3
4
  import { click, dblClick, tripleClick } from "./click.js";
4
5
  import { drag, dragAndDrop, drop, hover, longPress, rotate, swipe, unhover, zoom } from "./gesture.js";
5
6
  import { keyboard, tab } from "./keyboard.js";
6
7
  import { pointer, type PointerInput } from "./pointer.js";
7
8
  import { deselectOptions, selectOptions } from "./selection.js";
8
- import { createInitialState } from "./state.js";
9
+ import { createInitialState, type UserEventState } from "./state.js";
9
10
  import { clear, copy, cut, paste, type } from "./text.js";
10
11
 
12
+ /** Options shared by every helper on a {@link UserEvent} instance. */
13
+ type UserEventOptions = {
14
+ /** Milliseconds to wait after each helper resolves; null, the default, waits not at all. */
15
+ delay?: number | null | undefined;
16
+ };
17
+
11
18
  /**
12
19
  * The set of user interaction helpers exposed by {@link userEvent}, covering clicks, typing,
13
20
  * keyboard, pointer, gestures, selection, and scrolling.
14
21
  */
15
22
  type UserEvent = {
16
23
  /**
17
- * Clicks a button, toggles a switch, and otherwise activates the widget, falling back to a click
18
- * gesture on its nearest clickable ancestor when activation does nothing.
24
+ * Returns a fresh instance whose helpers share one keyboard and pointer state, so held modifiers
25
+ * and buttons carry across its calls but not across instances.
26
+ */
27
+ setup: (options?: UserEventOptions) => UserEvent;
28
+ /**
29
+ * Activates a widget that neither claims the click nor carries a click gesture of its own, and
30
+ * otherwise presses and releases outwards from the clicked widget through every widget carrying
31
+ * a click gesture, stopping at the first button or indexed child. A list box row or flow box
32
+ * child receives its click gesture at the clicked position, passes one on to the gestures its
33
+ * container carries, and is then activated, or exclusively selected when its container does not
34
+ * activate on a single click. The press also stops at a widget whose click GTK4 implements
35
+ * itself, where the gestures that widget carries of its own still take the press and the same
36
+ * outcome is then applied through GTK's own public action: a list, grid, or column-view row is
37
+ * focused and selected, and activated as well when its view activates on a single click; an
38
+ * expandable tree expander that is itself the clicked widget toggles its expansion; a notebook
39
+ * tab, clicked directly or through its label, focuses its notebook and switches to its page; a
40
+ * column header sorts by its column. A column-view cell and the row carrying the column headers
41
+ * stand in the way of the click rather than taking it, so a click on either reaches the row or
42
+ * the view behind it.
19
43
  */
20
44
  click: typeof click;
21
- /** Emits a two-press click gesture on the widget. */
45
+ /**
46
+ * Delivers a two-press click gesture along the same path a single click takes, without trying
47
+ * activation first, activating a list box row or flow box child on that path and replacing its
48
+ * container's selection whether or not that container activates on a single click. A widget
49
+ * whose click GTK4 implements itself receives that outcome once per press, so a row is selected
50
+ * and activated by the second press, a tree expander ends back where it started, a notebook tab
51
+ * stays on the page the first press opened, and a column header inverts its order after sorting.
52
+ */
22
53
  dblClick: typeof dblClick;
23
- /** Emits a three-press click gesture on the widget. */
54
+ /**
55
+ * Delivers a three-press click gesture the same way a double click is delivered, applying the
56
+ * same outcome to a list box row or flow box child, and applying the outcome GTK4 implements
57
+ * itself once per press to a row, tree expander, notebook tab, or column header.
58
+ */
24
59
  tripleClick: typeof tripleClick;
25
60
  /** Moves focus within the widget's root, forward by default and backward with `isShiftHeld`. */
26
61
  tab: typeof tab;
27
- /** Focuses an editable widget, applies any initial selection, and inserts the text at the cursor. */
62
+ /**
63
+ * Focuses an editable widget the way clicking into it does, leaving its text unselected, applies
64
+ * any initial selection, and inserts the text at the cursor, deleting the text the widget has
65
+ * selected first, the way GTK4 does.
66
+ */
28
67
  type: typeof type;
29
- /** Empties an editable widget's text. */
68
+ /** Focuses an editable widget and deletes its whole text, leaving nothing selected. */
30
69
  clear: typeof clear;
31
70
  /** Writes an editable widget's current selection to the clipboard. */
32
71
  copy: typeof copy;
33
72
  /** Writes an editable widget's current selection to the clipboard and deletes it. */
34
73
  cut: typeof cut;
35
- /** Inserts the given text, or the clipboard's text, at an editable widget's cursor. */
74
+ /**
75
+ * Inserts the given text, or the clipboard's text, at an editable widget's cursor, deleting the
76
+ * text the widget has selected first, the way GTK4 does.
77
+ */
36
78
  paste: typeof paste;
37
- /** Selects the items at the given positions in a list view, drop-down, combo box, or list box. */
79
+ /**
80
+ * Selects the items at those positions in a view or drop-down, or selects the indexed children of
81
+ * a list box or flow box through their container, without activating them.
82
+ */
38
83
  selectOptions: typeof selectOptions;
39
- /** Deselects the items at the given positions in a list view or list box. */
84
+ /** Unselects the items at those positions, leaving list box and flow box children unactivated. */
40
85
  deselectOptions: typeof deselectOptions;
41
86
  /** Emits a motion `enter` on the widget, adding a motion controller when it has none. */
42
87
  hover: typeof hover;
@@ -60,49 +105,94 @@ type UserEvent = {
60
105
  slide: typeof slide;
61
106
  /** Adds the delta to the adjustments of the widget itself, or of its nearest scrollable ancestor. */
62
107
  scroll: typeof scroll;
63
- /** Sends a key sequence, dispatching matching shortcuts and tracking held modifiers across calls. */
108
+ /**
109
+ * Sends a key sequence along the chain GTK4 propagates a key event through: the application's
110
+ * accelerators first, then the capture-phase controllers from the root down to the event widget,
111
+ * the event widget's target-phase controllers, and the bubble-phase controllers from it back up
112
+ * to the root, so a key controller or shortcut controller a container or window above the widget
113
+ * carries receives the key too, each in the phase it declares. The event widget is the editable
114
+ * delegate GTK4 focuses when the widget has one, the `Gtk.Text` inside a `Gtk.Entry` for
115
+ * instance, so the delegate stands ahead of the widget in the chain. A shortcut controller whose
116
+ * scope is not local runs where it registered instead, the root for a global one and the nearest
117
+ * `Gtk.ShortcutManager` for a managed one, so it fires wherever focus sits inside that subtree
118
+ * and never at its own widget, while a shortcut whose widget is insensitive or unmapped stays
119
+ * inert wherever it sits. The first controller that handles a press ends the chain, and a
120
+ * `Gdk.KEY_Return` press none of them handled reaches an editable widget's activate handler.
121
+ * Built-in key bindings take part, since GTK4 carries them on a shortcut controller: a
122
+ * `Gtk.Text`'s arrow keys or a `Gtk.TextView`'s undo can consume a press before an ancestor
123
+ * sees it. Key controllers GTK4 attaches itself are the exception, since they read a GDK event
124
+ * that off-screen synthesis cannot produce, so only the key controllers the tree connected a
125
+ * `key-pressed` or `key-released` handler to take part. Held modifiers carry across calls.
126
+ */
64
127
  keyboard: (widget: Gtk.Widget, input: string) => Promise<void>;
65
- /** Applies a pointer token, tracking whether the left button is held across calls. */
128
+ /**
129
+ * Applies a pointer token to the click gestures the widget already carries, tracking whether the
130
+ * left button is held across calls. On a widget whose click GTK4 implements itself, the release
131
+ * also applies GTK's own outcome, the way {@link UserEvent.click} does, and a column-view cell
132
+ * or the row carrying the column headers passes the token to the row or view behind it.
133
+ */
66
134
  pointer: (widget: Gtk.Widget, input: PointerInput) => Promise<void>;
67
135
  };
68
136
 
69
- const state = createInitialState();
70
-
71
137
  /**
72
138
  * High-level helpers that drive widgets by dispatching the same events and gestures GTK4 delivers
73
- * when someone clicks, types, or drags.
139
+ * when someone clicks, types, or drags. Call `setup` for an instance with its own keyboard and
140
+ * pointer state.
74
141
  */
75
142
  const userEvent: UserEvent = {
76
- click,
77
- dblClick,
78
- tripleClick,
79
- tab,
80
- type,
81
- clear,
82
- copy,
83
- cut,
84
- paste,
85
- selectOptions,
86
- deselectOptions,
87
- hover,
88
- unhover,
89
- rotate,
90
- zoom,
91
- swipe,
92
- longPress,
93
- drag,
94
- drop,
95
- dragAndDrop,
96
- slide,
97
- scroll,
98
- keyboard: (widget: Gtk.Widget, input: string): Promise<void> => keyboard(state, widget, input),
99
- pointer: (widget: Gtk.Widget, input: PointerInput): Promise<void> => pointer(state, widget, input),
143
+ ...createInstance(createInitialState(), {}),
144
+ setup: (options?: UserEventOptions): UserEvent => createInstance(createInitialState(), options ?? {}),
145
+ };
146
+
147
+ const settle = (ms: number | null | undefined): Promise<void> => {
148
+ if (ms === null || ms === undefined) {
149
+ return Promise.resolve();
150
+ }
151
+
152
+ return delay(ms);
100
153
  };
101
154
 
155
+ function createInstance(state: UserEventState, options: UserEventOptions): UserEvent {
156
+ const after =
157
+ <Args extends unknown[]>(helper: (...args: Args) => Promise<void>) =>
158
+ async (...args: Args): Promise<void> => {
159
+ await helper(...args);
160
+ await settle(options.delay);
161
+ };
162
+
163
+ return {
164
+ setup: (overrides?: UserEventOptions): UserEvent => createInstance(state, { ...options, ...overrides }),
165
+ click: after(click),
166
+ dblClick: after(dblClick),
167
+ tripleClick: after(tripleClick),
168
+ tab: after(tab),
169
+ type: after(type),
170
+ clear: after(clear),
171
+ copy: after(copy),
172
+ cut: after(cut),
173
+ paste: after(paste),
174
+ selectOptions: after(selectOptions),
175
+ deselectOptions: after(deselectOptions),
176
+ hover: after(hover),
177
+ unhover: after(unhover),
178
+ rotate: after(rotate),
179
+ zoom: after(zoom),
180
+ swipe: after(swipe),
181
+ longPress: after(longPress),
182
+ drag: after(drag),
183
+ drop: after(drop),
184
+ dragAndDrop: after(dragAndDrop),
185
+ slide: after(slide),
186
+ scroll: after(scroll),
187
+ keyboard: after((widget: Gtk.Widget, input: string): Promise<void> => keyboard(state, widget, input)),
188
+ pointer: after((widget: Gtk.Widget, input: PointerInput): Promise<void> => pointer(state, widget, input)),
189
+ };
190
+ }
191
+
102
192
  export type { ScrollDelta } from "./adjustment.js";
103
193
  export type { DragOffset, DragOptions, DropContent, DropOptions } from "./gesture.js";
104
194
  export type { TabOptions } from "./keyboard.js";
105
195
  export type { PointerInput } from "./pointer.js";
106
196
  export type { TypeOptions } from "./text.js";
107
197
  export { resetClipboard } from "./text.js";
108
- export { userEvent, type UserEvent };
198
+ export { userEvent, type UserEvent, type UserEventOptions };
@@ -0,0 +1,75 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { callBooleanGetter, getCallableMethod, hasWidgetMethod } from "../widget-getters.js";
3
+
4
+ const CHILD_AT_INDEX_GETTERS = ["getRowAtIndex", "getChildAtIndex"];
5
+ const CHILD_SELECTORS = ["selectRow", "selectChild"];
6
+ const CHILD_DESELECTORS = ["unselectRow", "unselectChild"];
7
+ const SELECTED_CHILD_GETTERS = ["getSelectedRows", "getSelectedChildren"];
8
+ const SELECTED_PROBE = "isSelected";
9
+
10
+ const hasIndexedChildren = (widget: Gtk.Widget): boolean =>
11
+ CHILD_AT_INDEX_GETTERS.some((getter) => hasWidgetMethod(widget, getter));
12
+
13
+ const getChildAtIndex = (widget: Gtk.Widget, index: number): Gtk.Widget | null => {
14
+ for (const getter of CHILD_AT_INDEX_GETTERS) {
15
+ const fn = getCallableMethod<[number], Gtk.Widget | null>(widget, getter);
16
+
17
+ if (fn) {
18
+ return fn(index) ?? null;
19
+ }
20
+ }
21
+
22
+ return null;
23
+ };
24
+
25
+ const applyChildMethod = (container: Gtk.Widget, names: string[], child: Gtk.Widget): void => {
26
+ for (const name of names) {
27
+ const fn = getCallableMethod<[Gtk.Widget], unknown>(container, name);
28
+
29
+ if (fn) {
30
+ fn(child);
31
+
32
+ return;
33
+ }
34
+ }
35
+ };
36
+
37
+ const selectContainerChild = (container: Gtk.Widget, child: Gtk.Widget): void => {
38
+ applyChildMethod(container, CHILD_SELECTORS, child);
39
+ };
40
+
41
+ const unselectContainerChild = (container: Gtk.Widget, child: Gtk.Widget): void => {
42
+ applyChildMethod(container, CHILD_DESELECTORS, child);
43
+ };
44
+
45
+ const isChildSelected = (child: Gtk.Widget): boolean => callBooleanGetter(child, SELECTED_PROBE) ?? false;
46
+
47
+ const getSelectedChildren = (container: Gtk.Widget): Gtk.Widget[] => {
48
+ for (const getter of SELECTED_CHILD_GETTERS) {
49
+ const fn = getCallableMethod<[], Gtk.Widget[]>(container, getter);
50
+
51
+ if (fn) {
52
+ return fn();
53
+ }
54
+ }
55
+
56
+ return [];
57
+ };
58
+
59
+ const unselectOtherChildren = (container: Gtk.Widget, child: Gtk.Widget): void => {
60
+ for (const selected of getSelectedChildren(container)) {
61
+ if (selected !== child) {
62
+ unselectContainerChild(container, selected);
63
+ }
64
+ }
65
+ };
66
+
67
+ export {
68
+ getChildAtIndex,
69
+ hasIndexedChildren,
70
+ isChildSelected,
71
+ SELECTED_PROBE,
72
+ selectContainerChild,
73
+ unselectContainerChild,
74
+ unselectOtherChildren,
75
+ };