@gtkx/testing 0.14.0 → 0.16.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.
package/README.md CHANGED
@@ -71,6 +71,7 @@ render(<App />, "com.example.counter");
71
71
  - **React 19** — Hooks, concurrent features, and the component model you know
72
72
  - **Native GTK4 widgets** — Real native controls, not web components in a webview
73
73
  - **Adwaita support** — Modern GNOME styling with Libadwaita components
74
+ - **Declarative animations** — Framer Motion-like API using native Adwaita animations
74
75
  - **Hot Module Replacement** — Fast refresh during development
75
76
  - **TypeScript first** — Full type safety with auto-generated bindings
76
77
  - **CSS-in-JS styling** — Familiar styling patterns adapted for GTK
@@ -80,11 +81,11 @@ render(<App />, "com.example.counter");
80
81
 
81
82
  Explore complete applications in the [`examples/`](./examples) directory:
82
83
 
83
- - **[browser](./examples/browser)** — Simple browser using WebKitWebView
84
84
  - **[gtk-demo](./examples/gtk-demo)** — Full replica of the official GTK demo app
85
85
  - **[hello-world](./examples/hello-world)** — Minimal application showing a counter
86
86
  - **[todo](./examples/todo)** — Full-featured todo application with Adwaita styling and testing
87
87
  - **[x-showcase](./examples/x-showcase)** — Showcase of all x.\* virtual components
88
+ - **[browser](./examples/browser)** — Simple browser using WebKitWebView
88
89
  - **[deploying](./examples/deploying)** — Example of packaging and distributing a GTKX app
89
90
 
90
91
  ## Documentation
@@ -1,5 +1,5 @@
1
+ import { Value } from "@gtkx/ffi/gobject";
1
2
  import type * as Gtk from "@gtkx/ffi/gtk";
2
- import type { Arg } from "@gtkx/native";
3
3
  /**
4
4
  * Emits a GTK signal on a widget or event controller.
5
5
  *
@@ -8,20 +8,21 @@ import type { Arg } from "@gtkx/native";
8
8
  *
9
9
  * @param element - The widget or event controller to emit the signal on
10
10
  * @param signalName - GTK signal name (e.g., "clicked", "activate", "drag-begin")
11
- * @param args - Additional signal arguments
11
+ * @param args - Additional signal arguments as GValues
12
12
  *
13
13
  * @example
14
14
  * ```tsx
15
15
  * import { fireEvent } from "@gtkx/testing";
16
+ * import { Value } from "@gtkx/ffi/gobject";
16
17
  *
17
18
  * // Emit signal on widget
18
19
  * await fireEvent(widget, "clicked");
19
20
  *
20
21
  * // Emit signal on gesture controller
21
22
  * const gesture = widget.observeControllers().getObject(0) as Gtk.GestureDrag;
22
- * await fireEvent(gesture, "drag-begin", { type: { type: "float", size: 64 }, value: 100 }, { type: { type: "float", size: 64 }, value: 100 });
23
+ * await fireEvent(gesture, "drag-begin", Value.newFromDouble(100), Value.newFromDouble(100));
23
24
  * ```
24
25
  *
25
26
  * @see {@link userEvent} for high-level user interactions
26
27
  */
27
- export declare const fireEvent: (element: Gtk.Widget | Gtk.EventController, signalName: string, ...args: Arg[]) => Promise<void>;
28
+ export declare const fireEvent: (element: Gtk.Widget | Gtk.EventController, signalName: string, ...args: Value[]) => Promise<void>;
@@ -1,4 +1,4 @@
1
- import { call } from "@gtkx/native";
1
+ import { signalEmitv, signalLookup, typeFromName, Value } from "@gtkx/ffi/gobject";
2
2
  import { tick } from "./timing.js";
