@gtkx/testing 1.0.0-rc.3 → 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 (215) hide show
  1. package/README.md +8 -9
  2. package/dist/accessible-native.d.ts +16 -0
  3. package/dist/accessible-native.d.ts.map +1 -0
  4. package/dist/accessible-native.js +206 -0
  5. package/dist/accessible-native.js.map +1 -0
  6. package/dist/act.d.ts +2 -0
  7. package/dist/act.d.ts.map +1 -1
  8. package/dist/act.js.map +1 -1
  9. package/dist/build-queries.d.ts +27 -10
  10. package/dist/build-queries.d.ts.map +1 -1
  11. package/dist/build-queries.js +39 -30
  12. package/dist/build-queries.js.map +1 -1
  13. package/dist/cleanup-registry.d.ts +1 -1
  14. package/dist/cleanup-registry.d.ts.map +1 -1
  15. package/dist/cleanup-registry.js.map +1 -1
  16. package/dist/config.d.ts +1 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +3 -3
  19. package/dist/config.js.map +1 -1
  20. package/dist/errors.d.ts +10 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +12 -3
  23. package/dist/errors.js.map +1 -1
  24. package/dist/fire-event.d.ts +35 -6
  25. package/dist/fire-event.d.ts.map +1 -1
  26. package/dist/fire-event.js +23 -7
  27. package/dist/fire-event.js.map +1 -1
  28. package/dist/frame-sync.d.ts +2 -1
  29. package/dist/frame-sync.d.ts.map +1 -1
  30. package/dist/frame-sync.js +15 -5
  31. package/dist/frame-sync.js.map +1 -1
  32. package/dist/harness-window.d.ts +4 -0
  33. package/dist/harness-window.d.ts.map +1 -0
  34. package/dist/harness-window.js +6 -0
  35. package/dist/harness-window.js.map +1 -0
  36. package/dist/index.d.ts +14 -7
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +9 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal.d.ts +4 -0
  41. package/dist/internal.d.ts.map +1 -0
  42. package/dist/internal.js +4 -0
  43. package/dist/internal.js.map +1 -0
  44. package/dist/matchers.d.ts +125 -39
  45. package/dist/matchers.d.ts.map +1 -1
  46. package/dist/matchers.js +156 -18
  47. package/dist/matchers.js.map +1 -1
  48. package/dist/pretty-format.d.ts +2 -0
  49. package/dist/pretty-format.d.ts.map +1 -0
  50. package/dist/pretty-format.js +2 -0
  51. package/dist/pretty-format.js.map +1 -0
  52. package/dist/pretty-widget.d.ts +7 -3
  53. package/dist/pretty-widget.d.ts.map +1 -1
  54. package/dist/pretty-widget.js +74 -57
  55. package/dist/pretty-widget.js.map +1 -1
  56. package/dist/production.d.ts +8 -0
  57. package/dist/production.d.ts.map +1 -0
  58. package/dist/production.js +34 -0
  59. package/dist/production.js.map +1 -0
  60. package/dist/queries.d.ts +99 -56
  61. package/dist/queries.d.ts.map +1 -1
  62. package/dist/queries.js +123 -83
  63. package/dist/queries.js.map +1 -1
  64. package/dist/query-helpers.d.ts +44 -0
  65. package/dist/query-helpers.d.ts.map +1 -0
  66. package/dist/query-helpers.js +57 -0
  67. package/dist/query-helpers.js.map +1 -0
  68. package/dist/render-children.d.ts +9 -0
  69. package/dist/render-children.d.ts.map +1 -0
  70. package/dist/render-children.js +11 -0
  71. package/dist/render-children.js.map +1 -0
  72. package/dist/render.d.ts.map +1 -1
  73. package/dist/render.js +13 -11
  74. package/dist/render.js.map +1 -1
  75. package/dist/role-helpers.d.ts +12 -3
  76. package/dist/role-helpers.d.ts.map +1 -1
  77. package/dist/role-helpers.js +13 -4
  78. package/dist/role-helpers.js.map +1 -1
  79. package/dist/role-naming.d.ts +5 -0
  80. package/dist/role-naming.d.ts.map +1 -0
  81. package/dist/role-naming.js +96 -0
  82. package/dist/role-naming.js.map +1 -0
  83. package/dist/screenshot.d.ts +12 -20
  84. package/dist/screenshot.d.ts.map +1 -1
  85. package/dist/screenshot.js +183 -91
  86. package/dist/screenshot.js.map +1 -1
  87. package/dist/suggestions.d.ts +3 -0
  88. package/dist/suggestions.d.ts.map +1 -1
  89. package/dist/suggestions.js.map +1 -1
  90. package/dist/traversal.d.ts +9 -1
  91. package/dist/traversal.d.ts.map +1 -1
  92. package/dist/traversal.js +26 -6
  93. package/dist/traversal.js.map +1 -1
  94. package/dist/types.d.ts +67 -17
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js.map +1 -1
  97. package/dist/user-event/adjustment.d.ts +16 -0
  98. package/dist/user-event/adjustment.d.ts.map +1 -1
  99. package/dist/user-event/adjustment.js +34 -2
  100. package/dist/user-event/adjustment.js.map +1 -1
  101. package/dist/user-event/click.d.ts +45 -3
  102. package/dist/user-event/click.d.ts.map +1 -1
  103. package/dist/user-event/click.js +177 -39
  104. package/dist/user-event/click.js.map +1 -1
  105. package/dist/user-event/column-header-click.d.ts +4 -0
  106. package/dist/user-event/column-header-click.d.ts.map +1 -0
  107. package/dist/user-event/column-header-click.js +65 -0
  108. package/dist/user-event/column-header-click.js.map +1 -0
  109. package/dist/user-event/controller.d.ts +29 -0
  110. package/dist/user-event/controller.d.ts.map +1 -1
  111. package/dist/user-event/controller.js +28 -0
  112. package/dist/user-event/controller.js.map +1 -1
  113. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  114. package/dist/user-event/event-wrapper.js +2 -1
  115. package/dist/user-event/event-wrapper.js.map +1 -1
  116. package/dist/user-event/gesture.d.ts +51 -0
  117. package/dist/user-event/gesture.d.ts.map +1 -1
  118. package/dist/user-event/gesture.js +47 -4
  119. package/dist/user-event/gesture.js.map +1 -1
  120. package/dist/user-event/index.d.ts +70 -2
  121. package/dist/user-event/index.d.ts.map +1 -1
  122. package/dist/user-event/index.js +43 -26
  123. package/dist/user-event/index.js.map +1 -1
  124. package/dist/user-event/indexed-children.d.ts +10 -0
  125. package/dist/user-event/indexed-children.d.ts.map +1 -0
  126. package/dist/user-event/indexed-children.js +50 -0
  127. package/dist/user-event/indexed-children.js.map +1 -0
  128. package/dist/user-event/keyboard.d.ts +4 -2
  129. package/dist/user-event/keyboard.d.ts.map +1 -1
  130. package/dist/user-event/keyboard.js +46 -16
  131. package/dist/user-event/keyboard.js.map +1 -1
  132. package/dist/user-event/list-row-click.d.ts +4 -0
  133. package/dist/user-event/list-row-click.d.ts.map +1 -0
  134. package/dist/user-event/list-row-click.js +39 -0
  135. package/dist/user-event/list-row-click.js.map +1 -0
  136. package/dist/user-event/native-click.d.ts +6 -0
  137. package/dist/user-event/native-click.d.ts.map +1 -0
  138. package/dist/user-event/native-click.js +32 -0
  139. package/dist/user-event/native-click.js.map +1 -0
  140. package/dist/user-event/pointer.d.ts +1 -1
  141. package/dist/user-event/pointer.d.ts.map +1 -1
  142. package/dist/user-event/pointer.js +37 -13
  143. package/dist/user-event/pointer.js.map +1 -1
  144. package/dist/user-event/selection.d.ts +18 -0
  145. package/dist/user-event/selection.d.ts.map +1 -1
  146. package/dist/user-event/selection.js +56 -30
  147. package/dist/user-event/selection.js.map +1 -1
  148. package/dist/user-event/state.d.ts +1 -1
  149. package/dist/user-event/state.d.ts.map +1 -1
  150. package/dist/user-event/state.js +1 -1
  151. package/dist/user-event/state.js.map +1 -1
  152. package/dist/user-event/text.d.ts +34 -3
  153. package/dist/user-event/text.d.ts.map +1 -1
  154. package/dist/user-event/text.js +94 -17
  155. package/dist/user-event/text.js.map +1 -1
  156. package/dist/wait-for.d.ts +2 -0
  157. package/dist/wait-for.d.ts.map +1 -1
  158. package/dist/wait-for.js.map +1 -1
  159. package/dist/widget-accessible-properties.d.ts +14 -10
  160. package/dist/widget-accessible-properties.d.ts.map +1 -1
  161. package/dist/widget-accessible-properties.js +135 -173
  162. package/dist/widget-accessible-properties.js.map +1 -1
  163. package/dist/widget-getters.d.ts +8 -0
  164. package/dist/widget-getters.d.ts.map +1 -0
  165. package/dist/widget-getters.js +19 -0
  166. package/dist/widget-getters.js.map +1 -0
  167. package/dist/within.d.ts +3 -2
  168. package/dist/within.d.ts.map +1 -1
  169. package/dist/within.js +3 -2
  170. package/dist/within.js.map +1 -1
  171. package/package.json +13 -8
  172. package/src/accessible-native.ts +333 -0
  173. package/src/act.ts +2 -0
  174. package/src/build-queries.ts +66 -48
  175. package/src/cleanup-registry.ts +1 -1
  176. package/src/config.ts +4 -3
  177. package/src/errors.ts +14 -3
  178. package/src/fire-event.ts +61 -8
  179. package/src/frame-sync.ts +21 -5
  180. package/src/harness-window.ts +9 -0
  181. package/src/index.ts +18 -11
  182. package/src/internal.ts +11 -0
  183. package/src/matchers.ts +466 -64
  184. package/src/pretty-format.ts +1 -0
  185. package/src/pretty-widget.ts +100 -72
  186. package/src/production.ts +52 -0
  187. package/src/queries.ts +128 -89
  188. package/src/query-helpers.ts +97 -0
  189. package/src/render-children.ts +25 -0
  190. package/src/render.tsx +18 -13
  191. package/src/role-helpers.ts +14 -4
  192. package/src/role-naming.ts +99 -0
  193. package/src/screenshot.ts +272 -103
  194. package/src/suggestions.ts +3 -0
  195. package/src/traversal.ts +49 -6
  196. package/src/types.ts +69 -19
  197. package/src/user-event/adjustment.ts +42 -2
  198. package/src/user-event/click.ts +248 -46
  199. package/src/user-event/column-header-click.ts +90 -0
  200. package/src/user-event/controller.ts +29 -0
  201. package/src/user-event/event-wrapper.ts +3 -1
  202. package/src/user-event/gesture.ts +57 -6
  203. package/src/user-event/index.ts +119 -29
  204. package/src/user-event/indexed-children.ts +75 -0
  205. package/src/user-event/keyboard.ts +70 -20
  206. package/src/user-event/list-row-click.ts +57 -0
  207. package/src/user-event/native-click.ts +46 -0
  208. package/src/user-event/pointer.ts +52 -13
  209. package/src/user-event/selection.ts +74 -33
  210. package/src/user-event/state.ts +2 -2
  211. package/src/user-event/text.ts +121 -23
  212. package/src/wait-for.ts +2 -0
  213. package/src/widget-accessible-properties.ts +183 -214
  214. package/src/widget-getters.ts +32 -0
  215. package/src/within.ts +3 -2
