@gtkx/testing 1.0.0-rc.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +5 -5
  2. package/dist/accessible-native.d.ts +16 -0
  3. package/dist/accessible-native.d.ts.map +1 -0
  4. package/dist/accessible-native.js +206 -0
  5. package/dist/accessible-native.js.map +1 -0
  6. package/dist/build-queries.d.ts +27 -10
  7. package/dist/build-queries.d.ts.map +1 -1
  8. package/dist/build-queries.js +22 -24
  9. package/dist/build-queries.js.map +1 -1
  10. package/dist/cleanup-registry.d.ts +1 -1
  11. package/dist/cleanup-registry.d.ts.map +1 -1
  12. package/dist/cleanup-registry.js.map +1 -1
  13. package/dist/errors.d.ts +10 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +10 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/fire-event.d.ts +35 -6
  18. package/dist/fire-event.d.ts.map +1 -1
  19. package/dist/fire-event.js +23 -7
  20. package/dist/fire-event.js.map +1 -1
  21. package/dist/frame-sync.d.ts +2 -1
  22. package/dist/frame-sync.d.ts.map +1 -1
  23. package/dist/frame-sync.js +15 -5
  24. package/dist/frame-sync.js.map +1 -1
  25. package/dist/harness-window.d.ts +4 -0
  26. package/dist/harness-window.d.ts.map +1 -0
  27. package/dist/harness-window.js +6 -0
  28. package/dist/harness-window.js.map +1 -0
  29. package/dist/index.d.ts +14 -7
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +9 -5
  32. package/dist/index.js.map +1 -1
  33. package/dist/internal.d.ts +4 -0
  34. package/dist/internal.d.ts.map +1 -0
  35. package/dist/internal.js +4 -0
  36. package/dist/internal.js.map +1 -0
  37. package/dist/matchers.d.ts +88 -46
  38. package/dist/matchers.d.ts.map +1 -1
  39. package/dist/matchers.js +156 -18
  40. package/dist/matchers.js.map +1 -1
  41. package/dist/pretty-format.d.ts +2 -0
  42. package/dist/pretty-format.d.ts.map +1 -0
  43. package/dist/pretty-format.js +2 -0
  44. package/dist/pretty-format.js.map +1 -0
  45. package/dist/pretty-widget.d.ts +5 -2
  46. package/dist/pretty-widget.d.ts.map +1 -1
  47. package/dist/pretty-widget.js +73 -56
  48. package/dist/pretty-widget.js.map +1 -1
  49. package/dist/production.d.ts +8 -0
  50. package/dist/production.d.ts.map +1 -0
  51. package/dist/production.js +34 -0
  52. package/dist/production.js.map +1 -0
  53. package/dist/queries.d.ts +98 -56
  54. package/dist/queries.d.ts.map +1 -1
  55. package/dist/queries.js +123 -83
  56. package/dist/queries.js.map +1 -1
  57. package/dist/query-helpers.d.ts +44 -0
  58. package/dist/query-helpers.d.ts.map +1 -0
  59. package/dist/query-helpers.js +57 -0
  60. package/dist/query-helpers.js.map +1 -0
  61. package/dist/render-children.d.ts +9 -0
  62. package/dist/render-children.d.ts.map +1 -0
  63. package/dist/render-children.js +11 -0
  64. package/dist/render-children.js.map +1 -0
  65. package/dist/render.d.ts.map +1 -1
  66. package/dist/render.js +11 -9
  67. package/dist/render.js.map +1 -1
  68. package/dist/role-helpers.d.ts +12 -3
  69. package/dist/role-helpers.d.ts.map +1 -1
  70. package/dist/role-helpers.js +13 -4
  71. package/dist/role-helpers.js.map +1 -1
  72. package/dist/role-naming.d.ts +5 -0
  73. package/dist/role-naming.d.ts.map +1 -0
  74. package/dist/role-naming.js +96 -0
  75. package/dist/role-naming.js.map +1 -0
  76. package/dist/screenshot.d.ts +12 -20
  77. package/dist/screenshot.d.ts.map +1 -1
  78. package/dist/screenshot.js +183 -91
  79. package/dist/screenshot.js.map +1 -1
  80. package/dist/traversal.d.ts +4 -1
  81. package/dist/traversal.d.ts.map +1 -1
  82. package/dist/traversal.js +25 -6
  83. package/dist/traversal.js.map +1 -1
  84. package/dist/types.d.ts +26 -14
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js.map +1 -1
  87. package/dist/user-event/adjustment.d.ts +3 -0
  88. package/dist/user-event/adjustment.d.ts.map +1 -1
  89. package/dist/user-event/adjustment.js +23 -2
  90. package/dist/user-event/adjustment.js.map +1 -1
  91. package/dist/user-event/click.d.ts +43 -8
  92. package/dist/user-event/click.d.ts.map +1 -1
  93. package/dist/user-event/click.js +175 -44
  94. package/dist/user-event/click.js.map +1 -1
  95. package/dist/user-event/column-header-click.d.ts +4 -0
  96. package/dist/user-event/column-header-click.d.ts.map +1 -0
  97. package/dist/user-event/column-header-click.js +65 -0
  98. package/dist/user-event/column-header-click.js.map +1 -0
  99. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  100. package/dist/user-event/event-wrapper.js +2 -1
  101. package/dist/user-event/event-wrapper.js.map +1 -1
  102. package/dist/user-event/index.d.ts +51 -10
  103. package/dist/user-event/index.d.ts.map +1 -1
  104. package/dist/user-event/index.js +43 -26
  105. package/dist/user-event/index.js.map +1 -1
  106. package/dist/user-event/indexed-children.d.ts +10 -0
  107. package/dist/user-event/indexed-children.d.ts.map +1 -0
  108. package/dist/user-event/indexed-children.js +50 -0
  109. package/dist/user-event/indexed-children.js.map +1 -0
  110. package/dist/user-event/keyboard.d.ts.map +1 -1
  111. package/dist/user-event/keyboard.js +33 -4
  112. package/dist/user-event/keyboard.js.map +1 -1
  113. package/dist/user-event/list-row-click.d.ts +4 -0
  114. package/dist/user-event/list-row-click.d.ts.map +1 -0
  115. package/dist/user-event/list-row-click.js +39 -0
  116. package/dist/user-event/list-row-click.js.map +1 -0
  117. package/dist/user-event/native-click.d.ts +6 -0
  118. package/dist/user-event/native-click.d.ts.map +1 -0
  119. package/dist/user-event/native-click.js +32 -0
  120. package/dist/user-event/native-click.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +1 -1
  122. package/dist/user-event/pointer.d.ts.map +1 -1
  123. package/dist/user-event/pointer.js +33 -9
  124. package/dist/user-event/pointer.js.map +1 -1
  125. package/dist/user-event/selection.d.ts +11 -8
  126. package/dist/user-event/selection.d.ts.map +1 -1
  127. package/dist/user-event/selection.js +49 -38
  128. package/dist/user-event/selection.js.map +1 -1
  129. package/dist/user-event/text.d.ts +6 -2
  130. package/dist/user-event/text.d.ts.map +1 -1
  131. package/dist/user-event/text.js +68 -18
  132. package/dist/user-event/text.js.map +1 -1
  133. package/dist/widget-accessible-properties.d.ts +14 -26
  134. package/dist/widget-accessible-properties.d.ts.map +1 -1
  135. package/dist/widget-accessible-properties.js +135 -190
  136. package/dist/widget-accessible-properties.js.map +1 -1
  137. package/dist/widget-getters.d.ts +8 -0
  138. package/dist/widget-getters.d.ts.map +1 -0
  139. package/dist/widget-getters.js +19 -0
  140. package/dist/widget-getters.js.map +1 -0
  141. package/dist/within.d.ts +3 -2
  142. package/dist/within.d.ts.map +1 -1
  143. package/dist/within.js +3 -2
  144. package/dist/within.js.map +1 -1
  145. package/package.json +12 -7
  146. package/src/accessible-native.ts +333 -0
  147. package/src/build-queries.ts +47 -41
  148. package/src/cleanup-registry.ts +1 -1
  149. package/src/errors.ts +12 -1
  150. package/src/fire-event.ts +61 -8
  151. package/src/frame-sync.ts +21 -5
  152. package/src/harness-window.ts +9 -0
  153. package/src/index.ts +18 -11
  154. package/src/internal.ts +11 -0
  155. package/src/matchers.ts +428 -70
  156. package/src/pretty-format.ts +1 -0
  157. package/src/pretty-widget.ts +97 -70
  158. package/src/production.ts +52 -0
  159. package/src/queries.ts +127 -89
  160. package/src/query-helpers.ts +97 -0
  161. package/src/render-children.ts +25 -0
  162. package/src/render.tsx +16 -11
  163. package/src/role-helpers.ts +14 -4
  164. package/src/role-naming.ts +99 -0
  165. package/src/screenshot.ts +272 -103
  166. package/src/traversal.ts +44 -6
  167. package/src/types.ts +28 -16
  168. package/src/user-event/adjustment.ts +29 -2
  169. package/src/user-event/click.ts +247 -52
  170. package/src/user-event/column-header-click.ts +90 -0
  171. package/src/user-event/event-wrapper.ts +3 -1
  172. package/src/user-event/index.ts +100 -37
  173. package/src/user-event/indexed-children.ts +75 -0
  174. package/src/user-event/keyboard.ts +52 -4
  175. package/src/user-event/list-row-click.ts +57 -0
  176. package/src/user-event/native-click.ts +46 -0
  177. package/src/user-event/pointer.ts +48 -9
  178. package/src/user-event/selection.ts +67 -41
  179. package/src/user-event/text.ts +92 -21
  180. package/src/widget-accessible-properties.ts +183 -230
  181. package/src/widget-getters.ts +32 -0
  182. package/src/within.ts +3 -2
