@gtkx/testing 0.16.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.d.ts CHANGED
@@ -13,6 +13,11 @@ export type Config = {
13
13
  * Allows customizing how errors are constructed.
14
14
  */
15
15
  getElementError: (message: string, container: Container) => Error;
16
+ /**
17
+ * Default timeout in milliseconds for async utilities (waitFor, findBy* queries).
18
+ * @default 1000
19
+ */
20
+ asyncUtilTimeout: number;
16
21
  };
17
22
  /**
18
23
  * Returns the current testing library configuration.
package/dist/config.js CHANGED
@@ -4,6 +4,7 @@ const defaultGetElementError = (message, _container) => {
4
4
  const defaultConfig = {
5
5
  showSuggestions: true,
6
6
  getElementError: defaultGetElementError,
7
+ asyncUtilTimeout: 1000,
7
8
  };
8
9
  let currentConfig = { ...defaultConfig };
9
10
  /**
package/dist/index.d.ts CHANGED
@@ -5,13 +5,14 @@ export type { PrettyWidgetOptions } from "./pretty-widget.js";
5
5
  export { prettyWidget } from "./pretty-widget.js";
6
6
  export { findAllByLabelText, findAllByRole, findAllByTestId, findAllByText, findByLabelText, findByRole, findByTestId, findByText, queryAllByLabelText, queryAllByRole, queryAllByTestId, queryAllByText, queryByLabelText, queryByRole, queryByTestId, queryByText, } from "./queries.js";
7
7
  export { cleanup, render } from "./render.js";
8
+ export { renderHook } from "./render-hook.js";
8
9
  export type { RoleInfo } from "./role-helpers.js";
9
10
  export { getRoles, logRoles, prettyRoles } from "./role-helpers.js";
10
11
  export { screen } from "./screen.js";
11
12
  export type { ScreenshotOptions } from "./screenshot.js";
12
13
  export { screenshot } from "./screenshot.js";
13
14
  export { tick } from "./timing.js";
14
- export type { BoundQueries, ByRoleOptions, NormalizerOptions, RenderOptions, RenderResult, ScreenshotResult, TextMatch, TextMatchFunction, TextMatchOptions, WaitForOptions, } from "./types.js";
15
+ export type { BoundQueries, ByRoleOptions, NormalizerOptions, RenderHookOptions, RenderHookResult, RenderOptions, RenderResult, ScreenshotResult, TextMatch, TextMatchFunction, TextMatchOptions, WaitForOptions, } from "./types.js";
15
16
  export type { PointerInput, TabOptions } from "./user-event.js";
16
17
  export { userEvent } from "./user-event.js";
17
18
  export { waitFor, waitForElementToBeRemoved } from "./wait-for.js";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { fireEvent } from "./fire-event.js";
3
3
  export { prettyWidget } from "./pretty-widget.js";
4
4
  export { findAllByLabelText, findAllByRole, findAllByTestId, findAllByText, findByLabelText, findByRole, findByTestId, findByText, queryAllByLabelText, queryAllByRole, queryAllByTestId, queryAllByText, queryByLabelText, queryByRole, queryByTestId, queryByText, } from "./queries.js";
5
5
  export { cleanup, render } from "./render.js";
6
+ export { renderHook } from "./render-hook.js";
6
7
  export { getRoles, logRoles, prettyRoles } from "./role-helpers.js";
7
8
  export { screen } from "./screen.js";
8
9
  export { screenshot } from "./screenshot.js";
package/dist/queries.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { getConfig } from "./config.js";
1
2
  import { buildMultipleFoundError, buildNotFoundError, buildTimeoutError } from "./error-builder.js";
2
3
  import { findAll } from "./traversal.js";
3
4
  import { getWidgetCheckedState, getWidgetExpandedState, getWidgetTestId, getWidgetText } from "./widget-text.js";
4
- const DEFAULT_TIMEOUT = 1000;
5
5
  const DEFAULT_INTERVAL = 50;
6
6
  const buildNormalizer = (options) => {
7
7
  if (options?.normalizer) {
@@ -60,7 +60,8 @@ const matchByRoleOptions = (widget, options) => {
60
60
  return true;
61
61
  };
62
62
  const waitFor = async (callback, options) => {
63
- const { timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
63
+ const config = getConfig();
64
+ const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
64
65
  const startTime = Date.now();
65
66
  let lastError = null;
66
67
  while (Date.now() - startTime < timeout) {
@@ -0,0 +1,40 @@
1
+ import type { RenderHookOptions, RenderHookResult } from "./types.js";
2
+ /**
3
+ * Renders a React hook for testing.
4
+ *
5
+ * Creates a test component that executes the hook and provides utilities
6
+ * for accessing the result, re-rendering with new props, and cleanup.
7
+ *
8
+ * @param callback - Function that calls the hook and returns its result
9
+ * @param options - Render options including initialProps and wrapper
10
+ * @returns A promise resolving to the hook result and utilities
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * import { renderHook } from "@gtkx/testing";
15
+ * import { useState } from "react";
16
+ *
17
+ * test("useState hook", async () => {
18
+ * const { result } = await renderHook(() => useState(0));
19
+ * expect(result.current[0]).toBe(0);
20
+ * });
21
+ * ```
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * import { renderHook } from "@gtkx/testing";
26
+ *
27
+ * test("hook with props", async () => {
28
+ * const { result, rerender } = await renderHook(
29
+ * ({ multiplier }) => useMultiplier(multiplier),
30
+ * { initialProps: { multiplier: 2 } }
31
+ * );
32
+ *
33
+ * expect(result.current).toBe(2);
34
+ *
35
+ * await rerender({ multiplier: 3 });
36
+ * expect(result.current).toBe(3);
37
+ * });
38
+ * ```
39
+ */
40
+ export declare const renderHook: <Result, Props>(callback: (props: Props) => Result, options?: RenderHookOptions<Props>) => Promise<RenderHookResult<Result, Props>>;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useRef } from "react";
3
+ import { render } from "./render.js";
4
+ /**
5
+ * Renders a React hook for testing.
6
+ *
7
+ * Creates a test component that executes the hook and provides utilities
8
+ * for accessing the result, re-rendering with new props, and cleanup.
9
+ *
10
+ * @param callback - Function that calls the hook and returns its result
11
+ * @param options - Render options including initialProps and wrapper
12
+ * @returns A promise resolving to the hook result and utilities
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * import { renderHook } from "@gtkx/testing";
17
+ * import { useState } from "react";
18
+ *
19
+ * test("useState hook", async () => {
20
+ * const { result } = await renderHook(() => useState(0));
21
+ * expect(result.current[0]).toBe(0);
22
+ * });
23
+ * ```
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { renderHook } from "@gtkx/testing";
28
+ *
29
+ * test("hook with props", async () => {
30
+ * const { result, rerender } = await renderHook(
31
+ * ({ multiplier }) => useMultiplier(multiplier),
32
+ * { initialProps: { multiplier: 2 } }
33
+ * );
34
+ *
35
+ * expect(result.current).toBe(2);
36
+ *
37
+ * await rerender({ multiplier: 3 });
38
+ * expect(result.current).toBe(3);
39
+ * });
40
+ * ```
41
+ */
42
+ export const renderHook = async (callback, options) => {
43
+ const resultRef = { current: undefined };
44
+ let currentProps = options?.initialProps;
45
+ const TestComponent = ({ props }) => {
46
+ const result = callback(props);
47
+ const ref = useRef(resultRef);
48
+ ref.current.current = result;
49
+ return null;
50
+ };
51
+ const renderResult = await render(_jsx(TestComponent, { props: currentProps }), {
52
+ wrapper: options?.wrapper ?? false,
53
+ });
54
+ return {
55
+ result: resultRef,
56
+ rerender: async (newProps) => {
57
+ if (newProps !== undefined) {
58
+ currentProps = newProps;
59
+ }
60
+ await renderResult.rerender(_jsx(TestComponent, { props: currentProps }));
61
+ },
62
+ unmount: renderResult.unmount,
63
+ };
64
+ };
package/dist/render.js CHANGED
@@ -26,7 +26,7 @@ const ensureInitialized = () => {
26
26
  application = start("org.gtkx.testing", Gio.ApplicationFlags.NON_UNIQUE);
27
27
  if (!container) {
28
28
  const instance = reconciler.getInstance();
29
- container = instance.createContainer(application, 1, null, false, null, "", handleError, handleError, () => { }, () => { }, null);
29
+ container = instance.createContainer(application, 1, null, false, null, "", handleError, handleError, () => { }, () => { });
30
30
  }
31
31
  return { app: application, container };
32
32
  };