@@ -0,0 +1 @@
1
+ export * as prettyFormat from "@vitest/pretty-format";
@@ -1,9 +1,11 @@
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
 
8
+ /** Produces the value of the `id` attribute printed first on a widget's opening tag. */
7
9
  type WidgetIdResolver = (widget: Gtk.Widget) => string;
8
10
 
9
11
  /**
@@ -14,34 +16,24 @@ type PrettyWidgetOptions = {
14
16
  /** Truncates the output once it exceeds this many characters. */
15
17
  maxLength?: number;
16
18
  /** Whether to apply ANSI color highlighting; defaults to the terminal capabilities. */
17
- highlight?: boolean;
19
+ shouldHighlight?: boolean;
18
20
  /** Resolves an `id` attribute to show for each widget. */
19
21
  getId?: WidgetIdResolver;
20
22
  /** Stops descending past this depth, replacing deeper children with a summary line. */
21
23
  maxDepth?: number;
24
+ /** Forwarded to pretty-format, which does the printing; `indent` and `min` shape the layout. */
25
+ prettyFormatOptions?: PrettyFormatOptions;
22
26
  };
23
27
 
24
- type Colors = {
25
- tag: (s: string) => string;
26
- attr: (s: string) => string;
27
- value: (s: string) => string;
28
- };
28
+ type Color = { open: string; close: string };
29
29
 
