@gtkx/testing 0.20.0 → 1.0.0-rc.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/README.md +122 -55
- package/dist/act.d.ts +19 -0
- package/dist/act.d.ts.map +1 -0
- package/dist/act.js +48 -0
- package/dist/act.js.map +1 -0
- package/dist/bound-queries.d.ts +32 -0
- package/dist/bound-queries.d.ts.map +1 -0
- package/dist/bound-queries.js +2 -0
- package/dist/bound-queries.js.map +1 -0
- package/dist/build-queries.d.ts +16 -0
- package/dist/build-queries.d.ts.map +1 -0
- package/dist/build-queries.js +103 -0
- package/dist/build-queries.js.map +1 -0
- package/dist/cleanup-registry.d.ts +4 -0
- package/dist/cleanup-registry.d.ts.map +1 -0
- package/dist/cleanup-registry.js +11 -0
- package/dist/cleanup-registry.js.map +1 -0
- package/dist/config.d.ts +18 -51
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -40
- package/dist/config.js.map +1 -1
- package/dist/editable.d.ts +7 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +24 -0
- package/dist/editable.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +119 -0
- package/dist/errors.js.map +1 -0
- package/dist/fire-event.d.ts +7 -25
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +9 -29
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +3 -0
- package/dist/frame-sync.d.ts.map +1 -0
- package/dist/frame-sync.js +30 -0
- package/dist/frame-sync.js.map +1 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +55 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +103 -0
- package/dist/matchers.js.map +1 -0
- package/dist/pretty-widget.d.ts +21 -25
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +69 -115
- package/dist/pretty-widget.js.map +1 -1
- package/dist/queries.d.ts +130 -177
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +212 -325
- package/dist/queries.js.map +1 -1
- package/dist/register-test-hooks.d.ts +2 -0
- package/dist/register-test-hooks.d.ts.map +1 -0
- package/dist/register-test-hooks.js +15 -0
- package/dist/register-test-hooks.js.map +1 -0
- package/dist/render-hook.d.ts +14 -35
- package/dist/render-hook.d.ts.map +1 -1
- package/dist/render-hook.js +8 -45
- package/dist/render-hook.js.map +1 -1
- package/dist/render.d.ts +11 -41
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +129 -118
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +16 -51
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +39 -58
- package/dist/role-helpers.js.map +1 -1
- package/dist/screen.d.ts +6 -63
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +21 -109
- package/dist/screen.js.map +1 -1
- package/dist/screenshot.d.ts +20 -23
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +89 -48
- package/dist/screenshot.js.map +1 -1
- package/dist/suggestions.d.ts +22 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/suggestions.js +56 -0
- package/dist/suggestions.js.map +1 -0
- package/dist/traversal.d.ts +10 -8
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +28 -13
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +103 -172
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +8 -0
- package/dist/user-event/adjustment.d.ts.map +1 -0
- package/dist/user-event/adjustment.js +33 -0
- package/dist/user-event/adjustment.js.map +1 -0
- package/dist/user-event/click.d.ts +7 -0
- package/dist/user-event/click.d.ts.map +1 -0
- package/dist/user-event/click.js +65 -0
- package/dist/user-event/click.js.map +1 -0
- package/dist/user-event/controller.d.ts +7 -0
- package/dist/user-event/controller.d.ts.map +1 -0
- package/dist/user-event/controller.js +28 -0
- package/dist/user-event/controller.js.map +1 -0
- package/dist/user-event/dispatch.d.ts +7 -0
- package/dist/user-event/dispatch.d.ts.map +1 -0
- package/dist/user-event/dispatch.js +10 -0
- package/dist/user-event/dispatch.js.map +1 -0
- package/dist/user-event/event-wrapper.d.ts +3 -0
- package/dist/user-event/event-wrapper.d.ts.map +1 -0
- package/dist/user-event/event-wrapper.js +48 -0
- package/dist/user-event/event-wrapper.js.map +1 -0
- package/dist/user-event/gesture.d.ts +31 -0
- package/dist/user-event/gesture.d.ts.map +1 -0
- package/dist/user-event/gesture.js +105 -0
- package/dist/user-event/gesture.js.map +1 -0
- package/dist/user-event/index.d.ts +44 -0
- package/dist/user-event/index.d.ts.map +1 -0
- package/dist/user-event/index.js +38 -0
- package/dist/user-event/index.js.map +1 -0
- package/dist/user-event/keyboard.d.ts +9 -0
- package/dist/user-event/keyboard.d.ts.map +1 -0
- package/dist/user-event/keyboard.js +173 -0
- package/dist/user-event/keyboard.js.map +1 -0
- package/dist/user-event/pointer.d.ts +6 -0
- package/dist/user-event/pointer.d.ts.map +1 -0
- package/dist/user-event/pointer.js +27 -0
- package/dist/user-event/pointer.js.map +1 -0
- package/dist/user-event/selection.d.ts +4 -0
- package/dist/user-event/selection.d.ts.map +1 -0
- package/dist/user-event/selection.js +106 -0
- package/dist/user-event/selection.js.map +1 -0
- package/dist/user-event/state.d.ts +6 -0
- package/dist/user-event/state.d.ts.map +1 -0
- package/dist/user-event/state.js +2 -0
- package/dist/user-event/state.js.map +1 -0
- package/dist/user-event/text.d.ts +17 -0
- package/dist/user-event/text.d.ts.map +1 -0
- package/dist/user-event/text.js +107 -0
- package/dist/user-event/text.js.map +1 -0
- package/dist/wait-for.d.ts +13 -32
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js +62 -73
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +33 -0
- package/dist/widget-accessible-properties.d.ts.map +1 -0
- package/dist/widget-accessible-properties.js +215 -0
- package/dist/widget-accessible-properties.js.map +1 -0
- package/dist/within.d.ts +9 -26
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +15 -25
- package/dist/within.js.map +1 -1
- package/package.json +21 -12
- package/src/act.ts +68 -0
- package/src/bound-queries.ts +36 -0
- package/src/build-queries.ts +185 -0
- package/src/cleanup-registry.ts +14 -0
- package/src/config.ts +28 -59
- package/src/editable.ts +30 -0
- package/src/errors.ts +130 -0
- package/src/fire-event.ts +11 -38
- package/src/frame-sync.ts +34 -0
- package/src/index.ts +60 -18
- package/src/matchers.ts +200 -0
- package/src/pretty-widget.ts +99 -138
- package/src/queries.ts +321 -425
- package/src/register-test-hooks.ts +16 -0
- package/src/render-hook.tsx +31 -44
- package/src/render.tsx +161 -149
- package/src/role-helpers.ts +47 -67
- package/src/screen.ts +21 -114
- package/src/screenshot.ts +104 -55
- package/src/suggestions.ts +87 -0
- package/src/traversal.ts +32 -18
- package/src/types.ts +115 -176
- package/src/user-event/adjustment.ts +49 -0
- package/src/user-event/click.ts +70 -0
- package/src/user-event/controller.ts +44 -0
- package/src/user-event/dispatch.ts +23 -0
- package/src/user-event/event-wrapper.ts +54 -0
- package/src/user-event/gesture.ts +154 -0
- package/src/user-event/index.ts +74 -0
- package/src/user-event/keyboard.ts +204 -0
- package/src/user-event/pointer.ts +34 -0
- package/src/user-event/selection.ts +122 -0
- package/src/user-event/state.ts +6 -0
- package/src/user-event/text.ts +137 -0
- package/src/wait-for.ts +79 -80
- package/src/widget-accessible-properties.ts +244 -0
- package/src/within.ts +26 -27
- package/dist/bind-queries.d.ts +0 -14
- package/dist/bind-queries.d.ts.map +0 -1
- package/dist/bind-queries.js +0 -29
- package/dist/bind-queries.js.map +0 -1
- package/dist/error-builder.d.ts +0 -28
- package/dist/error-builder.d.ts.map +0 -1
- package/dist/error-builder.js +0 -81
- package/dist/error-builder.js.map +0 -1
- package/dist/timing.d.ts +0 -18
- package/dist/timing.d.ts.map +0 -1
- package/dist/timing.js +0 -18
- package/dist/timing.js.map +0 -1
- package/dist/user-event.d.ts +0 -138
- package/dist/user-event.d.ts.map +0 -1
- package/dist/user-event.js +0 -411
- package/dist/user-event.js.map +0 -1
- package/dist/widget-text.d.ts +0 -69
- package/dist/widget-text.d.ts.map +0 -1
- package/dist/widget-text.js +0 -177
- package/dist/widget-text.js.map +0 -1
- package/dist/widget.d.ts +0 -3
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -13
- package/dist/widget.js.map +0 -1
- package/src/bind-queries.ts +0 -52
- package/src/error-builder.ts +0 -102
- package/src/timing.ts +0 -17
- package/src/user-event.ts +0 -492
- package/src/widget-text.ts +0 -202
- package/src/widget.ts +0 -15
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { quit } from "@gtkx/runtime";
|
|
2
|
+
import { registerMatchers } from "./matchers.js";
|
|
3
|
+
import { cleanup } from "./render.js";
|
|
4
|
+
|
|
5
|
+
const callRunnerHook = (name: "afterEach" | "afterAll", callback: () => unknown): void => {
|
|
6
|
+
const hook: unknown = Reflect.get(globalThis, name);
|
|
7
|
+
if (typeof hook === "function") (hook as (callback: () => unknown) => void)(callback);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const registerTestRuntimeHooks = (): void => {
|
|
11
|
+
callRunnerHook("afterEach", cleanup);
|
|
12
|
+
callRunnerHook("afterAll", quit);
|
|
13
|
+
registerMatchers();
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
registerTestRuntimeHooks();
|
package/src/render-hook.tsx
CHANGED
|
@@ -1,65 +1,52 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
2
|
import { render } from "./render.js";
|
|
3
3
|
import type { RenderHookOptions, RenderHookResult } from "./types.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Renders a
|
|
6
|
+
* Renders a test component that calls the given hook, exposing its latest
|
|
7
|
+
* return value along with rerender and unmount controls.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* test("useState hook", async () => {
|
|
21
|
-
* const { result } = await renderHook(() => useState(0));
|
|
22
|
-
* expect(result.current[0]).toBe(0);
|
|
23
|
-
* });
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```tsx
|
|
28
|
-
* import { renderHook } from "@gtkx/testing";
|
|
29
|
-
*
|
|
30
|
-
* test("hook with props", async () => {
|
|
31
|
-
* const { result, rerender } = await renderHook(
|
|
32
|
-
* ({ multiplier }) => useMultiplier(multiplier),
|
|
33
|
-
* { initialProps: { multiplier: 2 } }
|
|
34
|
-
* );
|
|
35
|
-
*
|
|
36
|
-
* expect(result.current).toBe(2);
|
|
9
|
+
* @param callback The hook to run, invoked with the current props.
|
|
10
|
+
* @param options Optional initial props and wrapper component.
|
|
11
|
+
* @returns A result whose `result.current` holds the hook's latest value.
|
|
12
|
+
*/
|
|
13
|
+
export function renderHook<Result>(
|
|
14
|
+
callback: () => Result,
|
|
15
|
+
options?: RenderHookOptions<undefined>,
|
|
16
|
+
): Promise<RenderHookResult<Result, undefined>>;
|
|
17
|
+
/**
|
|
18
|
+
* Renders a test component that calls the given hook with props, exposing its
|
|
19
|
+
* latest return value along with rerender and unmount controls.
|
|
37
20
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* ```
|
|
21
|
+
* @param callback The hook to run, invoked with the current props.
|
|
22
|
+
* @param options Initial props and optional wrapper component.
|
|
23
|
+
* @returns A result whose `result.current` holds the hook's latest value.
|
|
42
24
|
*/
|
|
43
|
-
export
|
|
25
|
+
export function renderHook<Result, Props>(
|
|
26
|
+
callback: (props: Props) => Result,
|
|
27
|
+
options: RenderHookOptions<Props>,
|
|
28
|
+
): Promise<RenderHookResult<Result, Props>>;
|
|
29
|
+
export async function renderHook<Result, Props>(
|
|
44
30
|
callback: (props: Props) => Result,
|
|
45
31
|
options?: RenderHookOptions<Props>,
|
|
46
|
-
): Promise<RenderHookResult<Result, Props>>
|
|
47
|
-
const
|
|
48
|
-
|
|
32
|
+
): Promise<RenderHookResult<Result, Props>> {
|
|
33
|
+
const initialProps = (options as { initialProps?: Props } | undefined)?.initialProps as Props;
|
|
34
|
+
const resultRef: { current: Result | undefined } = { current: undefined };
|
|
35
|
+
let currentProps: Props = initialProps;
|
|
49
36
|
|
|
50
37
|
const TestComponent = ({ props }: { props: Props }): null => {
|
|
51
38
|
const result = callback(props);
|
|
52
|
-
|
|
53
|
-
ref.current.current = result;
|
|
39
|
+
resultRef.current = result;
|
|
54
40
|
return null;
|
|
55
41
|
};
|
|
56
42
|
|
|
57
43
|
const renderResult = await render(<TestComponent props={currentProps} />, {
|
|
58
|
-
|
|
44
|
+
container: new Gtk.Box(),
|
|
45
|
+
wrapper: options?.wrapper,
|
|
59
46
|
});
|
|
60
47
|
|
|
61
48
|
return {
|
|
62
|
-
result: resultRef,
|
|
49
|
+
result: resultRef as { current: Result },
|
|
63
50
|
rerender: async (newProps?: Props) => {
|
|
64
51
|
if (newProps !== undefined) {
|
|
65
52
|
currentProps = newProps;
|
|
@@ -68,4 +55,4 @@ export const renderHook = async <Result, Props>(
|
|
|
68
55
|
},
|
|
69
56
|
unmount: renderResult.unmount,
|
|
70
57
|
};
|
|
71
|
-
}
|
|
58
|
+
}
|
package/src/render.tsx
CHANGED
|
@@ -1,192 +1,204 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import type { RootElement } from "@gtkx/react";
|
|
3
|
+
import {
|
|
4
|
+
createReconcilerRoot,
|
|
5
|
+
isRootElement,
|
|
6
|
+
type ReconcilerRoot,
|
|
7
|
+
setReconcilerErrorHandler,
|
|
8
|
+
} from "@gtkx/react/internal";
|
|
9
|
+
import { type ErrorInfo, type ReactNode, StrictMode } from "react";
|
|
10
|
+
import { runInAct } from "./act.js";
|
|
11
|
+
import type { RenderResult } from "./bound-queries.js";
|
|
12
|
+
import { addToCleanupQueue, runCleanup } from "./cleanup-registry.js";
|
|
13
|
+
import { scheduleAfterLayout } from "./frame-sync.js";
|
|
14
|
+
import { logWidget, type PrettyWidgetOptions } from "./pretty-widget.js";
|
|
15
|
+
import { logRoles } from "./role-helpers.js";
|
|
16
|
+
import { clearScreen, setScreen } from "./screen.js";
|
|
17
|
+
import { captureAndSaveScreenshot } from "./screenshot.js";
|
|
18
|
+
import { type Container, TOPLEVELS, traverse } from "./traversal.js";
|
|
19
|
+
import type { QueryMap, RenderOptions, ScreenshotOptions, WindowSelector } from "./types.js";
|
|
20
|
+
import { resetClipboard } from "./user-event/index.js";
|
|
21
|
+
import { within } from "./within.js";
|
|
22
|
+
|
|
16
23
|
let lastRenderError: Error | null = null;
|
|
24
|
+
let errorHandlerInstalled = false;
|
|
17
25
|
|
|
18
|
-
type
|
|
26
|
+
type ActiveRender = {
|
|
27
|
+
root: ReconcilerRoot;
|
|
28
|
+
window: Gtk.Window | null;
|
|
29
|
+
};
|
|
19
30
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
const activeRenders = new Set<ActiveRender>();
|
|
32
|
+
|
|
33
|
+
const HARNESS_WINDOW_WIDTH = 800;
|
|
34
|
+
const HARNESS_WINDOW_HEIGHT = 600;
|
|
35
|
+
|
|
36
|
+
const flushLayout = (window: Gtk.Window | null): Promise<void> =>
|
|
37
|
+
new Promise((resolve) => {
|
|
38
|
+
scheduleAfterLayout(window, resolve);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const update = async (element: ReactNode, root: ReconcilerRoot): Promise<void> => {
|
|
42
|
+
await runInAct(() => {
|
|
43
|
+
root.update(element);
|
|
44
|
+
});
|
|
28
45
|
|
|
29
46
|
if (lastRenderError) {
|
|
30
|
-
const
|
|
47
|
+
const captured = lastRenderError;
|
|
31
48
|
lastRenderError = null;
|
|
32
|
-
throw
|
|
49
|
+
throw captured;
|
|
33
50
|
}
|
|
34
51
|
};
|
|
35
52
|
|
|
36
|
-
const
|
|
37
|
-
|
|
53
|
+
const disposeActiveRender = async (active: ActiveRender): Promise<void> => {
|
|
54
|
+
if (!activeRenders.delete(active)) return;
|
|
55
|
+
await active.root.unmount(async (root) => {
|
|
56
|
+
await update(null, root);
|
|
57
|
+
active.window?.destroy();
|
|
58
|
+
});
|
|
38
59
|
};
|
|
39
60
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!container) {
|
|
44
|
-
const instance = reconciler.getInstance();
|
|
45
|
-
container = instance.createContainer(
|
|
46
|
-
application,
|
|
47
|
-
1,
|
|
48
|
-
null,
|
|
49
|
-
false,
|
|
50
|
-
null,
|
|
51
|
-
"",
|
|
52
|
-
handleError,
|
|
53
|
-
handleError,
|
|
54
|
-
() => {},
|
|
55
|
-
() => {},
|
|
56
|
-
);
|
|
61
|
+
const disposeAllActiveRenders = async (): Promise<void> => {
|
|
62
|
+
for (const active of [...activeRenders]) {
|
|
63
|
+
await disposeActiveRender(active);
|
|
57
64
|
}
|
|
65
|
+
};
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
const handleError = (error: unknown): void => {
|
|
68
|
+
lastRenderError = error instanceof Error ? error : new Error(String(error));
|
|
60
69
|
};
|
|
61
70
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
const installErrorHandler = (): void => {
|
|
72
|
+
if (errorHandlerInstalled) return;
|
|
73
|
+
setReconcilerErrorHandler(handleError);
|
|
74
|
+
errorHandlerInstalled = true;
|
|
75
|
+
};
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
type ResolvedContainer = {
|
|
78
|
+
containerInfo: Gtk.Widget | RootElement;
|
|
79
|
+
window: Gtk.Window | null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const resolveContainer = (container: RenderOptions["container"]): ResolvedContainer => {
|
|
83
|
+
if (isRootElement(container)) {
|
|
84
|
+
return { containerInfo: container, window: null };
|
|
85
|
+
}
|
|
86
|
+
if (container instanceof Gtk.Widget) {
|
|
87
|
+
return { containerInfo: container, window: null };
|
|
72
88
|
}
|
|
73
|
-
|
|
89
|
+
const window = new Gtk.Window({ defaultWidth: HARNESS_WINDOW_WIDTH, defaultHeight: HARNESS_WINDOW_HEIGHT });
|
|
90
|
+
window.setTitlebar(new Gtk.HeaderBar({ showTitleButtons: false }));
|
|
91
|
+
return { containerInfo: window, window };
|
|
74
92
|
};
|
|
75
93
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
):
|
|
81
|
-
if (wrapper === false || wrapper === undefined) return element;
|
|
82
|
-
const Wrapper = wrapper === true ? DefaultWrapper : wrapper;
|
|
83
|
-
return <Wrapper ref={wrapperRef}>{element}</Wrapper>;
|
|
94
|
+
const firstToplevelWidget = (baseElement: Container): Gtk.Widget => {
|
|
95
|
+
if (baseElement instanceof Gtk.Widget) return baseElement;
|
|
96
|
+
const [first] = traverse(baseElement);
|
|
97
|
+
if (first) return first;
|
|
98
|
+
throw new Error("render() produced no widgets: ensure the element renders visible content");
|
|
84
99
|
};
|
|
85
100
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
const resolveResultContainer = (
|
|
102
|
+
resolved: ResolvedContainer,
|
|
103
|
+
container: RenderOptions["container"],
|
|
89
104
|
baseElement: Container,
|
|
90
105
|
): Gtk.Widget => {
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
if (resolved.window) return resolved.window;
|
|
107
|
+
if (container instanceof Gtk.Widget) return container;
|
|
108
|
+
return firstToplevelWidget(baseElement);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const renderErrorHandlers = <Q extends QueryMap>(options: RenderOptions<Q> | undefined) => ({
|
|
112
|
+
onUncaughtError: handleError,
|
|
113
|
+
onCaughtError: (error: unknown, errorInfo: ErrorInfo): void => {
|
|
114
|
+
handleError(error);
|
|
115
|
+
options?.onCaughtError?.(error, errorInfo);
|
|
116
|
+
},
|
|
117
|
+
onRecoverableError: (error: unknown, errorInfo: ErrorInfo): void => {
|
|
118
|
+
options?.onRecoverableError?.(error, errorInfo);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const applyEnableAnimations = (enabled: boolean): void => {
|
|
123
|
+
const settings = Gtk.Settings.getDefault();
|
|
124
|
+
if (settings) {
|
|
125
|
+
settings.gtkEnableAnimations = enabled;
|
|
97
126
|
}
|
|
98
|
-
return firstWidget;
|
|
99
127
|
};
|
|
100
128
|
|
|
101
129
|
/**
|
|
102
|
-
* Renders a React element
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* query methods and utilities for interacting with the rendered widgets.
|
|
106
|
-
*
|
|
107
|
-
* @param element - The React element to render
|
|
108
|
-
* @param options - Render options including wrapper configuration
|
|
109
|
-
* @returns A promise resolving to query methods and utilities
|
|
130
|
+
* Renders a React element into a GTK4 widget tree and returns queries
|
|
131
|
+
* scoped to it along with controls for rerendering and unmounting. When no
|
|
132
|
+
* container is supplied, a harness window is created and presented.
|
|
110
133
|
*
|
|
111
|
-
* @
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* test("button click", async () => {
|
|
116
|
-
* await render(<MyButton />);
|
|
117
|
-
* const button = await screen.findByRole(Gtk.AccessibleRole.BUTTON);
|
|
118
|
-
* await userEvent.click(button);
|
|
119
|
-
* });
|
|
120
|
-
* ```
|
|
121
|
-
*
|
|
122
|
-
* @see {@link cleanup} for cleaning up after tests
|
|
123
|
-
* @see {@link screen} for global query access
|
|
134
|
+
* @param element The React element to render.
|
|
135
|
+
* @param options Optional container, wrapper, custom queries, and other render settings.
|
|
136
|
+
* @returns A render result with bound queries, debug helpers, and lifecycle controls.
|
|
124
137
|
*/
|
|
125
|
-
export const render = async
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
export const render = async <Q extends QueryMap = Record<never, never>>(
|
|
139
|
+
element: ReactNode,
|
|
140
|
+
options?: RenderOptions<Q>,
|
|
141
|
+
): Promise<RenderResult<Q>> => {
|
|
142
|
+
installErrorHandler();
|
|
143
|
+
|
|
144
|
+
applyEnableAnimations(options?.animations === true);
|
|
145
|
+
|
|
146
|
+
const baseElement: Container = options?.baseElement ?? TOPLEVELS;
|
|
147
|
+
const Wrapper = options?.wrapper;
|
|
148
|
+
|
|
149
|
+
const resolved = resolveContainer(options?.container);
|
|
150
|
+
const root = createReconcilerRoot({
|
|
151
|
+
containerInfo: resolved.containerInfo,
|
|
152
|
+
...renderErrorHandlers(options),
|
|
153
|
+
});
|
|
154
|
+
const active: ActiveRender = { root, window: resolved.window };
|
|
155
|
+
activeRenders.add(active);
|
|
156
|
+
|
|
157
|
+
addToCleanupQueue(disposeAllActiveRenders);
|
|
158
|
+
addToCleanupQueue(clearScreen);
|
|
159
|
+
addToCleanupQueue(resetClipboard);
|
|
160
|
+
|
|
161
|
+
const wrap = (node: ReactNode): ReactNode => {
|
|
162
|
+
const wrapped = Wrapper ? <Wrapper>{node}</Wrapper> : node;
|
|
163
|
+
return options?.reactStrictMode ? <StrictMode>{wrapped}</StrictMode> : wrapped;
|
|
164
|
+
};
|
|
130
165
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
await update(instance, withContext, fiberRoot);
|
|
166
|
+
await update(wrap(element), root);
|
|
167
|
+
resolved.window?.present();
|
|
168
|
+
await flushLayout(resolved.window);
|
|
135
169
|
|
|
136
|
-
|
|
170
|
+
const container = resolveResultContainer(resolved, options?.container, baseElement);
|
|
137
171
|
|
|
138
|
-
|
|
139
|
-
|
|
172
|
+
const result: RenderResult<Q> = {
|
|
173
|
+
...within(baseElement, options?.queries),
|
|
174
|
+
container,
|
|
140
175
|
baseElement,
|
|
141
|
-
|
|
142
|
-
|
|
176
|
+
unmount: async () => {
|
|
177
|
+
await disposeActiveRender(active);
|
|
178
|
+
},
|
|
143
179
|
rerender: async (newElement: ReactNode) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
180
|
+
await update(wrap(newElement), root);
|
|
181
|
+
await flushLayout(resolved.window);
|
|
182
|
+
},
|
|
183
|
+
debug: (element: Container | Container[] = baseElement, debugOptions?: PrettyWidgetOptions) => {
|
|
184
|
+
logWidget(element, debugOptions);
|
|
148
185
|
},
|
|
149
|
-
|
|
150
|
-
|
|
186
|
+
logRoles: () => {
|
|
187
|
+
logRoles(baseElement);
|
|
151
188
|
},
|
|
189
|
+
screenshot: (selector?: WindowSelector, screenshotOptions?: ScreenshotOptions) =>
|
|
190
|
+
captureAndSaveScreenshot(selector, screenshotOptions),
|
|
152
191
|
};
|
|
192
|
+
|
|
193
|
+
setScreen(result);
|
|
194
|
+
|
|
195
|
+
return result;
|
|
153
196
|
};
|
|
154
197
|
|
|
155
198
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* Unmounts all rendered components and resets the testing environment.
|
|
159
|
-
* Call this in `afterEach` to ensure tests don't affect each other.
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* ```tsx
|
|
163
|
-
* import { render, cleanup } from "@gtkx/testing";
|
|
164
|
-
*
|
|
165
|
-
* afterEach(async () => {
|
|
166
|
-
* await cleanup();
|
|
167
|
-
* });
|
|
168
|
-
*
|
|
169
|
-
* test("my test", async () => {
|
|
170
|
-
* await render(<MyComponent />);
|
|
171
|
-
* // ...
|
|
172
|
-
* });
|
|
173
|
-
* ```
|
|
199
|
+
* Unmounts every active render and runs all registered cleanup callbacks,
|
|
200
|
+
* resetting the screen and clipboard. Called automatically after each test.
|
|
174
201
|
*/
|
|
175
202
|
export const cleanup = async (): Promise<void> => {
|
|
176
|
-
|
|
177
|
-
const instance = reconciler.getInstance();
|
|
178
|
-
await update(instance, null, container);
|
|
179
|
-
}
|
|
180
|
-
container = null;
|
|
181
|
-
setScreenRoot(null);
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const handleSignal = (): void => {
|
|
185
|
-
try {
|
|
186
|
-
stop();
|
|
187
|
-
} catch {}
|
|
188
|
-
process.exit(0);
|
|
203
|
+
await runCleanup();
|
|
189
204
|
};
|
|
190
|
-
|
|
191
|
-
process.on("SIGTERM", handleSignal);
|
|
192
|
-
process.on("SIGINT", handleSignal);
|
package/src/role-helpers.ts
CHANGED
|
@@ -1,64 +1,59 @@
|
|
|
1
|
-
import * as Gtk from "@gtkx/
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { sortStringsBy } from "@gtkx/utils";
|
|
2
3
|
import { type Container, traverse } from "./traversal.js";
|
|
3
|
-
import { getWidgetAccessibleName } from "./widget-
|
|
4
|
+
import { getWidgetAccessibleName } from "./widget-accessible-properties.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
const enumNamesByValue = (enumObject: Record<string, string | number>): Map<number, string> =>
|
|
7
|
+
new Map<number, string>(
|
|
8
|
+
Object.entries(enumObject)
|
|
9
|
+
.filter((entry): entry is [string, number] => typeof entry[1] === "number")
|
|
10
|
+
.map(([name, value]) => [value, name]),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const ROLE_NAMES_BY_VALUE = enumNamesByValue(Gtk.AccessibleRole);
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
|
-
*
|
|
16
|
+
* Converts an accessible role enum value into its lowercase name, falling back
|
|
17
|
+
* to the numeric value when the role is unknown.
|
|
15
18
|
*
|
|
16
|
-
* @param role
|
|
17
|
-
* @returns Lowercase role name (e.g., "button", "checkbox")
|
|
19
|
+
* @param role The accessible role to format.
|
|
18
20
|
*/
|
|
19
|
-
export const formatRole = (role: Gtk.AccessibleRole
|
|
20
|
-
|
|
21
|
-
const name = Gtk.AccessibleRole[role];
|
|
21
|
+
export const formatRole = (role: Gtk.AccessibleRole): string => {
|
|
22
|
+
const name = ROLE_NAMES_BY_VALUE.get(role);
|
|
22
23
|
if (!name) return String(role);
|
|
23
24
|
return name.toLowerCase();
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
export const formatRoleList = (roles: Iterable<Gtk.AccessibleRole>): string => {
|
|
28
|
+
const names = [...roles].map((role) => formatRole(role).toUpperCase());
|
|
29
|
+
if (names.length <= 1) {
|
|
30
|
+
return names.join("");
|
|
31
|
+
}
|
|
32
|
+
if (names.length === 2) {
|
|
33
|
+
return `${names[0]} or ${names[1]}`;
|
|
34
|
+
}
|
|
35
|
+
const head = names.slice(0, -1);
|
|
36
|
+
const last = names[names.length - 1];
|
|
37
|
+
return `${head.join(", ")}, or ${last}`;
|
|
38
|
+
};
|
|
39
|
+
|
|
26
40
|
/**
|
|
27
|
-
*
|
|
41
|
+
* Groups every widget in a container's tree by its accessible role name.
|
|
28
42
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @param container - The container to scan for roles
|
|
33
|
-
* @returns Map of role names to arrays of RoleInfo
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```tsx
|
|
37
|
-
* import { getRoles } from "@gtkx/testing";
|
|
38
|
-
*
|
|
39
|
-
* const roles = getRoles(container);
|
|
40
|
-
* // Map {
|
|
41
|
-
* // "button" => [{ widget: ..., name: "Submit" }, { widget: ..., name: "Cancel" }],
|
|
42
|
-
* // "checkbox" => [{ widget: ..., name: "Remember me" }]
|
|
43
|
-
* // }
|
|
44
|
-
* ```
|
|
43
|
+
* @param container The scope to traverse.
|
|
44
|
+
* @returns A map from role name to the widgets that have that role.
|
|
45
45
|
*/
|
|
46
|
-
export const getRoles = (container: Container): Map<string,
|
|
47
|
-
const roles = new Map<string,
|
|
46
|
+
export const getRoles = (container: Container): Map<string, Gtk.Widget[]> => {
|
|
47
|
+
const roles = new Map<string, Gtk.Widget[]>();
|
|
48
48
|
|
|
49
49
|
for (const widget of traverse(container)) {
|
|
50
|
-
const
|
|
51
|
-
if (role === undefined) continue;
|
|
52
|
-
|
|
53
|
-
const roleName = formatRole(role);
|
|
54
|
-
const name = getWidgetAccessibleName(widget);
|
|
55
|
-
const info: RoleInfo = { widget, name };
|
|
50
|
+
const roleName = formatRole(widget.getAccessibleRole());
|
|
56
51
|
|
|
57
52
|
const existing = roles.get(roleName);
|
|
58
53
|
if (existing) {
|
|
59
|
-
existing.push(
|
|
54
|
+
existing.push(widget);
|
|
60
55
|
} else {
|
|
61
|
-
roles.set(roleName, [
|
|
56
|
+
roles.set(roleName, [widget]);
|
|
62
57
|
}
|
|
63
58
|
}
|
|
64
59
|
|
|
@@ -67,16 +62,16 @@ export const getRoles = (container: Container): Map<string, RoleInfo[]> => {
|
|
|
67
62
|
|
|
68
63
|
const formatWidgetPreview = (widget: Gtk.Widget, name: string | null): string => {
|
|
69
64
|
const tagName = widget.constructor.name;
|
|
70
|
-
const roleAttr = formatRole(widget.getAccessibleRole
|
|
65
|
+
const roleAttr = formatRole(widget.getAccessibleRole());
|
|
71
66
|
const nameDisplay = name ? `Name "${name}"` : 'Name ""';
|
|
72
67
|
return `${nameDisplay}: <${tagName} role="${roleAttr}">${name ?? ""}</${tagName}>`;
|
|
73
68
|
};
|
|
74
69
|
|
|
75
70
|
/**
|
|
76
|
-
* Formats roles
|
|
71
|
+
* Formats the accessible roles in a container's tree as a readable string,
|
|
72
|
+
* listing each role together with its widgets and their accessible names.
|
|
77
73
|
*
|
|
78
|
-
* @param container
|
|
79
|
-
* @returns Formatted string showing all roles and their accessible names
|
|
74
|
+
* @param container The scope to inspect.
|
|
80
75
|
*/
|
|
81
76
|
export const prettyRoles = (container: Container): string => {
|
|
82
77
|
const roles = getRoles(container);
|
|
@@ -87,12 +82,12 @@ export const prettyRoles = (container: Container): string => {
|
|
|
87
82
|
|
|
88
83
|
const lines: string[] = [];
|
|
89
84
|
|
|
90
|
-
const sortedRoles = [...roles.entries()]
|
|
85
|
+
const sortedRoles = sortStringsBy([...roles.entries()], ([roleName]) => roleName);
|
|
91
86
|
|
|
92
87
|
for (const [roleName, widgets] of sortedRoles) {
|
|
93
88
|
lines.push(`${roleName}:`);
|
|
94
|
-
for (const
|
|
95
|
-
lines.push(` ${formatWidgetPreview(widget,
|
|
89
|
+
for (const widget of widgets) {
|
|
90
|
+
lines.push(` ${formatWidgetPreview(widget, getWidgetAccessibleName(widget))}`);
|
|
96
91
|
}
|
|
97
92
|
lines.push("");
|
|
98
93
|
}
|
|
@@ -101,25 +96,10 @@ export const prettyRoles = (container: Container): string => {
|
|
|
101
96
|
};
|
|
102
97
|
|
|
103
98
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* Useful for debugging test failures and discovering available roles.
|
|
107
|
-
*
|
|
108
|
-
* @param container - The container to log roles for
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
* ```tsx
|
|
112
|
-
* import { render, logRoles } from "@gtkx/testing";
|
|
99
|
+
* Prints the accessible roles in a container's tree to the console using
|
|
100
|
+
* {@link prettyRoles}.
|
|
113
101
|
*
|
|
114
|
-
*
|
|
115
|
-
* logRoles(container);
|
|
116
|
-
* // Console output:
|
|
117
|
-
* // button:
|
|
118
|
-
* // Name "Submit": <GtkButton role="button">Submit</GtkButton>
|
|
119
|
-
* // Name "Cancel": <GtkButton role="button">Cancel</GtkButton>
|
|
120
|
-
* // checkbox:
|
|
121
|
-
* // Name "Remember me": <GtkCheckButton role="checkbox">Remember me</GtkCheckButton>
|
|
122
|
-
* ```
|
|
102
|
+
* @param container The scope to inspect.
|
|
123
103
|
*/
|
|
124
104
|
export const logRoles = (container: Container): void => {
|
|
125
105
|
console.log(prettyRoles(container));
|