@gtkx/testing 1.0.0-rc.4 → 1.1.0

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