30
30
  type FormatContext = {
31
31
  getId: WidgetIdResolver | undefined;
32
- colors: Colors;
32
+ config: Config;
33
33
  maxDepth: number | undefined;
34
34
  };
35
35
 
36
36
  const DEFAULT_MAX_LENGTH = 7000;
37
- const INDENT = " ";
38
-
39
- const ansi = {
40
- cyan: "\u{1B}[36m",
41
- yellow: "\u{1B}[33m",
42
- green: "\u{1B}[32m",
43
- reset: "\u{1B}[0m",
44
- };
45
37
 
46
38
  const buildAttrs = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): [string, string][] => {
47
39
  const attrs: [string, string][] = [];
@@ -66,6 +58,10 @@ const buildAttrs = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): [s
66
58
  attrs.push(["accessible-hidden", "true"]);
67
59
  }
68
60
 
61
+ if (!isOnScreen(widget)) {
62
+ attrs.push(["mapped", "false"]);
63
+ }
64
+
69
65
  const idAttrs = attrs.filter(([key]) => key === "id");
70
66
 
71
67
  const otherAttrs = sortStringsBy(
@@ -76,7 +72,7 @@ const buildAttrs = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): [s
76
72
  return [...idAttrs, ...otherAttrs];
77
73
  };
78
74
 