package/dist/types.d.ts CHANGED
@@ -151,3 +151,36 @@ export type ScreenshotResult = {
151
151
  /** Height of the captured image in pixels */
152
152
  height: number;
153
153
  };
154
+ /**
155
+ * Options for {@link renderHook}.
156
+ */
157
+ export type RenderHookOptions<Props> = {
158
+ /**
159
+ * Initial props passed to the hook callback.
160
+ */
161
+ initialProps?: Props;
162
+ /**
163
+ * Wrapper component or boolean.
164
+ * - `false` (default): No wrapper
165
+ * - `true`: Wrap in GtkApplicationWindow
166
+ * - Component: Custom wrapper component
167
+ */
168
+ wrapper?: boolean | ComponentType<{
169
+ children: ReactNode;
170
+ }>;
171
+ };
172
+ /**
173
+ * Result returned by {@link renderHook}.
174
+ *
175
+ * Provides access to the hook result and utilities for re-rendering and cleanup.
176
+ */
177
+ export type RenderHookResult<Result, Props> = {
178
+ /** Object containing the current hook return value */
179
+ result: {
180
+ current: Result;
181
+ };
182
+ /** Re-render the hook with optional new props */
183
+ rerender: (newProps?: Props) => Promise<void>;
184
+ /** Unmount the component containing the hook */
185
+ unmount: () => Promise<void>;
186
+ };
@@ -1,4 +1,3 @@
1
- import { getNativeObject } from "@gtkx/ffi";
2
1
  import * as Gdk from "@gtkx/ffi/gdk";
