@gtkx/testing 1.0.0-rc.1 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/dist/act.d.ts +6 -6
  2. package/dist/act.d.ts.map +1 -1
  3. package/dist/act.js +34 -26
  4. package/dist/act.js.map +1 -1
  5. package/dist/bound-queries.d.ts +4 -3
  6. package/dist/bound-queries.d.ts.map +1 -1
  7. package/dist/bound-queries.js.map +1 -1
  8. package/dist/build-queries.d.ts +5 -5
  9. package/dist/build-queries.d.ts.map +1 -1
  10. package/dist/build-queries.js +21 -9
  11. package/dist/build-queries.js.map +1 -1
  12. package/dist/cleanup-registry.d.ts +4 -3
  13. package/dist/cleanup-registry.d.ts.map +1 -1
  14. package/dist/cleanup-registry.js +13 -3
  15. package/dist/cleanup-registry.js.map +1 -1
  16. package/dist/config.d.ts +5 -4
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +11 -9
  19. package/dist/config.js.map +1 -1
  20. package/dist/editable.d.ts +6 -5
  21. package/dist/editable.d.ts.map +1 -1
  22. package/dist/editable.js +7 -5
  23. package/dist/editable.js.map +1 -1
  24. package/dist/errors.d.ts +7 -6
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +64 -48
  27. package/dist/errors.js.map +1 -1
  28. package/dist/fire-event.d.ts +2 -1
  29. package/dist/fire-event.d.ts.map +1 -1
  30. package/dist/fire-event.js +2 -1
  31. package/dist/fire-event.js.map +1 -1
  32. package/dist/frame-sync.d.ts +2 -1
  33. package/dist/frame-sync.d.ts.map +1 -1
  34. package/dist/frame-sync.js +28 -21
  35. package/dist/frame-sync.js.map +1 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/matchers.d.ts +38 -34
  41. package/dist/matchers.d.ts.map +1 -1
  42. package/dist/matchers.js +72 -58
  43. package/dist/matchers.js.map +1 -1
  44. package/dist/pretty-widget.d.ts +6 -4
  45. package/dist/pretty-widget.d.ts.map +1 -1
  46. package/dist/pretty-widget.js +64 -24
  47. package/dist/pretty-widget.js.map +1 -1
  48. package/dist/queries.d.ts +143 -86
  49. package/dist/queries.d.ts.map +1 -1
  50. package/dist/queries.js +231 -147
  51. package/dist/queries.js.map +1 -1
  52. package/dist/register-test-hooks.js +2 -1
  53. package/dist/register-test-hooks.js.map +1 -1
  54. package/dist/render-hook.d.ts +3 -2
  55. package/dist/render-hook.d.ts.map +1 -1
  56. package/dist/render-hook.js +2 -1
  57. package/dist/render-hook.js.map +1 -1
  58. package/dist/render.d.ts +3 -2
  59. package/dist/render.d.ts.map +1 -1
  60. package/dist/render.js +24 -18
  61. package/dist/render.js.map +1 -1
  62. package/dist/role-helpers.d.ts +6 -5
  63. package/dist/role-helpers.d.ts.map +1 -1
  64. package/dist/role-helpers.js +16 -12
  65. package/dist/role-helpers.js.map +1 -1
  66. package/dist/screen.d.ts +4 -3
  67. package/dist/screen.d.ts.map +1 -1
  68. package/dist/screen.js +13 -11
  69. package/dist/screen.js.map +1 -1
  70. package/dist/screenshot.d.ts +4 -3
  71. package/dist/screenshot.d.ts.map +1 -1
  72. package/dist/screenshot.js +43 -36
  73. package/dist/screenshot.js.map +1 -1
  74. package/dist/suggestions.d.ts +11 -6
  75. package/dist/suggestions.d.ts.map +1 -1
  76. package/dist/suggestions.js +10 -5
  77. package/dist/suggestions.js.map +1 -1
  78. package/dist/traversal.d.ts +7 -7
  79. package/dist/traversal.d.ts.map +1 -1
  80. package/dist/traversal.js +17 -11
  81. package/dist/traversal.js.map +1 -1
  82. package/dist/types.d.ts +39 -24
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js.map +1 -1
  85. package/dist/user-event/adjustment.d.ts +5 -3
  86. package/dist/user-event/adjustment.d.ts.map +1 -1
  87. package/dist/user-event/adjustment.js +6 -4
  88. package/dist/user-event/adjustment.js.map +1 -1
  89. package/dist/user-event/click.d.ts +6 -5
  90. package/dist/user-event/click.d.ts.map +1 -1
  91. package/dist/user-event/click.js +29 -21
  92. package/dist/user-event/click.js.map +1 -1
  93. package/dist/user-event/controller.d.ts +6 -5
  94. package/dist/user-event/controller.d.ts.map +1 -1
  95. package/dist/user-event/controller.js +9 -6
  96. package/dist/user-event/controller.js.map +1 -1
  97. package/dist/user-event/dispatch.d.ts +3 -3
  98. package/dist/user-event/dispatch.d.ts.map +1 -1
  99. package/dist/user-event/dispatch.js +3 -2
  100. package/dist/user-event/dispatch.js.map +1 -1
  101. package/dist/user-event/event-wrapper.d.ts +2 -1
  102. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  103. package/dist/user-event/event-wrapper.js +27 -16
  104. package/dist/user-event/event-wrapper.js.map +1 -1
  105. package/dist/user-event/gesture.d.ts +18 -14
  106. package/dist/user-event/gesture.d.ts.map +1 -1
  107. package/dist/user-event/gesture.js +70 -37
  108. package/dist/user-event/gesture.js.map +1 -1
  109. package/dist/user-event/index.d.ts +17 -10
  110. package/dist/user-event/index.d.ts.map +1 -1
  111. package/dist/user-event/index.js +7 -3
  112. package/dist/user-event/index.js.map +1 -1
  113. package/dist/user-event/keyboard.d.ts +4 -3
  114. package/dist/user-event/keyboard.d.ts.map +1 -1
  115. package/dist/user-event/keyboard.js +89 -58
  116. package/dist/user-event/keyboard.js.map +1 -1
  117. package/dist/user-event/pointer.d.ts +7 -3
  118. package/dist/user-event/pointer.d.ts.map +1 -1
  119. package/dist/user-event/pointer.js +4 -1
  120. package/dist/user-event/pointer.js.map +1 -1
  121. package/dist/user-event/selection.d.ts +3 -2
  122. package/dist/user-event/selection.d.ts.map +1 -1
  123. package/dist/user-event/selection.js +26 -27
  124. package/dist/user-event/selection.js.map +1 -1
  125. package/dist/user-event/state.d.ts +3 -2
  126. package/dist/user-event/state.d.ts.map +1 -1
  127. package/dist/user-event/state.js +2 -1
  128. package/dist/user-event/state.js.map +1 -1
  129. package/dist/user-event/text.d.ts +8 -7
  130. package/dist/user-event/text.d.ts.map +1 -1
  131. package/dist/user-event/text.js +39 -33
  132. package/dist/user-event/text.js.map +1 -1
  133. package/dist/wait-for.d.ts +5 -5
  134. package/dist/wait-for.d.ts.map +1 -1
  135. package/dist/wait-for.js +45 -30
  136. package/dist/wait-for.js.map +1 -1
  137. package/dist/widget-accessible-properties.d.ts +26 -25
  138. package/dist/widget-accessible-properties.d.ts.map +1 -1
  139. package/dist/widget-accessible-properties.js +106 -61
  140. package/dist/widget-accessible-properties.js.map +1 -1
  141. package/dist/within.d.ts +2 -1
  142. package/dist/within.d.ts.map +1 -1
  143. package/dist/within.js +2 -1
  144. package/dist/within.js.map +1 -1
  145. package/package.json +11 -9
  146. package/src/act.ts +44 -35
  147. package/src/bound-queries.ts +5 -4
  148. package/src/build-queries.ts +84 -43
  149. package/src/cleanup-registry.ts +18 -5
  150. package/src/config.ts +16 -12
  151. package/src/editable.ts +13 -6
  152. package/src/errors.ts +106 -45
  153. package/src/fire-event.ts +3 -1
  154. package/src/frame-sync.ts +38 -21
  155. package/src/index.ts +2 -1
  156. package/src/matchers.ts +158 -107
  157. package/src/pretty-widget.ts +114 -42
  158. package/src/queries.ts +422 -233
  159. package/src/register-test-hooks.ts +4 -1
  160. package/src/render-hook.tsx +10 -4
  161. package/src/render.tsx +58 -33
  162. package/src/role-helpers.ts +29 -16
  163. package/src/screen.ts +17 -12
  164. package/src/screenshot.ts +61 -38
  165. package/src/suggestions.ts +29 -10
  166. package/src/traversal.ts +35 -15
  167. package/src/types.ts +65 -32
  168. package/src/user-event/adjustment.ts +23 -10
  169. package/src/user-event/click.ts +49 -20
  170. package/src/user-event/controller.ts +21 -7
  171. package/src/user-event/dispatch.ts +4 -2
  172. package/src/user-event/event-wrapper.ts +42 -16
  173. package/src/user-event/gesture.ts +135 -50
  174. package/src/user-event/index.ts +19 -12
  175. package/src/user-event/keyboard.ts +147 -61
  176. package/src/user-event/pointer.ts +20 -8
  177. package/src/user-event/selection.ts +69 -32
  178. package/src/user-event/state.ts +4 -2
  179. package/src/user-event/text.ts +73 -38
  180. package/src/wait-for.ts +77 -36
  181. package/src/widget-accessible-properties.ts +218 -74
  182. package/src/within.ts +7 -4
