@gtkx/testing 0.17.2 → 0.18.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 +7 -7
- package/dist/fire-event.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/pretty-widget.js +11 -2
- package/dist/queries.d.ts +14 -11
- package/dist/queries.js +46 -40
- package/dist/render-hook.js +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +36 -13
- package/dist/types.d.ts +26 -13
- package/dist/user-event.d.ts +3 -3
- package/dist/user-event.js +35 -41
- package/dist/wait-for.js +2 -1
- package/dist/widget-text.d.ts +14 -0
- package/dist/widget-text.js +66 -28
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://www.npmjs.com/package/@gtkx/react"><img src="https://img.shields.io/npm/v/@gtkx/react.svg" alt="npm version"></a>
|
|
13
|
-
<a href="https://github.com/
|
|
14
|
-
<a href="https://github.com/
|
|
15
|
-
<a href="https://github.com/
|
|
13
|
+
<a href="https://github.com/gtkx-org/gtkx/actions"><img src="https://img.shields.io/github/actions/workflow/status/eugeniodepalo/gtkx/ci.yml" alt="CI"></a>
|
|
14
|
+
<a href="https://github.com/gtkx-org/gtkx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MPL--2.0-blue.svg" alt="License"></a>
|
|
15
|
+
<a href="https://github.com/gtkx-org/gtkx/discussions"><img src="https://img.shields.io/badge/discussions-GitHub-blue" alt="GitHub Discussions"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
---
|
|
@@ -90,16 +90,16 @@ Explore complete applications in the [`examples/`](./examples) directory:
|
|
|
90
90
|
|
|
91
91
|
## Documentation
|
|
92
92
|
|
|
93
|
-
Visit [https://
|
|
93
|
+
Visit [https://gtkx.dev](https://gtkx.dev) for the full documentation.
|
|
94
94
|
|
|
95
95
|
## Contributing
|
|
96
96
|
|
|
97
|
-
Contributions are welcome! Please see the [contributing guidelines](./CONTRIBUTING.md) and check out the [good first issues](https://github.com/
|
|
97
|
+
Contributions are welcome! Please see the [contributing guidelines](./CONTRIBUTING.md) and check out the [good first issues](https://github.com/gtkx-org/gtkx/labels/good%20first%20issue).
|
|
98
98
|
|
|
99
99
|
## Community
|
|
100
100
|
|
|
101
|
-
- [GitHub Discussions](https://github.com/
|
|
102
|
-
- [Issue Tracker](https://github.com/
|
|
101
|
+
- [GitHub Discussions](https://github.com/gtkx-org/gtkx/discussions) — Questions, ideas, and general discussion
|
|
102
|
+
- [Issue Tracker](https://github.com/gtkx-org/gtkx/issues) — Bug reports and feature requests
|
|
103
103
|
|
|
104
104
|
## License
|
|
105
105
|
|
package/dist/fire-event.js
CHANGED
|
@@ -29,6 +29,6 @@ export const fireEvent = async (element, signalName, ...args) => {
|
|
|
29
29
|
const gtype = typeFromName(element.constructor.glibTypeName);
|
|
30
30
|
const signalId = signalLookup(signalName, gtype);
|
|
31
31
|
const instanceValue = Value.newFromObject(element);
|
|
32
|
-
signalEmitv([instanceValue, ...args], signalId, 0
|
|
32
|
+
signalEmitv([instanceValue, ...args], signalId, 0);
|
|
33
33
|
await tick();
|
|
34
34
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,8 @@ export { screen } from "./screen.js";
|
|
|
12
12
|
export type { ScreenshotOptions } from "./screenshot.js";
|
|
13
13
|
export { screenshot } from "./screenshot.js";
|
|
14
14
|
export { tick } from "./timing.js";
|
|
15
|
-
export type {
|
|
15
|
+
export type { Container } from "./traversal.js";
|
|
16
|
+
export type { BoundQueries, ByRoleOptions, NormalizerOptions, RenderHookOptions, RenderHookResult, RenderOptions, RenderResult, ScreenshotResult, TextMatch, TextMatchFunction, TextMatchOptions, WaitForOptions, WrapperComponent, } from "./types.js";
|
|
16
17
|
export type { PointerInput, TabOptions } from "./user-event.js";
|
|
17
18
|
export { userEvent } from "./user-event.js";
|
|
18
19
|
export { waitFor, waitForElementToBeRemoved } from "./wait-for.js";
|
package/dist/pretty-widget.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { getNativeId } from "@gtkx/ffi";
|
|
2
1
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
3
2
|
import { formatRole } from "./role-helpers.js";
|
|
4
3
|
import { isApplication } from "./traversal.js";
|
|
5
4
|
import { getWidgetText } from "./widget-text.js";
|
|
6
5
|
const DEFAULT_MAX_LENGTH = 7000;
|
|
7
6
|
const INDENT = " ";
|
|
7
|
+
const debugIdMap = new WeakMap();
|
|
8
|
+
let nextDebugId = 0;
|
|
9
|
+
const getWidgetDebugId = (widget) => {
|
|
10
|
+
let id = debugIdMap.get(widget);
|
|
11
|
+
if (!id) {
|
|
12
|
+
id = String(nextDebugId++);
|
|
13
|
+
debugIdMap.set(widget, id);
|
|
14
|
+
}
|
|
15
|
+
return id;
|
|
16
|
+
};
|
|
8
17
|
const shouldHighlight = () => {
|
|
9
18
|
if (typeof process === "undefined")
|
|
10
19
|
return false;
|
|
@@ -42,7 +51,7 @@ const escapeAttrValue = (value) => {
|
|
|
42
51
|
const formatAttributes = (widget, colors, includeIds) => {
|
|
43
52
|
const attrs = [];
|
|
44
53
|
if (includeIds) {
|
|
45
|
-
attrs.push(["id",
|
|
54
|
+
attrs.push(["id", getWidgetDebugId(widget)]);
|
|
46
55
|
}
|
|
47
56
|
const name = widget.getName();
|
|
48
57
|
if (name) {
|
package/dist/queries.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
import { type Container } from "./traversal.js";
|
|
3
3
|
import type { ByRoleOptions, TextMatch, TextMatchOptions } from "./types.js";
|
|
4
4
|
/**
|
|
@@ -21,12 +21,16 @@ export declare const queryAllByRole: (container: Container, role: Gtk.Accessible
|
|
|
21
21
|
*/
|
|
22
22
|
export declare const queryByRole: (container: Container, role: Gtk.AccessibleRole, options?: ByRoleOptions) => Gtk.Widget | null;
|
|
23
23
|
/**
|
|
24
|
-
* Finds all elements
|
|
24
|
+
* Finds all elements that are labelled by a GtkLabel whose text matches.
|
|
25
|
+
*
|
|
26
|
+
* Uses GtkLabel's mnemonic widget association to find form elements
|
|
27
|
+
* by their label text. Only returns widgets that are properly labelled
|
|
28
|
+
* via GtkLabel's mnemonic-widget property.
|
|
25
29
|
*
|
|
26
30
|
* @param container - The container to search within
|
|
27
|
-
* @param text -
|
|
31
|
+
* @param text - Label text to match (string, RegExp, or custom matcher)
|
|
28
32
|
* @param options - Query options including normalization
|
|
29
|
-
* @returns Array of
|
|
33
|
+
* @returns Array of labelled widgets (empty if none found)
|
|
30
34
|
*/
|
|
31
35
|
export declare const queryAllByLabelText: (container: Container, text: TextMatch, options?: TextMatchOptions) => Gtk.Widget[];
|
|
32
36
|
/**
|
|
@@ -103,20 +107,19 @@ export declare const findByRole: (container: Container, role: Gtk.AccessibleRole
|
|
|
103
107
|
*/
|
|
104
108
|
export declare const findAllByRole: (container: Container, role: Gtk.AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
105
109
|
/**
|
|
106
|
-
* Finds a single element by
|
|
110
|
+
* Finds a single element that is labelled by a GtkLabel whose text matches.
|
|
107
111
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
112
|
+
* Waits for the element to appear, throwing if not found within timeout.
|
|
113
|
+
* Uses GtkLabel's mnemonic widget association to find form elements.
|
|
110
114
|
*
|
|
111
115
|
* @param container - The container to search within
|
|
112
|
-
* @param text -
|
|
116
|
+
* @param text - Label text to match (string, RegExp, or custom matcher)
|
|
113
117
|
* @param options - Query options including normalization and timeout
|
|
114
|
-
* @returns Promise resolving to the
|
|
118
|
+
* @returns Promise resolving to the labelled widget
|
|
115
119
|
*
|
|
116
120
|
* @example
|
|
117
121
|
* ```tsx
|
|
118
|
-
* const
|
|
119
|
-
* const input = await findByLabelText(container, /search/i);
|
|
122
|
+
* const input = await findByLabelText(container, "Username");
|
|
120
123
|
* ```
|
|
121
124
|
*/
|
|
122
125
|
export declare const findByLabelText: (container: Container, text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
package/dist/queries.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { buildMultipleFoundError, buildNotFoundError
|
|
3
|
-
import { findAll } from "./traversal.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
1
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
import { buildMultipleFoundError, buildNotFoundError } from "./error-builder.js";
|
|
3
|
+
import { findAll, traverse } from "./traversal.js";
|
|
4
|
+
import { waitFor } from "./wait-for.js";
|
|
5
|
+
import { getWidgetCheckedState, getWidgetExpandedState, getWidgetPressedState, getWidgetSelectedState, getWidgetTestId, getWidgetText, } from "./widget-text.js";
|
|
6
6
|
const buildNormalizer = (options) => {
|
|
7
7
|
if (options?.normalizer) {
|
|
8
8
|
return options.normalizer;
|
|
@@ -37,7 +37,9 @@ const matchText = (actual, expected, widget, options) => {
|
|
|
37
37
|
}
|
|
38
38
|
const normalizedExpected = normalizeText(expected, options);
|
|
39
39
|
const exact = options?.exact ?? true;
|
|
40
|
-
return exact
|
|
40
|
+
return exact
|
|
41
|
+
? normalizedActual === normalizedExpected
|
|
42
|
+
: normalizedActual.toLowerCase().includes(normalizedExpected.toLowerCase());
|
|
41
43
|
};
|
|
42
44
|
const matchByRoleOptions = (widget, options) => {
|
|
43
45
|
if (!options)
|
|
@@ -52,32 +54,22 @@ const matchByRoleOptions = (widget, options) => {
|
|
|
52
54
|
if (checked !== options.checked)
|
|
53
55
|
return false;
|
|
54
56
|
}
|
|
57
|
+
if (options.pressed !== undefined) {
|
|
58
|
+
const pressed = getWidgetPressedState(widget);
|
|
59
|
+
if (pressed !== options.pressed)
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
55
62
|
if (options.expanded !== undefined) {
|
|
56
63
|
const expanded = getWidgetExpandedState(widget);
|
|
57
64
|
if (expanded !== options.expanded)
|
|
58
65
|
return false;
|
|
59
66
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
|
|
65
|
-
const startTime = Date.now();
|
|
66
|
-
let lastError = null;
|
|
67
|
-
while (Date.now() - startTime < timeout) {
|
|
68
|
-
try {
|
|
69
|
-
return await callback();
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
lastError = error;
|
|
73
|
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const timeoutError = buildTimeoutError(timeout, lastError);
|
|
77
|
-
if (onTimeout) {
|
|
78
|
-
throw onTimeout(timeoutError);
|
|
67
|
+
if (options.selected !== undefined) {
|
|
68
|
+
const selected = getWidgetSelectedState(widget);
|
|
69
|
+
if (selected !== options.selected)
|
|
70
|
+
return false;
|
|
79
71
|
}
|
|
80
|
-
|
|
72
|
+
return true;
|
|
81
73
|
};
|
|
82
74
|
/**
|
|
83
75
|
* Finds all elements matching a role without throwing.
|
|
@@ -111,18 +103,33 @@ export const queryByRole = (container, role, options) => {
|
|
|
111
103
|
return matches[0] ?? null;
|
|
112
104
|
};
|
|
113
105
|
/**
|
|
114
|
-
* Finds all elements
|
|
106
|
+
* Finds all elements that are labelled by a GtkLabel whose text matches.
|
|
107
|
+
*
|
|
108
|
+
* Uses GtkLabel's mnemonic widget association to find form elements
|
|
109
|
+
* by their label text. Only returns widgets that are properly labelled
|
|
110
|
+
* via GtkLabel's mnemonic-widget property.
|
|
115
111
|
*
|
|
116
112
|
* @param container - The container to search within
|
|
117
|
-
* @param text -
|
|
113
|
+
* @param text - Label text to match (string, RegExp, or custom matcher)
|
|
118
114
|
* @param options - Query options including normalization
|
|
119
|
-
* @returns Array of
|
|
115
|
+
* @returns Array of labelled widgets (empty if none found)
|
|
120
116
|
*/
|
|
121
117
|
export const queryAllByLabelText = (container, text, options) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
const results = [];
|
|
119
|
+
for (const node of traverse(container)) {
|
|
120
|
+
if (!(node instanceof Gtk.Label))
|
|
121
|
+
continue;
|
|
122
|
+
const labelText = node.getLabel();
|
|
123
|
+
if (!labelText)
|
|
124
|
+
continue;
|
|
125
|
+
if (!matchText(labelText, text, node, options))
|
|
126
|
+
continue;
|
|
127
|
+
const target = node.getMnemonicWidget();
|
|
128
|
+
if (target) {
|
|
129
|
+
results.push(target);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
126
133
|
};
|
|
127
134
|
/**
|
|
128
135
|
* Finds a single element matching label text without throwing.
|
|
@@ -286,20 +293,19 @@ export const findAllByRole = async (container, role, options) => waitFor(() => g
|
|
|
286
293
|
timeout: options?.timeout,
|
|
287
294
|
});
|
|
288
295
|
/**
|
|
289
|
-
* Finds a single element by
|
|
296
|
+
* Finds a single element that is labelled by a GtkLabel whose text matches.
|
|
290
297
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
298
|
+
* Waits for the element to appear, throwing if not found within timeout.
|
|
299
|
+
* Uses GtkLabel's mnemonic widget association to find form elements.
|
|
293
300
|
*
|
|
294
301
|
* @param container - The container to search within
|
|
295
|
-
* @param text -
|
|
302
|
+
* @param text - Label text to match (string, RegExp, or custom matcher)
|
|
296
303
|
* @param options - Query options including normalization and timeout
|
|
297
|
-
* @returns Promise resolving to the
|
|
304
|
+
* @returns Promise resolving to the labelled widget
|
|
298
305
|
*
|
|
299
306
|
* @example
|
|
300
307
|
* ```tsx
|
|
301
|
-
* const
|
|
302
|
-
* const input = await findByLabelText(container, /search/i);
|
|
308
|
+
* const input = await findByLabelText(container, "Username");
|
|
303
309
|
* ```
|
|
304
310
|
*/
|
|
305
311
|
export const findByLabelText = async (container, text, options) => waitFor(() => getByLabelText(container, text, options), {
|
package/dist/render-hook.js
CHANGED
|
@@ -49,7 +49,7 @@ export const renderHook = async (callback, options) => {
|
|
|
49
49
|
return null;
|
|
50
50
|
};
|
|
51
51
|
const renderResult = await render(_jsx(TestComponent, { props: currentProps }), {
|
|
52
|
-
wrapper: options?.wrapper ??
|
|
52
|
+
wrapper: options?.wrapper ?? true,
|
|
53
53
|
});
|
|
54
54
|
return {
|
|
55
55
|
result: resultRef,
|
package/dist/render.d.ts
CHANGED
package/dist/render.js
CHANGED
|
@@ -2,10 +2,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
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
|
+
import { createRef } from "react";
|
|
5
6
|
import { bindQueries } from "./bind-queries.js";
|
|
6
7
|
import { prettyWidget } from "./pretty-widget.js";
|
|
7
8
|
import { setScreenRoot } from "./screen.js";
|
|
8
9
|
import { tick } from "./timing.js";
|
|
10
|
+
import { isApplication, traverse } from "./traversal.js";
|
|
9
11
|
let application = null;
|
|
10
12
|
let container = null;
|
|
11
13
|
let lastRenderError = null;
|
|
@@ -30,14 +32,30 @@ const ensureInitialized = () => {
|
|
|
30
32
|
}
|
|
31
33
|
return { app: application, container };
|
|
32
34
|
};
|
|
33
|
-
const DefaultWrapper = ({ children }) => (_jsx(GtkApplicationWindow, { defaultWidth: 800, defaultHeight: 600, children: children }));
|
|
34
|
-
const
|
|
35
|
-
|
|
35
|
+
const DefaultWrapper = ({ children, ref }) => (_jsx(GtkApplicationWindow, { ref: ref, defaultWidth: 800, defaultHeight: 600, children: children }));
|
|
36
|
+
const findFirstWidget = (root) => {
|
|
37
|
+
for (const widget of traverse(root)) {
|
|
38
|
+
if (isApplication(root))
|
|
39
|
+
return widget;
|
|
40
|
+
return root;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
44
|
+
const wrapElement = (element, wrapperRef, wrapper) => {
|
|
45
|
+
if (wrapper === false || wrapper === undefined)
|
|
36
46
|
return element;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
const Wrapper = wrapper === true ? DefaultWrapper : wrapper;
|
|
48
|
+
return _jsx(Wrapper, { ref: wrapperRef, children: element });
|
|
49
|
+
};
|
|
50
|
+
const resolveContainer = (wrapper, wrapperRef, baseElement) => {
|
|
51
|
+
if (wrapper !== false && wrapper !== undefined && wrapperRef.current) {
|
|
52
|
+
return wrapperRef.current;
|
|
53
|
+
}
|
|
54
|
+
const firstWidget = findFirstWidget(baseElement);
|
|
55
|
+
if (!firstWidget) {
|
|
56
|
+
throw new Error("render() produced no widgets. Ensure the element renders visible content.");
|
|
57
|
+
}
|
|
58
|
+
return firstWidget;
|
|
41
59
|
};
|
|
42
60
|
/**
|
|
43
61
|
* Renders a React element for testing.
|
|
@@ -66,18 +84,23 @@ const wrapElement = (element, wrapper = true) => {
|
|
|
66
84
|
export const render = async (element, options) => {
|
|
67
85
|
const { app: application, container: fiberRoot } = ensureInitialized();
|
|
68
86
|
const instance = reconciler.getInstance();
|
|
69
|
-
const
|
|
87
|
+
const baseElement = options?.baseElement ?? application;
|
|
88
|
+
const wrapper = options?.wrapper ?? true;
|
|
89
|
+
const wrapperRef = createRef();
|
|
90
|
+
const wrappedElement = wrapElement(element, wrapperRef, wrapper);
|
|
70
91
|
const withContext = _jsx(ApplicationContext.Provider, { value: application, children: wrappedElement });
|
|
71
92
|
await update(instance, withContext, fiberRoot);
|
|
72
93
|
setScreenRoot(application);
|
|
73
94
|
return {
|
|
74
|
-
container:
|
|
75
|
-
|
|
95
|
+
container: resolveContainer(wrapper, wrapperRef, baseElement),
|
|
96
|
+
baseElement,
|
|
97
|
+
...bindQueries(baseElement),
|
|
76
98
|
unmount: () => update(instance, null, fiberRoot),
|
|
77
|
-
rerender: (newElement) => {
|
|
78
|
-
const
|
|
99
|
+
rerender: async (newElement) => {
|
|
100
|
+
const newWrapperRef = createRef();
|
|
101
|
+
const wrapped = wrapElement(newElement, newWrapperRef, wrapper);
|
|
79
102
|
const withCtx = _jsx(ApplicationContext.Provider, { value: application, children: wrapped });
|
|
80
|
-
|
|
103
|
+
await update(instance, withCtx, fiberRoot);
|
|
81
104
|
},
|
|
82
105
|
debug: () => {
|
|
83
106
|
console.log(prettyWidget(application));
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
-
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import type { ComponentType, ReactNode, Ref } from "react";
|
|
3
|
+
import type { Container } from "./traversal.js";
|
|
3
4
|
/**
|
|
4
5
|
* Custom function for matching text content.
|
|
5
6
|
*
|
|
@@ -69,6 +70,14 @@ export type WaitForOptions = {
|
|
|
69
70
|
/** Custom error handler called on timeout */
|
|
70
71
|
onTimeout?: (error: Error) => Error;
|
|
71
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* A wrapper component that exposes its root GTK widget via `ref`.
|
|
75
|
+
* Accept `ref` as a prop and pass it through to the root intrinsic element.
|
|
76
|
+
*/
|
|
77
|
+
export type WrapperComponent = ComponentType<{
|
|
78
|
+
children: ReactNode;
|
|
79
|
+
ref?: Ref<Gtk.Widget>;
|
|
80
|
+
}>;
|
|
72
81
|
/**
|
|
73
82
|
* Options for {@link render}.
|
|
74
83
|
*/
|
|
@@ -77,11 +86,15 @@ export type RenderOptions = {
|
|
|
77
86
|
* Wrapper component or boolean.
|
|
78
87
|
* - `true` (default): Wrap in GtkApplicationWindow
|
|
79
88
|
* - `false`: No wrapper
|
|
80
|
-
* - Component: Custom wrapper
|
|
89
|
+
* - Component: Custom wrapper that passes `ref` to its root element
|
|
90
|
+
*/
|
|
91
|
+
wrapper?: boolean | WrapperComponent;
|
|
92
|
+
/**
|
|
93
|
+
* The element queries are bound to.
|
|
94
|
+
* Defaults to the GTK Application (searches all toplevel windows).
|
|
95
|
+
* Provide a specific widget or application to scope queries.
|
|
81
96
|
*/
|
|
82
|
-
|
|
83
|
-
children: ReactNode;
|
|
84
|
-
}>;
|
|
97
|
+
baseElement?: Container;
|
|
85
98
|
};
|
|
86
99
|
/**
|
|
87
100
|
* Query methods bound to a container.
|
|
@@ -129,8 +142,10 @@ export type BoundQueries = {
|
|
|
129
142
|
* Provides query methods and utilities for testing rendered components.
|
|
130
143
|
*/
|
|
131
144
|
export type RenderResult = BoundQueries & {
|
|
132
|
-
/** The
|
|
133
|
-
container: Gtk.
|
|
145
|
+
/** The direct container widget wrapping the rendered content */
|
|
146
|
+
container: Gtk.Widget;
|
|
147
|
+
/** The element queries are bound to (defaults to the GTK Application) */
|
|
148
|
+
baseElement: Container;
|
|
134
149
|
/** Unmount the rendered component */
|
|
135
150
|
unmount: () => Promise<void>;
|
|
136
151
|
/** Re-render with a new element */
|
|
@@ -161,13 +176,11 @@ export type RenderHookOptions<Props> = {
|
|
|
161
176
|
initialProps?: Props;
|
|
162
177
|
/**
|
|
163
178
|
* Wrapper component or boolean.
|
|
164
|
-
* - `
|
|
165
|
-
* - `
|
|
166
|
-
* - Component: Custom wrapper
|
|
179
|
+
* - `true` (default): Wrap in GtkApplicationWindow
|
|
180
|
+
* - `false`: No wrapper
|
|
181
|
+
* - Component: Custom wrapper that passes `ref` to its root element
|
|
167
182
|
*/
|
|
168
|
-
wrapper?: boolean |
|
|
169
|
-
children: ReactNode;
|
|
170
|
-
}>;
|
|
183
|
+
wrapper?: boolean | WrapperComponent;
|
|
171
184
|
};
|
|
172
185
|
/**
|
|
173
186
|
* Result returned by {@link renderHook}.
|
package/dist/user-event.d.ts
CHANGED
|
@@ -37,10 +37,10 @@ export type PointerInput = "click" | "down" | "up" | "[MouseLeft]" | "[MouseLeft
|
|
|
37
37
|
*/
|
|
38
38
|
export declare const userEvent: {
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Activates a widget.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* Uses GTK's native {@link Gtk.Widget.activate} to trigger the widget's
|
|
43
|
+
* default action — clicking buttons, toggling checkboxes/switches, etc.
|
|
44
44
|
*/
|
|
45
45
|
click: (element: Gtk.Widget) => Promise<void>;
|
|
46
46
|
/**
|
package/dist/user-event.js
CHANGED
|
@@ -4,35 +4,9 @@ import * as Gtk from "@gtkx/ffi/gtk";
|
|
|
4
4
|
import { fireEvent } from "./fire-event.js";
|
|
5
5
|
import { tick } from "./timing.js";
|
|
6
6
|
import { isEditable } from "./widget.js";
|
|
7
|
-
const TOGGLEABLE_ROLES = new Set([
|
|
8
|
-
Gtk.AccessibleRole.CHECKBOX,
|
|
9
|
-
Gtk.AccessibleRole.RADIO,
|
|
10
|
-
Gtk.AccessibleRole.TOGGLE_BUTTON,
|
|
11
|
-
Gtk.AccessibleRole.SWITCH,
|
|
12
|
-
]);
|
|
13
|
-
const isToggleable = (widget) => {
|
|
14
|
-
return TOGGLEABLE_ROLES.has(widget.getAccessibleRole());
|
|
15
|
-
};
|
|
16
7
|
const click = async (element) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (role === Gtk.AccessibleRole.CHECKBOX || role === Gtk.AccessibleRole.RADIO) {
|
|
20
|
-
const checkButton = element;
|
|
21
|
-
checkButton.setActive(!checkButton.getActive());
|
|
22
|
-
}
|
|
23
|
-
else if (role === Gtk.AccessibleRole.SWITCH) {
|
|
24
|
-
const switchWidget = element;
|
|
25
|
-
switchWidget.setActive(!switchWidget.getActive());
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
const toggleButton = element;
|
|
29
|
-
toggleButton.setActive(!toggleButton.getActive());
|
|
30
|
-
}
|
|
31
|
-
await tick();
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
await fireEvent(element, "clicked");
|
|
35
|
-
}
|
|
8
|
+
element.activate();
|
|
9
|
+
await tick();
|
|
36
10
|
};
|
|
37
11
|
const emitClickSequence = async (element, nPress) => {
|
|
38
12
|
const controller = getOrCreateController(element, Gtk.GestureClick);
|
|
@@ -43,8 +17,8 @@ const emitClickSequence = async (element, nPress) => {
|
|
|
43
17
|
Value.newFromDouble(0),
|
|
44
18
|
Value.newFromDouble(0),
|
|
45
19
|
];
|
|
46
|
-
signalEmitv(args, getSignalId(controller, "pressed"), 0
|
|
47
|
-
signalEmitv(args, getSignalId(controller, "released"), 0
|
|
20
|
+
signalEmitv(args, getSignalId(controller, "pressed"), 0);
|
|
21
|
+
signalEmitv(args, getSignalId(controller, "released"), 0);
|
|
48
22
|
}
|
|
49
23
|
await tick();
|
|
50
24
|
};
|
|
@@ -181,12 +155,12 @@ const getSignalId = (target, signalName) => {
|
|
|
181
155
|
};
|
|
182
156
|
const hover = async (element) => {
|
|
183
157
|
const controller = getOrCreateController(element, Gtk.EventControllerMotion);
|
|
184
|
-
signalEmitv([Value.newFromObject(controller), Value.newFromDouble(0), Value.newFromDouble(0)], getSignalId(controller, "enter"), 0
|
|
158
|
+
signalEmitv([Value.newFromObject(controller), Value.newFromDouble(0), Value.newFromDouble(0)], getSignalId(controller, "enter"), 0);
|
|
185
159
|
await tick();
|
|
186
160
|
};
|
|
187
161
|
const unhover = async (element) => {
|
|
188
162
|
const controller = getOrCreateController(element, Gtk.EventControllerMotion);
|
|
189
|
-
signalEmitv([Value.newFromObject(controller)], getSignalId(controller, "leave"), 0
|
|
163
|
+
signalEmitv([Value.newFromObject(controller)], getSignalId(controller, "leave"), 0);
|
|
190
164
|
await tick();
|
|
191
165
|
};
|
|
192
166
|
const KEY_MAP = {
|
|
@@ -247,19 +221,39 @@ const parseKeyboardInput = (input) => {
|
|
|
247
221
|
}
|
|
248
222
|
return actions;
|
|
249
223
|
};
|
|
224
|
+
const MODIFIER_KEYVAL_TO_MASK = {
|
|
225
|
+
[Gdk.KEY_Shift_L]: Gdk.ModifierType.SHIFT_MASK,
|
|
226
|
+
[Gdk.KEY_Shift_R]: Gdk.ModifierType.SHIFT_MASK,
|
|
227
|
+
[Gdk.KEY_Control_L]: Gdk.ModifierType.CONTROL_MASK,
|
|
228
|
+
[Gdk.KEY_Control_R]: Gdk.ModifierType.CONTROL_MASK,
|
|
229
|
+
[Gdk.KEY_Alt_L]: Gdk.ModifierType.ALT_MASK,
|
|
230
|
+
[Gdk.KEY_Alt_R]: Gdk.ModifierType.ALT_MASK,
|
|
231
|
+
[Gdk.KEY_Meta_L]: Gdk.ModifierType.META_MASK,
|
|
232
|
+
[Gdk.KEY_Meta_R]: Gdk.ModifierType.META_MASK,
|
|
233
|
+
};
|
|
250
234
|
let gdkModifierType = null;
|
|
251
235
|
const keyboard = async (element, input) => {
|
|
252
236
|
gdkModifierType ??= typeFromName("GdkModifierType");
|
|
253
237
|
const controller = getOrCreateController(element, Gtk.EventControllerKey);
|
|
254
238
|
const actions = parseKeyboardInput(input);
|
|
239
|
+
let modifierState = 0;
|
|
255
240
|
for (const action of actions) {
|
|
241
|
+
const mask = MODIFIER_KEYVAL_TO_MASK[action.keyval];
|
|
242
|
+
if (mask) {
|
|
243
|
+
if (action.press) {
|
|
244
|
+
modifierState |= mask;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
modifierState &= ~mask;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
256
250
|
const signalName = action.press ? "key-pressed" : "key-released";
|
|
257
|
-
const returnValue = action.press ? Value.newFromBoolean(false) :
|
|
251
|
+
const returnValue = action.press ? Value.newFromBoolean(false) : undefined;
|
|
258
252
|
signalEmitv([
|
|
259
253
|
Value.newFromObject(controller),
|
|
260
254
|
Value.newFromUint(action.keyval),
|
|
261
255
|
Value.newFromUint(0),
|
|
262
|
-
Value.newFromFlags(gdkModifierType,
|
|
256
|
+
Value.newFromFlags(gdkModifierType, modifierState),
|
|
263
257
|
], getSignalId(controller, signalName), 0, returnValue);
|
|
264
258
|
if (action.press && action.keyval === Gdk.KEY_Return && isEditable(element)) {
|
|
265
259
|
await fireEvent(element, "activate");
|
|
@@ -282,14 +276,14 @@ const pointer = async (element, input) => {
|
|
|
282
276
|
Value.newFromDouble(0),
|
|
283
277
|
];
|
|
284
278
|
if (input === "[MouseLeft]" || input === "click") {
|
|
285
|
-
signalEmitv(pressedArgs, getSignalId(controller, "pressed"), 0
|
|
286
|
-
signalEmitv(releasedArgs, getSignalId(controller, "released"), 0
|
|
279
|
+
signalEmitv(pressedArgs, getSignalId(controller, "pressed"), 0);
|
|
280
|
+
signalEmitv(releasedArgs, getSignalId(controller, "released"), 0);
|
|
287
281
|
}
|
|
288
282
|
else if (input === "[MouseLeft>]" || input === "down") {
|
|
289
|
-
signalEmitv(pressedArgs, getSignalId(controller, "pressed"), 0
|
|
283
|
+
signalEmitv(pressedArgs, getSignalId(controller, "pressed"), 0);
|
|
290
284
|
}
|
|
291
285
|
else if (input === "[/MouseLeft]" || input === "up") {
|
|
292
|
-
signalEmitv(releasedArgs, getSignalId(controller, "released"), 0
|
|
286
|
+
signalEmitv(releasedArgs, getSignalId(controller, "released"), 0);
|
|
293
287
|
}
|
|
294
288
|
await tick();
|
|
295
289
|
};
|
|
@@ -316,10 +310,10 @@ const pointer = async (element, input) => {
|
|
|
316
310
|
*/
|
|
317
311
|
export const userEvent = {
|
|
318
312
|
/**
|
|
319
|
-
*
|
|
313
|
+
* Activates a widget.
|
|
320
314
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
315
|
+
* Uses GTK's native {@link Gtk.Widget.activate} to trigger the widget's
|
|
316
|
+
* default action — clicking buttons, toggling checkboxes/switches, etc.
|
|
323
317
|
*/
|
|
324
318
|
click,
|
|
325
319
|
/**
|
package/dist/wait-for.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getConfig } from "./config.js";
|
|
2
|
+
import { buildTimeoutError } from "./error-builder.js";
|
|
2
3
|
const DEFAULT_INTERVAL = 50;
|
|
3
4
|
/**
|
|
4
5
|
* Waits for a callback to succeed.
|
|
@@ -33,7 +34,7 @@ export const waitFor = async (callback, options) => {
|
|
|
33
34
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
const timeoutError =
|
|
37
|
+
const timeoutError = buildTimeoutError(timeout, lastError);
|
|
37
38
|
if (onTimeout) {
|
|
38
39
|
throw onTimeout(timeoutError);
|
|
39
40
|
}
|
package/dist/widget-text.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ export declare const getWidgetTestId: (widget: Gtk.Widget) => string | null;
|
|
|
20
20
|
* @returns The checked state or null if not applicable
|
|
21
21
|
*/
|
|
22
22
|
export declare const getWidgetCheckedState: (widget: Gtk.Widget) => boolean | null;
|
|
23
|
+
/**
|
|
24
|
+
* Gets the pressed state from toggle button widgets.
|
|
25
|
+
*
|
|
26
|
+
* @param widget - The widget to get the pressed state from
|
|
27
|
+
* @returns The pressed state or null if not applicable
|
|
28
|
+
*/
|
|
29
|
+
export declare const getWidgetPressedState: (widget: Gtk.Widget) => boolean | null;
|
|
23
30
|
/**
|
|
24
31
|
* Gets the expanded state from expander widgets.
|
|
25
32
|
*
|
|
@@ -27,3 +34,10 @@ export declare const getWidgetCheckedState: (widget: Gtk.Widget) => boolean | nu
|
|
|
27
34
|
* @returns The expanded state or null if not applicable
|
|
28
35
|
*/
|
|
29
36
|
export declare const getWidgetExpandedState: (widget: Gtk.Widget) => boolean | null;
|
|
37
|
+
/**
|
|
38
|
+
* Gets the selected state from selectable widgets.
|
|
39
|
+
*
|
|
40
|
+
* @param widget - The widget to get the selected state from
|
|
41
|
+
* @returns The selected state or null if not applicable
|
|
42
|
+
*/
|
|
43
|
+
export declare const getWidgetSelectedState: (widget: Gtk.Widget) => boolean | null;
|
package/dist/widget-text.js
CHANGED
|
@@ -5,9 +5,11 @@ const ROLES_WITH_INTERNAL_LABELS = new Set([
|
|
|
5
5
|
Gtk.AccessibleRole.TOGGLE_BUTTON,
|
|
6
6
|
Gtk.AccessibleRole.CHECKBOX,
|
|
7
7
|
Gtk.AccessibleRole.RADIO,
|
|
8
|
+
Gtk.AccessibleRole.LIST_ITEM,
|
|
8
9
|
Gtk.AccessibleRole.MENU_ITEM,
|
|
9
10
|
Gtk.AccessibleRole.MENU_ITEM_CHECKBOX,
|
|
10
11
|
Gtk.AccessibleRole.MENU_ITEM_RADIO,
|
|
12
|
+
Gtk.AccessibleRole.ROW,
|
|
11
13
|
Gtk.AccessibleRole.TAB,
|
|
12
14
|
Gtk.AccessibleRole.LINK,
|
|
13
15
|
]);
|
|
@@ -22,13 +24,40 @@ const isInternalLabel = (widget) => {
|
|
|
22
24
|
const parentRole = parent.getAccessibleRole();
|
|
23
25
|
if (!parentRole)
|
|
24
26
|
return false;
|
|
25
|
-
|
|
27
|
+
if (ROLES_WITH_INTERNAL_LABELS.has(parentRole))
|
|
28
|
+
return true;
|
|
29
|
+
const labelText = getLabelText(widget);
|
|
30
|
+
if (!labelText)
|
|
31
|
+
return false;
|
|
32
|
+
let ancestor = parent;
|
|
33
|
+
while (ancestor) {
|
|
34
|
+
if (ancestor.getAccessibleRole === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
const role = ancestor.getAccessibleRole();
|
|
37
|
+
if (role && ROLES_WITH_INTERNAL_LABELS.has(role)) {
|
|
38
|
+
return getDefaultText(ancestor) === labelText;
|
|
39
|
+
}
|
|
40
|
+
ancestor = ancestor.getParent();
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
26
43
|
};
|
|
27
44
|
const getLabelText = (widget) => {
|
|
28
45
|
const asLabel = widget;
|
|
29
46
|
const asInscription = widget;
|
|
30
47
|
return asLabel.getLabel?.() ?? asInscription.getText?.() ?? null;
|
|
31
48
|
};
|
|
49
|
+
const getDefaultText = (widget) => {
|
|
50
|
+
if ("getLabel" in widget && typeof widget.getLabel === "function") {
|
|
51
|
+
return widget.getLabel() ?? null;
|
|
52
|
+
}
|
|
53
|
+
if ("getText" in widget && typeof widget.getText === "function") {
|
|
54
|
+
return widget.getText() ?? null;
|
|
55
|
+
}
|
|
56
|
+
if ("getTitle" in widget && typeof widget.getTitle === "function") {
|
|
57
|
+
return widget.getTitle() ?? null;
|
|
58
|
+
}
|
|
59
|
+
return getNativeInterface(widget, Gtk.Editable)?.getText() ?? null;
|
|
60
|
+
};
|
|
32
61
|
const collectChildLabels = (widget) => {
|
|
33
62
|
const labels = [];
|
|
34
63
|
let child = widget.getFirstChild();
|
|
@@ -58,32 +87,16 @@ export const getWidgetText = (widget) => {
|
|
|
58
87
|
switch (role) {
|
|
59
88
|
case Gtk.AccessibleRole.BUTTON:
|
|
60
89
|
case Gtk.AccessibleRole.LINK:
|
|
61
|
-
case Gtk.AccessibleRole.TAB:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
case Gtk.AccessibleRole.TAB:
|
|
91
|
+
case Gtk.AccessibleRole.MENU_ITEM:
|
|
92
|
+
case Gtk.AccessibleRole.MENU_ITEM_CHECKBOX:
|
|
93
|
+
case Gtk.AccessibleRole.MENU_ITEM_RADIO: {
|
|
94
|
+
const directLabel = getDefaultText(widget);
|
|
65
95
|
if (directLabel)
|
|
66
96
|
return directLabel;
|
|
67
97
|
const childLabels = collectChildLabels(widget);
|
|
68
98
|
return childLabels.length > 0 ? childLabels.join(" ") : null;
|
|
69
99
|
}
|
|
70
|
-
case Gtk.AccessibleRole.TOGGLE_BUTTON:
|
|
71
|
-
return widget.getLabel?.() ?? null;
|
|
72
|
-
case Gtk.AccessibleRole.CHECKBOX:
|
|
73
|
-
case Gtk.AccessibleRole.RADIO:
|
|
74
|
-
return widget.getLabel?.() ?? null;
|
|
75
|
-
case Gtk.AccessibleRole.LABEL:
|
|
76
|
-
return getLabelText(widget);
|
|
77
|
-
case Gtk.AccessibleRole.TEXT_BOX:
|
|
78
|
-
case Gtk.AccessibleRole.SEARCH_BOX:
|
|
79
|
-
case Gtk.AccessibleRole.SPIN_BUTTON:
|
|
80
|
-
return getNativeInterface(widget, Gtk.Editable)?.getText() ?? null;
|
|
81
|
-
case Gtk.AccessibleRole.GROUP:
|
|
82
|
-
return widget.getLabel?.() ?? null;
|
|
83
|
-
case Gtk.AccessibleRole.WINDOW:
|
|
84
|
-
case Gtk.AccessibleRole.DIALOG:
|
|
85
|
-
case Gtk.AccessibleRole.ALERT_DIALOG:
|
|
86
|
-
return widget.getTitle() ?? null;
|
|
87
100
|
case Gtk.AccessibleRole.TAB_PANEL: {
|
|
88
101
|
const parent = widget.getParent();
|
|
89
102
|
if (parent) {
|
|
@@ -96,7 +109,7 @@ export const getWidgetText = (widget) => {
|
|
|
96
109
|
return null;
|
|
97
110
|
}
|
|
98
111
|
default:
|
|
99
|
-
return
|
|
112
|
+
return getDefaultText(widget);
|
|
100
113
|
}
|
|
101
114
|
};
|
|
102
115
|
/**
|
|
@@ -128,6 +141,19 @@ export const getWidgetCheckedState = (widget) => {
|
|
|
128
141
|
return null;
|
|
129
142
|
}
|
|
130
143
|
};
|
|
144
|
+
/**
|
|
145
|
+
* Gets the pressed state from toggle button widgets.
|
|
146
|
+
*
|
|
147
|
+
* @param widget - The widget to get the pressed state from
|
|
148
|
+
* @returns The pressed state or null if not applicable
|
|
149
|
+
*/
|
|
150
|
+
export const getWidgetPressedState = (widget) => {
|
|
151
|
+
const role = widget.getAccessibleRole();
|
|
152
|
+
if (role === Gtk.AccessibleRole.TOGGLE_BUTTON) {
|
|
153
|
+
return widget.getActive();
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
};
|
|
131
157
|
/**
|
|
132
158
|
* Gets the expanded state from expander widgets.
|
|
133
159
|
*
|
|
@@ -135,12 +161,24 @@ export const getWidgetCheckedState = (widget) => {
|
|
|
135
161
|
* @returns The expanded state or null if not applicable
|
|
136
162
|
*/
|
|
137
163
|
export const getWidgetExpandedState = (widget) => {
|
|
164
|
+
if (widget instanceof Gtk.Expander) {
|
|
165
|
+
return widget.getExpanded();
|
|
166
|
+
}
|
|
167
|
+
if (widget instanceof Gtk.TreeExpander) {
|
|
168
|
+
return widget.getListRow()?.getExpanded() ?? null;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Gets the selected state from selectable widgets.
|
|
174
|
+
*
|
|
175
|
+
* @param widget - The widget to get the selected state from
|
|
176
|
+
* @returns The selected state or null if not applicable
|
|
177
|
+
*/
|
|
178
|
+
export const getWidgetSelectedState = (widget) => {
|
|
138
179
|
const role = widget.getAccessibleRole();
|
|
139
|
-
if (role === Gtk.AccessibleRole.
|
|
140
|
-
|
|
141
|
-
if (!parent)
|
|
142
|
-
return null;
|
|
143
|
-
return parent.getExpanded?.() ?? null;
|
|
180
|
+
if (role === Gtk.AccessibleRole.ROW) {
|
|
181
|
+
return widget.isSelected();
|
|
144
182
|
}
|
|
145
183
|
return null;
|
|
146
184
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Testing utilities for GTKX applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"linux",
|
|
14
14
|
"desktop"
|
|
15
15
|
],
|
|
16
|
-
"homepage": "https://
|
|
16
|
+
"homepage": "https://gtkx.dev",
|
|
17
17
|
"bugs": {
|
|
18
|
-
"url": "https://github.com/
|
|
18
|
+
"url": "https://github.com/gtkx-org/gtkx/issues"
|
|
19
19
|
},
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "https://github.com/
|
|
22
|
+
"url": "https://github.com/gtkx-org/gtkx.git",
|
|
23
23
|
"directory": "packages/testing"
|
|
24
24
|
},
|
|
25
25
|
"license": "MPL-2.0",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@gtkx/
|
|
40
|
-
"@gtkx/
|
|
39
|
+
"@gtkx/ffi": "0.18.0",
|
|
40
|
+
"@gtkx/react": "0.18.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/react-reconciler": "^0.33.0",
|
|
44
44
|
"react-reconciler": "^0.33.0",
|
|
45
|
-
"@gtkx/vitest": "0.
|
|
45
|
+
"@gtkx/vitest": "0.18.0"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsc -b && cp ../../README.md .",
|