@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.
- package/README.md +137 -34
- package/dist/act.d.ts +19 -0
- package/dist/act.d.ts.map +1 -0
- package/dist/act.js +56 -0
- package/dist/act.js.map +1 -0
- package/dist/bound-queries.d.ts +33 -0
- package/dist/bound-queries.d.ts.map +1 -0
- package/dist/bound-queries.js +2 -0
- package/dist/bound-queries.js.map +1 -0
- package/dist/build-queries.d.ts +16 -0
- package/dist/build-queries.d.ts.map +1 -0
- package/dist/build-queries.js +115 -0
- package/dist/build-queries.js.map +1 -0
- package/dist/cleanup-registry.d.ts +5 -0
- package/dist/cleanup-registry.d.ts.map +1 -0
- package/dist/cleanup-registry.js +21 -0
- package/dist/cleanup-registry.js.map +1 -0
- package/dist/config.d.ts +20 -52
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -46
- package/dist/config.js.map +1 -1
- package/dist/editable.d.ts +8 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +26 -0
- package/dist/editable.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +135 -0
- package/dist/errors.js.map +1 -0
- package/dist/fire-event.d.ts +8 -25
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +11 -30
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +4 -0
- package/dist/frame-sync.d.ts.map +1 -0
- package/dist/frame-sync.js +37 -0
- package/dist/frame-sync.js.map +1 -0
- package/dist/index.d.ts +17 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +59 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +117 -0
- package/dist/matchers.js.map +1 -0
- package/dist/pretty-widget.d.ts +24 -26
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +111 -117
- package/dist/pretty-widget.js.map +1 -1
- package/dist/queries.d.ts +190 -180
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +291 -320
- package/dist/queries.js.map +1 -1
- package/dist/register-test-hooks.d.ts +2 -0
- package/dist/register-test-hooks.d.ts.map +1 -0
- package/dist/register-test-hooks.js +16 -0
- package/dist/register-test-hooks.js.map +1 -0
- package/dist/render-hook.d.ts +15 -35
- package/dist/render-hook.d.ts.map +1 -1
- package/dist/render-hook.js +9 -45
- package/dist/render-hook.js.map +1 -1
- package/dist/render.d.ts +13 -42
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +138 -121
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +19 -53
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +48 -63
- package/dist/role-helpers.js.map +1 -1
- package/dist/screen.d.ts +7 -63
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +24 -110
- package/dist/screen.js.map +1 -1
- package/dist/screenshot.d.ts +21 -23
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +98 -50
- package/dist/screenshot.js.map +1 -1
- package/dist/suggestions.d.ts +27 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/suggestions.js +61 -0
- package/dist/suggestions.js.map +1 -0
- package/dist/traversal.d.ts +12 -10
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +36 -15
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +115 -169
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +10 -0
- package/dist/user-event/adjustment.d.ts.map +1 -0
- package/dist/user-event/adjustment.js +35 -0
- package/dist/user-event/adjustment.js.map +1 -0
- package/dist/user-event/click.d.ts +8 -0
- package/dist/user-event/click.d.ts.map +1 -0
- package/dist/user-event/click.js +73 -0
- package/dist/user-event/click.js.map +1 -0
- package/dist/user-event/controller.d.ts +8 -0
- package/dist/user-event/controller.d.ts.map +1 -0
- package/dist/user-event/controller.js +31 -0
- package/dist/user-event/controller.js.map +1 -0
- package/dist/user-event/dispatch.d.ts +7 -0
- package/dist/user-event/dispatch.d.ts.map +1 -0
- package/dist/user-event/dispatch.js +11 -0
- package/dist/user-event/dispatch.js.map +1 -0
- package/dist/user-event/event-wrapper.d.ts +4 -0
- package/dist/user-event/event-wrapper.d.ts.map +1 -0
- package/dist/user-event/event-wrapper.js +59 -0
- package/dist/user-event/event-wrapper.js.map +1 -0
- package/dist/user-event/gesture.d.ts +35 -0
- package/dist/user-event/gesture.d.ts.map +1 -0
- package/dist/user-event/gesture.js +138 -0
- package/dist/user-event/gesture.js.map +1 -0
- package/dist/user-event/index.d.ts +51 -0
- package/dist/user-event/index.d.ts.map +1 -0
- package/dist/user-event/index.js +42 -0
- package/dist/user-event/index.js.map +1 -0
- package/dist/user-event/keyboard.d.ts +10 -0
- package/dist/user-event/keyboard.d.ts.map +1 -0
- package/dist/user-event/keyboard.js +204 -0
- package/dist/user-event/keyboard.js.map +1 -0
- package/dist/user-event/pointer.d.ts +10 -0
- package/dist/user-event/pointer.d.ts.map +1 -0
- package/dist/user-event/pointer.js +30 -0
- package/dist/user-event/pointer.js.map +1 -0
- package/dist/user-event/selection.d.ts +5 -0
- package/dist/user-event/selection.d.ts.map +1 -0
- package/dist/user-event/selection.js +105 -0
- package/dist/user-event/selection.js.map +1 -0
- package/dist/user-event/state.d.ts +7 -0
- package/dist/user-event/state.d.ts.map +1 -0
- package/dist/user-event/state.js +3 -0
- package/dist/user-event/state.js.map +1 -0
- package/dist/user-event/text.d.ts +18 -0
- package/dist/user-event/text.d.ts.map +1 -0
- package/dist/user-event/text.js +113 -0
- package/dist/user-event/text.js.map +1 -0
- package/dist/wait-for.d.ts +16 -36
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js +74 -70
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +34 -0
- package/dist/widget-accessible-properties.d.ts.map +1 -0
- package/dist/widget-accessible-properties.js +260 -0
- package/dist/widget-accessible-properties.js.map +1 -0
- package/dist/within.d.ts +10 -26
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +16 -25
- package/dist/within.js.map +1 -1
- package/package.json +23 -12
- package/src/act.ts +77 -0
- package/src/bound-queries.ts +37 -0
- package/src/build-queries.ts +226 -0
- package/src/cleanup-registry.ts +27 -0
- package/src/config.ts +35 -62
- package/src/editable.ts +37 -0
- package/src/errors.ts +191 -0
- package/src/fire-event.ts +13 -38
- package/src/frame-sync.ts +51 -0
- package/src/index.ts +58 -15
- package/src/matchers.ts +251 -0
- package/src/pretty-widget.ts +163 -130
- package/src/queries.ts +485 -396
- package/src/register-test-hooks.ts +19 -0
- package/src/render-hook.tsx +38 -45
- package/src/render.tsx +192 -155
- package/src/role-helpers.ts +65 -72
- package/src/screen.ts +26 -114
- package/src/screenshot.ts +128 -56
- package/src/suggestions.ts +106 -0
- package/src/traversal.ts +56 -22
- package/src/types.ts +144 -172
- package/src/user-event/adjustment.ts +62 -0
- package/src/user-event/click.ts +99 -0
- package/src/user-event/controller.ts +58 -0
- package/src/user-event/dispatch.ts +25 -0
- package/src/user-event/event-wrapper.ts +80 -0
- package/src/user-event/gesture.ts +239 -0
- package/src/user-event/index.ts +81 -0
- package/src/user-event/keyboard.ts +290 -0
- package/src/user-event/pointer.ts +46 -0
- package/src/user-event/selection.ts +159 -0
- package/src/user-event/state.ts +8 -0
- package/src/user-event/text.ts +172 -0
- package/src/wait-for.ts +117 -78
- package/src/widget-accessible-properties.ts +388 -0
- package/src/within.ts +29 -27
- package/dist/bind-queries.d.ts +0 -14
- package/dist/bind-queries.d.ts.map +0 -1
- package/dist/bind-queries.js +0 -29
- package/dist/bind-queries.js.map +0 -1
- package/dist/error-builder.d.ts +0 -28
- package/dist/error-builder.d.ts.map +0 -1
- package/dist/error-builder.js +0 -81
- package/dist/error-builder.js.map +0 -1
- package/dist/timing.d.ts +0 -18
- package/dist/timing.d.ts.map +0 -1
- package/dist/timing.js +0 -18
- package/dist/timing.js.map +0 -1
- package/dist/user-event.d.ts +0 -138
- package/dist/user-event.d.ts.map +0 -1
- package/dist/user-event.js +0 -411
- package/dist/user-event.js.map +0 -1
- package/dist/widget-text.d.ts +0 -69
- package/dist/widget-text.d.ts.map +0 -1
- package/dist/widget-text.js +0 -177
- package/dist/widget-text.js.map +0 -1
- package/dist/widget.d.ts +0 -3
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -13
- package/dist/widget.js.map +0 -1
- package/src/bind-queries.ts +0 -52
- package/src/error-builder.ts +0 -102
- package/src/timing.ts +0 -17
- package/src/user-event.ts +0 -492
- package/src/widget-text.ts +0 -202
- package/src/widget.ts +0 -15
package/src/editable.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
|
|
3
|
+
type EditableTarget = Gtk.Editable | Gtk.TextView;
|
|
4
|
+
|
|
5
|
+
const EDITABLE_ROLES: Set<Gtk.AccessibleRole> = new Set<Gtk.AccessibleRole>([
|
|
6
|
+
Gtk.AccessibleRole.TEXT_BOX,
|
|
7
|
+
Gtk.AccessibleRole.SEARCH_BOX,
|
|
8
|
+
Gtk.AccessibleRole.SPIN_BUTTON,
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const isEditable = (widget: unknown): widget is EditableTarget =>
|
|
12
|
+
widget instanceof Gtk.Editable || widget instanceof Gtk.TextView;
|
|
13
|
+
|
|
14
|
+
const getEditableDelegate = (widget: Gtk.Widget): Gtk.Widget | null => {
|
|
15
|
+
if (!(widget instanceof Gtk.Editable)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return widget.getDelegate();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const readTextViewBufferText = (widget: Gtk.TextView): string => {
|
|
23
|
+
const buffer = widget.getBuffer();
|
|
24
|
+
const [start, end] = buffer.getBounds();
|
|
25
|
+
|
|
26
|
+
return buffer.getText(start, end, true);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const readEditableText = (widget: EditableTarget): string => {
|
|
30
|
+
if (widget instanceof Gtk.TextView) {
|
|
31
|
+
return readTextViewBufferText(widget);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return widget.getText();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { EDITABLE_ROLES, isEditable, getEditableDelegate, readEditableText, type EditableTarget };
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import type { Container } from "./traversal.js";
|
|
3
|
+
import type { ByRoleOptions, ByRoleValue, Matcher } from "./types.js";
|
|
4
|
+
import { getConfig } from "./config.js";
|
|
5
|
+
import { prettyWidget } from "./pretty-widget.js";
|
|
6
|
+
import { formatRole, prettyRoles } from "./role-helpers.js";
|
|
7
|
+
|
|
8
|
+
type QueryDescriptor =
|
|
9
|
+
| { queryType: "role"; role: Gtk.AccessibleRole; options?: ByRoleOptions | undefined } |
|
|
10
|
+
{ queryType: "text"; text: Matcher } |
|
|
11
|
+
{ queryType: "labelText"; text: Matcher } |
|
|
12
|
+
{ queryType: "name"; name: Matcher } |
|
|
13
|
+
{ queryType: "placeholderText"; text: Matcher } |
|
|
14
|
+
{ queryType: "displayValue"; value: Matcher };
|
|
15
|
+
|
|
16
|
+
const roleOptionFormatters: ((options: ByRoleOptions) => string | null)[] = [
|
|
17
|
+
(o) => (o.name ? `name ${formatTextMatcher(o.name)}` : null),
|
|
18
|
+
(o) => (o.checked === undefined ? null : `checked=${String(o.checked)}`),
|
|
19
|
+
(o) => (o.pressed === undefined ? null : `pressed=${String(o.pressed)}`),
|
|
20
|
+
(o) => (o.selected === undefined ? null : `selected=${String(o.selected)}`),
|
|
21
|
+
(o) => (o.expanded === undefined ? null : `expanded=${String(o.expanded)}`),
|
|
22
|
+
(o) => (o.level === undefined ? null : `level=${String(o.level)}`),
|
|
23
|
+
(o) => (o.busy === undefined ? null : `busy=${String(o.busy)}`),
|
|
24
|
+
(o) => (o.description ? `description ${formatTextMatcher(o.description)}` : null),
|
|
25
|
+
(o) => (o.value ? `value ${formatByRoleValue(o.value)}` : null),
|
|
26
|
+
(o) => (o.hidden === undefined ? null : `hidden=${String(o.hidden)}`),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const expensiveErrorDiagnostics = { isDisabled: false };
|
|
30
|
+
|
|
31
|
+
const formatTextMatcher = (text: Matcher): string => {
|
|
32
|
+
if (typeof text === "function") {
|
|
33
|
+
return "custom function";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (text instanceof RegExp) {
|
|
37
|
+
return text.toString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return `'${String(text)}'`;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const formatByRoleValue = (value: ByRoleValue): string => {
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
|
|
46
|
+
if (value.now !== undefined) {
|
|
47
|
+
parts.push(`now=${String(value.now)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (value.min !== undefined) {
|
|
51
|
+
parts.push(`min=${String(value.min)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (value.max !== undefined) {
|
|
55
|
+
parts.push(`max=${String(value.max)}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (value.text !== undefined) {
|
|
59
|
+
parts.push(`text ${formatTextMatcher(value.text)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parts.join(", ");
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const roleOptionParts = (options: ByRoleOptions): string[] => {
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
|
|
68
|
+
for (const formatter of roleOptionFormatters) {
|
|
69
|
+
const part = formatter(options);
|
|
70
|
+
|
|
71
|
+
if (part !== null) {
|
|
72
|
+
parts.push(part);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return parts;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const formatByRoleDescription = (role: Gtk.AccessibleRole, options?: ByRoleOptions): string => {
|
|
80
|
+
const parts = [`role '${formatRole(role).toUpperCase()}'`];
|
|
81
|
+
|
|
82
|
+
if (options) {
|
|
83
|
+
parts.push(...roleOptionParts(options));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return parts.join(" and ");
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const formatQueryDescription = (descriptor: QueryDescriptor): string => {
|
|
90
|
+
switch (descriptor.queryType) {
|
|
91
|
+
case "role": {
|
|
92
|
+
return formatByRoleDescription(descriptor.role, descriptor.options);
|
|
93
|
+
}
|
|
94
|
+
case "text": {
|
|
95
|
+
return `text ${formatTextMatcher(descriptor.text)}`;
|
|
96
|
+
}
|
|
97
|
+
case "labelText": {
|
|
98
|
+
return `label text ${formatTextMatcher(descriptor.text)}`;
|
|
99
|
+
}
|
|
100
|
+
case "name": {
|
|
101
|
+
return `name ${formatTextMatcher(descriptor.name)}`;
|
|
102
|
+
}
|
|
103
|
+
case "placeholderText": {
|
|
104
|
+
return `placeholder text ${formatTextMatcher(descriptor.text)}`;
|
|
105
|
+
}
|
|
106
|
+
case "displayValue": {
|
|
107
|
+
return `display value ${formatTextMatcher(descriptor.value)}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const runWithExpensiveErrorDiagnosticsDisabled = <T>(callback: () => T): T => {
|
|
113
|
+
const isPrevious = expensiveErrorDiagnostics.isDisabled;
|
|
114
|
+
expensiveErrorDiagnostics.isDisabled = true;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
return callback();
|
|
118
|
+
} finally {
|
|
119
|
+
expensiveErrorDiagnostics.isDisabled = isPrevious;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const buildElementError = (container: Container, headLines: string[]): Error => {
|
|
124
|
+
const config = getConfig();
|
|
125
|
+
|
|
126
|
+
const lines = expensiveErrorDiagnostics.isDisabled
|
|
127
|
+
? headLines
|
|
128
|
+
: [...headLines, "", prettyWidget(container, { highlight: false })];
|
|
129
|
+
|
|
130
|
+
return config.getElementError(lines.join("\n"), container);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const notFoundError = (container: Container, descriptor: QueryDescriptor): Error => {
|
|
134
|
+
const description = formatQueryDescription(descriptor);
|
|
135
|
+
const headLines = [`Unable to find an element with ${description}`];
|
|
136
|
+
|
|
137
|
+
if (!expensiveErrorDiagnostics.isDisabled && descriptor.queryType === "role") {
|
|
138
|
+
headLines.push("", "Here are the accessible roles:", "", prettyRoles(container));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return buildElementError(container, headLines);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const allByVariantHint = (descriptor: QueryDescriptor): string => {
|
|
145
|
+
const variant = descriptor.queryType === "role" ? "getAllByRole" : "getAllBy*";
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
"(If this is intentional, use the *AllBy* variant of the query, " +
|
|
149
|
+
`e.g. queryAllBy*/getAllBy*/findAllBy*, such as ${variant}.)`
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const multipleFoundError = (container: Container, descriptor: QueryDescriptor, matches: Gtk.Widget[]): Error => {
|
|
154
|
+
const description = formatQueryDescription(descriptor);
|
|
155
|
+
|
|
156
|
+
const headLines = [
|
|
157
|
+
`Found ${String(matches.length)} elements with ${description}, but expected only one`,
|
|
158
|
+
"",
|
|
159
|
+
allByVariantHint(descriptor),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
if (!expensiveErrorDiagnostics.isDisabled) {
|
|
163
|
+
const renderedMatches = matches.map((widget) => prettyWidget(widget, { highlight: false }));
|
|
164
|
+
headLines.push("", "Here are the matching elements:", "", ...renderedMatches);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return buildElementError(container, headLines);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const suggestionError = (suggestion: string, container: Container): Error => {
|
|
171
|
+
const config = getConfig();
|
|
172
|
+
const message = `A better query is available, try this:\n${suggestion}\n`;
|
|
173
|
+
|
|
174
|
+
return config.getElementError(message, container);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const timeoutError = (timeout: number, lastError: Error | null): Error => {
|
|
178
|
+
const baseMessage = `Timed out after ${String(timeout)}ms`;
|
|
179
|
+
const message = lastError ? `${baseMessage}.\n\n${lastError.message}` : baseMessage;
|
|
180
|
+
|
|
181
|
+
return getConfig().getElementError(message);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export {
|
|
185
|
+
runWithExpensiveErrorDiagnosticsDisabled,
|
|
186
|
+
notFoundError,
|
|
187
|
+
multipleFoundError,
|
|
188
|
+
suggestionError,
|
|
189
|
+
timeoutError,
|
|
190
|
+
type QueryDescriptor,
|
|
191
|
+
};
|
package/src/fire-event.ts
CHANGED
|
@@ -1,43 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { tick } from "./timing.js";
|
|
1
|
+
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
|
+
import { runInAct } from "./act.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* Emits a
|
|
5
|
+
* Emits a GObject signal on a widget or object inside React's act environment,
|
|
6
|
+
* so any resulting state updates are flushed before the promise resolves.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @param element - The widget or event controller to emit the signal on
|
|
12
|
-
* @param signalName - GTK signal name (e.g., "clicked", "activate", "drag-begin")
|
|
13
|
-
* @param args - Additional signal arguments as GValues
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```tsx
|
|
17
|
-
* import { fireEvent } from "@gtkx/testing";
|
|
18
|
-
* import { Value } from "@gtkx/ffi/gobject";
|
|
19
|
-
*
|
|
20
|
-
* // Emit signal on widget
|
|
21
|
-
* await fireEvent(widget, "clicked");
|
|
22
|
-
*
|
|
23
|
-
* // Emit signal on gesture controller
|
|
24
|
-
* const gesture = widget.observeControllers().getObject(0) as Gtk.GestureDrag;
|
|
25
|
-
* await fireEvent(gesture, "drag-begin", Value.newFromDouble(100), Value.newFromDouble(100));
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* @see {@link userEvent} for high-level user interactions
|
|
8
|
+
* @param element The GObject instance to emit the signal on.
|
|
9
|
+
* @param signalName Name of the signal to emit.
|
|
10
|
+
* @param args Arguments passed to the signal handlers.
|
|
29
11
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
): Promise<void> => {
|
|
35
|
-
const gtype = typeFromName((element.constructor as typeof GObject).glibTypeName);
|
|
36
|
-
const signalId = signalLookup(signalName, gtype);
|
|
37
|
-
|
|
38
|
-
const instanceValue = Value.newFromObject(element as GObject);
|
|
39
|
-
|
|
40
|
-
signalEmitv([instanceValue, ...args], signalId, 0);
|
|
41
|
-
|
|
42
|
-
await tick();
|
|
12
|
+
const fireEvent = async (element: GObject.Object, signalName: string, ...args: unknown[]): Promise<void> => {
|
|
13
|
+
await runInAct(() => {
|
|
14
|
+
element.emit(signalName, ...args);
|
|
15
|
+
});
|
|
43
16
|
};
|
|
17
|
+
|
|
18
|
+
export { fireEvent };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import * as GLib from "@gtkx/gi/glib";
|
|
3
|
+
|
|
4
|
+
const CLOCK_STALL_FALLBACK_MS = 500;
|
|
5
|
+
|
|
6
|
+
const once = (callback: () => void): (() => void) => {
|
|
7
|
+
let isCalled = false;
|
|
8
|
+
|
|
9
|
+
return () => {
|
|
10
|
+
if (isCalled) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
isCalled = true;
|
|
15
|
+
callback();
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const runWhenSized = (widget: Gtk.Widget, finish: () => void): void => {
|
|
20
|
+
let tickId = 0;
|
|
21
|
+
|
|
22
|
+
const fallback = setTimeout(() => {
|
|
23
|
+
widget.removeTickCallback(tickId);
|
|
24
|
+
finish();
|
|
25
|
+
}, CLOCK_STALL_FALLBACK_MS);
|
|
26
|
+
|
|
27
|
+
tickId = widget.addTickCallback(() => {
|
|
28
|
+
if (widget.getWidth() === 0) {
|
|
29
|
+
return GLib.SOURCE_CONTINUE;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clearTimeout(fallback);
|
|
33
|
+
finish();
|
|
34
|
+
|
|
35
|
+
return GLib.SOURCE_REMOVE;
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const scheduleAfterLayout = (widget: Gtk.Widget | null, callback: () => void): void => {
|
|
40
|
+
const finish = once(callback);
|
|
41
|
+
|
|
42
|
+
if (widget?.getFrameClock() == null || widget.getWidth() > 0) {
|
|
43
|
+
queueMicrotask(finish);
|
|
44
|
+
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
runWhenSized(widget, finish);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export { scheduleAfterLayout };
|
package/src/index.ts
CHANGED
|
@@ -1,51 +1,94 @@
|
|
|
1
|
-
|
|
1
|
+
import "./register-test-hooks.js";
|
|
2
|
+
|
|
3
|
+
export { act } from "./act.js";
|
|
4
|
+
export type { BoundQueries, RenderResult, Screen } from "./bound-queries.js";
|
|
5
|
+
export type { Config, ConfigFn } from "./config.js";
|
|
2
6
|
export { configure, getConfig } from "./config.js";
|
|
3
7
|
export { fireEvent } from "./fire-event.js";
|
|
8
|
+
export type { TextExpectation } from "./matchers.js";
|
|
9
|
+
export { matchers, registerMatchers } from "./matchers.js";
|
|
4
10
|
export type { PrettyWidgetOptions } from "./pretty-widget.js";
|
|
5
|
-
export { prettyWidget } from "./pretty-widget.js";
|
|
11
|
+
export { logWidget, prettyWidget } from "./pretty-widget.js";
|
|
6
12
|
export {
|
|
13
|
+
findAllByDisplayValue,
|
|
7
14
|
findAllByLabelText,
|
|
8
15
|
findAllByName,
|
|
16
|
+
findAllByPlaceholderText,
|
|
9
17
|
findAllByRole,
|
|
10
18
|
findAllByText,
|
|
19
|
+
findByDisplayValue,
|
|
11
20
|
findByLabelText,
|
|
12
21
|
findByName,
|
|
22
|
+
findByPlaceholderText,
|
|
13
23
|
findByRole,
|
|
14
24
|
findByText,
|
|
25
|
+
getAllByDisplayValue,
|
|
26
|
+
getAllByLabelText,
|
|
27
|
+
getAllByName,
|
|
28
|
+
getAllByPlaceholderText,
|
|
29
|
+
getAllByRole,
|
|
30
|
+
getAllByText,
|
|
31
|
+
getByDisplayValue,
|
|
32
|
+
getByLabelText,
|
|
33
|
+
getByName,
|
|
34
|
+
getByPlaceholderText,
|
|
35
|
+
getByRole,
|
|
36
|
+
getByText,
|
|
37
|
+
getDefaultNormalizer,
|
|
38
|
+
queryAllByDisplayValue,
|
|
15
39
|
queryAllByLabelText,
|
|
16
40
|
queryAllByName,
|
|
41
|
+
queryAllByPlaceholderText,
|
|
17
42
|
queryAllByRole,
|
|
18
43
|
queryAllByText,
|
|
44
|
+
queryByDisplayValue,
|
|
19
45
|
queryByLabelText,
|
|
20
46
|
queryByName,
|
|
47
|
+
queryByPlaceholderText,
|
|
21
48
|
queryByRole,
|
|
22
49
|
queryByText,
|
|
23
50
|
} from "./queries.js";
|
|
24
|
-
export { cleanup, render } from "./render.js";
|
|
25
51
|
export { renderHook } from "./render-hook.js";
|
|
26
|
-
export
|
|
27
|
-
export { getRoles, logRoles, prettyRoles } from "./role-helpers.js";
|
|
52
|
+
export { cleanup, render } from "./render.js";
|
|
53
|
+
export { formatRole, getRoles, logRoles, prettyRoles } from "./role-helpers.js";
|
|
28
54
|
export { screen } from "./screen.js";
|
|
29
|
-
export
|
|
30
|
-
export {
|
|
31
|
-
export {
|
|
55
|
+
export { captureAndSaveScreenshot, logScreenshotPath, screenshot } from "./screenshot.js";
|
|
56
|
+
export type { Method, Suggestion, Variant } from "./suggestions.js";
|
|
57
|
+
export { getSuggestedQuery } from "./suggestions.js";
|
|
32
58
|
export type { Container } from "./traversal.js";
|
|
33
59
|
export type {
|
|
34
|
-
BoundQueries,
|
|
35
60
|
ByRoleOptions,
|
|
61
|
+
ByRoleValue,
|
|
62
|
+
Matcher,
|
|
63
|
+
MatcherFunction,
|
|
64
|
+
MatcherOptions,
|
|
65
|
+
NormalizerFn,
|
|
36
66
|
NormalizerOptions,
|
|
37
67
|
RenderHookOptions,
|
|
38
68
|
RenderHookResult,
|
|
39
69
|
RenderOptions,
|
|
40
|
-
|
|
70
|
+
ScreenshotOptions,
|
|
41
71
|
ScreenshotResult,
|
|
42
|
-
TextMatch,
|
|
43
|
-
TextMatchFunction,
|
|
44
|
-
TextMatchOptions,
|
|
45
72
|
WaitForOptions,
|
|
73
|
+
WindowSelector,
|
|
46
74
|
WrapperComponent,
|
|
47
75
|
} from "./types.js";
|
|
48
|
-
export type {
|
|
49
|
-
|
|
76
|
+
export type {
|
|
77
|
+
DragOffset,
|
|
78
|
+
DragOptions,
|
|
79
|
+
DropContent,
|
|
80
|
+
DropOptions,
|
|
81
|
+
PointerInput,
|
|
82
|
+
ScrollDelta,
|
|
83
|
+
TabOptions,
|
|
84
|
+
TypeOptions,
|
|
85
|
+
UserEvent,
|
|
86
|
+
} from "./user-event/index.js";
|
|
87
|
+
export { userEvent } from "./user-event/index.js";
|
|
50
88
|
export { waitFor, waitForElementToBeRemoved } from "./wait-for.js";
|
|
89
|
+
export {
|
|
90
|
+
getWidgetErrorMessage,
|
|
91
|
+
getWidgetInvalidState,
|
|
92
|
+
getWidgetNodeText,
|
|
93
|
+
} from "./widget-accessible-properties.js";
|
|
51
94
|
export { within } from "./within.js";
|
package/src/matchers.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/// <reference types="@vitest/expect" />
|
|
2
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
+
import {
|
|
4
|
+
getWidgetAccessibleName,
|
|
5
|
+
getWidgetCheckedState,
|
|
6
|
+
getWidgetDisplayValue,
|
|
7
|
+
getWidgetExpandedState,
|
|
8
|
+
getWidgetNodeText,
|
|
9
|
+
getWidgetPlaceholderText,
|
|
10
|
+
getWidgetPressedState,
|
|
11
|
+
getWidgetSelectedState,
|
|
12
|
+
getWidgetValue,
|
|
13
|
+
} from "./widget-accessible-properties.js";
|
|
14
|
+
|
|
15
|
+
/** The expected value for a text matcher: an exact string or a regular expression. */
|
|
16
|
+
type TextExpectation = string | RegExp;
|
|
17
|
+
type MatcherResult = { pass: boolean; message: () => string };
|
|
18
|
+
type TextMatcher = (received: unknown, expected?: TextExpectation) => MatcherResult;
|
|
19
|
+
type StateMatcher = (received: unknown) => MatcherResult;
|
|
20
|
+
type ValueMatcher = (received: unknown, expected: number) => MatcherResult;
|
|
21
|
+
type TextMatcherContext = { matcherName: string; widget: Gtk.Widget; actual: string | null };
|
|
22
|
+
|
|
23
|
+
type MatcherImplementations = {
|
|
24
|
+
toHaveDisplayValue: TextMatcher;
|
|
25
|
+
toHaveTextContent: TextMatcher;
|
|
26
|
+
toHaveAccessibleName: TextMatcher;
|
|
27
|
+
toHavePlaceholderText: TextMatcher;
|
|
28
|
+
toBeChecked: StateMatcher;
|
|
29
|
+
toBePressed: StateMatcher;
|
|
30
|
+
toBeExpanded: StateMatcher;
|
|
31
|
+
toBeSelected: StateMatcher;
|
|
32
|
+
toHaveValue: ValueMatcher;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ExpectExtend = { extend: (m: MatcherImplementations) => void };
|
|
36
|
+
|
|
37
|
+
const toHaveDisplayValue: TextMatcher = textMatcher("toHaveDisplayValue", getWidgetDisplayValue, "exact");
|
|
38
|
+
|
|
39
|
+
const toHaveTextContent: TextMatcher = textMatcher(
|
|
40
|
+
"toHaveTextContent",
|
|
41
|
+
(widget) => getWidgetNodeText(widget) ?? getWidgetAccessibleName(widget),
|
|
42
|
+
"substring",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const toHaveAccessibleName: TextMatcher = textMatcher("toHaveAccessibleName", getWidgetAccessibleName, "exact");
|
|
46
|
+
|
|
47
|
+
const toHavePlaceholderText: TextMatcher = textMatcher(
|
|
48
|
+
"toHavePlaceholderText",
|
|
49
|
+
getWidgetPlaceholderText,
|
|
50
|
+
"exact",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const toBeChecked: StateMatcher = booleanStateMatcher("toBeChecked", "checked", getWidgetCheckedState);
|
|
54
|
+
const toBePressed: StateMatcher = booleanStateMatcher("toBePressed", "pressed", getWidgetPressedState);
|
|
55
|
+
const toBeExpanded: StateMatcher = booleanStateMatcher("toBeExpanded", "expanded", getWidgetExpandedState);
|
|
56
|
+
const toBeSelected: StateMatcher = booleanStateMatcher("toBeSelected", "selected", getWidgetSelectedState);
|
|
57
|
+
|
|
58
|
+
/** The widget assertion matchers keyed by name, suitable for passing to `expect.extend`. */
|
|
59
|
+
const matchers: MatcherImplementations = {
|
|
60
|
+
toHaveDisplayValue,
|
|
61
|
+
toHaveTextContent,
|
|
62
|
+
toHaveAccessibleName,
|
|
63
|
+
toHavePlaceholderText,
|
|
64
|
+
toBeChecked,
|
|
65
|
+
toBePressed,
|
|
66
|
+
toBeExpanded,
|
|
67
|
+
toBeSelected,
|
|
68
|
+
toHaveValue,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const registration = { isRegistered: false };
|
|
72
|
+
|
|
73
|
+
const asWidget = (received: unknown, matcherName: string): Gtk.Widget => {
|
|
74
|
+
if (!(received instanceof Gtk.Widget)) {
|
|
75
|
+
throw new TypeError(`${matcherName}: received value must be a Gtk.Widget, got ${typeof received}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return received;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const describeWidget = (widget: Gtk.Widget): string => {
|
|
82
|
+
const role = Gtk.AccessibleRole[widget.getAccessibleRole()];
|
|
83
|
+
const name = getWidgetAccessibleName(widget);
|
|
84
|
+
|
|
85
|
+
return name === null ? `<${role}>` : `<${role} name=${JSON.stringify(name)}>`;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const isTextMatch = (actual: string, expected: TextExpectation, mode: "exact" | "substring"): boolean => {
|
|
89
|
+
if (expected instanceof RegExp) {
|
|
90
|
+
expected.lastIndex = 0;
|
|
91
|
+
|
|
92
|
+
return expected.test(actual);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return mode === "exact" ? actual === expected : actual.includes(expected);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const describeExpected = (expected: TextExpectation): string =>
|
|
99
|
+
expected instanceof RegExp ? String(expected) : JSON.stringify(expected);
|
|
100
|
+
|
|
101
|
+
const negationPrefix = (isPass: boolean): string => (isPass ? "not " : "");
|
|
102
|
+
|
|
103
|
+
const nonEmptyResult = ({ matcherName, widget, actual }: TextMatcherContext): MatcherResult => {
|
|
104
|
+
const isPass = actual !== null && actual !== "";
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
pass: isPass,
|
|
108
|
+
message: () =>
|
|
109
|
+
`expected widget ${negationPrefix(isPass)}to have a non-empty value for ${matcherName}, ` +
|
|
110
|
+
`but got ${JSON.stringify(actual)}\n${describeWidget(widget)}`,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const matchedResult = (context: TextMatcherContext, expected: TextExpectation, isPass: boolean): MatcherResult => ({
|
|
115
|
+
pass: isPass,
|
|
116
|
+
message: () =>
|
|
117
|
+
`expected widget ${negationPrefix(isPass)}${context.matcherName} ${describeExpected(expected)}, ` +
|
|
118
|
+
`but received ${JSON.stringify(context.actual)}\n${describeWidget(context.widget)}`,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function textMatcher(
|
|
122
|
+
matcherName: string,
|
|
123
|
+
read: (widget: Gtk.Widget) => string | null,
|
|
124
|
+
mode: "exact" | "substring",
|
|
125
|
+
): TextMatcher {
|
|
126
|
+
return (received: unknown, expected?: TextExpectation): MatcherResult => {
|
|
127
|
+
const widget = asWidget(received, matcherName);
|
|
128
|
+
const actual = read(widget);
|
|
129
|
+
const context: TextMatcherContext = { matcherName, widget, actual };
|
|
130
|
+
|
|
131
|
+
if (expected === undefined) {
|
|
132
|
+
return nonEmptyResult(context);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return matchedResult(context, expected, actual !== null && isTextMatch(actual, expected, mode));
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function booleanStateMatcher(
|
|
140
|
+
matcherName: string,
|
|
141
|
+
stateName: string,
|
|
142
|
+
read: (widget: Gtk.Widget) => boolean | null,
|
|
143
|
+
): StateMatcher {
|
|
144
|
+
return (received: unknown): MatcherResult => {
|
|
145
|
+
const widget = asWidget(received, matcherName);
|
|
146
|
+
const state = read(widget);
|
|
147
|
+
|
|
148
|
+
if (state === null) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`${matcherName}: widget does not expose a ${stateName} state ` +
|
|
151
|
+
`(role ${Gtk.AccessibleRole[widget.getAccessibleRole()]})\n${describeWidget(widget)}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
pass: state,
|
|
157
|
+
message: () =>
|
|
158
|
+
`expected widget ${negationPrefix(state)}to be ${stateName}\n${describeWidget(widget)}`,
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toHaveValue(received: unknown, expected: number): MatcherResult {
|
|
164
|
+
const widget = asWidget(received, "toHaveValue");
|
|
165
|
+
const actual = getWidgetValue(widget).now;
|
|
166
|
+
|
|
167
|
+
if (actual === null) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
"toHaveValue: widget does not expose a numeric value " +
|
|
170
|
+
`(role ${Gtk.AccessibleRole[widget.getAccessibleRole()]})\n${describeWidget(widget)}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const isPass = actual === expected;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
pass: isPass,
|
|
178
|
+
message: () =>
|
|
179
|
+
`expected widget ${negationPrefix(isPass)}to have value ${String(expected)}, ` +
|
|
180
|
+
`but received ${String(actual)}\n${describeWidget(widget)}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const globalExpect = (): ExpectExtend | null => {
|
|
185
|
+
const candidate: unknown = Reflect.get(globalThis, "expect");
|
|
186
|
+
|
|
187
|
+
if (candidate && typeof (candidate as ExpectExtend).extend === "function") {
|
|
188
|
+
return candidate as ExpectExtend;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** Registers the widget matchers on the global `expect`, when one is available. Safe to call more than once. */
|
|
195
|
+
const registerMatchers = (): void => {
|
|
196
|
+
if (registration.isRegistered) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const expect = globalExpect();
|
|
201
|
+
|
|
202
|
+
if (!expect) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
expect.extend(matchers);
|
|
207
|
+
registration.isRegistered = true;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
declare module "@vitest/expect" {
|
|
211
|
+
/* eslint-disable @typescript-eslint/consistent-type-definitions -- declaration merging requires interfaces */
|
|
212
|
+
interface Assertion {
|
|
213
|
+
toHaveDisplayValue(expected?: TextExpectation): void;
|
|
214
|
+
toHaveTextContent(expected?: TextExpectation): void;
|
|
215
|
+
toHaveAccessibleName(expected?: TextExpectation): void;
|
|
216
|
+
toHavePlaceholderText(expected?: TextExpectation): void;
|
|
217
|
+
toBeChecked(): void;
|
|
218
|
+
toBePressed(): void;
|
|
219
|
+
toBeExpanded(): void;
|
|
220
|
+
toBeSelected(): void;
|
|
221
|
+
toHaveValue(expected: number): void;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
interface AsymmetricMatchersContaining {
|
|
225
|
+
toHaveDisplayValue(expected?: TextExpectation): void;
|
|
226
|
+
toHaveTextContent(expected?: TextExpectation): void;
|
|
227
|
+
toHaveAccessibleName(expected?: TextExpectation): void;
|
|
228
|
+
toHavePlaceholderText(expected?: TextExpectation): void;
|
|
229
|
+
toBeChecked(): void;
|
|
230
|
+
toBePressed(): void;
|
|
231
|
+
toBeExpanded(): void;
|
|
232
|
+
toBeSelected(): void;
|
|
233
|
+
toHaveValue(expected: number): void;
|
|
234
|
+
}
|
|
235
|
+
/* eslint-enable @typescript-eslint/consistent-type-definitions */
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export {
|
|
239
|
+
toHaveDisplayValue,
|
|
240
|
+
toHaveTextContent,
|
|
241
|
+
toHaveAccessibleName,
|
|
242
|
+
toHavePlaceholderText,
|
|
243
|
+
toBeChecked,
|
|
244
|
+
toBePressed,
|
|
245
|
+
toBeExpanded,
|
|
246
|
+
toBeSelected,
|
|
247
|
+
matchers,
|
|
248
|
+
toHaveValue,
|
|
249
|
+
registerMatchers,
|
|
250
|
+
type TextExpectation,
|
|
251
|
+
};
|