@@ -4,7 +4,10 @@ import { cleanup } from "./render.js";
4
4
 
5
5
  const callRunnerHook = (name: "afterEach" | "afterAll", callback: () => unknown): void => {
6
6
  const hook: unknown = Reflect.get(globalThis, name);
7
- if (typeof hook === "function") (hook as (callback: () => unknown) => void)(callback);
7
+
8
+ if (typeof hook === "function") {
9
+ (hook as (callback: () => unknown) => void)(callback);
10
+ }
8
11
  };
9
12
 
10
13
  const registerTestRuntimeHooks = (): void => {
@@ -1,6 +1,6 @@
1
1
  import * as Gtk from "@gtkx/gi/gtk";
2
- import { render } from "./render.js";
3
2
  import type { RenderHookOptions, RenderHookResult } from "./types.js";
3
+ import { render } from "./render.js";
4
4
 
5
5
  /**
6
6
  * Renders a test component that calls the given hook, exposing its latest
@@ -10,10 +10,11 @@ import type { RenderHookOptions, RenderHookResult } from "./types.js";
10
10
  * @param options Optional initial props and wrapper component.
11
11
  * @returns A result whose `result.current` holds the hook's latest value.
12
12
  */
13
- export function renderHook<Result>(
13
+ function renderHook<Result>(
14
14
  callback: () => Result,
15
15
  options?: RenderHookOptions<undefined>,
16
16
  ): Promise<RenderHookResult<Result, undefined>>;
17
+
17
18
  /**
18
19
  * Renders a test component that calls the given hook with props, exposing its
19
20
  * latest return value along with rerender and unmount controls.
@@ -22,11 +23,12 @@ export function renderHook<Result>(
22
23
  * @param options Initial props and optional wrapper component.
23
24
  * @returns A result whose `result.current` holds the hook's latest value.
24
25
  */
25
- export function renderHook<Result, Props>(
26
+ function renderHook<Result, Props>(
26
27
  callback: (props: Props) => Result,
27
28
  options: RenderHookOptions<Props>,
28
29
  ): Promise<RenderHookResult<Result, Props>>;
29
- export async function renderHook<Result, Props>(
30
+
31
+ async function renderHook<Result, Props>(
30
32
  callback: (props: Props) => Result,
31
33
  options?: RenderHookOptions<Props>,
32
34
  ): Promise<RenderHookResult<Result, Props>> {
@@ -37,6 +39,7 @@ export async function renderHook<Result, Props>(
37
39
  const TestComponent = ({ props }: { props: Props }): null => {
38
40
  const result = callback(props);
39
41
  resultRef.current = result;
42
+
40
43
  return null;
41
44
  };
42
45
 
@@ -51,8 +54,11 @@ export async function renderHook<Result, Props>(
51
54
  if (newProps !== undefined) {
52
55
  currentProps = newProps;
53
56
  }
57
+
54
58
  await renderResult.rerender(<TestComponent props={currentProps} />);
55
59
  },
56
60
  unmount: renderResult.unmount,
57
61
  };
58
62
  }
63
+
64
+ export { renderHook };
package/src/render.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import * as Gtk from "@gtkx/gi/gtk";
2
1
  import type { RootElement } from "@gtkx/react";
2
+ import * as Gtk from "@gtkx/gi/gtk";
3
3
  import {
4
4
  createReconcilerRoot,
5
5
  isRootElement,
@@ -7,8 +7,9 @@ import {
7
7
  setReconcilerErrorHandler,
8
8
  } from "@gtkx/react/internal";
9
9
  import { type ErrorInfo, type ReactNode, StrictMode } from "react";
10
- import { runInAct } from "./act.js";
11
10
  import type { RenderResult } from "./bound-queries.js";
11
+ import type { QueryMap, RenderOptions, ScreenshotOptions, WindowSelector } from "./types.js";
12
+ import { runInAct } from "./act.js";
12
13
  import { addToCleanupQueue, runCleanup } from "./cleanup-registry.js";
13
14
  import { scheduleAfterLayout } from "./frame-sync.js";
14
15
  import { logWidget, type PrettyWidgetOptions } from "./pretty-widget.js";
@@ -16,20 +17,26 @@ import { logRoles } from "./role-helpers.js";
16
17
  import { clearScreen, setScreen } from "./screen.js";
17
18
  import { captureAndSaveScreenshot } from "./screenshot.js";
18
19
  import { type Container, TOPLEVELS, traverse } from "./traversal.js";
19
- import type { QueryMap, RenderOptions, ScreenshotOptions, WindowSelector } from "./types.js";
20
20
  import { resetClipboard } from "./user-event/index.js";
21
21
  import { within } from "./within.js";
22
22
 
23
- let lastRenderError: Error | null = null;
24
- let errorHandlerInstalled = false;
25
-
26
23
  type ActiveRender = {
27
24
  root: ReconcilerRoot;
28
25
  window: Gtk.Window | null;
29
26
  };
30
27
 
31
- const activeRenders = new Set<ActiveRender>();
28
+ type ResolvedContainer = {
29
+ containerInfo: Gtk.Widget | RootElement;
30
+ window: Gtk.Window | null;
31
+ };
32
32
 
33
+ type ReconcilerErrorState = {
34
+ lastError: Error | null;
35
+ isHandlerInstalled: boolean;
36
+ };
37
+
38
+ const reconcilerErrors: ReconcilerErrorState = { lastError: null, isHandlerInstalled: false };
39
+ const activeRenders: Set<ActiveRender> = new Set();
33
40
  const HARNESS_WINDOW_WIDTH = 800;
34
41
  const HARNESS_WINDOW_HEIGHT = 600;
35
42
 
@@ -43,15 +50,18 @@ const update = async (element: ReactNode, root: ReconcilerRoot): Promise<void> =
43
50
  root.update(element);
44
51
  });
45
52
 
46
- if (lastRenderError) {
47
- const captured = lastRenderError;
48
- lastRenderError = null;
53
+ if (reconcilerErrors.lastError) {
54
+ const captured = reconcilerErrors.lastError;
55
+ reconcilerErrors.lastError = null;
49
56
  throw captured;
50
57
  }
51
58
  };
52
59
 
53
60
  const disposeActiveRender = async (active: ActiveRender): Promise<void> => {
54
- if (!activeRenders.delete(active)) return;
61
+ if (!activeRenders.delete(active)) {
62
+ return;
63
+ }
64
+
55
65
  await active.root.unmount(async (root) => {
56
66
  await update(null, root);
57
67
  active.window?.destroy();
@@ -59,42 +69,50 @@ const disposeActiveRender = async (active: ActiveRender): Promise<void> => {
59
69
  };
60
70
 
61
71
  const disposeAllActiveRenders = async (): Promise<void> => {
62
- for (const active of [...activeRenders]) {
72
+ for (const active of activeRenders) {
63
73
  await disposeActiveRender(active);
64
74
  }
65
75
  };
66
76
 
67
77
  const handleError = (error: unknown): void => {
68
- lastRenderError = error instanceof Error ? error : new Error(String(error));
78
+ reconcilerErrors.lastError = error instanceof Error ? error : new Error(String(error));
69
79
  };
70
80
 
71
81
  const installErrorHandler = (): void => {
72
- if (errorHandlerInstalled) return;
73
- setReconcilerErrorHandler(handleError);
74
- errorHandlerInstalled = true;
75
- };
82
+ if (reconcilerErrors.isHandlerInstalled) {
83
+ return;
84
+ }
76
85
 
77
- type ResolvedContainer = {
78
- containerInfo: Gtk.Widget | RootElement;
79
- window: Gtk.Window | null;
86
+ setReconcilerErrorHandler(handleError);
87
+ reconcilerErrors.isHandlerInstalled = true;
80
88
  };
81
89
 
82
90
  const resolveContainer = (container: RenderOptions["container"]): ResolvedContainer => {
83
91
  if (isRootElement(container)) {
84
92
  return { containerInfo: container, window: null };
85
93
  }
94
+
86
95
  if (container instanceof Gtk.Widget) {
87
96
  return { containerInfo: container, window: null };
88
97
  }
98
+
89
99
  const window = new Gtk.Window({ defaultWidth: HARNESS_WINDOW_WIDTH, defaultHeight: HARNESS_WINDOW_HEIGHT });
90
100
  window.setTitlebar(new Gtk.HeaderBar({ showTitleButtons: false }));
101
+
91
102
  return { containerInfo: window, window };
92
103
  };
93
104
 
94
105
  const firstToplevelWidget = (baseElement: Container): Gtk.Widget => {
95
- if (baseElement instanceof Gtk.Widget) return baseElement;
106
+ if (baseElement instanceof Gtk.Widget) {
107
+ return baseElement;
108
+ }
109
+
96
110
  const [first] = traverse(baseElement);
97
- if (first) return first;
111
+
112
+ if (first) {
113
+ return first;
114
+ }
115
+
98
116
  throw new Error("render() produced no widgets: ensure the element renders visible content");
99
117
  };
100
118
 
@@ -103,8 +121,14 @@ const resolveResultContainer = (
103
121
  container: RenderOptions["container"],
104
122
  baseElement: Container,
105
123
  ): Gtk.Widget => {
106
- if (resolved.window) return resolved.window;
107
- if (container instanceof Gtk.Widget) return container;
124
+ if (resolved.window) {
125
+ return resolved.window;
126
+ }
127
+
128
+ if (container instanceof Gtk.Widget) {
129
+ return container;
130
+ }
131
+
108
132
  return firstToplevelWidget(baseElement);
109
133
  };
110
134
 
@@ -119,10 +143,11 @@ const renderErrorHandlers = <Q extends QueryMap>(options: RenderOptions<Q> | und
119
143
  },
120
144
  });
121
145
 
122
- const applyEnableAnimations = (enabled: boolean): void => {
146
+ const applyEnableAnimations = (areAnimationsEnabled: boolean): void => {
123
147
  const settings = Gtk.Settings.getDefault();
148
+
124
149
  if (settings) {
125
- settings.gtkEnableAnimations = enabled;
150
+ settings.gtkEnableAnimations = areAnimationsEnabled;
126
151
  }
127
152
  };
128
153
 
@@ -135,38 +160,36 @@ const applyEnableAnimations = (enabled: boolean): void => {
135
160
  * @param options Optional container, wrapper, custom queries, and other render settings.
136
161
  * @returns A render result with bound queries, debug helpers, and lifecycle controls.
137
162
  */
138
- export const render = async <Q extends QueryMap = Record<never, never>>(
163
+ const render = async <Q extends QueryMap = Record<never, never>>(
139
164
  element: ReactNode,
140
165
  options?: RenderOptions<Q>,
141
166
  ): Promise<RenderResult<Q>> => {
142
167
  installErrorHandler();
143
-
144
168
  applyEnableAnimations(options?.animations === true);
145
-
146
169
  const baseElement: Container = options?.baseElement ?? TOPLEVELS;
147
170
  const Wrapper = options?.wrapper;
148
-
149
171
  const resolved = resolveContainer(options?.container);
172
+
150
173
  const root = createReconcilerRoot({
151
174
  containerInfo: resolved.containerInfo,
152
175
  ...renderErrorHandlers(options),
153
176
  });
177
+
154
178
  const active: ActiveRender = { root, window: resolved.window };
155
179
  activeRenders.add(active);
156
-
157
180
  addToCleanupQueue(disposeAllActiveRenders);
158
181
  addToCleanupQueue(clearScreen);
159
182
  addToCleanupQueue(resetClipboard);
160
183
 
161
184
  const wrap = (node: ReactNode): ReactNode => {
162
185
  const wrapped = Wrapper ? <Wrapper>{node}</Wrapper> : node;
186
+
163
187
  return options?.reactStrictMode ? <StrictMode>{wrapped}</StrictMode> : wrapped;
164
188
  };
165
189
 
166
190
  await update(wrap(element), root);
167
191
  resolved.window?.present();
168
192
  await flushLayout(resolved.window);
169
-
170
193
  const container = resolveResultContainer(resolved, options?.container, baseElement);
171
194
 
172
195
  const result: RenderResult<Q> = {
@@ -199,6 +222,8 @@ export const render = async <Q extends QueryMap = Record<never, never>>(
199
222
  * Unmounts every active render and runs all registered cleanup callbacks,
200
223
  * resetting the screen and clipboard. Called automatically after each test.
201
224
  */
202
- export const cleanup = async (): Promise<void> => {
225
+ const cleanup = async (): Promise<void> => {
203
226
  await runCleanup();
204
227
  };
228
+
229
+ export { render, cleanup };
@@ -3,14 +3,15 @@ import { sortStringsBy } from "@gtkx/utils";
3
3
  import { type Container, traverse } from "./traversal.js";
4
4
  import { getWidgetAccessibleName } from "./widget-accessible-properties.js";
5
5
 
6
- const enumNamesByValue = (enumObject: Record<string, string | number>): Map<number, string> =>
7
- new Map<number, string>(
6
+ const ROLE_NAMES_BY_VALUE = enumNamesByValue(Gtk.AccessibleRole);
7
+
8
+ function enumNamesByValue(enumObject: Record<string, string | number>): Map<number, string> {
9
+ return new Map<number, string>(
8
10
  Object.entries(enumObject)
9
11
  .filter((entry): entry is [string, number] => typeof entry[1] === "number")
10
12
  .map(([name, value]) => [value, name]),
11
13
  );
12
-
13
- const ROLE_NAMES_BY_VALUE = enumNamesByValue(Gtk.AccessibleRole);
14
+ }
14
15
 
15
16
  /**
16
17
  * Converts an accessible role enum value into its lowercase name, falling back
@@ -18,22 +19,30 @@ const ROLE_NAMES_BY_VALUE = enumNamesByValue(Gtk.AccessibleRole);
18
19
  *
19
20
  * @param role The accessible role to format.
20
21
  */
21
- export const formatRole = (role: Gtk.AccessibleRole): string => {
22
+ const formatRole = (role: Gtk.AccessibleRole): string => {
22
23
  const name = ROLE_NAMES_BY_VALUE.get(role);
23
- if (!name) return String(role);
24
+
25
+ if (!name) {
26
+ return String(role);
27
+ }
28
+
24
29
  return name.toLowerCase();
25
30
  };
26
31
 
27
- export const formatRoleList = (roles: Iterable<Gtk.AccessibleRole>): string => {
32
+ const formatRoleList = (roles: Iterable<Gtk.AccessibleRole>): string => {
28
33
  const names = [...roles].map((role) => formatRole(role).toUpperCase());
34
+
29
35
  if (names.length <= 1) {
30
36
  return names.join("");
31
37
  }
38
+
32
39
  if (names.length === 2) {
33
- return `${names[0]} or ${names[1]}`;
40
+ return names.join(" or ");
34
41
  }
42
+
35
43
  const head = names.slice(0, -1);
36
- const last = names[names.length - 1];
44
+ const last = names.at(-1) ?? "";
45
+
37
46
  return `${head.join(", ")}, or ${last}`;
38
47
  };
39
48
 
@@ -43,13 +52,13 @@ export const formatRoleList = (roles: Iterable<Gtk.AccessibleRole>): string => {
43
52
  * @param container The scope to traverse.
44
53
  * @returns A map from role name to the widgets that have that role.
45
54
  */
46
- export const getRoles = (container: Container): Map<string, Gtk.Widget[]> => {
47
- const roles = new Map<string, Gtk.Widget[]>();
55
+ const getRoles = (container: Container): Map<string, Gtk.Widget[]> => {
56
+ const roles: Map<string, Gtk.Widget[]> = new Map();
48
57
 
49
58
  for (const widget of traverse(container)) {
50
59
  const roleName = formatRole(widget.getAccessibleRole());
51
-
52
60
  const existing = roles.get(roleName);
61
+
53
62
  if (existing) {
54
63
  existing.push(widget);
55
64
  } else {
@@ -64,6 +73,7 @@ const formatWidgetPreview = (widget: Gtk.Widget, name: string | null): string =>
64
73
  const tagName = widget.constructor.name;
65
74
  const roleAttr = formatRole(widget.getAccessibleRole());
66
75
  const nameDisplay = name ? `Name "${name}"` : 'Name ""';
76
+
67
77
  return `${nameDisplay}: <${tagName} role="${roleAttr}">${name ?? ""}</${tagName}>`;
68
78
  };
69
79
 
@@ -73,7 +83,7 @@ const formatWidgetPreview = (widget: Gtk.Widget, name: string | null): string =>
73
83
  *
74
84
  * @param container The scope to inspect.
75
85
  */
76
- export const prettyRoles = (container: Container): string => {
86
+ const prettyRoles = (container: Container): string => {
77
87
  const roles = getRoles(container);
78
88
 
79
89
  if (roles.size === 0) {
@@ -81,14 +91,15 @@ export const prettyRoles = (container: Container): string => {
81
91
  }
82
92
 
83
93
  const lines: string[] = [];
84
-
85
- const sortedRoles = sortStringsBy([...roles.entries()], ([roleName]) => roleName);
94
+ const sortedRoles = sortStringsBy([...roles], ([roleName]) => roleName);
86
95
 
87
96
  for (const [roleName, widgets] of sortedRoles) {
88
97
  lines.push(`${roleName}:`);
98
+
89
99
  for (const widget of widgets) {
90
100
  lines.push(` ${formatWidgetPreview(widget, getWidgetAccessibleName(widget))}`);
91
101
  }
102
+
92
103
  lines.push("");
93
104
  }
94
105
 
@@ -101,6 +112,8 @@ export const prettyRoles = (container: Container): string => {
101
112
  *
102
113
  * @param container The scope to inspect.
103
114
  */
104
- export const logRoles = (container: Container): void => {
115
+ const logRoles = (container: Container): void => {
105
116
  console.log(prettyRoles(container));
106
117
  };
118
+
119
+ export { formatRole, formatRoleList, getRoles, prettyRoles, logRoles };
package/src/screen.ts CHANGED
@@ -2,31 +2,36 @@ import type { RenderResult, Screen } from "./bound-queries.js";
2
2
 
3
3
  const NO_RENDER_MESSAGE = "No render has been performed: call render() before using screen queries";
4
4
 
5
- const throwNoRender = (): never => {
6
- throw new Error(NO_RENDER_MESSAGE);
7
- };
8
-
9
5
  const defaultScreen: RenderResult = new Proxy({} as RenderResult, {
10
6
  get: (_target, property) => {
11
- if (property === "baseElement" || property === "container") return throwNoRender();
7
+ if (property === "baseElement" || property === "container") {
8
+ return throwNoRender();
9
+ }
10
+
12
11
  return throwNoRender;
13
12
  },
14
13
  });
15
14
 
16
- let activeScreen: RenderResult = defaultScreen;
15
+ const activeScreen: { current: RenderResult } = { current: defaultScreen };
17
16
 
18
17
  /**
19
18
  * Queries and debug utilities bound to the most recent render, scoped to the
20
19
  * current toplevel windows. Accessing it before any render throws.
21
20
  */
22
- export const screen: Screen = new Proxy({} as Screen, {
23
- get: (_target, property) => Reflect.get(activeScreen, property),
21
+ const screen: Screen = new Proxy({} as Screen, {
22
+ get: (_target, property): unknown => Reflect.get(activeScreen.current, property),
24
23
  });
25
24
 
26
- export const setScreen = (result: RenderResult): void => {
27
- activeScreen = result;
25
+ const throwNoRender = (): never => {
26
+ throw new Error(NO_RENDER_MESSAGE);
28
27
  };
29
28
 
30
- export const clearScreen = (): void => {
31
- activeScreen = defaultScreen;
29
+ const setScreen = (result: RenderResult): void => {
30
+ activeScreen.current = result;
32
31
  };
32
+
33
+ const clearScreen = (): void => {
34
+ activeScreen.current = defaultScreen;
35
+ };
36
+
37
+ export { screen, setScreen, clearScreen };
package/src/screenshot.ts CHANGED
@@ -1,19 +1,20 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
1
  import * as Gsk from "@gtkx/gi/gsk";
5
2
  import * as Gtk from "@gtkx/gi/gtk";
3
+ import { mkdtempSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
6
  import type { ScreenshotOptions, ScreenshotResult, WindowSelector } from "./types.js";
7
7
  import { waitFor } from "./wait-for.js";
8
8
 
9
+ const DEFAULT_SCREENSHOT_INTERVAL = 10;
10
+
9
11
  const bytesToBase64 = (bytes: number[]): string => {
10
12
  return Buffer.from(bytes).toString("base64");
11
13
  };
12
14
 
13
- const DEFAULT_SCREENSHOT_INTERVAL = 10;
14
-
15
15
  const describeWidgetState = (widget: Gtk.Widget): string =>
16
- `realized=${widget.getRealized()} mapped=${widget.getMapped()} visible=${widget.getVisible()}`;
16
+ `realized=${String(widget.getRealized())} mapped=${String(widget.getMapped())} ` +
17
+ `visible=${String(widget.getVisible())}`;
17
18
 
18
19
  const captureSnapshot = (widget: Gtk.Widget, scale: number): ScreenshotResult => {
19
20
  const paintable = new Gtk.WidgetPaintable({ widget });
@@ -34,7 +35,6 @@ const captureSnapshot = (widget: Gtk.Widget, scale: number): ScreenshotResult =>
34
35
  }
35
36
 
36
37
  const display = widget.getDisplay();
37
-
38
38
  const renderer = new Gsk.CairoRenderer();
39
39
  renderer.realizeForDisplay(display);
40
40
 
@@ -65,11 +65,11 @@ const captureSnapshot = (widget: Gtk.Widget, scale: number): ScreenshotResult =>
65
65
  * @param options Optional scale, timeout, and retry interval.
66
66
  * @returns The base64-encoded PNG data along with its mime type and dimensions.
67
67
  */
68
- export const screenshot = async (widget: Gtk.Widget, options?: ScreenshotOptions): Promise<ScreenshotResult> => {
68
+ const screenshot = async (widget: Gtk.Widget, options?: ScreenshotOptions): Promise<ScreenshotResult> => {
69
69
  const scale = options?.scale ?? 1;
70
70
 
71
71
  if (!Number.isFinite(scale) || scale <= 0) {
72
- throw new Error(`Screenshot scale must be a positive number, got ${scale}`);
72
+ throw new Error(`Screenshot scale must be a positive number, got ${String(scale)}`);
73
73
  }
74
74
 
75
75
  return waitFor(() => captureSnapshot(widget, scale), {
@@ -78,6 +78,47 @@ export const screenshot = async (widget: Gtk.Widget, options?: ScreenshotOptions
78
78
  });
79
79
  };
80
80
 
81
+ const firstToplevelWindow = (windows: Gtk.Widget[]): Gtk.Window => {
82
+ const [first] = windows;
83
+
84
+ if (!(first instanceof Gtk.Window)) {
85
+ throw new TypeError("First toplevel is not a Window");
86
+ }
87
+
88
+ return first;
89
+ };
90
+
91
+ const windowAtIndex = (windows: Gtk.Widget[], index: number): Gtk.Window => {
92
+ const indexed = windows[index];
93
+
94
+ if (!(indexed instanceof Gtk.Window)) {
95
+ throw new TypeError(`Window at index ${String(index)} not found`);
96
+ }
97
+
98
+ return indexed;
99
+ };
100
+
101
+ const isWindow = (widget: Gtk.Widget): widget is Gtk.Window => widget instanceof Gtk.Window;
102
+
103
+ const hasMatchingTitle = (window: Gtk.Window, selector: string | RegExp): boolean => {
104
+ const title = window.getTitle() ?? "";
105
+
106
+ return selector instanceof RegExp ? selector.test(title) : title.includes(selector);
107
+ };
108
+
109
+ const describeTitleSelector = (selector: string | RegExp): string =>
110
+ selector instanceof RegExp ? selector.toString() : `"${selector}"`;
111
+
112
+ const windowByTitle = (windows: Gtk.Widget[], selector: string | RegExp): Gtk.Window => {
113
+ const found = windows.filter(isWindow).find((window) => hasMatchingTitle(window, selector));
114
+
115
+ if (!found) {
116
+ throw new Error(`No window found with title matching ${describeTitleSelector(selector)}`);
117
+ }
118
+
119
+ return found;
120
+ };
121
+
81
122
  const resolveWindow = (selector?: WindowSelector): Gtk.Window => {
82
123
  const windows = Gtk.Window.listToplevels();
83
124
 
@@ -86,42 +127,21 @@ const resolveWindow = (selector?: WindowSelector): Gtk.Window => {
86
127
  }
87
128
 
88
129
  if (selector === undefined) {
89
- const [first] = windows;
90
- if (!(first instanceof Gtk.Window)) {
91
- throw new TypeError("First toplevel is not a Window");
92
- }
93
- return first;
130
+ return firstToplevelWindow(windows);
94
131
  }
95
132
 
96
133
  if (typeof selector === "number") {
97
- const indexed = windows[selector];
98
- if (!(indexed instanceof Gtk.Window)) {
99
- throw new TypeError(`Window at index ${selector} not found`);
100
- }
101
- return indexed;
134
+ return windowAtIndex(windows, selector);
102
135
  }
103
136
 
104
- const isRegex = selector instanceof RegExp;
105
- const found = windows.find((w): w is Gtk.Window => {
106
- if (!(w instanceof Gtk.Window)) return false;
107
- const title = w.getTitle() ?? "";
108
- return isRegex ? selector.test(title) : title.includes(selector);
109
- });
110
-
111
- if (!found) {
112
- const pattern = isRegex ? selector.toString() : `"${selector}"`;
113
- throw new Error(`No window found with title matching ${pattern}`);
114
- }
115
- return found;
137
+ return windowByTitle(windows, selector);
116
138
  };
117
139
 
118
140
  const saveScreenshotToTempFile = (result: ScreenshotResult): string => {
119
- const dir = join(tmpdir(), "gtkx-screenshots");
120
- if (!existsSync(dir)) {
121
- mkdirSync(dir, { recursive: true });
122
- }
123
- const filepath = join(dir, `${Date.now()}-screenshot.png`);
141
+ const dir = mkdtempSync(join(tmpdir(), "gtkx-screenshots-"));
142
+ const filepath = join(dir, "screenshot.png");
124
143
  writeFileSync(filepath, Buffer.from(result.data, "base64"));
144
+
125
145
  return filepath;
126
146
  };
127
147
 
@@ -130,7 +150,7 @@ const saveScreenshotToTempFile = (result: ScreenshotResult): string => {
130
150
  *
131
151
  * @param filepath Absolute path of the saved screenshot file.
132
152
  */
133
- export const logScreenshotPath = (filepath: string): void => {
153
+ const logScreenshotPath = (filepath: string): void => {
134
154
  console.log(`Screenshot saved: file://${filepath}`);
135
155
  };
136
156
 
@@ -143,12 +163,15 @@ export const logScreenshotPath = (filepath: string): void => {
143
163
  * @param options Optional scale, timeout, and retry interval.
144
164
  * @returns The base64-encoded PNG data along with its mime type and dimensions.
145
165
  */
146
- export const captureAndSaveScreenshot = async (
166
+ const captureAndSaveScreenshot = async (
147
167
  selector?: WindowSelector,
148
168
  options?: ScreenshotOptions,
149
169
  ): Promise<ScreenshotResult> => {
150
170
  const target = resolveWindow(selector);
151
171
  const result = await screenshot(target, options);
152
172
  logScreenshotPath(saveScreenshotToTempFile(result));
173
+
153
174
  return result;
154
175
  };
176
+
177
+ export { screenshot, logScreenshotPath, captureAndSaveScreenshot };