3
3
  /**
4
4
  * Emits a GTK signal on a widget or event controller.
@@ -8,27 +8,27 @@ import { tick } from "./timing.js";
8
8
  *
9
9
  * @param element - The widget or event controller to emit the signal on
10
10
  * @param signalName - GTK signal name (e.g., "clicked", "activate", "drag-begin")
11
- * @param args - Additional signal arguments
11
+ * @param args - Additional signal arguments as GValues
12
12
  *
13
13
  * @example
14
14
  * ```tsx
15
15
  * import { fireEvent } from "@gtkx/testing";
16
+ * import { Value } from "@gtkx/ffi/gobject";
16
17
  *
17
18
  * // Emit signal on widget
18
19
  * await fireEvent(widget, "clicked");
19
20
  *
20
21
  * // Emit signal on gesture controller
21
22
  * const gesture = widget.observeControllers().getObject(0) as Gtk.GestureDrag;
22
- * await fireEvent(gesture, "drag-begin", { type: { type: "float", size: 64 }, value: 100 }, { type: { type: "float", size: 64 }, value: 100 });
23
+ * await fireEvent(gesture, "drag-begin", Value.newFromDouble(100), Value.newFromDouble(100));
23
24
  * ```
24
25
  *
25
26
  * @see {@link userEvent} for high-level user interactions
26
27
  */
27
28
  export const fireEvent = async (element, signalName, ...args) => {
28
- call("libgobject-2.0.so.0", "g_signal_emit_by_name", [
29
- { type: { type: "gobject", ownership: "borrowed" }, value: element.handle },
30
- { type: { type: "string", ownership: "borrowed" }, value: signalName },
31
- ...args,
32
- ], { type: "undefined" });
29
+ const gtype = typeFromName(element.constructor.glibTypeName);
30
+ const signalId = signalLookup(signalName, gtype);
31
+ const instanceValue = Value.newFromObject(element);
32
+ signalEmitv([instanceValue, ...args], signalId, 0, null);
33
33
  await tick();
34
34
  };
@@ -1,4 +1,5 @@
1
1
  import { getNativeId } from "@gtkx/ffi";
2
+ import * as Gtk from "@gtkx/ffi/gtk";
2
3
  import { formatRole } from "./role-helpers.js";
3
4
  import { isApplication } from "./traversal.js";
4
5
  import { getWidgetText } from "./widget-text.js";
@@ -105,7 +106,7 @@ const printWidget = (widget, colors, depth, includeIds) => {
105
106
  };
106
107
  const printContainer = (container, colors, includeIds) => {
107
108
  if (isApplication(container)) {
108
- const windows = container.getWindows();
109
+ const windows = Gtk.Window.listToplevels();
109
110
  return windows.map((window) => printWidget(window, colors, 0, includeIds)).join("");
110
111
  }
111
112
  return printWidget(container, colors, 0, includeIds);
package/dist/render.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { discardAllBatches, start, stop } from "@gtkx/ffi";
2
+ import { start, stop } from "@gtkx/ffi";
3
3
  import * as Gio from "@gtkx/ffi/gio";
4
4
  import { ApplicationContext, GtkApplicationWindow, reconciler } from "@gtkx/react";
5
5
  import { bindQueries } from "./bind-queries.js";
@@ -20,7 +20,6 @@ const update = async (instance, element, fiberRoot) => {
20
20
  }
21
21
  };
22
22
  const handleError = (error) => {
23
- discardAllBatches();
24
23
  lastRenderError = error;
25
24
  };