@@ -1,8 +1,9 @@
1
- import type * as Gtk from "@gtkx/gi/gtk";
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
2
  import { sortStringsBy } from "@gtkx/utils";
3
+ import { type Config, format, type NewPlugin, type PrettyFormatOptions } from "@vitest/pretty-format";
3
4
  import { formatRole } from "./role-helpers.js";
4
- import { type Container, roots } from "./traversal.js";
5
- import { getWidgetNodeText } from "./widget-accessible-properties.js";
5
+ import { type Container, descendants, isOnScreen, roots } from "./traversal.js";
6
+ import { getWidgetText } from "./widget-accessible-properties.js";
6
7
 
7
8
  /** Produces the value of the `id` attribute printed first on a widget's opening tag. */
8
9
  type WidgetIdResolver = (widget: Gtk.Widget) => string;
@@ -20,29 +21,19 @@ type PrettyWidgetOptions = {
20
21
  getId?: WidgetIdResolver;
21
22
  /** Stops descending past this depth, replacing deeper children with a summary line. */
22
23
  maxDepth?: number;
24
+ /** Forwarded to pretty-format, which does the printing; `indent` and `min` shape the layout. */
25
+ prettyFormatOptions?: PrettyFormatOptions;
23
26
  };
24
27
 
25
- type Colors = {
26
- tag: (s: string) => string;
27
- attr: (s: string) => string;
28
- value: (s: string) => string;
29
- };
28
+ type Color = { open: string; close: string };
30
29
 
