@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.
- package/README.md +5 -5
- package/dist/accessible-native.d.ts +16 -0
- package/dist/accessible-native.d.ts.map +1 -0
- package/dist/accessible-native.js +206 -0
- package/dist/accessible-native.js.map +1 -0
- package/dist/act.d.ts +1 -3
- package/dist/act.d.ts.map +1 -1
- package/dist/act.js +1 -1
- package/dist/act.js.map +1 -1
- package/dist/build-queries.d.ts +27 -10
- package/dist/build-queries.d.ts.map +1 -1
- package/dist/build-queries.js +22 -24
- package/dist/build-queries.js.map +1 -1
- package/dist/cleanup-registry.d.ts +1 -1
- package/dist/cleanup-registry.d.ts.map +1 -1
- package/dist/cleanup-registry.js.map +1 -1
- package/dist/config.d.ts +8 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +10 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -1
- package/dist/errors.js.map +1 -1
- package/dist/fire-event.d.ts +35 -6
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +23 -7
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +3 -2
- package/dist/frame-sync.d.ts.map +1 -1
- package/dist/frame-sync.js +19 -9
- package/dist/frame-sync.js.map +1 -1
- package/dist/harness-window.d.ts +5 -0
- package/dist/harness-window.d.ts.map +1 -0
- package/dist/harness-window.js +17 -0
- package/dist/harness-window.js.map +1 -0
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +5 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +5 -0
- package/dist/internal.js.map +1 -0
- package/dist/matchers.d.ts +88 -46
- package/dist/matchers.d.ts.map +1 -1
- package/dist/matchers.js +178 -20
- package/dist/matchers.js.map +1 -1
- package/dist/pretty-format.d.ts +2 -0
- package/dist/pretty-format.d.ts.map +1 -0
- package/dist/pretty-format.js +2 -0
- package/dist/pretty-format.js.map +1 -0
- package/dist/pretty-widget.d.ts +5 -2
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +75 -57
- package/dist/pretty-widget.js.map +1 -1
- package/dist/production.d.ts +8 -0
- package/dist/production.d.ts.map +1 -0
- package/dist/production.js +34 -0
- package/dist/production.js.map +1 -0
- package/dist/queries.d.ts +98 -56
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +123 -83
- package/dist/queries.js.map +1 -1
- package/dist/query-helpers.d.ts +44 -0
- package/dist/query-helpers.d.ts.map +1 -0
- package/dist/query-helpers.js +57 -0
- package/dist/query-helpers.js.map +1 -0
- package/dist/render-children.d.ts +9 -0
- package/dist/render-children.d.ts.map +1 -0
- package/dist/render-children.js +11 -0
- package/dist/render-children.js.map +1 -0
- package/dist/render.d.ts +12 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +52 -16
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +12 -3
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +15 -5
- package/dist/role-helpers.js.map +1 -1
- package/dist/role-naming.d.ts +5 -0
- package/dist/role-naming.d.ts.map +1 -0
- package/dist/role-naming.js +96 -0
- package/dist/role-naming.js.map +1 -0
- package/dist/screenshot.d.ts +12 -20
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +179 -93
- package/dist/screenshot.js.map +1 -1
- package/dist/timers.d.ts +9 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +21 -0
- package/dist/timers.js.map +1 -0
- package/dist/traversal.d.ts +7 -1
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +40 -6
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +32 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +3 -0
- package/dist/user-event/adjustment.d.ts.map +1 -1
- package/dist/user-event/adjustment.js +25 -3
- package/dist/user-event/adjustment.js.map +1 -1
- package/dist/user-event/click.d.ts +44 -8
- package/dist/user-event/click.d.ts.map +1 -1
- package/dist/user-event/click.js +176 -44
- package/dist/user-event/click.js.map +1 -1
- package/dist/user-event/column-header-click.d.ts +4 -0
- package/dist/user-event/column-header-click.d.ts.map +1 -0
- package/dist/user-event/column-header-click.js +55 -0
- package/dist/user-event/column-header-click.js.map +1 -0
- package/dist/user-event/event-wrapper.d.ts.map +1 -1
- package/dist/user-event/event-wrapper.js +34 -20
- package/dist/user-event/event-wrapper.js.map +1 -1
- package/dist/user-event/index.d.ts +80 -13
- package/dist/user-event/index.d.ts.map +1 -1
- package/dist/user-event/index.js +44 -26
- package/dist/user-event/index.js.map +1 -1
- package/dist/user-event/indexed-children.d.ts +10 -0
- package/dist/user-event/indexed-children.d.ts.map +1 -0
- package/dist/user-event/indexed-children.js +50 -0
- package/dist/user-event/indexed-children.js.map +1 -0
- package/dist/user-event/keyboard.d.ts.map +1 -1
- package/dist/user-event/keyboard.js +115 -31
- package/dist/user-event/keyboard.js.map +1 -1
- package/dist/user-event/list-row-click.d.ts +4 -0
- package/dist/user-event/list-row-click.d.ts.map +1 -0
- package/dist/user-event/list-row-click.js +38 -0
- package/dist/user-event/list-row-click.js.map +1 -0
- package/dist/user-event/native-click.d.ts +6 -0
- package/dist/user-event/native-click.d.ts.map +1 -0
- package/dist/user-event/native-click.js +36 -0
- package/dist/user-event/native-click.js.map +1 -0
- package/dist/user-event/notebook-tab-click.d.ts +5 -0
- package/dist/user-event/notebook-tab-click.d.ts.map +1 -0
- package/dist/user-event/notebook-tab-click.js +39 -0
- package/dist/user-event/notebook-tab-click.js.map +1 -0
- package/dist/user-event/pointer.d.ts +1 -1
- package/dist/user-event/pointer.d.ts.map +1 -1
- package/dist/user-event/pointer.js +33 -9
- package/dist/user-event/pointer.js.map +1 -1
- package/dist/user-event/selection.d.ts +11 -8
- package/dist/user-event/selection.d.ts.map +1 -1
- package/dist/user-event/selection.js +49 -38
- package/dist/user-event/selection.js.map +1 -1
- package/dist/user-event/text.d.ts +12 -5
- package/dist/user-event/text.d.ts.map +1 -1
- package/dist/user-event/text.js +151 -46
- package/dist/user-event/text.js.map +1 -1
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js +4 -3
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +14 -26
- package/dist/widget-accessible-properties.d.ts.map +1 -1
- package/dist/widget-accessible-properties.js +135 -190
- package/dist/widget-accessible-properties.js.map +1 -1
- package/dist/widget-getters.d.ts +11 -0
- package/dist/widget-getters.d.ts.map +1 -0
- package/dist/widget-getters.js +46 -0
- package/dist/widget-getters.js.map +1 -0
- package/dist/window-state.d.ts +10 -0
- package/dist/window-state.d.ts.map +1 -0
- package/dist/window-state.js +41 -0
- package/dist/window-state.js.map +1 -0
- package/dist/within.d.ts +3 -2
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +3 -2
- package/dist/within.js.map +1 -1
- package/package.json +21 -8
- package/src/accessible-native.ts +333 -0
- package/src/act.ts +1 -1
- package/src/build-queries.ts +47 -41
- package/src/cleanup-registry.ts +1 -1
- package/src/config.ts +10 -1
- package/src/errors.ts +12 -1
- package/src/fire-event.ts +61 -8
- package/src/frame-sync.ts +30 -9
- package/src/harness-window.ts +23 -0
- package/src/index.ts +18 -11
- package/src/internal.ts +12 -0
- package/src/matchers.ts +457 -72
- package/src/pretty-format.ts +1 -0
- package/src/pretty-widget.ts +98 -70
- package/src/production.ts +51 -0
- package/src/queries.ts +127 -89
- package/src/query-helpers.ts +97 -0
- package/src/render-children.ts +25 -0
- package/src/render.tsx +77 -17
- package/src/role-helpers.ts +16 -5
- package/src/role-naming.ts +99 -0
- package/src/screenshot.ts +266 -104
- package/src/timers.ts +31 -0
- package/src/traversal.ts +66 -6
- package/src/types.ts +34 -17
- package/src/user-event/adjustment.ts +31 -3
- package/src/user-event/click.ts +248 -52
- package/src/user-event/column-header-click.ts +76 -0
- package/src/user-event/event-wrapper.ts +41 -23
- package/src/user-event/index.ts +130 -40
- package/src/user-event/indexed-children.ts +75 -0
- package/src/user-event/keyboard.ts +161 -43
- package/src/user-event/list-row-click.ts +54 -0
- package/src/user-event/native-click.ts +51 -0
- package/src/user-event/notebook-tab-click.ts +57 -0
- package/src/user-event/pointer.ts +48 -9
- package/src/user-event/selection.ts +67 -41
- package/src/user-event/text.ts +192 -46
- package/src/wait-for.ts +4 -4
- package/src/widget-accessible-properties.ts +183 -230
- package/src/widget-getters.ts +85 -0
- package/src/window-state.ts +70 -0
- package/src/within.ts +3 -2
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
2
|
import { formatRoleList } from "../role-helpers.js";
|
|
3
|
+
import { getCallableMethod } from "../widget-getters.js";
|
|
3
4
|
import { wrapEvent } from "./event-wrapper.js";
|
|
5
|
+
import {
|
|
6
|
+
getChildAtIndex,
|
|
7
|
+
hasIndexedChildren,
|
|
8
|
+
isChildSelected,
|
|
9
|
+
selectContainerChild,
|
|
10
|
+
unselectContainerChild,
|
|
11
|
+
} from "./indexed-children.js";
|
|
4
12
|
|
|
5
13
|
type CollectionWidget = Gtk.ListView | Gtk.GridView | Gtk.ColumnView;
|
|
6
|
-
type
|
|
14
|
+
type IndexedChildAction = (container: Gtk.Widget, child: Gtk.Widget) => void;
|
|
7
15
|
|
|
8
|
-
const SELECTABLE_ROLES: Set<Gtk.AccessibleRole> = new Set([
|
|
16
|
+
const SELECTABLE_ROLES: Set<Gtk.AccessibleRole> = new Set([
|
|
17
|
+
Gtk.AccessibleRole.COMBO_BOX,
|
|
18
|
+
Gtk.AccessibleRole.GRID,
|
|
19
|
+
Gtk.AccessibleRole.LIST,
|
|
20
|
+
]);
|
|
9
21
|
|
|
10
22
|
const isSelectable = (widget: Gtk.Widget): boolean => {
|
|
11
23
|
return SELECTABLE_ROLES.has(widget.getAccessibleRole());
|
|
@@ -44,42 +56,40 @@ const selectListViewItems = (
|
|
|
44
56
|
const isListView = (widget: Gtk.Widget): widget is CollectionWidget =>
|
|
45
57
|
widget instanceof Gtk.ListView || widget instanceof Gtk.GridView || widget instanceof Gtk.ColumnView;
|
|
46
58
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
throw new Error("Cannot select multiple options: ComboBox only supports single selection");
|
|
50
|
-
}
|
|
59
|
+
const getIndexSelector = (widget: Gtk.Widget): ((position: number) => void) | null =>
|
|
60
|
+
getCallableMethod<[number], unknown>(widget, "setSelected");
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
const selectDropDownOption = (widget: Gtk.Widget, valueArray: number[]): void => {
|
|
63
|
+
const setSelected = getIndexSelector(widget);
|
|
53
64
|
|
|
54
|
-
if (
|
|
55
|
-
|
|
65
|
+
if (!setSelected) {
|
|
66
|
+
throw new TypeError("Cannot select options: the COMBO_BOX role needs a widget exposing setSelected");
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
} else if (widget instanceof Gtk.ComboBox) {
|
|
61
|
-
widget.setActive(selection);
|
|
69
|
+
if (valueArray.length > 1) {
|
|
70
|
+
throw new Error("Cannot select multiple options: a drop-down only supports single selection");
|
|
62
71
|
}
|
|
63
|
-
};
|
|
64
72
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
const [selection] = valueArray;
|
|
74
|
+
|
|
75
|
+
if (selection !== undefined) {
|
|
76
|
+
setSelected(selection);
|
|
77
|
+
}
|
|
68
78
|
};
|
|
69
79
|
|
|
70
|
-
const
|
|
71
|
-
|
|
80
|
+
const unselectChildRow = (container: Gtk.Widget, child: Gtk.Widget): void => {
|
|
81
|
+
if (isChildSelected(child)) {
|
|
82
|
+
unselectContainerChild(container, child);
|
|
83
|
+
}
|
|
72
84
|
};
|
|
73
85
|
|
|
74
|
-
const
|
|
86
|
+
const applyIndexedChildren = (widget: Gtk.Widget, valueArray: number[], apply: IndexedChildAction): void => {
|
|
75
87
|
for (const value of valueArray) {
|
|
76
|
-
const
|
|
88
|
+
const child = getChildAtIndex(widget, value);
|
|
77
89
|
|
|
78
|
-
if (
|
|
79
|
-
|
|
90
|
+
if (child) {
|
|
91
|
+
apply(widget, child);
|
|
80
92
|
}
|
|
81
|
-
|
|
82
|
-
apply(listBox, row);
|
|
83
93
|
}
|
|
84
94
|
};
|
|
85
95
|
|
|
@@ -102,18 +112,29 @@ const selectInListView = (widget: CollectionWidget, valueArray: number[]): void
|
|
|
102
112
|
selectListViewItems(selectionModel, valueArray, !isMultiSelection);
|
|
103
113
|
};
|
|
104
114
|
|
|
105
|
-
const
|
|
115
|
+
const requireSelectableRole = (widget: Gtk.Widget, verb: string): void => {
|
|
106
116
|
if (!isSelectable(widget)) {
|
|
107
|
-
throw new Error(
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Cannot ${verb} options: expected selectable widget (${formatRoleList(SELECTABLE_ROLES)})`,
|
|
119
|
+
);
|
|
108
120
|
}
|
|
121
|
+
};
|
|
109
122
|
|
|
123
|
+
const selectByRole = (widget: Gtk.Widget, valueArray: number[]): void => {
|
|
124
|
+
requireSelectableRole(widget, "select");
|
|
110
125
|
const role = widget.getAccessibleRole();
|
|
111
126
|
|
|
112
127
|
if (role === Gtk.AccessibleRole.COMBO_BOX) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
selectDropDownOption(widget, valueArray);
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!hasIndexedChildren(widget)) {
|
|
134
|
+
throw new TypeError("Cannot select options: the widget exposes no children to select by index");
|
|
116
135
|
}
|
|
136
|
+
|
|
137
|
+
applyIndexedChildren(widget, valueArray, selectContainerChild);
|
|
117
138
|
};
|
|
118
139
|
|
|
119
140
|
const runSelectionEvent = (
|
|
@@ -136,11 +157,13 @@ const runSelectionEvent = (
|
|
|
136
157
|
|
|
137
158
|
/**
|
|
138
159
|
* Selects the items at the given positions through a list, grid, or column view's selection model,
|
|
139
|
-
* by setting the
|
|
140
|
-
* array clears a view's
|
|
160
|
+
* by setting the selected position of a widget with the COMBO_BOX role, or by selecting the indexed
|
|
161
|
+
* children of a list box or flow box through their container. An empty array clears a view's
|
|
162
|
+
* selection. The children are never activated, so no row-activated handler runs.
|
|
141
163
|
*
|
|
142
|
-
* @throws When a view has no selection model, when a
|
|
143
|
-
*
|
|
164
|
+
* @throws When a view has no selection model, when a COMBO_BOX-role widget is given more than one
|
|
165
|
+
* position or exposes no `setSelected`, or when the widget's role is none of COMBO_BOX, GRID, or
|
|
166
|
+
* LIST.
|
|
144
167
|
*/
|
|
145
168
|
const selectOptions = (widget: Gtk.Widget, values: number | number[]): Promise<void> =>
|
|
146
169
|
runSelectionEvent(widget, values, selectInListView, selectByRole);
|
|
@@ -154,19 +177,22 @@ const deselectInListView = (widget: Gtk.ListView | Gtk.GridView | Gtk.ColumnView
|
|
|
154
177
|
};
|
|
155
178
|
|
|
156
179
|
const deselectByRole = (widget: Gtk.Widget, valueArray: number[]): void => {
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
requireSelectableRole(widget, "deselect");
|
|
181
|
+
|
|
182
|
+
if (!hasIndexedChildren(widget)) {
|
|
183
|
+
throw new TypeError("Cannot deselect options: the widget exposes no children to deselect by index");
|
|
159
184
|
}
|
|
160
185
|
|
|
161
|
-
|
|
186
|
+
applyIndexedChildren(widget, valueArray, unselectChildRow);
|
|
162
187
|
};
|
|
163
188
|
|
|
164
189
|
/**
|
|
165
|
-
* Unselects the items at the given positions in a list, grid, or column view, or the
|
|
166
|
-
*
|
|
190
|
+
* Unselects the items at the given positions in a list, grid, or column view, or unselects the
|
|
191
|
+
* indexed children of a list box or flow box through their container. A child that is not selected
|
|
192
|
+
* is left alone, and no child is activated.
|
|
167
193
|
*
|
|
168
|
-
* @throws When a view has no selection model,
|
|
169
|
-
*
|
|
194
|
+
* @throws When a view has no selection model, when the widget's role is none of COMBO_BOX, GRID, or
|
|
195
|
+
* LIST, or when it exposes no indexed children.
|
|
170
196
|
*/
|
|
171
197
|
const deselectOptions = (widget: Gtk.Widget, values: number | number[]): Promise<void> =>
|
|
172
198
|
runSelectionEvent(widget, values, deselectInListView, deselectByRole);
|
package/src/user-event/text.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import * as Gdk from "@gtkx/gi/gdk";
|
|
2
2
|
import * as GObject from "@gtkx/gi/gobject";
|
|
3
3
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
4
|
-
import { EDITABLE_ROLES, type EditableTarget, getEditableDelegate, isEditable } from "../editable.js";
|
|
4
|
+
import { EDITABLE_ROLES, type EditableTarget, getEditableDelegate, isEditable, readEditableText } from "../editable.js";
|
|
5
5
|
import { formatRoleList } from "../role-helpers.js";
|
|
6
|
+
import { getWidgetSelection } from "../widget-accessible-properties.js";
|
|
7
|
+
import { callBooleanGetter } from "../widget-getters.js";
|
|
6
8
|
import { wrapEvent } from "./event-wrapper.js";
|
|
7
9
|
|
|
10
|
+
type SignalWatchTarget = {
|
|
11
|
+
on(signal: string, handler: (...args: unknown[]) => unknown, isAfter?: boolean): unknown;
|
|
12
|
+
off(signal: string, handler: (...args: unknown[]) => unknown): unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type InsertKind = "typing" | "pasting";
|
|
16
|
+
|
|
8
17
|
/** Options for `userEvent.type`. */
|
|
9
18
|
type TypeOptions = {
|
|
10
19
|
/** Focus the widget before typing; defaults to true. */
|
|
@@ -16,77 +25,139 @@ type TypeOptions = {
|
|
|
16
25
|
};
|
|
17
26
|
|
|
18
27
|
const EDITABLE_REQUIRED = `expected editable widget (${formatRoleList(EDITABLE_ROLES)})`;
|
|
28
|
+
const CLEAR_REFUSED = "Cannot clear element: the widget refuses to delete part of its text";
|
|
29
|
+
const SELECTION_DELETE_COUNT = 1;
|
|
30
|
+
const SINGLE_CHARACTER_LENGTH = 1;
|
|
31
|
+
const TEXT_END_POSITION = -1;
|
|
32
|
+
|
|
33
|
+
const isWidgetEditable = (widget: Gtk.Widget): boolean => callBooleanGetter(widget, "getEditable") ?? true;
|
|
34
|
+
const readSelection = (widget: EditableTarget): string => getWidgetSelection(widget) ?? "";
|
|
35
|
+
const hasSelectedText = (widget: Gtk.Editable): boolean => widget.getSelectionBounds()[0];
|
|
36
|
+
|
|
37
|
+
const getDelegateText = (widget: Gtk.Editable): Gtk.Text | null => {
|
|
38
|
+
const delegate = getEditableDelegate(widget) ?? widget;
|
|
39
|
+
|
|
40
|
+
return delegate instanceof Gtk.Text ? delegate : null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getSelectionLength = (buffer: Gtk.TextBuffer): number => {
|
|
44
|
+
const [isSelected, start, end] = buffer.getSelectionBounds();
|
|
45
|
+
|
|
46
|
+
return isSelected ? end.getOffset() - start.getOffset() : 0;
|
|
47
|
+
};
|
|
19
48
|
|
|
20
|
-
const
|
|
49
|
+
const deleteSelection = (widget: EditableTarget): void => {
|
|
21
50
|
if (widget instanceof Gtk.TextView) {
|
|
22
|
-
widget.
|
|
51
|
+
widget.getBuffer().deleteSelection(false, true);
|
|
23
52
|
|
|
24
53
|
return;
|
|
25
54
|
}
|
|
26
55
|
|
|
27
|
-
|
|
56
|
+
widget.deleteSelection();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const focusPlainEditable = (widget: Gtk.Editable): void => {
|
|
60
|
+
const [isSelected, start, end] = widget.getSelectionBounds();
|
|
61
|
+
widget.grabFocus();
|
|
28
62
|
|
|
29
|
-
if (
|
|
30
|
-
|
|
63
|
+
if (isSelected) {
|
|
64
|
+
widget.selectRegion(start, end);
|
|
31
65
|
|
|
32
66
|
return;
|
|
33
67
|
}
|
|
34
68
|
|
|
35
|
-
|
|
36
|
-
const newPosition = widget.insertText(text, text.length, position);
|
|
37
|
-
widget.setPosition(newPosition);
|
|
69
|
+
widget.setPosition(TEXT_END_POSITION);
|
|
38
70
|
};
|
|
39
71
|
|
|
40
|
-
const
|
|
72
|
+
const focusEditable = (widget: EditableTarget): void => {
|
|
41
73
|
if (widget instanceof Gtk.TextView) {
|
|
42
|
-
|
|
43
|
-
|
|
74
|
+
widget.grabFocus();
|
|
75
|
+
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const target = getDelegateText(widget);
|
|
44
80
|
|
|
45
|
-
|
|
81
|
+
if (target === null) {
|
|
82
|
+
focusPlainEditable(widget);
|
|
83
|
+
|
|
84
|
+
return;
|
|
46
85
|
}
|
|
47
86
|
|
|
48
|
-
|
|
87
|
+
target.grabFocusWithoutSelecting();
|
|
49
88
|
|
|
50
|
-
|
|
89
|
+
if (!hasSelectedText(widget)) {
|
|
90
|
+
widget.setPosition(TEXT_END_POSITION);
|
|
91
|
+
}
|
|
51
92
|
};
|
|
52
93
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
94
|
+
const insertTextViewText = (widget: Gtk.TextView, text: string, kind: InsertKind): void => {
|
|
95
|
+
const buffer = widget.getBuffer();
|
|
96
|
+
const length = getSelectionLength(buffer);
|
|
97
|
+
buffer.beginUserAction();
|
|
98
|
+
|
|
99
|
+
if (length > 0) {
|
|
100
|
+
widget.emit("delete-from-cursor", Gtk.DeleteType.CHARS, SELECTION_DELETE_COUNT);
|
|
101
|
+
|
|
102
|
+
if (kind === "typing" && length > SINGLE_CHARACTER_LENGTH) {
|
|
103
|
+
buffer.endUserAction();
|
|
104
|
+
buffer.beginUserAction();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
56
107
|
|
|
108
|
+
widget.emit("insert-at-cursor", text);
|
|
109
|
+
buffer.endUserAction();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const insertDelegateText = (target: Gtk.Text, text: string): void => {
|
|
113
|
+
if (hasSelectedText(target)) {
|
|
114
|
+
target.emit("delete-from-cursor", Gtk.DeleteType.CHARS, SELECTION_DELETE_COUNT);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
target.emit("insert-at-cursor", text);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const insertPlainText = (widget: Gtk.Editable, text: string): void => {
|
|
121
|
+
if (!isWidgetEditable(widget)) {
|
|
57
122
|
return;
|
|
58
123
|
}
|
|
59
124
|
|
|
60
|
-
widget
|
|
125
|
+
if (hasSelectedText(widget)) {
|
|
126
|
+
widget.deleteSelection();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const position = widget.getPosition();
|
|
130
|
+
const newPosition = widget.insertText(text, text.length, position);
|
|
131
|
+
widget.setPosition(newPosition);
|
|
61
132
|
};
|
|
62
133
|
|
|
63
|
-
const
|
|
134
|
+
const insertEditableText = (widget: EditableTarget, text: string, kind: InsertKind): void => {
|
|
64
135
|
if (widget instanceof Gtk.TextView) {
|
|
65
|
-
widget
|
|
136
|
+
insertTextViewText(widget, text, kind);
|
|
66
137
|
|
|
67
138
|
return;
|
|
68
139
|
}
|
|
69
140
|
|
|
70
|
-
widget
|
|
71
|
-
};
|
|
141
|
+
const target = getDelegateText(widget);
|
|
72
142
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
buffer.selectRange(buffer.getIterAtOffset(start), buffer.getIterAtOffset(end));
|
|
143
|
+
if (target === null) {
|
|
144
|
+
insertPlainText(widget, text);
|
|
76
145
|
|
|
77
|
-
|
|
78
|
-
deleteSelection(widget);
|
|
146
|
+
return;
|
|
79
147
|
}
|
|
148
|
+
|
|
149
|
+
insertDelegateText(target, text);
|
|
80
150
|
};
|
|
81
151
|
|
|
82
|
-
const
|
|
83
|
-
widget.
|
|
152
|
+
const deleteAllText = (widget: EditableTarget): void => {
|
|
153
|
+
if (widget instanceof Gtk.TextView) {
|
|
154
|
+
const buffer = widget.getBuffer();
|
|
155
|
+
buffer.deleteInteractive(buffer.getStartIter(), buffer.getEndIter(), widget.getEditable());
|
|
84
156
|
|
|
85
|
-
|
|
86
|
-
widget.deleteSelection();
|
|
157
|
+
return;
|
|
87
158
|
}
|
|
88
159
|
|
|
89
|
-
widget.
|
|
160
|
+
widget.deleteText(0, -1);
|
|
90
161
|
};
|
|
91
162
|
|
|
92
163
|
const applyInitialSelection = (widget: EditableTarget, options: TypeOptions): void => {
|
|
@@ -98,12 +169,13 @@ const applyInitialSelection = (widget: EditableTarget, options: TypeOptions): vo
|
|
|
98
169
|
const end = options.initialSelectionEnd ?? start;
|
|
99
170
|
|
|
100
171
|
if (widget instanceof Gtk.TextView) {
|
|
101
|
-
|
|
172
|
+
const buffer = widget.getBuffer();
|
|
173
|
+
buffer.selectRange(buffer.getIterAtOffset(start), buffer.getIterAtOffset(end));
|
|
102
174
|
|
|
103
175
|
return;
|
|
104
176
|
}
|
|
105
177
|
|
|
106
|
-
|
|
178
|
+
widget.selectRegion(start, end);
|
|
107
179
|
};
|
|
108
180
|
|
|
109
181
|
const writeClipboardText = (widget: Gtk.Widget, text: string): void => {
|
|
@@ -132,30 +204,104 @@ const runEditableEvent = (
|
|
|
132
204
|
});
|
|
133
205
|
|
|
134
206
|
/**
|
|
135
|
-
* Focuses the widget unless `shouldFocus` is false, applies any initial selection, and inserts the
|
|
136
|
-
* at the cursor
|
|
207
|
+
* Focuses the widget unless `shouldFocus` is false, applies any initial selection, and inserts the
|
|
208
|
+
* text at the cursor, deleting the text the widget has selected first, the way typing over a
|
|
209
|
+
* selection does in GTK4. Focusing works the way clicking into the widget does, leaving its text
|
|
210
|
+
* unselected: a `Gtk.Editable` takes its caret to the end of its text, a `Gtk.TextView` keeps the
|
|
211
|
+
* caret where it stands, and the replaced text stays on the widget's undo stack either way.
|
|
137
212
|
*
|
|
138
213
|
* @throws When the widget is neither a Gtk.Editable nor a Gtk.TextView.
|
|
139
214
|
*/
|
|
140
215
|
const type = (widget: Gtk.Widget, text: string, options?: TypeOptions): Promise<void> =>
|
|
141
216
|
runEditableEvent(widget, "Cannot type into element", (editable) => {
|
|
142
217
|
if (options?.shouldFocus ?? true) {
|
|
143
|
-
editable
|
|
218
|
+
focusEditable(editable);
|
|
144
219
|
}
|
|
145
220
|
|
|
146
221
|
applyInitialSelection(editable, options ?? {});
|
|
147
|
-
insertEditableText(editable, text);
|
|
222
|
+
insertEditableText(editable, text, "typing");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const hasNonEditableChar = (iter: Gtk.TextIter, isDefaultEditable: boolean): boolean => {
|
|
226
|
+
while (!iter.isEnd()) {
|
|
227
|
+
if (!iter.editable(isDefaultEditable)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!iter.forwardChar()) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const hasProtectedText = (widget: EditableTarget): boolean =>
|
|
240
|
+
widget instanceof Gtk.TextView && hasNonEditableChar(widget.getBuffer().getStartIter(), widget.getEditable());
|
|
241
|
+
|
|
242
|
+
const requireClearableText = (editable: EditableTarget): void => {
|
|
243
|
+
if (!isWidgetEditable(editable)) {
|
|
244
|
+
throw new Error("Cannot clear element: the widget is not editable");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (hasProtectedText(editable)) {
|
|
248
|
+
throw new Error(CLEAR_REFUSED);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const deletionSignalFor = (widget: EditableTarget): [SignalWatchTarget, string] =>
|
|
253
|
+
widget instanceof Gtk.TextView ? [widget.getBuffer(), "delete-range"] : [widget, "delete-text"];
|
|
254
|
+
|
|
255
|
+
const isDeletionApplied = (emitter: SignalWatchTarget, signal: string, apply: () => void): boolean => {
|
|
256
|
+
let isApplied = false;
|
|
257
|
+
|
|
258
|
+
const watch = (): void => {
|
|
259
|
+
isApplied = true;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
emitter.on(signal, watch, true);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
apply();
|
|
266
|
+
} finally {
|
|
267
|
+
emitter.off(signal, watch);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return isApplied;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const didDeleteAllText = (widget: EditableTarget): boolean => {
|
|
274
|
+
const [emitter, signal] = deletionSignalFor(widget);
|
|
275
|
+
|
|
276
|
+
return isDeletionApplied(emitter, signal, () => {
|
|
277
|
+
deleteAllText(widget);
|
|
148
278
|
});
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const clearEditable = (editable: EditableTarget): void => {
|
|
282
|
+
requireClearableText(editable);
|
|
283
|
+
focusEditable(editable);
|
|
284
|
+
|
|
285
|
+
if (readEditableText(editable) === "") {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!didDeleteAllText(editable)) {
|
|
290
|
+
throw new Error(CLEAR_REFUSED);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
149
293
|
|
|
150
294
|
/**
|
|
151
|
-
*
|
|
295
|
+
* Focuses an editable widget and deletes its whole text through the widget's own editing API, so the
|
|
296
|
+
* deletion emits the signals an edit made by hand emits. Text a tag protects is detected before the
|
|
297
|
+
* deletion runs, leaving such a widget untouched. A widget that writes a new value back in response
|
|
298
|
+
* to the deletion, as an input mask does, is cleared successfully and keeps whatever it wrote.
|
|
152
299
|
*
|
|
153
|
-
* @throws When the widget is neither a Gtk.Editable nor a Gtk.TextView
|
|
300
|
+
* @throws When the widget is neither a Gtk.Editable nor a Gtk.TextView, when it refuses edits, when
|
|
301
|
+
* a tag protects part of its text, or when a handler blocks the deletion.
|
|
154
302
|
*/
|
|
155
303
|
const clear = (widget: Gtk.Widget): Promise<void> =>
|
|
156
|
-
runEditableEvent(widget, "Cannot clear element",
|
|
157
|
-
setEditableText(editable, "");
|
|
158
|
-
});
|
|
304
|
+
runEditableEvent(widget, "Cannot clear element", clearEditable);
|
|
159
305
|
|
|
160
306
|
/**
|
|
161
307
|
* Writes an editable widget's selected text to its clipboard, or the empty string when nothing is selected.
|
|
@@ -180,7 +326,7 @@ const cut = (widget: Gtk.Widget): Promise<void> =>
|
|
|
180
326
|
|
|
181
327
|
/**
|
|
182
328
|
* Inserts the given text at an editable widget's cursor, reading the clipboard instead when no text
|
|
183
|
-
* is given.
|
|
329
|
+
* is given, and deleting the text the widget has selected first, the way a paste does in GTK4.
|
|
184
330
|
*
|
|
185
331
|
* @throws When the widget is neither a Gtk.Editable nor a Gtk.TextView.
|
|
186
332
|
*/
|
|
@@ -192,7 +338,7 @@ const paste = async (widget: Gtk.Widget, text?: string): Promise<void> => {
|
|
|
192
338
|
const content = text ?? (await widget.getClipboard().readTextAsync(null)) ?? "";
|
|
193
339
|
|
|
194
340
|
await wrapEvent(widget, () => {
|
|
195
|
-
insertEditableText(widget, content);
|
|
341
|
+
insertEditableText(widget, content, "pasting");
|
|
196
342
|
});
|
|
197
343
|
};
|
|
198
344
|
|
package/src/wait-for.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { WaitForOptions } from "./types.js";
|
|
|
3
3
|
import { runWithActEnvironment } from "./act.js";
|
|
4
4
|
import { getConfig } from "./config.js";
|
|
5
5
|
import { timeoutError } from "./errors.js";
|
|
6
|
+
import { advanceFakeClock, delay, now } from "./timers.js";
|
|
6
7
|
|
|
7
8
|
type PollResult<T> = { status: "resolved"; value: T } | { status: "timedout"; lastError: Error | null };
|
|
8
9
|
/** Widgets watched for removal; null counts as already removed. */
|
|
@@ -13,8 +14,6 @@ type ElementOrCallback = Gtk.Widget | Gtk.Widget[] | (() => RemovalTarget);
|
|
|
13
14
|
const DEFAULT_INTERVAL = 50;
|
|
14
15
|
const ELEMENT_NOT_REMOVED = new Error("Element not yet removed");
|
|
15
16
|
|
|
16
|
-
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
-
|
|
18
17
|
const copyStackTrace = (target: Error, source: Error): void => {
|
|
19
18
|
const { stack } = source;
|
|
20
19
|
|
|
@@ -38,10 +37,10 @@ const pollUntilSuccess = async <T>(
|
|
|
38
37
|
timeout: number,
|
|
39
38
|
interval: number,
|
|
40
39
|
): Promise<PollResult<T>> => {
|
|
41
|
-
const startTime =
|
|
40
|
+
const startTime = now();
|
|
42
41
|
let lastError: Error | null = null;
|
|
43
42
|
|
|
44
|
-
while (
|
|
43
|
+
while (now() - startTime < timeout) {
|
|
45
44
|
try {
|
|
46
45
|
const result = await callback();
|
|
47
46
|
await delay(0);
|
|
@@ -49,6 +48,7 @@ const pollUntilSuccess = async <T>(
|
|
|
49
48
|
return { status: "resolved", value: result };
|
|
50
49
|
} catch (error) {
|
|
51
50
|
lastError = error as Error;
|
|
51
|
+
await advanceFakeClock(interval);
|
|
52
52
|
await delay(interval);
|
|
53
53
|
}
|
|
54
54
|
}
|