3
2
  import { signalEmitv, signalLookup, typeFromName, Value } from "@gtkx/ffi/gobject";
4
3
  import * as Gtk from "@gtkx/ffi/gtk";
@@ -58,8 +57,8 @@ const tripleClick = async (element) => {
58
57
  const tab = async (element, options) => {
59
58
  const direction = options?.shift ? Gtk.DirectionType.TAB_BACKWARD : Gtk.DirectionType.TAB_FORWARD;
60
59
  const root = element.getRoot();
61
- if (root) {
62
- getNativeObject(root.handle).childFocus(direction);
60
+ if (root && root instanceof Gtk.Widget) {
61
+ root.childFocus(direction);
63
62
  }
64
63
  await tick();
65
64
  };
@@ -67,16 +66,15 @@ const type = async (element, text) => {
67
66
  if (!isEditable(element)) {
68
67
  throw new Error("Cannot type into element: expected editable widget (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
69
68
  }
70
- const editable = getNativeObject(element.handle, Gtk.Editable);
71
- const currentText = editable.getText();
72
- editable.setText(currentText + text);
69
+ const currentText = element.getText();
70
+ element.setText(currentText + text);
73
71
  await tick();
74
72
  };
75
73
  const clear = async (element) => {
76
74
  if (!isEditable(element)) {
77
75
  throw new Error("Cannot clear element: expected editable widget (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
78
76
  }
79
- getNativeObject(element.handle, Gtk.Editable)?.setText("");
77
+ element.setText("");
80
78
  await tick();
81
79
  };
82
80
  const SELECTABLE_ROLES = new Set([Gtk.AccessibleRole.COMBO_BOX, Gtk.AccessibleRole.LIST]);
package/dist/wait-for.js CHANGED
@@ -1,4 +1,4 @@
1
- const DEFAULT_TIMEOUT = 1000;
1
+ import { getConfig } from "./config.js";
2
2
  const DEFAULT_INTERVAL = 50;
3
3
  /**
4
4
  * Waits for a callback to succeed.
@@ -20,7 +20,8 @@ const DEFAULT_INTERVAL = 50;
20
20
  * ```
21
21
  */
22
22
  export const waitFor = async (callback, options) => {
23
- const { timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
23
+ const config = getConfig();
24
+ const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
24
25
  const startTime = Date.now();
25
26
  let lastError = null;
26
27
  while (Date.now() - startTime < timeout) {
@@ -73,7 +74,8 @@ const isElementRemoved = (element) => {
73
74
  * ```
74
75
  */
75
76
  export const waitForElementToBeRemoved = async (elementOrCallback, options) => {
76
- const { timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
77
+ const config = getConfig();
78
+ const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
77
79
  const initialElement = getElement(elementOrCallback);
78
80
  if (initialElement === null || isElementRemoved(initialElement)) {
79
81
  throw new Error("Element already removed: waitForElementToBeRemoved requires the element to be present initially");
package/dist/widget.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import * as Gtk from "@gtkx/ffi/gtk";
2
- export declare const isEditable: (widget: Gtk.Widget) => boolean;
2
+ export declare const isEditable: (widget: unknown) => widget is Gtk.Editable;
package/dist/widget.js CHANGED
@@ -5,5 +5,8 @@ const EDITABLE_ROLES = new Set([
5
5
  Gtk.AccessibleRole.SPIN_BUTTON,
6
6
  ]);
7
7
  export const isEditable = (widget) => {
8
+ if (!(widget instanceof Gtk.Widget)) {
9
+ return false;
10
+ }
8
11
  return EDITABLE_ROLES.has(widget.getAccessibleRole());
9
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/testing",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Testing utilities for GTKX applications",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -36,13 +36,13 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@gtkx/ffi": "0.16.0",
40
- "@gtkx/react": "0.16.0"
39
+ "@gtkx/react": "0.17.1",
40
+ "@gtkx/ffi": "0.17.1"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/react-reconciler": "^0.32.3",
43
+ "@types/react-reconciler": "^0.33.0",
44
44
  "react-reconciler": "^0.33.0",
45
- "@gtkx/vitest": "0.16.0"
45
+ "@gtkx/vitest": "0.17.1"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -b && cp ../../README.md .",