31
30
  type FormatContext = {
32
31
  getId: WidgetIdResolver | undefined;
33
- colors: Colors;
32
+ config: Config;
34
33
  maxDepth: number | undefined;
35
34
  };
36
35
 
37
36
  const DEFAULT_MAX_LENGTH = 7000;
38
- const INDENT = " ";
39
-
40
- const ansi = {
41
- cyan: "\u{1B}[36m",
42
- yellow: "\u{1B}[33m",
43
- green: "\u{1B}[32m",
44
- reset: "\u{1B}[0m",
45
- };
46
37
 
47
38
  const buildAttrs = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): [string, string][] => {
48
39
  const attrs: [string, string][] = [];
@@ -67,6 +58,10 @@ const buildAttrs = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): [s
67
58
  attrs.push(["accessible-hidden", "true"]);
68
59
  }
69
60
 
61
+ if (!isOnScreen(widget)) {
62
+ attrs.push(["mapped", "false"]);
63
+ }
64
+
70
65
  const idAttrs = attrs.filter(([key]) => key === "id");
71
66
 
72
67
  const otherAttrs = sortStringsBy(
@@ -93,29 +88,16 @@ const isHighlightSupported = (): boolean => {
93
88
  return process.stdout.isTTY;
94
89
  };
95
90
 
96
- const createColors = (isEnabled: boolean): Colors => {
97
- if (!isEnabled) {
98
- const identity = (s: string): string => s;
99
-
100
- return { tag: identity, attr: identity, value: identity };
101
- }
102
-
103
- return {
104
- tag: (s) => `${ansi.cyan}${s}${ansi.reset}`,
105
- attr: (s) => `${ansi.yellow}${s}${ansi.reset}`,
106
- value: (s) => `${ansi.green}${s}${ansi.reset}`,
107
- };
108
- };
109
-
91
+ const paint = (color: Color, text: string): string => `${color.open}${text}${color.close}`;
110
92
  const escapeAttrValue = (value: string): string => value.replaceAll('"', """);
111
93
 
112
- const formatAttr = (key: string, value: string, colors: Colors): string => {
94
+ const formatAttr = (key: string, value: string, colors: Config["colors"]): string => {
113
95
  const quoted = `"${escapeAttrValue(value)}"`;
114
96
 
115
- return ` ${colors.attr(key)}=${colors.value(quoted)}`;
97
+ return ` ${paint(colors.prop, key)}=${paint(colors.value, quoted)}`;
116
98
  };
117
99
 
118
- const formatAttrs = (attrs: [string, string][], colors: Colors): string =>
100
+ const formatAttrs = (attrs: [string, string][], colors: Config["colors"]): string =>
119
101
  attrs.map(([key, value]) => formatAttr(key, value, colors)).join("");
120
102
 
121
103
  const countChildren = (widget: Gtk.Widget): number => {
@@ -130,68 +112,114 @@ const countChildren = (widget: Gtk.Widget): number => {
130
112
  return count;
131
113
  };
132
114
 
133
- const formatHiddenChildrenLine = (widget: Gtk.Widget, depth: number, ctx: FormatContext): string => {
134
- const { getId, colors } = ctx;
135
- const indent = INDENT.repeat(depth);
136
- const count = countChildren(widget);
115
+ const depthLimitReason = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): string => {
137
116
  const hint = getId ? ` (pass rootId="${getId(widget)}" or raise maxDepth to expand)` : "";
117
+
118
+ return `hidden${hint}`;
119
+ };
120
+
121
+ const hasMappedDescendant = (widget: Gtk.Widget): boolean => {
122
+ for (const descendant of descendants(widget)) {
123
+ if (isOnScreen(descendant)) {
124
+ return true;
125
+ }
126
+ }
127
+
128
+ return false;
129
+ };
130
+
131
+ const collapseReasonFor = (widget: Gtk.Widget, depth: number, ctx: FormatContext): string | null => {
132
+ if (!widget.getFirstChild()) {
133
+ return null;
134
+ }
135
+
136
+ if (!isOnScreen(widget) && !hasMappedDescendant(widget)) {
137
+ return "not mapped";
138
+ }
139
+
140
+ if (ctx.maxDepth !== undefined && depth >= ctx.maxDepth) {
141
+ return depthLimitReason(widget, ctx.getId);
142
+ }
143
+
144
+ return null;
145
+ };
146
+
147
+ const formatCollapsedChildrenLine = (
148
+ widget: Gtk.Widget,
149
+ indentation: string,
150
+ config: Config,
151
+ reason: string,
152
+ ): string => {
153
+ const count = countChildren(widget);
138
154
  const plural = count === 1 ? "" : "s";
139
- const summary = `… ${String(count)} child widget${plural} hidden${hint}`;
155
+ const summary = `… ${String(count)} child widget${plural} ${reason}`;
140
156
 
141
- return `${indent}${INDENT}${colors.tag(summary)}\n`;
157
+ return `${indentation}${config.indent}${paint(config.colors.tag, summary)}${config.spacingOuter}`;
142
158
  };
143
159
 
144
- const formatChildren = (widget: Gtk.Widget, depth: number, ctx: FormatContext): string => {
160
+ const formatChildren = (widget: Gtk.Widget, indentation: string, depth: number, ctx: FormatContext): string => {
145
161
  let output = "";
146
162
  let child = widget.getFirstChild();
147
163
 
148
164
  while (child) {
149
- output += formatWidget(child, depth + 1, ctx);
165
+ output += formatWidget(child, indentation + ctx.config.indent, depth + 1, ctx);
150
166
  child = child.getNextSibling();
151
167
  }
152
168
 
153
169
  return output;
154
170
  };
155
171
 
156
- const formatWidget = (widget: Gtk.Widget, depth: number, ctx: FormatContext): string => {
157
- const { getId, colors, maxDepth } = ctx;
158
- const indent = INDENT.repeat(depth);
159
- const tag = widget.constructor.name;
160
- const attrs = formatAttrs(buildAttrs(widget, getId), colors);
161
- const openTag = `${colors.tag("<")}${colors.tag(tag)}${attrs}${colors.tag(">")}`;
162
- const closeTag = `${colors.tag("</")}${colors.tag(tag)}${colors.tag(">")}`;
163
- const text = getWidgetNodeText(widget);
164
- const firstChild = widget.getFirstChild();
172
+ const formatBody = (widget: Gtk.Widget, indentation: string, depth: number, ctx: FormatContext): string => {
173
+ const collapseReason = collapseReasonFor(widget, depth, ctx);
165
174
 
166
- if (!text && !firstChild) {
167
- return `${indent}${openTag}\n`;
175
+ if (collapseReason === null) {
176
+ return formatChildren(widget, indentation, depth, ctx);
168
177
  }
169
178
 
170
- let output = `${indent}${openTag}\n`;
171
-
172
- if (text) {
173
- output += `${indent}${INDENT}${text}\n`;
174
- }
175
-
176
- if (firstChild && maxDepth !== undefined && depth >= maxDepth) {
177
- output += formatHiddenChildrenLine(widget, depth, ctx);
178
- output += `${indent}${closeTag}\n`;
179
+ return formatCollapsedChildrenLine(widget, indentation, ctx.config, collapseReason);
180
+ };
179
181
 
180
- return output;
182
+ const formatWidget = (widget: Gtk.Widget, indentation: string, depth: number, ctx: FormatContext): string => {
183
+ const { config } = ctx;
184
+ const tag = widget.constructor.name;
185
+ const attrs = formatAttrs(buildAttrs(widget, ctx.getId), config.colors);
186
+ const openTag = `${paint(config.colors.tag, "<" + tag)}${attrs}${paint(config.colors.tag, ">")}`;
187
+ const closeTag = paint(config.colors.tag, `</${tag}>`);
188
+ const text = getWidgetText(widget);
189
+ const openLine = `${indentation}${openTag}${config.spacingOuter}`;
190
+
191
+ if (!text && !widget.getFirstChild()) {
192
+ return openLine;
181
193
  }
182
194
 
183
- output += formatChildren(widget, depth, ctx);
184
- output += `${indent}${closeTag}\n`;
195
+ const textLine = text ? `${indentation}${config.indent}${text}${config.spacingOuter}` : "";
196
+ const body = formatBody(widget, indentation, depth, ctx);
185
197
 
186
- return output;
198
+ return `${openLine}${textLine}${body}${indentation}${closeTag}${config.spacingOuter}`;
187
199
  };
188
200
 
201
+ const createWidgetPlugin = (options: PrettyWidgetOptions): NewPlugin => ({
202
+ test: (value: unknown): boolean => value instanceof Gtk.Widget,
203
+ serialize: (widget: Gtk.Widget, config: Config, indentation: string): string =>
204
+ formatWidget(widget, indentation, 0, { getId: options.getId, config, maxDepth: options.maxDepth }),
205
+ });
206
+
189
207
  const resolveMaxLength = (options: PrettyWidgetOptions): number => {
190
208
  const envLimit = process.env.DEBUG_PRINT_LIMIT ? Number(process.env.DEBUG_PRINT_LIMIT) : DEFAULT_MAX_LENGTH;
191
209
 
192
210
  return options.maxLength ?? envLimit;
193
211
  };
194
212
 
213
+ const formatOptions = (options: PrettyWidgetOptions): PrettyFormatOptions => {
214
+ const { plugins, ...rest } = options.prettyFormatOptions ?? {};
215
+
216
+ return {
217
+ highlight: options.shouldHighlight ?? isHighlightSupported(),
218
+ ...rest,
219
+ plugins: [createWidgetPlugin(options), ...(plugins ?? [])],
220
+ };
221
+ };
222
+
195
223
  /**
196
224
  * Renders a widget tree as an indented, HTML-like string showing each widget's
197
225
  * tag, accessible attributes, and text content.
@@ -207,12 +235,11 @@ const prettyWidget = (container: Container, options: PrettyWidgetOptions = {}):
207
235
  return "";
208
236
  }
209
237
 
210
- const shouldHighlight = options.shouldHighlight ?? isHighlightSupported();
211
- const colors = createColors(shouldHighlight);
238
+ const prettyFormatOptions = formatOptions(options);
212
239
  let output = "";
213
240
 
214
241
  for (const root of roots(container)) {
215
- output += formatWidget(root, 0, { getId: options.getId, colors, maxDepth: options.maxDepth });
242
+ output += format(root, prettyFormatOptions);
216
243
  }
217
244
 
218
245
  if (output.length > maxLength) {
@@ -236,4 +263,4 @@ const logWidget = (container: Container | Container[], options?: PrettyWidgetOpt
236
263
  }
237
264
  };
238
265
 
239
- export { prettyWidget, logWidget, type PrettyWidgetOptions };
266
+ export { logWidget, prettyWidget, type PrettyWidgetOptions };
@@ -0,0 +1,52 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import type { ReactNode } from "react";
3
+ import { createRoot, type Root } from "@gtkx/react";
4
+ import { createHarnessWindow } from "./harness-window.js";
5
+
6
+ type ProductionRenderResult = {
7
+ rerender: (element: ReactNode) => Promise<void>;
8
+ };
9
+
10
+ type ActiveRender = {
11
+ root: Root;
12
+ window: Gtk.Window;
13
+ };
14
+
15
+ const SETTLE_TURNS = 3;
16
+ const activeRenders: Set<ActiveRender> = new Set();
17
+
18
+ const turn = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
19
+
20
+ const settle = async (): Promise<void> => {
21
+ for (let index = 0; index < SETTLE_TURNS; index++) {
22
+ await turn();
23
+ }
24
+ };
25
+
26
+ const render = async (element: ReactNode): Promise<ProductionRenderResult> => {
27
+ const window = createHarnessWindow();
28
+ const root = createRoot(window);
29
+ activeRenders.add({ root, window });
30
+ root.render(element);
31
+ window.present();
32
+ await settle();
33
+
34
+ return {
35
+ rerender: async (next: ReactNode): Promise<void> => {
36
+ root.render(next);
37
+ await settle();
38
+ },
39
+ };
40
+ };
41
+
42
+ const cleanup = async (): Promise<void> => {
43
+ for (const active of activeRenders) {
44
+ active.root.unmount();
45
+ active.window.destroy();
46
+ }
47
+
48
+ activeRenders.clear();
49
+ await settle();
50
+ };
51
+
52
+ export { cleanup, render };