@gtkx/testing 0.20.0 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -55
- package/dist/act.d.ts +19 -0
- package/dist/act.d.ts.map +1 -0
- package/dist/act.js +48 -0
- package/dist/act.js.map +1 -0
- package/dist/bound-queries.d.ts +32 -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 +103 -0
- package/dist/build-queries.js.map +1 -0
- package/dist/cleanup-registry.d.ts +4 -0
- package/dist/cleanup-registry.d.ts.map +1 -0
- package/dist/cleanup-registry.js +11 -0
- package/dist/cleanup-registry.js.map +1 -0
- package/dist/config.d.ts +18 -51
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -40
- package/dist/config.js.map +1 -1
- package/dist/editable.d.ts +7 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +24 -0
- package/dist/editable.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +119 -0
- package/dist/errors.js.map +1 -0
- package/dist/fire-event.d.ts +7 -25
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +9 -29
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +3 -0
- package/dist/frame-sync.d.ts.map +1 -0
- package/dist/frame-sync.js +30 -0
- package/dist/frame-sync.js.map +1 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +55 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +103 -0
- package/dist/matchers.js.map +1 -0
- package/dist/pretty-widget.d.ts +21 -25
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +69 -115
- package/dist/pretty-widget.js.map +1 -1
- package/dist/queries.d.ts +130 -177
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +212 -325
- 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 +15 -0
- package/dist/register-test-hooks.js.map +1 -0
- package/dist/render-hook.d.ts +14 -35
- package/dist/render-hook.d.ts.map +1 -1
- package/dist/render-hook.js +8 -45
- package/dist/render-hook.js.map +1 -1
- package/dist/render.d.ts +11 -41
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +129 -118
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +16 -51
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +39 -58
- package/dist/role-helpers.js.map +1 -1
- package/dist/screen.d.ts +6 -63
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +21 -109
- package/dist/screen.js.map +1 -1
- package/dist/screenshot.d.ts +20 -23
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +89 -48
- package/dist/screenshot.js.map +1 -1
- package/dist/suggestions.d.ts +22 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/suggestions.js +56 -0
- package/dist/suggestions.js.map +1 -0
- package/dist/traversal.d.ts +10 -8
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +28 -13
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +103 -172
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +8 -0
- package/dist/user-event/adjustment.d.ts.map +1 -0
- package/dist/user-event/adjustment.js +33 -0
- package/dist/user-event/adjustment.js.map +1 -0
- package/dist/user-event/click.d.ts +7 -0
- package/dist/user-event/click.d.ts.map +1 -0
- package/dist/user-event/click.js +65 -0
- package/dist/user-event/click.js.map +1 -0
- package/dist/user-event/controller.d.ts +7 -0
- package/dist/user-event/controller.d.ts.map +1 -0
- package/dist/user-event/controller.js +28 -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 +10 -0
- package/dist/user-event/dispatch.js.map +1 -0
- package/dist/user-event/event-wrapper.d.ts +3 -0
- package/dist/user-event/event-wrapper.d.ts.map +1 -0
- package/dist/user-event/event-wrapper.js +48 -0
- package/dist/user-event/event-wrapper.js.map +1 -0
- package/dist/user-event/gesture.d.ts +31 -0
- package/dist/user-event/gesture.d.ts.map +1 -0
- package/dist/user-event/gesture.js +105 -0
- package/dist/user-event/gesture.js.map +1 -0
- package/dist/user-event/index.d.ts +44 -0
- package/dist/user-event/index.d.ts.map +1 -0
- package/dist/user-event/index.js +38 -0
- package/dist/user-event/index.js.map +1 -0
- package/dist/user-event/keyboard.d.ts +9 -0
- package/dist/user-event/keyboard.d.ts.map +1 -0
- package/dist/user-event/keyboard.js +173 -0
- package/dist/user-event/keyboard.js.map +1 -0
- package/dist/user-event/pointer.d.ts +6 -0
- package/dist/user-event/pointer.d.ts.map +1 -0
- package/dist/user-event/pointer.js +27 -0
- package/dist/user-event/pointer.js.map +1 -0
- package/dist/user-event/selection.d.ts +4 -0
- package/dist/user-event/selection.d.ts.map +1 -0
- package/dist/user-event/selection.js +106 -0
- package/dist/user-event/selection.js.map +1 -0
- package/dist/user-event/state.d.ts +6 -0
- package/dist/user-event/state.d.ts.map +1 -0
- package/dist/user-event/state.js +2 -0
- package/dist/user-event/state.js.map +1 -0
- package/dist/user-event/text.d.ts +17 -0
- package/dist/user-event/text.d.ts.map +1 -0
- package/dist/user-event/text.js +107 -0
- package/dist/user-event/text.js.map +1 -0
- package/dist/wait-for.d.ts +13 -32
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js +62 -73
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +33 -0
- package/dist/widget-accessible-properties.d.ts.map +1 -0
- package/dist/widget-accessible-properties.js +215 -0
- package/dist/widget-accessible-properties.js.map +1 -0
- package/dist/within.d.ts +9 -26
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +15 -25
- package/dist/within.js.map +1 -1
- package/package.json +21 -12
- package/src/act.ts +68 -0
- package/src/bound-queries.ts +36 -0
- package/src/build-queries.ts +185 -0
- package/src/cleanup-registry.ts +14 -0
- package/src/config.ts +28 -59
- package/src/editable.ts +30 -0
- package/src/errors.ts +130 -0
- package/src/fire-event.ts +11 -38
- package/src/frame-sync.ts +34 -0
- package/src/index.ts +60 -18
- package/src/matchers.ts +200 -0
- package/src/pretty-widget.ts +99 -138
- package/src/queries.ts +321 -425
- package/src/register-test-hooks.ts +16 -0
- package/src/render-hook.tsx +31 -44
- package/src/render.tsx +161 -149
- package/src/role-helpers.ts +47 -67
- package/src/screen.ts +21 -114
- package/src/screenshot.ts +104 -55
- package/src/suggestions.ts +87 -0
- package/src/traversal.ts +32 -18
- package/src/types.ts +115 -176
- package/src/user-event/adjustment.ts +49 -0
- package/src/user-event/click.ts +70 -0
- package/src/user-event/controller.ts +44 -0
- package/src/user-event/dispatch.ts +23 -0
- package/src/user-event/event-wrapper.ts +54 -0
- package/src/user-event/gesture.ts +154 -0
- package/src/user-event/index.ts +74 -0
- package/src/user-event/keyboard.ts +204 -0
- package/src/user-event/pointer.ts +34 -0
- package/src/user-event/selection.ts +122 -0
- package/src/user-event/state.ts +6 -0
- package/src/user-event/text.ts +137 -0
- package/src/wait-for.ts +79 -80
- package/src/widget-accessible-properties.ts +244 -0
- package/src/within.ts +26 -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/matchers.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
export type TextExpectation = string | RegExp;
|
|
17
|
+
|
|
18
|
+
type MatcherResult = { pass: boolean; message: () => string };
|
|
19
|
+
|
|
20
|
+
interface MatcherContext {
|
|
21
|
+
isNot: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type TextMatcher = (this: MatcherContext, received: unknown, expected?: TextExpectation) => MatcherResult;
|
|
25
|
+
type StateMatcher = (this: MatcherContext, received: unknown) => MatcherResult;
|
|
26
|
+
type ValueMatcher = (this: MatcherContext, received: unknown, expected: number) => MatcherResult;
|
|
27
|
+
|
|
28
|
+
const asWidget = (received: unknown, matcherName: string): Gtk.Widget => {
|
|
29
|
+
if (!(received instanceof Gtk.Widget)) {
|
|
30
|
+
throw new TypeError(`${matcherName}: received value must be a Gtk.Widget, got ${typeof received}`);
|
|
31
|
+
}
|
|
32
|
+
return received;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const describeWidget = (widget: Gtk.Widget): string => {
|
|
36
|
+
const role = Gtk.AccessibleRole[widget.getAccessibleRole()];
|
|
37
|
+
const name = getWidgetAccessibleName(widget);
|
|
38
|
+
return name !== null ? `<${role} name=${JSON.stringify(name)}>` : `<${role}>`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const matchesText = (actual: string, expected: TextExpectation, mode: "exact" | "substring"): boolean => {
|
|
42
|
+
if (expected instanceof RegExp) {
|
|
43
|
+
expected.lastIndex = 0;
|
|
44
|
+
return expected.test(actual);
|
|
45
|
+
}
|
|
46
|
+
return mode === "exact" ? actual === expected : actual.includes(expected);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const describeExpected = (expected: TextExpectation): string =>
|
|
50
|
+
expected instanceof RegExp ? String(expected) : JSON.stringify(expected);
|
|
51
|
+
|
|
52
|
+
const textMatcher = (
|
|
53
|
+
matcherName: string,
|
|
54
|
+
read: (widget: Gtk.Widget) => string | null,
|
|
55
|
+
mode: "exact" | "substring",
|
|
56
|
+
): TextMatcher =>
|
|
57
|
+
function (this: MatcherContext, received: unknown, expected?: TextExpectation): MatcherResult {
|
|
58
|
+
const widget = asWidget(received, matcherName);
|
|
59
|
+
const actual = read(widget);
|
|
60
|
+
|
|
61
|
+
if (expected === undefined) {
|
|
62
|
+
const pass = actual !== null && actual !== "";
|
|
63
|
+
return {
|
|
64
|
+
pass,
|
|
65
|
+
message: () =>
|
|
66
|
+
`expected widget ${this.isNot ? "not " : ""}to have a non-empty value for ${matcherName}, ` +
|
|
67
|
+
`but got ${JSON.stringify(actual)}\n${describeWidget(widget)}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pass = actual !== null && matchesText(actual, expected, mode);
|
|
72
|
+
return {
|
|
73
|
+
pass,
|
|
74
|
+
message: () =>
|
|
75
|
+
`expected widget ${this.isNot ? "not " : ""}${matcherName} ${describeExpected(expected)}, ` +
|
|
76
|
+
`but received ${JSON.stringify(actual)}\n${describeWidget(widget)}`,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const booleanStateMatcher = (
|
|
81
|
+
matcherName: string,
|
|
82
|
+
stateName: string,
|
|
83
|
+
read: (widget: Gtk.Widget) => boolean | null,
|
|
84
|
+
): StateMatcher =>
|
|
85
|
+
function (this: MatcherContext, received: unknown): MatcherResult {
|
|
86
|
+
const widget = asWidget(received, matcherName);
|
|
87
|
+
const state = read(widget);
|
|
88
|
+
if (state === null) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`${matcherName}: widget does not expose a ${stateName} state ` +
|
|
91
|
+
`(role ${Gtk.AccessibleRole[widget.getAccessibleRole()]})\n${describeWidget(widget)}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
pass: state,
|
|
96
|
+
message: () => `expected widget ${this.isNot ? "not " : ""}to be ${stateName}\n${describeWidget(widget)}`,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const toHaveDisplayValue: TextMatcher = textMatcher("toHaveDisplayValue", getWidgetDisplayValue, "exact");
|
|
101
|
+
export const toHaveTextContent: TextMatcher = textMatcher(
|
|
102
|
+
"toHaveTextContent",
|
|
103
|
+
(widget) => getWidgetNodeText(widget) ?? getWidgetAccessibleName(widget),
|
|
104
|
+
"substring",
|
|
105
|
+
);
|
|
106
|
+
export const toHaveAccessibleName: TextMatcher = textMatcher("toHaveAccessibleName", getWidgetAccessibleName, "exact");
|
|
107
|
+
export const toHavePlaceholderText: TextMatcher = textMatcher(
|
|
108
|
+
"toHavePlaceholderText",
|
|
109
|
+
getWidgetPlaceholderText,
|
|
110
|
+
"exact",
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
export const toBeChecked: StateMatcher = booleanStateMatcher("toBeChecked", "checked", getWidgetCheckedState);
|
|
114
|
+
export const toBePressed: StateMatcher = booleanStateMatcher("toBePressed", "pressed", getWidgetPressedState);
|
|
115
|
+
export const toBeExpanded: StateMatcher = booleanStateMatcher("toBeExpanded", "expanded", getWidgetExpandedState);
|
|
116
|
+
export const toBeSelected: StateMatcher = booleanStateMatcher("toBeSelected", "selected", getWidgetSelectedState);
|
|
117
|
+
|
|
118
|
+
export const toHaveValue: ValueMatcher = function (
|
|
119
|
+
this: MatcherContext,
|
|
120
|
+
received: unknown,
|
|
121
|
+
expected: number,
|
|
122
|
+
): MatcherResult {
|
|
123
|
+
const widget = asWidget(received, "toHaveValue");
|
|
124
|
+
const actual = getWidgetValue(widget).now;
|
|
125
|
+
if (actual === null) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`toHaveValue: widget does not expose a numeric value ` +
|
|
128
|
+
`(role ${Gtk.AccessibleRole[widget.getAccessibleRole()]})\n${describeWidget(widget)}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
pass: actual === expected,
|
|
133
|
+
message: () =>
|
|
134
|
+
`expected widget ${this.isNot ? "not " : ""}to have value ${expected}, but received ${actual}\n${describeWidget(widget)}`,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
interface MatcherImplementations {
|
|
139
|
+
toHaveDisplayValue: TextMatcher;
|
|
140
|
+
toHaveTextContent: TextMatcher;
|
|
141
|
+
toHaveAccessibleName: TextMatcher;
|
|
142
|
+
toHavePlaceholderText: TextMatcher;
|
|
143
|
+
toBeChecked: StateMatcher;
|
|
144
|
+
toBePressed: StateMatcher;
|
|
145
|
+
toBeExpanded: StateMatcher;
|
|
146
|
+
toBeSelected: StateMatcher;
|
|
147
|
+
toHaveValue: ValueMatcher;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The widget assertion matchers keyed by name, suitable for passing to `expect.extend`. */
|
|
151
|
+
export const matchers: MatcherImplementations = {
|
|
152
|
+
toHaveDisplayValue,
|
|
153
|
+
toHaveTextContent,
|
|
154
|
+
toHaveAccessibleName,
|
|
155
|
+
toHavePlaceholderText,
|
|
156
|
+
toBeChecked,
|
|
157
|
+
toBePressed,
|
|
158
|
+
toBeExpanded,
|
|
159
|
+
toBeSelected,
|
|
160
|
+
toHaveValue,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
type ExpectExtend = { extend: (m: MatcherImplementations) => void };
|
|
164
|
+
|
|
165
|
+
const globalExpect = (): ExpectExtend | null => {
|
|
166
|
+
const candidate: unknown = Reflect.get(globalThis, "expect");
|
|
167
|
+
if (candidate && typeof (candidate as ExpectExtend).extend === "function") {
|
|
168
|
+
return candidate as ExpectExtend;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let registered = false;
|
|
174
|
+
|
|
175
|
+
/** Registers the widget matchers on the global `expect`, when one is available. Safe to call more than once. */
|
|
176
|
+
export const registerMatchers = (): void => {
|
|
177
|
+
if (registered) return;
|
|
178
|
+
const expect = globalExpect();
|
|
179
|
+
if (!expect) return;
|
|
180
|
+
expect.extend(matchers);
|
|
181
|
+
registered = true;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
interface WidgetMatchers<R = void> {
|
|
185
|
+
toHaveDisplayValue(expected?: TextExpectation): R;
|
|
186
|
+
toHaveTextContent(expected?: TextExpectation): R;
|
|
187
|
+
toHaveAccessibleName(expected?: TextExpectation): R;
|
|
188
|
+
toHavePlaceholderText(expected?: TextExpectation): R;
|
|
189
|
+
toBeChecked(): R;
|
|
190
|
+
toBePressed(): R;
|
|
191
|
+
toBeExpanded(): R;
|
|
192
|
+
toBeSelected(): R;
|
|
193
|
+
toHaveValue(expected: number): R;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
declare module "@vitest/expect" {
|
|
197
|
+
// biome-ignore lint/suspicious/noExplicitAny: must match the `Assertion<T>` signature it augments for declaration merging
|
|
198
|
+
interface Assertion<T = any> extends WidgetMatchers {}
|
|
199
|
+
interface AsymmetricMatchersContaining extends WidgetMatchers {}
|
|
200
|
+
}
|
package/src/pretty-widget.ts
CHANGED
|
@@ -1,48 +1,61 @@
|
|
|
1
|
-
import * as Gtk from "@gtkx/
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { sortStringsBy } from "@gtkx/utils";
|
|
2
3
|
import { formatRole } from "./role-helpers.js";
|
|
3
|
-
import { type Container,
|
|
4
|
-
import {
|
|
4
|
+
import { type Container, roots } from "./traversal.js";
|
|
5
|
+
import { getWidgetNodeText } from "./widget-accessible-properties.js";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_MAX_LENGTH = 7000;
|
|
7
8
|
const INDENT = " ";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
let nextDebugId = 0;
|
|
11
|
-
|
|
12
|
-
const getWidgetDebugId = (widget: Gtk.Widget): string => {
|
|
13
|
-
let id = debugIdMap.get(widget);
|
|
14
|
-
if (!id) {
|
|
15
|
-
id = String(nextDebugId++);
|
|
16
|
-
debugIdMap.set(widget, id);
|
|
17
|
-
}
|
|
18
|
-
return id;
|
|
19
|
-
};
|
|
10
|
+
type WidgetIdResolver = (widget: Gtk.Widget) => string;
|
|
20
11
|
|
|
21
12
|
/**
|
|
22
|
-
* Options
|
|
13
|
+
* Options controlling how a widget tree is rendered to a string by
|
|
14
|
+
* {@link prettyWidget} and {@link logWidget}.
|
|
23
15
|
*/
|
|
24
16
|
export type PrettyWidgetOptions = {
|
|
25
|
-
/**
|
|
17
|
+
/** Truncates the output once it exceeds this many characters. */
|
|
26
18
|
maxLength?: number;
|
|
27
|
-
/**
|
|
19
|
+
/** Whether to apply ANSI color highlighting; defaults to the terminal capabilities. */
|
|
28
20
|
highlight?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
|
|
21
|
+
/** Resolves an `id` attribute to show for each widget. */
|
|
22
|
+
getId?: WidgetIdResolver;
|
|
31
23
|
};
|
|
32
24
|
|
|
33
|
-
|
|
25
|
+
const buildAttrs = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): [string, string][] => {
|
|
26
|
+
const attrs: [string, string][] = [];
|
|
27
|
+
|
|
28
|
+
if (getId) {
|
|
29
|
+
attrs.push(["id", getId(widget)]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const name = widget.getName();
|
|
33
|
+
if (name) {
|
|
34
|
+
attrs.push(["name", name]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
attrs.push(["role", formatRole(widget.getAccessibleRole())]);
|
|
38
|
+
|
|
39
|
+
if (!widget.getSensitive()) {
|
|
40
|
+
attrs.push(["accessible-disabled", "true"]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!widget.getVisible()) {
|
|
44
|
+
attrs.push(["accessible-hidden", "true"]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const idAttrs = attrs.filter(([key]) => key === "id");
|
|
48
|
+
const otherAttrs = sortStringsBy(
|
|
49
|
+
attrs.filter(([key]) => key !== "id"),
|
|
50
|
+
([key]) => key,
|
|
51
|
+
);
|
|
52
|
+
return [...idAttrs, ...otherAttrs];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type Colors = {
|
|
34
56
|
tag: (s: string) => string;
|
|
35
57
|
attr: (s: string) => string;
|
|
36
58
|
value: (s: string) => string;
|
|
37
|
-
text: (s: string) => string;
|
|
38
|
-
reset: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const shouldHighlight = (): boolean => {
|
|
42
|
-
if (typeof process === "undefined") return false;
|
|
43
|
-
if (process.env.COLORS === "false" || process.env.NO_COLOR) return false;
|
|
44
|
-
if (process.env.COLORS === "true" || process.env.FORCE_COLOR) return true;
|
|
45
|
-
return process.stdout?.isTTY ?? false;
|
|
46
59
|
};
|
|
47
60
|
|
|
48
61
|
const ansi = {
|
|
@@ -52,150 +65,85 @@ const ansi = {
|
|
|
52
65
|
reset: "\x1b[0m",
|
|
53
66
|
};
|
|
54
67
|
|
|
55
|
-
const
|
|
68
|
+
const shouldHighlight = (): boolean => {
|
|
69
|
+
if (typeof process === "undefined") return false;
|
|
70
|
+
if (process.env.COLORS === "false" || process.env.NO_COLOR) return false;
|
|
71
|
+
if (process.env.COLORS === "true" || process.env.FORCE_COLOR) return true;
|
|
72
|
+
return process.stdout.isTTY;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const createColors = (enabled: boolean): Colors => {
|
|
56
76
|
if (!enabled) {
|
|
57
77
|
const identity = (s: string): string => s;
|
|
58
|
-
return { tag: identity, attr: identity, value: identity
|
|
78
|
+
return { tag: identity, attr: identity, value: identity };
|
|
59
79
|
}
|
|
60
80
|
return {
|
|
61
81
|
tag: (s) => `${ansi.cyan}${s}${ansi.reset}`,
|
|
62
82
|
attr: (s) => `${ansi.yellow}${s}${ansi.reset}`,
|
|
63
83
|
value: (s) => `${ansi.green}${s}${ansi.reset}`,
|
|
64
|
-
text: (s) => s,
|
|
65
|
-
reset: ansi.reset,
|
|
66
84
|
};
|
|
67
85
|
};
|
|
68
86
|
|
|
69
|
-
const
|
|
70
|
-
return widget.constructor.name;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const escapeAttrValue = (value: string): string => {
|
|
74
|
-
return value.replace(/"/g, """);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const formatAttributes = (widget: Gtk.Widget, colors: HighlightColors, includeIds: boolean): string => {
|
|
78
|
-
const attrs: [string, string][] = [];
|
|
87
|
+
const escapeAttrValue = (value: string): string => value.replaceAll('"', """);
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
89
|
+
const formatAttrs = (attrs: [string, string][], colors: Colors): string =>
|
|
90
|
+
attrs.map(([key, value]) => ` ${colors.attr(key)}=${colors.value(`"${escapeAttrValue(value)}"`)}`).join("");
|
|
83
91
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (role !== undefined) {
|
|
91
|
-
attrs.push(["role", formatRole(role)]);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!widget.getSensitive()) {
|
|
95
|
-
attrs.push(["aria-disabled", "true"]);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!widget.getVisible()) {
|
|
99
|
-
attrs.push(["aria-hidden", "true"]);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (attrs.length === 0) return "";
|
|
103
|
-
|
|
104
|
-
return attrs
|
|
105
|
-
.sort(([a], [b]) => {
|
|
106
|
-
if (a === "id") return -1;
|
|
107
|
-
if (b === "id") return 1;
|
|
108
|
-
return a.localeCompare(b);
|
|
109
|
-
})
|
|
110
|
-
.map(([key, value]) => ` ${colors.attr(key)}=${colors.value(`"${escapeAttrValue(value)}"`)}`)
|
|
111
|
-
.join("");
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const hasChildren = (widget: Gtk.Widget): boolean => {
|
|
115
|
-
return widget.getFirstChild() !== null;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const printWidget = (widget: Gtk.Widget, colors: HighlightColors, depth: number, includeIds: boolean): string => {
|
|
92
|
+
const formatWidget = (
|
|
93
|
+
widget: Gtk.Widget,
|
|
94
|
+
depth: number,
|
|
95
|
+
getId: WidgetIdResolver | undefined,
|
|
96
|
+
colors: Colors,
|
|
97
|
+
): string => {
|
|
119
98
|
const indent = INDENT.repeat(depth);
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
let child = widget.getFirstChild();
|
|
126
|
-
while (child) {
|
|
127
|
-
children.push(printWidget(child, colors, depth + 1, includeIds));
|
|
128
|
-
child = child.getNextSibling();
|
|
129
|
-
}
|
|
99
|
+
const tag = widget.constructor.name;
|
|
100
|
+
const attrs = formatAttrs(buildAttrs(widget, getId), colors);
|
|
101
|
+
const openTag = `${colors.tag("<")}${colors.tag(tag)}${attrs}${colors.tag(">")}`;
|
|
102
|
+
const closeTag = `${colors.tag("</")}${colors.tag(tag)}${colors.tag(">")}`;
|
|
130
103
|
|
|
131
|
-
const
|
|
104
|
+
const text = getWidgetNodeText(widget);
|
|
105
|
+
const firstChild = widget.getFirstChild();
|
|
132
106
|
|
|
133
|
-
if (!
|
|
107
|
+
if (!text && !firstChild) {
|
|
134
108
|
return `${indent}${openTag}\n`;
|
|
135
109
|
}
|
|
136
110
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (text && !hasChildren(widget)) {
|
|
140
|
-
const textContent = colors.text(text);
|
|
141
|
-
return `${indent}${openTag}\n${indent}${INDENT}${textContent}\n${indent}${closeTag}\n`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
let result = `${indent}${openTag}\n`;
|
|
111
|
+
let output = `${indent}${openTag}\n`;
|
|
145
112
|
if (text) {
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
for (const childOutput of children) {
|
|
149
|
-
result += childOutput;
|
|
113
|
+
output += `${indent}${INDENT}${text}\n`;
|
|
150
114
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const printContainer = (container: Container, colors: HighlightColors, includeIds: boolean): string => {
|
|
157
|
-
if (isApplication(container)) {
|
|
158
|
-
const windows = Gtk.Window.listToplevels();
|
|
159
|
-
return windows.map((window) => printWidget(window, colors, 0, includeIds)).join("");
|
|
115
|
+
let child = firstChild;
|
|
116
|
+
while (child) {
|
|
117
|
+
output += formatWidget(child, depth + 1, getId, colors);
|
|
118
|
+
child = child.getNextSibling();
|
|
160
119
|
}
|
|
161
|
-
|
|
120
|
+
output += `${indent}${closeTag}\n`;
|
|
121
|
+
return output;
|
|
162
122
|
};
|
|
163
123
|
|
|
164
124
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* Renders the widget hierarchy in an HTML-like format with accessibility
|
|
168
|
-
* attributes like role, data-testid, and text content.
|
|
169
|
-
*
|
|
170
|
-
* @param container - The container widget or application to format
|
|
171
|
-
* @param options - Formatting options for length and highlighting
|
|
172
|
-
* @returns Formatted string representation of the widget tree
|
|
173
|
-
*
|
|
174
|
-
* @example
|
|
175
|
-
* ```tsx
|
|
176
|
-
* import { prettyWidget } from "@gtkx/testing";
|
|
125
|
+
* Renders a widget tree as an indented, HTML-like string showing each widget's
|
|
126
|
+
* tag, accessible attributes, and text content.
|
|
177
127
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* // <GtkButton role="button">
|
|
182
|
-
* // Click me
|
|
183
|
-
* // </GtkButton>
|
|
184
|
-
* // </GtkApplicationWindow>
|
|
185
|
-
* ```
|
|
128
|
+
* @param container The scope whose widget tree is formatted.
|
|
129
|
+
* @param options Formatting options such as truncation length and highlighting.
|
|
130
|
+
* @returns The formatted representation of the tree.
|
|
186
131
|
*/
|
|
187
132
|
export const prettyWidget = (container: Container, options: PrettyWidgetOptions = {}): string => {
|
|
188
133
|
const envLimit = process.env.DEBUG_PRINT_LIMIT ? Number(process.env.DEBUG_PRINT_LIMIT) : DEFAULT_MAX_LENGTH;
|
|
189
134
|
const maxLength = options.maxLength ?? envLimit;
|
|
190
|
-
const highlight = options.highlight ?? shouldHighlight();
|
|
191
|
-
const includeIds = options.includeIds ?? false;
|
|
192
135
|
|
|
193
136
|
if (maxLength === 0) {
|
|
194
137
|
return "";
|
|
195
138
|
}
|
|
196
139
|
|
|
140
|
+
const highlight = options.highlight ?? shouldHighlight();
|
|
197
141
|
const colors = createColors(highlight);
|
|
198
|
-
|
|
142
|
+
|
|
143
|
+
let output = "";
|
|
144
|
+
for (const root of roots(container)) {
|
|
145
|
+
output += formatWidget(root, 0, options.getId, colors);
|
|
146
|
+
}
|
|
199
147
|
|
|
200
148
|
if (output.length > maxLength) {
|
|
201
149
|
return `${output.slice(0, maxLength)}...`;
|
|
@@ -203,3 +151,16 @@ export const prettyWidget = (container: Container, options: PrettyWidgetOptions
|
|
|
203
151
|
|
|
204
152
|
return output.trimEnd();
|
|
205
153
|
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Prints one or more widget trees to the console using {@link prettyWidget}.
|
|
157
|
+
*
|
|
158
|
+
* @param container A single scope or an array of scopes to format and print.
|
|
159
|
+
* @param options Formatting options passed through to {@link prettyWidget}.
|
|
160
|
+
*/
|
|
161
|
+
export const logWidget = (container: Container | Container[], options?: PrettyWidgetOptions): void => {
|
|
162
|
+
const containers: Container[] = Array.isArray(container) ? container : [container];
|
|
163
|
+
for (const target of containers) {
|
|
164
|
+
console.log(prettyWidget(target, options));
|
|
165
|
+
}
|
|
166
|
+
};
|