26
25
  const ensureInitialized = () => {
package/dist/screen.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type * as Gtk from "@gtkx/ffi/gtk";
1
+ import * as Gtk from "@gtkx/ffi/gtk";
2
2
  import { type ScreenshotOptions } from "./screenshot.js";
3
3
  import type { ScreenshotResult } from "./types.js";
4
4
  /** @internal */
package/dist/screen.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import * as Gtk from "@gtkx/ffi/gtk";
4
5
  import { bindQueries } from "./bind-queries.js";
5
6
  import { prettyWidget } from "./pretty-widget.js";
6
7
  import { logRoles } from "./role-helpers.js";
@@ -80,8 +81,7 @@ export const screen = {
80
81
  * ```
81
82
  */
82
83
  screenshot: async (selector, options) => {
83
- const root = getRoot();
84
- const windows = root.getWindows();
84
+ const windows = Gtk.Window.listToplevels();
85
85
  if (windows.length === 0) {
86
86
  throw new Error("No windows available for screenshot");
87
87
  }
@@ -1,4 +1,4 @@
1
- import type * as Gtk from "@gtkx/ffi/gtk";
1
+ import * as Gtk from "@gtkx/ffi/gtk";
2
2
  export type Container = Gtk.Application | Gtk.Widget;
3
3
  export declare const isApplication: (container: Container) => container is Gtk.Application;
4
4
  export declare const traverse: (container: Container) => Generator<Gtk.Widget>;
package/dist/traversal.js CHANGED
@@ -1,3 +1,4 @@
1
+ import * as Gtk from "@gtkx/ffi/gtk";
1
2
  export const isApplication = (container) => "getWindows" in container && typeof container.getWindows === "function";
2
3
  const traverseWidgetTree = function* (root) {
3
4
  yield root;
@@ -7,15 +8,15 @@ const traverseWidgetTree = function* (root) {
7
8
  child = child.getNextSibling();
8
9
  }
9
10
  };
10
- const traverseApplication = function* (app) {
11
- const windows = app.getWindows();
11
+ const traverseWindows = function* () {
12
+ const windows = Gtk.Window.listToplevels();
12
13
  for (const window of windows) {
13
14
  yield* traverseWidgetTree(window);
14
15
  }
15
16
  };
16
17
  export const traverse = function* (container) {
17
18
  if (isApplication(container)) {
18
- yield* traverseApplication(container);
19
+ yield* traverseWindows();
19
20
  }
20
21
  else {
21
22
  yield* traverseWidgetTree(container);
@@ -6,6 +6,13 @@ export type TabOptions = {
6
6
  /** Navigate backwards (Shift+Tab) instead of forwards */
7
7
  shift?: boolean;
8
8
  };
9
+ /**
10
+ * Pointer input actions for simulating mouse interactions.
11
+ *
12
+ * - `"click"` or `"[MouseLeft]"`: Full click (press + release)
13
+ * - `"down"` or `"[MouseLeft>]"`: Press and hold
14
+ * - `"up"` or `"[/MouseLeft]"`: Release
15
+ */
9
16
  export type PointerInput = "click" | "down" | "up" | "[MouseLeft]" | "[MouseLeft>]" | "[/MouseLeft]";
10
17
  /**
11
18
  * User interaction utilities for testing.
@@ -39,13 +46,13 @@ export declare const userEvent: {
39
46
  /**
40
47
  * Double-clicks a widget.
41
48
  *
42
- * Emits two consecutive clicked signals.
49
+ * Emits pressed/released signals with n_press=1, then n_press=2.
43
50
  */
44
51
  dblClick: (element: Gtk.Widget) => Promise<void>;
45
52
  /**
46
53
  * Triple-clicks a widget.
47
54
  *
48
- * Emits three consecutive clicked signals. Useful for text selection.
55
+ * Emits pressed/released signals with n_press=1, 2, then 3. Useful for text selection.
49
56
  */
50
57
  tripleClick: (element: Gtk.Widget) => Promise<void>;
51
58
  /**
@@ -1,7 +1,7 @@
1
1
  import { getNativeObject } from "@gtkx/ffi";
2
2
  import * as Gdk from "@gtkx/ffi/gdk";
3
+ import { signalEmitv, signalLookup, typeFromName, Value } from "@gtkx/ffi/gobject";
3
4
  import * as Gtk from "@gtkx/ffi/gtk";
4
- import { call, createRef } from "@gtkx/native";
5
5
  import { fireEvent } from "./fire-event.js";
6
6
  import { tick } from "./timing.js";
7
7
  import { isEditable } from "./widget.js";
@@ -35,14 +35,25 @@ const click = async (element) => {
35
35
  await fireEvent(element, "clicked");
36
36
  }
37
37
  };
38
+ const emitClickSequence = async (element, nPress) => {
39
+ const controller = getOrCreateController(element, Gtk.GestureClick);
40
+ for (let i = 1; i <= nPress; i++) {
41
+ const args = [
42
+ Value.newFromObject(controller),
43
+ Value.newFromInt(i),
44
+ Value.newFromDouble(0),
45
+ Value.newFromDouble(0),
46
+ ];
47
+ signalEmitv(args, getSignalId(controller, "pressed"), 0, null);
48
+ signalEmitv(args, getSignalId(controller, "released"), 0, null);
49
+ }
50
+ await tick();
51
+ };
38
52
  const dblClick = async (element) => {
39
- await fireEvent(element, "clicked");
40
- await fireEvent(element, "clicked");
53
+ await emitClickSequence(element, 2);
41
54
  };
42
55
  const tripleClick = async (element) => {
43
- await fireEvent(element, "clicked");
44
- await fireEvent(element, "clicked");
45
- await fireEvent(element, "clicked");
56
+ await emitClickSequence(element, 3);
46
57
  };
47
58
  const tab = async (element, options) => {
48
59
  const direction = options?.shift ? Gtk.DirectionType.TAB_BACKWARD : Gtk.DirectionType.TAB_FORWARD;
@@ -166,36 +177,18 @@ const getOrCreateController = (element, controllerType) => {
166
177
  element.addController(controller);
167
178
  return controller;
168
179
  };
169
- const SIGNALS_WITH_RETURN_VALUE = new Set(["key-pressed", "key-released"]);
170
- const emitSignal = (target, signalName, ...args) => {
171
- const signalArgs = args.map((arg) => {
172
- if (arg.type === "float") {
173
- return { type: { type: "float", size: 64 }, value: arg.value };
174
- }
175
- return { type: { type: "int", size: 32, unsigned: true }, value: arg.value };
176
- });
177
- const ffiArgs = [
178
- { type: { type: "gobject", ownership: "borrowed" }, value: target.handle },
179
- { type: { type: "string", ownership: "borrowed" }, value: signalName },
180
- ...signalArgs,
181
- ];
182
- if (SIGNALS_WITH_RETURN_VALUE.has(signalName)) {
183
- const returnRef = createRef(0);
184
- ffiArgs.push({
185
- type: { type: "ref", innerType: { type: "int", size: 32, unsigned: false } },
186
- value: returnRef,
187
- });
188
- }
189
- call("libgobject-2.0.so.0", "g_signal_emit_by_name", ffiArgs, { type: "undefined" });
180
+ const getSignalId = (target, signalName) => {
181
+ const gtype = typeFromName(target.constructor.glibTypeName);
182
+ return signalLookup(signalName, gtype);
190
183
  };
191
184
  const hover = async (element) => {
192
185
  const controller = getOrCreateController(element, Gtk.EventControllerMotion);
193
- emitSignal(controller, "enter", { type: "float", value: 0 }, { type: "float", value: 0 });
186
+ signalEmitv([Value.newFromObject(controller), Value.newFromDouble(0), Value.newFromDouble(0)], getSignalId(controller, "enter"), 0, null);
194
187
  await tick();
195
188
  };
196
189
  const unhover = async (element) => {
197
190
  const controller = getOrCreateController(element, Gtk.EventControllerMotion);
198
- emitSignal(controller, "leave");
191
+ signalEmitv([Value.newFromObject(controller)], getSignalId(controller, "leave"), 0, null);
199
192
  await tick();
200
193
  };
201
194
  const KEY_MAP = {
@@ -256,12 +249,20 @@ const parseKeyboardInput = (input) => {
256
249
  }
257
250
  return actions;
258
251
  };
252
+ let gdkModifierType = null;
259
253
  const keyboard = async (element, input) => {
254
+ gdkModifierType ??= typeFromName("GdkModifierType");
260
255
  const controller = getOrCreateController(element, Gtk.EventControllerKey);
261
256
  const actions = parseKeyboardInput(input);
262
257
  for (const action of actions) {
263
258
  const signalName = action.press ? "key-pressed" : "key-released";
264
- emitSignal(controller, signalName, { type: "int", value: action.keyval }, { type: "int", value: 0 }, { type: "int", value: 0 });
259
+ const returnValue = action.press ? Value.newFromBoolean(false) : null;
260
+ signalEmitv([
261
+ Value.newFromObject(controller),
262
+ Value.newFromUint(action.keyval),
263
+ Value.newFromUint(0),
264
+ Value.newFromFlags(gdkModifierType, 0),
265
+ ], getSignalId(controller, signalName), 0, returnValue);
265
266
  if (action.press && action.keyval === Gdk.KEY_Return && isEditable(element)) {
266
267
  await fireEvent(element, "activate");
267
268
  }
@@ -270,15 +271,27 @@ const keyboard = async (element, input) => {
270
271
  };
271
272
  const pointer = async (element, input) => {
272
273
  const controller = getOrCreateController(element, Gtk.GestureClick);
274
+ const pressedArgs = [
275
+ Value.newFromObject(controller),
276
+ Value.newFromInt(1),
277
+ Value.newFromDouble(0),
278
+ Value.newFromDouble(0),
279
+ ];
280
+ const releasedArgs = [
281
+ Value.newFromObject(controller),
282
+ Value.newFromInt(1),
283
+ Value.newFromDouble(0),
284
+ Value.newFromDouble(0),
285
+ ];
273
286
  if (input === "[MouseLeft]" || input === "click") {
274
- emitSignal(controller, "pressed", { type: "int", value: 1 }, { type: "float", value: 0 }, { type: "float", value: 0 });
275
- emitSignal(controller, "released", { type: "int", value: 1 }, { type: "float", value: 0 }, { type: "float", value: 0 });
287
+ signalEmitv(pressedArgs, getSignalId(controller, "pressed"), 0, null);
288
+ signalEmitv(releasedArgs, getSignalId(controller, "released"), 0, null);
276
289
  }
277
290
  else if (input === "[MouseLeft>]" || input === "down") {
278
- emitSignal(controller, "pressed", { type: "int", value: 1 }, { type: "float", value: 0 }, { type: "float", value: 0 });
291
+ signalEmitv(pressedArgs, getSignalId(controller, "pressed"), 0, null);
279
292
  }
280
293
  else if (input === "[/MouseLeft]" || input === "up") {
281
- emitSignal(controller, "released", { type: "int", value: 1 }, { type: "float", value: 0 }, { type: "float", value: 0 });
294
+ signalEmitv(releasedArgs, getSignalId(controller, "released"), 0, null);
282
295
  }
283
296
  await tick();
284
297
  };
@@ -314,13 +327,13 @@ export const userEvent = {
314
327
  /**
315
328
  * Double-clicks a widget.
316
329
  *
317
- * Emits two consecutive clicked signals.
330
+ * Emits pressed/released signals with n_press=1, then n_press=2.
318
331
  */
319
332
  dblClick,
320
333
  /**
321
334
  * Triple-clicks a widget.
322
335
  *
323
- * Emits three consecutive clicked signals. Useful for text selection.
336
+ * Emits pressed/released signals with n_press=1, 2, then 3. Useful for text selection.
324
337
  */
325
338
  tripleClick,
326
339
  /**
@@ -1,4 +1,4 @@
1
- import { getNativeObject } from "@gtkx/ffi";
1
+ import { getNativeInterface } from "@gtkx/ffi";
2
2
  import * as Gtk from "@gtkx/ffi/gtk";
3
3
  const ROLES_WITH_INTERNAL_LABELS = new Set([
4
4
  Gtk.AccessibleRole.BUTTON,
@@ -77,7 +77,7 @@ export const getWidgetText = (widget) => {
77
77
  case Gtk.AccessibleRole.TEXT_BOX:
78
78
  case Gtk.AccessibleRole.SEARCH_BOX:
79
79
  case Gtk.AccessibleRole.SPIN_BUTTON:
80
- return getNativeObject(widget.handle, Gtk.Editable).getText() ?? null;
80
+ return getNativeInterface(widget, Gtk.Editable)?.getText() ?? null;
81
81
  case Gtk.AccessibleRole.GROUP:
82
82
  return widget.getLabel?.() ?? null;
83
83
  case Gtk.AccessibleRole.WINDOW:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/testing",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Testing utilities for GTKX applications",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -36,14 +36,13 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@gtkx/ffi": "0.14.0",
40
- "@gtkx/native": "0.14.0",
41
- "@gtkx/react": "0.14.0"
39
+ "@gtkx/ffi": "0.16.0",
40
+ "@gtkx/react": "0.16.0"
42
41
  },
43
42
  "devDependencies": {
44
43
  "@types/react-reconciler": "^0.32.3",
45
44
  "react-reconciler": "^0.33.0",
46
- "@gtkx/vitest": "0.14.0"
45
+ "@gtkx/vitest": "0.16.0"
47
46
  },
48
47
  "scripts": {
49
48
  "build": "tsc -b && cp ../../README.md .",