79
- const shouldHighlight = (): boolean => {
75
+ const isHighlightSupported = (): boolean => {
80
76
  if (typeof process === "undefined") {
81
77
  return false;
82
78
  }
@@ -92,29 +88,16 @@ const shouldHighlight = (): boolean => {
92
88
  return process.stdout.isTTY;
93
89
  };
94
90
 
95
- const createColors = (isEnabled: boolean): Colors => {
96
- if (!isEnabled) {
97
- const identity = (s: string): string => s;
98
-
99
- return { tag: identity, attr: identity, value: identity };
100
- }
101
-
102
- return {
103
- tag: (s) => `${ansi.cyan}${s}${ansi.reset}`,
104
- attr: (s) => `${ansi.yellow}${s}${ansi.reset}`,
105
- value: (s) => `${ansi.green}${s}${ansi.reset}`,
106
- };
107
- };
108
-
91
+ const paint = (color: Color, text: string): string => `${color.open}${text}${color.close}`;
109
92
  const escapeAttrValue = (value: string): string => value.replaceAll('"', """);
110
93
 
111
- const formatAttr = (key: string, value: string, colors: Colors): string => {
94
+ const formatAttr = (key: string, value: string, colors: Config["colors"]): string => {
112
95
  const quoted = `"${escapeAttrValue(value)}"`;
113
96
 
114
- return ` ${colors.attr(key)}=${colors.value(quoted)}`;
97
+ return ` ${paint(colors.prop, key)}=${paint(colors.value, quoted)}`;
115
98
  };
116
99
 
117
- const formatAttrs = (attrs: [string, string][], colors: Colors): string =>
100
+ const formatAttrs = (attrs: [string, string][], colors: Config["colors"]): string =>
118
101
  attrs.map(([key, value]) => formatAttr(key, value, colors)).join("");
119
102
 
120
103
  const countChildren = (widget: Gtk.Widget): number => {
@@ -129,68 +112,114 @@ const countChildren = (widget: Gtk.Widget): number => {
129
112
  return count;
130
113
  };
131
114
 
132
- const formatHiddenChildrenLine = (widget: Gtk.Widget, depth: number, ctx: FormatContext): string => {
133
- const { getId, colors } = ctx;
134
- const indent = INDENT.repeat(depth);
135
- const count = countChildren(widget);
115
+ const depthLimitReason = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): string => {
136
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);
137
154
  const plural = count === 1 ? "" : "s";
138
- const summary = `… ${String(count)} child widget${plural} hidden${hint}`;
155
+ const summary = `… ${String(count)} child widget${plural} ${reason}`;
139
156
 
140
- return `${indent}${INDENT}${colors.tag(summary)}\n`;
157
+ return `${indentation}${config.indent}${paint(config.colors.tag, summary)}${config.spacingOuter}`;
141
158
  };
142
159
 
143
- const formatChildren = (widget: Gtk.Widget, depth: number, ctx: FormatContext): string => {
160
+ const formatChildren = (widget: Gtk.Widget, indentation: string, depth: number, ctx: FormatContext): string => {
144
161
  let output = "";
145
162
  let child = widget.getFirstChild();
146
163
 
147
164
  while (child) {
148
- output += formatWidget(child, depth + 1, ctx);
165
+ output += formatWidget(child, indentation + ctx.config.indent, depth + 1, ctx);
149
166
  child = child.getNextSibling();
150
167
  }
151
168
 
152
169
  return output;
153
170
  };
154
171
 
155
- const formatWidget = (widget: Gtk.Widget, depth: number, ctx: FormatContext): string => {
156
- const { getId, colors, maxDepth } = ctx;
157
- const indent = INDENT.repeat(depth);
158
- const tag = widget.constructor.name;
159
- const attrs = formatAttrs(buildAttrs(widget, getId), colors);
160
- const openTag = `${colors.tag("<")}${colors.tag(tag)}${attrs}${colors.tag(">")}`;
161
- const closeTag = `${colors.tag("</")}${colors.tag(tag)}${colors.tag(">")}`;
162
- const text = getWidgetNodeText(widget);
163
- const firstChild = widget.getFirstChild();
172
+ const formatBody = (widget: Gtk.Widget, indentation: string, depth: number, ctx: FormatContext): string => {
173
+ const collapseReason = collapseReasonFor(widget, depth, ctx);
164
174
 
165
- if (!text && !firstChild) {
166
- return `${indent}${openTag}\n`;
175
+ if (collapseReason === null) {
176
+ return formatChildren(widget, indentation, depth, ctx);
167
177
  }
168
178
 
169
- let output = `${indent}${openTag}\n`;
170
-
171
- if (text) {
172
- output += `${indent}${INDENT}${text}\n`;
173
- }
174
-
175
- if (firstChild && maxDepth !== undefined && depth >= maxDepth) {
176
- output += formatHiddenChildrenLine(widget, depth, ctx);
177
- output += `${indent}${closeTag}\n`;
179
+ return formatCollapsedChildrenLine(widget, indentation, ctx.config, collapseReason);
180
+ };
178
181
 
179
- 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;
180
193
  }
181
194
 
182
- output += formatChildren(widget, depth, ctx);
183
- output += `${indent}${closeTag}\n`;
195
+ const textLine = text ? `${indentation}${config.indent}${text}${config.spacingOuter}` : "";
196
+ const body = formatBody(widget, indentation, depth, ctx);
184
197
 
185
- return output;
198
+ return `${openLine}${textLine}${body}${indentation}${closeTag}${config.spacingOuter}`;
186
199
  };
187
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
+
188
207
  const resolveMaxLength = (options: PrettyWidgetOptions): number => {
189
208
  const envLimit = process.env.DEBUG_PRINT_LIMIT ? Number(process.env.DEBUG_PRINT_LIMIT) : DEFAULT_MAX_LENGTH;
190
209
 
191
210
  return options.maxLength ?? envLimit;
192
211
  };
193
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
+
194
223
  /**
195
224
  * Renders a widget tree as an indented, HTML-like string showing each widget's
196
225
  * tag, accessible attributes, and text content.
@@ -206,12 +235,11 @@ const prettyWidget = (container: Container, options: PrettyWidgetOptions = {}):
206
235
  return "";
207
236
  }
208
237
 
209
- const isHighlight = options.highlight ?? shouldHighlight();
210
- const colors = createColors(isHighlight);
238
+ const prettyFormatOptions = formatOptions(options);
211
239
  let output = "";
212
240
 
213
241
  for (const root of roots(container)) {
214
- output += formatWidget(root, 0, { getId: options.getId, colors, maxDepth: options.maxDepth });
242
+ output += format(root, prettyFormatOptions);
215
243
  }
216
244
 
217
245
  if (output.length > maxLength) {
@@ -235,4 +263,4 @@ const logWidget = (container: Container | Container[], options?: PrettyWidgetOpt
235
263
  }
236
264
  };
237
265
 
238
- 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 };