@gtkx/testing 0.21.0 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +137 -34
  2. package/dist/act.d.ts +19 -0
  3. package/dist/act.d.ts.map +1 -0
  4. package/dist/act.js +56 -0
  5. package/dist/act.js.map +1 -0
  6. package/dist/bound-queries.d.ts +33 -0
  7. package/dist/bound-queries.d.ts.map +1 -0
  8. package/dist/bound-queries.js +2 -0
  9. package/dist/bound-queries.js.map +1 -0
  10. package/dist/build-queries.d.ts +16 -0
  11. package/dist/build-queries.d.ts.map +1 -0
  12. package/dist/build-queries.js +115 -0
  13. package/dist/build-queries.js.map +1 -0
  14. package/dist/cleanup-registry.d.ts +5 -0
  15. package/dist/cleanup-registry.d.ts.map +1 -0
  16. package/dist/cleanup-registry.js +21 -0
  17. package/dist/cleanup-registry.js.map +1 -0
  18. package/dist/config.d.ts +20 -52
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +17 -46
  21. package/dist/config.js.map +1 -1
  22. package/dist/editable.d.ts +8 -0
  23. package/dist/editable.d.ts.map +1 -0
  24. package/dist/editable.js +26 -0
  25. package/dist/editable.js.map +1 -0
  26. package/dist/errors.d.ts +30 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +135 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fire-event.d.ts +8 -25
  31. package/dist/fire-event.d.ts.map +1 -1
  32. package/dist/fire-event.js +11 -30
  33. package/dist/fire-event.js.map +1 -1
  34. package/dist/frame-sync.d.ts +4 -0
  35. package/dist/frame-sync.d.ts.map +1 -0
  36. package/dist/frame-sync.js +37 -0
  37. package/dist/frame-sync.js.map +1 -0
  38. package/dist/index.d.ts +17 -12
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +11 -7
  41. package/dist/index.js.map +1 -1
  42. package/dist/matchers.d.ts +59 -0
  43. package/dist/matchers.d.ts.map +1 -0
  44. package/dist/matchers.js +117 -0
  45. package/dist/matchers.js.map +1 -0
  46. package/dist/pretty-widget.d.ts +24 -26
  47. package/dist/pretty-widget.d.ts.map +1 -1
  48. package/dist/pretty-widget.js +111 -117
  49. package/dist/pretty-widget.js.map +1 -1
  50. package/dist/queries.d.ts +190 -180
  51. package/dist/queries.d.ts.map +1 -1
  52. package/dist/queries.js +291 -320
  53. package/dist/queries.js.map +1 -1
  54. package/dist/register-test-hooks.d.ts +2 -0
  55. package/dist/register-test-hooks.d.ts.map +1 -0
  56. package/dist/register-test-hooks.js +16 -0
  57. package/dist/register-test-hooks.js.map +1 -0
  58. package/dist/render-hook.d.ts +15 -35
  59. package/dist/render-hook.d.ts.map +1 -1
  60. package/dist/render-hook.js +9 -45
  61. package/dist/render-hook.js.map +1 -1
  62. package/dist/render.d.ts +13 -42
  63. package/dist/render.d.ts.map +1 -1
  64. package/dist/render.js +138 -121
  65. package/dist/render.js.map +1 -1
  66. package/dist/role-helpers.d.ts +19 -53
  67. package/dist/role-helpers.d.ts.map +1 -1
  68. package/dist/role-helpers.js +48 -63
  69. package/dist/role-helpers.js.map +1 -1
  70. package/dist/screen.d.ts +7 -63
  71. package/dist/screen.d.ts.map +1 -1
  72. package/dist/screen.js +24 -110
  73. package/dist/screen.js.map +1 -1
  74. package/dist/screenshot.d.ts +21 -23
  75. package/dist/screenshot.d.ts.map +1 -1
  76. package/dist/screenshot.js +98 -50
  77. package/dist/screenshot.js.map +1 -1
  78. package/dist/suggestions.d.ts +27 -0
  79. package/dist/suggestions.d.ts.map +1 -0
  80. package/dist/suggestions.js +61 -0
  81. package/dist/suggestions.js.map +1 -0
  82. package/dist/traversal.d.ts +12 -10
  83. package/dist/traversal.d.ts.map +1 -1
  84. package/dist/traversal.js +36 -15
  85. package/dist/traversal.js.map +1 -1
  86. package/dist/types.d.ts +115 -169
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/user-event/adjustment.d.ts +10 -0
  90. package/dist/user-event/adjustment.d.ts.map +1 -0
  91. package/dist/user-event/adjustment.js +35 -0
  92. package/dist/user-event/adjustment.js.map +1 -0
  93. package/dist/user-event/click.d.ts +8 -0
  94. package/dist/user-event/click.d.ts.map +1 -0
  95. package/dist/user-event/click.js +73 -0
  96. package/dist/user-event/click.js.map +1 -0
  97. package/dist/user-event/controller.d.ts +8 -0
  98. package/dist/user-event/controller.d.ts.map +1 -0
  99. package/dist/user-event/controller.js +31 -0
  100. package/dist/user-event/controller.js.map +1 -0
  101. package/dist/user-event/dispatch.d.ts +7 -0
  102. package/dist/user-event/dispatch.d.ts.map +1 -0
  103. package/dist/user-event/dispatch.js +11 -0
  104. package/dist/user-event/dispatch.js.map +1 -0
  105. package/dist/user-event/event-wrapper.d.ts +4 -0
  106. package/dist/user-event/event-wrapper.d.ts.map +1 -0
  107. package/dist/user-event/event-wrapper.js +59 -0
  108. package/dist/user-event/event-wrapper.js.map +1 -0
  109. package/dist/user-event/gesture.d.ts +35 -0
  110. package/dist/user-event/gesture.d.ts.map +1 -0
  111. package/dist/user-event/gesture.js +138 -0
  112. package/dist/user-event/gesture.js.map +1 -0
  113. package/dist/user-event/index.d.ts +51 -0
  114. package/dist/user-event/index.d.ts.map +1 -0
  115. package/dist/user-event/index.js +42 -0
  116. package/dist/user-event/index.js.map +1 -0
  117. package/dist/user-event/keyboard.d.ts +10 -0
  118. package/dist/user-event/keyboard.d.ts.map +1 -0
  119. package/dist/user-event/keyboard.js +204 -0
  120. package/dist/user-event/keyboard.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +10 -0
  122. package/dist/user-event/pointer.d.ts.map +1 -0
  123. package/dist/user-event/pointer.js +30 -0
  124. package/dist/user-event/pointer.js.map +1 -0
  125. package/dist/user-event/selection.d.ts +5 -0
  126. package/dist/user-event/selection.d.ts.map +1 -0
  127. package/dist/user-event/selection.js +105 -0
  128. package/dist/user-event/selection.js.map +1 -0
  129. package/dist/user-event/state.d.ts +7 -0
  130. package/dist/user-event/state.d.ts.map +1 -0
  131. package/dist/user-event/state.js +3 -0
  132. package/dist/user-event/state.js.map +1 -0
  133. package/dist/user-event/text.d.ts +18 -0
  134. package/dist/user-event/text.d.ts.map +1 -0
  135. package/dist/user-event/text.js +113 -0
  136. package/dist/user-event/text.js.map +1 -0
  137. package/dist/wait-for.d.ts +16 -36
  138. package/dist/wait-for.d.ts.map +1 -1
  139. package/dist/wait-for.js +74 -70
  140. package/dist/wait-for.js.map +1 -1
  141. package/dist/widget-accessible-properties.d.ts +34 -0
  142. package/dist/widget-accessible-properties.d.ts.map +1 -0
  143. package/dist/widget-accessible-properties.js +260 -0
  144. package/dist/widget-accessible-properties.js.map +1 -0
  145. package/dist/within.d.ts +10 -26
  146. package/dist/within.d.ts.map +1 -1
  147. package/dist/within.js +16 -25
  148. package/dist/within.js.map +1 -1
  149. package/package.json +23 -12
  150. package/src/act.ts +77 -0
  151. package/src/bound-queries.ts +37 -0
  152. package/src/build-queries.ts +226 -0
  153. package/src/cleanup-registry.ts +27 -0
  154. package/src/config.ts +35 -62
  155. package/src/editable.ts +37 -0
  156. package/src/errors.ts +191 -0
  157. package/src/fire-event.ts +13 -38
  158. package/src/frame-sync.ts +51 -0
  159. package/src/index.ts +58 -15
  160. package/src/matchers.ts +251 -0
  161. package/src/pretty-widget.ts +163 -130
  162. package/src/queries.ts +485 -396
  163. package/src/register-test-hooks.ts +19 -0
  164. package/src/render-hook.tsx +38 -45
  165. package/src/render.tsx +192 -155
  166. package/src/role-helpers.ts +65 -72
  167. package/src/screen.ts +26 -114
  168. package/src/screenshot.ts +128 -56
  169. package/src/suggestions.ts +106 -0
  170. package/src/traversal.ts +56 -22
  171. package/src/types.ts +144 -172
  172. package/src/user-event/adjustment.ts +62 -0
  173. package/src/user-event/click.ts +99 -0
  174. package/src/user-event/controller.ts +58 -0
  175. package/src/user-event/dispatch.ts +25 -0
  176. package/src/user-event/event-wrapper.ts +80 -0
  177. package/src/user-event/gesture.ts +239 -0
  178. package/src/user-event/index.ts +81 -0
  179. package/src/user-event/keyboard.ts +290 -0
  180. package/src/user-event/pointer.ts +46 -0
  181. package/src/user-event/selection.ts +159 -0
  182. package/src/user-event/state.ts +8 -0
  183. package/src/user-event/text.ts +172 -0
  184. package/src/wait-for.ts +117 -78
  185. package/src/widget-accessible-properties.ts +388 -0
  186. package/src/within.ts +29 -27
  187. package/dist/bind-queries.d.ts +0 -14
  188. package/dist/bind-queries.d.ts.map +0 -1
  189. package/dist/bind-queries.js +0 -29
  190. package/dist/bind-queries.js.map +0 -1
  191. package/dist/error-builder.d.ts +0 -28
  192. package/dist/error-builder.d.ts.map +0 -1
  193. package/dist/error-builder.js +0 -81
  194. package/dist/error-builder.js.map +0 -1
  195. package/dist/timing.d.ts +0 -18
  196. package/dist/timing.d.ts.map +0 -1
  197. package/dist/timing.js +0 -18
  198. package/dist/timing.js.map +0 -1
  199. package/dist/user-event.d.ts +0 -138
  200. package/dist/user-event.d.ts.map +0 -1
  201. package/dist/user-event.js +0 -411
  202. package/dist/user-event.js.map +0 -1
  203. package/dist/widget-text.d.ts +0 -69
  204. package/dist/widget-text.d.ts.map +0 -1
  205. package/dist/widget-text.js +0 -177
  206. package/dist/widget-text.js.map +0 -1
  207. package/dist/widget.d.ts +0 -3
  208. package/dist/widget.d.ts.map +0 -1
  209. package/dist/widget.js +0 -13
  210. package/dist/widget.js.map +0 -1
  211. package/src/bind-queries.ts +0 -52
  212. package/src/error-builder.ts +0 -102
  213. package/src/timing.ts +0 -17
  214. package/src/user-event.ts +0 -492
  215. package/src/widget-text.ts +0 -202
  216. package/src/widget.ts +0 -15
package/dist/within.d.ts CHANGED
@@ -1,30 +1,14 @@
1
- import type * as Gtk from "@gtkx/ffi/gtk";
2
- import type { BoundQueries } from "./types.js";
1
+ import type { BoundQueries } from "./bound-queries.js";
2
+ import type { Container } from "./traversal.js";
3
+ import type { BoundCustomQueries, QueryMap } from "./types.js";
3
4
  /**
4
- * Creates scoped query methods for a container widget.
5
+ * Binds the built-in queries, and any custom queries, to a specific container
6
+ * so they search only within that scope.
5
7
  *
6
- * Use this to query within a specific section of your UI rather than
7
- * the entire application.
8
- *
9
- * @param container - The widget to scope queries to
10
- * @returns Object with query methods bound to the container
11
- *
12
- * @example
13
- * ```tsx
14
- * import { render, within } from "@gtkx/testing";
15
- *
16
- * test("scoped queries", async () => {
17
- * await render(<MyPage />);
18
- *
19
- * const sidebar = await screen.findByRole(Gtk.AccessibleRole.NAVIGATION);
20
- * const sidebarQueries = within(sidebar);
21
- *
22
- * // Only searches within the sidebar
23
- * const navButton = await sidebarQueries.findByRole(Gtk.AccessibleRole.BUTTON);
24
- * });
25
- * ```
26
- *
27
- * @see {@link screen} for global queries
8
+ * @param container The scope the returned queries operate on.
9
+ * @param queries Optional custom queries to bind alongside the built-in ones.
10
+ * @returns The queries bound to the container.
28
11
  */
29
- export declare const within: (container: Gtk.Widget) => BoundQueries;
12
+ declare const within: <Q extends QueryMap = Record<never, never>>(container: Container, queries?: Q) => BoundQueries & BoundCustomQueries<Q>;
13
+ export { within };
30
14
  //# sourceMappingURL=within.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"within.d.ts","sourceRoot":"","sources":["../src/within.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,eAAe,CAAC;AAE1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,MAAM,GAAI,WAAW,GAAG,CAAC,MAAM,KAAG,YAAsC,CAAC"}
1
+ {"version":3,"file":"within.d.ts","sourceRoot":"","sources":["../src/within.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAc/D;;;;;;;GAOG;AACH,QAAA,MAAM,MAAM,GAAI,CAAC,SAAS,QAAQ,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EACrD,WAAW,SAAS,EACpB,UAAU,CAAC,KACZ,YAAY,GAAG,kBAAkB,CAAC,CAAC,CAGpC,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC"}
package/dist/within.js CHANGED
@@ -1,29 +1,20 @@
1
- import { bindQueries } from "./bind-queries.js";
1
+ import { builtinQueries } from "./queries.js";
2
+ const bindQuery = (query, container) => (...args) => query(container, ...args);
3
+ const bindCustomQueries = (customQueries, container) => {
4
+ const entries = Object.entries(customQueries).map(([key, query]) => [key, bindQuery(query, container)]);
5
+ return Object.fromEntries(entries);
6
+ };
2
7
  /**
3
- * Creates scoped query methods for a container widget.
8
+ * Binds the built-in queries, and any custom queries, to a specific container
9
+ * so they search only within that scope.
4
10
  *
5
- * Use this to query within a specific section of your UI rather than
6
- * the entire application.
7
- *
8
- * @param container - The widget to scope queries to
9
- * @returns Object with query methods bound to the container
10
- *
11
- * @example
12
- * ```tsx
13
- * import { render, within } from "@gtkx/testing";
14
- *
15
- * test("scoped queries", async () => {
16
- * await render(<MyPage />);
17
- *
18
- * const sidebar = await screen.findByRole(Gtk.AccessibleRole.NAVIGATION);
19
- * const sidebarQueries = within(sidebar);
20
- *
21
- * // Only searches within the sidebar
22
- * const navButton = await sidebarQueries.findByRole(Gtk.AccessibleRole.BUTTON);
23
- * });
24
- * ```
25
- *
26
- * @see {@link screen} for global queries
11
+ * @param container The scope the returned queries operate on.
12
+ * @param queries Optional custom queries to bind alongside the built-in ones.
13
+ * @returns The queries bound to the container.
27
14
  */
28
- export const within = (container) => bindQueries(container);
15
+ const within = (container, queries) => ({
16
+ ...bindCustomQueries(builtinQueries, container),
17
+ ...(queries ? bindCustomQueries(queries, container) : {}),
18
+ });
19
+ export { within };
29
20
  //# sourceMappingURL=within.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"within.js","sourceRoot":"","sources":["../src/within.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,SAAqB,EAAgB,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"within.js","sourceRoot":"","sources":["../src/within.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,SAAS,GACX,CAAiC,KAAsD,EAAE,SAAoB,EAAE,EAAE,CAC7G,CAAC,GAAG,IAAU,EAAU,EAAE,CACtB,KAAK,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,CAAC;AAEtC,MAAM,iBAAiB,GAAG,CAAqB,aAAgB,EAAE,SAAoB,EAAyB,EAAE;IAC5G,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAU,CAAC,CAAC;IAEjH,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,CAA0B,CAAC;AAChE,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,GAAG,CACX,SAAoB,EACpB,OAAW,EACyB,EAAE,CAAC,CAAC;IACxC,GAAG,iBAAiB,CAAC,cAAc,EAAE,SAAS,CAAC;IAC/C,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,CAAE,EAA4B,CAAC;CACvF,CAAC,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC","sourcesContent":["import type { BoundQueries } from \"./bound-queries.js\";\nimport type { Container } from \"./traversal.js\";\nimport type { BoundCustomQueries, QueryMap } from \"./types.js\";\nimport { builtinQueries } from \"./queries.js\";\n\nconst bindQuery =\n <Args extends unknown[], Result>(query: (container: Container, ...args: Args) => Result, container: Container) =>\n (...args: Args): Result =>\n query(container, ...args);\n\nconst bindCustomQueries = <Q extends QueryMap>(customQueries: Q, container: Container): BoundCustomQueries<Q> => {\n const entries = Object.entries(customQueries).map(([key, query]) => [key, bindQuery(query, container)] as const);\n\n return Object.fromEntries(entries) as BoundCustomQueries<Q>;\n};\n\n/**\n * Binds the built-in queries, and any custom queries, to a specific container\n * so they search only within that scope.\n *\n * @param container The scope the returned queries operate on.\n * @param queries Optional custom queries to bind alongside the built-in ones.\n * @returns The queries bound to the container.\n */\nconst within = <Q extends QueryMap = Record<never, never>>(\n container: Container,\n queries?: Q,\n): BoundQueries & BoundCustomQueries<Q> => ({\n ...bindCustomQueries(builtinQueries, container),\n ...(queries ? bindCustomQueries(queries, container) : ({} as BoundCustomQueries<Q>)),\n});\n\nexport { within };\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gtkx/testing",
3
- "version": "0.21.0",
4
- "description": "Testing utilities for GTKX applications",
3
+ "version": "1.0.0-rc.2",
4
+ "description": "Testing Library-style queries, user events, and screenshots for GTK4 widget trees rendered by GTKX",
5
5
  "keywords": [
6
6
  "gtkx",
7
7
  "gtk",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/gtkx-org/gtkx.git",
22
+ "url": "git+https://github.com/gtkx-org/gtkx.git",
23
23
  "directory": "packages/testing"
24
24
  },
25
25
  "license": "MPL-2.0",
@@ -32,23 +32,34 @@
32
32
  "default": "./dist/index.js"
33
33
  }
34
34
  },
35
- "sideEffects": false,
35
+ "sideEffects": [
36
+ "./src/register-test-hooks.ts",
37
+ "./dist/register-test-hooks.js"
38
+ ],
36
39
  "files": [
37
40
  "dist",
38
41
  "src"
39
42
  ],
43
+ "engines": {
44
+ "node": ">=24"
45
+ },
40
46
  "dependencies": {
41
- "@gtkx/ffi": "0.21.0",
42
- "@gtkx/react": "0.21.0"
47
+ "@gtkx/react": "1.0.0-rc.2",
48
+ "@gtkx/runtime": "1.0.0-rc.2",
49
+ "@gtkx/utils": "1.0.0-rc.2"
50
+ },
51
+ "peerDependencies": {
52
+ "@types/react": "^19.2",
53
+ "react": "^19.2"
43
54
  },
44
55
  "devDependencies": {
45
- "@types/react-reconciler": "^0.33.0",
46
- "react-reconciler": "^0.33.0",
47
- "@gtkx/vitest": "0.21.0"
56
+ "@types/react": "^19.2.17",
57
+ "@vitest/expect": "^4.1.10",
58
+ "vitest": "^4.1.10",
59
+ "@gtkx/vitest": "1.0.0-rc.2",
60
+ "@gtkx/config": "1.0.0-rc.2"
48
61
  },
49
62
  "scripts": {
50
- "build": "tsc -b && cp ../../README.md .",
51
- "test": "vitest run",
52
- "typecheck": "tsc -b --emitDeclarationOnly"
63
+ "release": "tsx ../../scripts/release-package.ts"
53
64
  }
54
65
  }
package/src/act.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { act as reactAct } from "react";
2
+
3
+ declare global {
4
+ var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
5
+ }
6
+
7
+ type ActCallback<T> = () => T | PromiseLike<T>;
8
+ type ActImplementation = <T>(callback: ActCallback<T>) => PromiseLike<T>;
9
+
10
+ const actImplementation: ActImplementation = reactAct;
11
+ /**
12
+ * Runs a callback inside React's act environment, flushing pending state
13
+ * updates and effects, and resolves once the work has settled.
14
+ *
15
+ * @param callback Work to run within the act environment.
16
+ * @returns A promise that resolves with the callback's result after updates flush.
17
+ */
18
+ const act: ActImplementation = withGlobalActEnvironment(actImplementation);
19
+
20
+ const getGlobalThis = (): typeof globalThis => {
21
+ if (typeof globalThis !== "undefined") {
22
+ return globalThis;
23
+ }
24
+
25
+ throw new Error("unable to locate global object");
26
+ };
27
+
28
+ const getIsReactActEnvironment = (): boolean | undefined => getGlobalThis().IS_REACT_ACT_ENVIRONMENT;
29
+
30
+ const setIsReactActEnvironment = (value: boolean | undefined): void => {
31
+ getGlobalThis().IS_REACT_ACT_ENVIRONMENT = value;
32
+ };
33
+
34
+ const isThenable = <T>(value: unknown): value is PromiseLike<T> =>
35
+ value !== null && typeof value === "object" && typeof (value as PromiseLike<T>).then === "function";
36
+
37
+ const restoreAfter = async <T>(result: PromiseLike<T>, previous: boolean | undefined): Promise<T> => {
38
+ try {
39
+ return await result;
40
+ } finally {
41
+ setIsReactActEnvironment(previous);
42
+ }
43
+ };
44
+
45
+ const runWithActEnvironment = <T>(isForced: boolean, fn: () => T | PromiseLike<T>): Promise<T> => {
46
+ const previousActEnvironment = getIsReactActEnvironment();
47
+ setIsReactActEnvironment(isForced);
48
+
49
+ try {
50
+ const result: T | PromiseLike<T> = fn();
51
+
52
+ if (isThenable<T>(result)) {
53
+ return restoreAfter(result, previousActEnvironment);
54
+ }
55
+
56
+ setIsReactActEnvironment(previousActEnvironment);
57
+
58
+ return Promise.resolve(result);
59
+ } catch (error) {
60
+ setIsReactActEnvironment(previousActEnvironment);
61
+ throw error;
62
+ }
63
+ };
64
+
65
+ function withGlobalActEnvironment(actImplementation: ActImplementation): ActImplementation {
66
+ return <T>(callback: ActCallback<T>): PromiseLike<T> => {
67
+ const settled = runWithActEnvironment(true, () => actImplementation(() => callback()));
68
+
69
+ return Promise.resolve(settled);
70
+ };
71
+ }
72
+
73
+ const runInAct = async (callback: () => unknown): Promise<void> => {
74
+ await act(() => callback());
75
+ };
76
+
77
+ export { act, getIsReactActEnvironment, setIsReactActEnvironment, runWithActEnvironment, runInAct };
@@ -0,0 +1,37 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import type { ReactNode } from "react";
3
+ import type { builtinQueries } from "./queries.js";
4
+ import type { Container } from "./traversal.js";
5
+ import type { BoundCustomQueries, DebugUtilities, QueryMap } from "./types.js";
6
+
7
+ /**
8
+ * The set of built-in queries (getBy, findBy, queryBy, and their All variants)
9
+ * bound to a specific container.
10
+ */
11
+ type BoundQueries = BoundCustomQueries<typeof builtinQueries>;
12
+ /**
13
+ * The global screen object's shape: built-in queries scoped to the current
14
+ * toplevels, plus debug utilities.
15
+ */
16
+ type Screen = BoundQueries & DebugUtilities;
17
+
18
+ /**
19
+ * The result of a render call: queries scoped to the rendered tree, debug
20
+ * utilities, and controls for updating or tearing down the render.
21
+ *
22
+ * @typeParam Q Custom queries provided at render time, merged with the built-in ones.
23
+ */
24
+ type RenderResult<Q extends QueryMap = Record<never, never>> = BoundQueries &
25
+ BoundCustomQueries<Q> &
26
+ DebugUtilities & {
27
+ /** The top-level widget the element was rendered into. */
28
+ container: Gtk.Widget;
29
+ /** The root the queries are scoped to (defaults to all toplevels). */
30
+ baseElement: Container;
31
+ /** Unmounts the rendered tree and disposes its resources. */
32
+ unmount: () => Promise<void>;
33
+ /** Re-renders the tree with a new element, reusing the same root. */
34
+ rerender: (element: ReactNode) => Promise<void>;
35
+ };
36
+
37
+ export { type BoundQueries, type Screen, type RenderResult };
@@ -0,0 +1,226 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import type { Container } from "./traversal.js";
3
+ import type { MatcherOptions, WaitForOptions } from "./types.js";
4
+ import { getConfig } from "./config.js";
5
+ import { runWithExpensiveErrorDiagnosticsDisabled, suggestionError } from "./errors.js";
6
+ import { getSuggestedQuery, type Method, type Variant } from "./suggestions.js";
7
+ import { waitFor } from "./wait-for.js";
8
+
9
+ type QueryAllBy<Args extends unknown[]> = (container: Container, ...args: Args) => Gtk.Widget[];
10
+
11
+ type MultipleErrorBuilder<Args extends unknown[]> = (
12
+ container: Container,
13
+ matches: Gtk.Widget[],
14
+ ...args: Args
15
+ ) => Error;
16
+
17
+ type MissingErrorBuilder<Args extends unknown[]> = (container: Container, ...args: Args) => Error;
18
+
19
+ type BuiltQueries<Args extends unknown[]> = {
20
+ queryBy: (container: Container, ...args: Args) => Gtk.Widget | null;
21
+ getAllBy: QueryAllBy<Args>;
22
+ getBy: (container: Container, ...args: Args) => Gtk.Widget;
23
+ findAllBy: (container: Container, ...args: Args) => Promise<Gtk.Widget[]>;
24
+ findBy: (container: Container, ...args: Args) => Promise<Gtk.Widget>;
25
+ };
26
+
27
+ type FindQuery<Args extends unknown[]> = (container: Container, ...args: Args) => unknown;
28
+ type SingleQuery<Args extends unknown[]> = (container: Container, ...args: Args) => Gtk.Widget | null;
29
+
30
+ const extractWaitForOptions = (args: unknown[]): WaitForOptions => {
31
+ const last = args.at(-1);
32
+
33
+ if (last && typeof last === "object" && !(last instanceof RegExp)) {
34
+ const { timeout, interval, onTimeout } = last as WaitForOptions;
35
+
36
+ return { timeout, interval, onTimeout };
37
+ }
38
+
39
+ return {};
40
+ };
41
+
42
+ const extractSuggestOption = (args: unknown[]): boolean | undefined => {
43
+ const last = args.at(-1);
44
+
45
+ if (last && typeof last === "object" && !(last instanceof RegExp)) {
46
+ return (last as MatcherOptions).suggest;
47
+ }
48
+
49
+ return undefined;
50
+ };
51
+
52
+ const maybeThrowSuggestion = (options: {
53
+ container: Container;
54
+ match: Gtk.Widget;
55
+ queryName: Method;
56
+ variant: Variant;
57
+ suggest: boolean | undefined;
58
+ }): void => {
59
+ const { container, match, queryName, variant, suggest } = options;
60
+ const shouldSuggest = suggest ?? getConfig().throwSuggestions;
61
+
62
+ if (!shouldSuggest) {
63
+ return;
64
+ }
65
+
66
+ const suggestion = getSuggestedQuery(match, variant);
67
+
68
+ if (suggestion && suggestion.queryName !== queryName) {
69
+ throw suggestionError(suggestion.toString(), container);
70
+ }
71
+ };
72
+
73
+ const reRunForDiagnostics = <Args extends unknown[]>(
74
+ query: FindQuery<Args>,
75
+ container: Container,
76
+ args: Args,
77
+ fallback: Error,
78
+ ): Error => {
79
+ try {
80
+ query(container, ...args);
81
+ } catch (error) {
82
+ if (error instanceof Error) {
83
+ return error;
84
+ }
85
+ }
86
+
87
+ return fallback;
88
+ };
89
+
90
+ const findOptions = <Args extends unknown[]>(
91
+ query: FindQuery<Args>,
92
+ container: Container,
93
+ args: Args,
94
+ ): WaitForOptions => {
95
+ const userOptions = extractWaitForOptions(args);
96
+
97
+ const onTimeout =
98
+ userOptions.onTimeout ?? ((fallback: Error) => reRunForDiagnostics(query, container, args, fallback));
99
+
100
+ return {
101
+ stackTraceError: new Error("STACK_TRACE_MESSAGE"),
102
+ onTimeout,
103
+ timeout: userOptions.timeout,
104
+ interval: userOptions.interval,
105
+ };
106
+ };
107
+
108
+ const singleFrom =
109
+ <Args extends unknown[]>(
110
+ allQuery: QueryAllBy<Args>,
111
+ getMultipleError: MultipleErrorBuilder<Args>,
112
+ ): SingleQuery<Args> =>
113
+ (container, ...args) => {
114
+ const matches = allQuery(container, ...args);
115
+
116
+ if (matches.length > 1) {
117
+ throw getMultipleError(container, matches, ...args);
118
+ }
119
+
120
+ return matches[0] ?? null;
121
+ };
122
+
123
+ const allOrThrow =
124
+ <Args extends unknown[]>(
125
+ allQuery: QueryAllBy<Args>,
126
+ getMissingError: MissingErrorBuilder<Args>,
127
+ ): QueryAllBy<Args> =>
128
+ (container, ...args) => {
129
+ const matches = allQuery(container, ...args);
130
+
131
+ if (matches.length === 0) {
132
+ throw getMissingError(container, ...args);
133
+ }
134
+
135
+ return matches;
136
+ };
137
+
138
+ const wrapSingleWithSuggestion =
139
+ <Args extends unknown[]>(query: SingleQuery<Args>, queryName: Method, variant: Variant): SingleQuery<Args> =>
140
+ (container, ...args) => {
141
+ const match = query(container, ...args);
142
+
143
+ if (match) {
144
+ maybeThrowSuggestion({ container, match, queryName, variant, suggest: extractSuggestOption(args) });
145
+ }
146
+
147
+ return match;
148
+ };
149
+
150
+ const wrapAllWithSuggestion =
151
+ <Args extends unknown[]>(query: QueryAllBy<Args>, queryName: Method, variant: Variant): QueryAllBy<Args> =>
152
+ (container, ...args) => {
153
+ const matches = query(container, ...args);
154
+ const [first] = matches;
155
+
156
+ if (first !== undefined) {
157
+ maybeThrowSuggestion({
158
+ container,
159
+ match: first,
160
+ queryName,
161
+ variant,
162
+ suggest: extractSuggestOption(args),
163
+ });
164
+ }
165
+
166
+ return matches;
167
+ };
168
+
169
+ const requireSingle =
170
+ <Args extends unknown[]>(query: SingleQuery<Args>, getMissingError: MissingErrorBuilder<Args>) =>
171
+ (container: Container, ...args: Args): Gtk.Widget => {
172
+ const match = query(container, ...args);
173
+
174
+ if (match === null) {
175
+ throw getMissingError(container, ...args);
176
+ }
177
+
178
+ return match;
179
+ };
180
+
181
+ const buildQueries = <Args extends unknown[]>(
182
+ queryName: Method,
183
+ queryAllBy: QueryAllBy<Args>,
184
+ getMultipleError: MultipleErrorBuilder<Args>,
185
+ getMissingError: MissingErrorBuilder<Args>,
186
+ ): BuiltQueries<Args> => {
187
+ const getAllBy = allOrThrow(queryAllBy, getMissingError);
188
+ const getBy = requireSingle(singleFrom(getAllBy, getMultipleError), getMissingError);
189
+
190
+ const queryByWithSuggestion = wrapSingleWithSuggestion(
191
+ singleFrom(queryAllBy, getMultipleError),
192
+ queryName,
193
+ "query",
194
+ );
195
+
196
+ const getAllByWithSuggestion = wrapAllWithSuggestion(getAllBy, queryName, "getAll");
197
+
198
+ const getByWithSuggestion: (container: Container, ...args: Args) => Gtk.Widget = (container, ...args) => {
199
+ const match = getBy(container, ...args);
200
+ maybeThrowSuggestion({ container, match, queryName, variant: "get", suggest: extractSuggestOption(args) });
201
+
202
+ return match;
203
+ };
204
+
205
+ const findAllBy = (container: Container, ...args: Args): Promise<Gtk.Widget[]> =>
206
+ waitFor(
207
+ () => runWithExpensiveErrorDiagnosticsDisabled(() => getAllByWithSuggestion(container, ...args)),
208
+ findOptions(getAllByWithSuggestion, container, args),
209
+ );
210
+
211
+ const findBy = (container: Container, ...args: Args): Promise<Gtk.Widget> =>
212
+ waitFor(
213
+ () => runWithExpensiveErrorDiagnosticsDisabled(() => getByWithSuggestion(container, ...args)),
214
+ findOptions(getByWithSuggestion, container, args),
215
+ );
216
+
217
+ return {
218
+ queryBy: queryByWithSuggestion,
219
+ getAllBy: getAllByWithSuggestion,
220
+ getBy: getByWithSuggestion,
221
+ findAllBy,
222
+ findBy,
223
+ };
224
+ };
225
+
226
+ export { buildQueries, type QueryAllBy, type BuiltQueries };
@@ -0,0 +1,27 @@
1
+ type CleanupFunction = () => void | Promise<void>;
2
+
3
+ const cleanupQueue: Set<CleanupFunction> = new Set();
4
+
5
+ const addToCleanupQueue = (fn: CleanupFunction): void => {
6
+ cleanupQueue.add(fn);
7
+ };
8
+
9
+ const runCleanup = async (): Promise<void> => {
10
+ const failures: unknown[] = [];
11
+
12
+ for (const fn of cleanupQueue) {
13
+ try {
14
+ await fn();
15
+ } catch (error) {
16
+ failures.push(error);
17
+ }
18
+ }
19
+
20
+ cleanupQueue.clear();
21
+
22
+ if (failures.length > 0) {
23
+ throw new AggregateError(failures, "Cleanup failed");
24
+ }
25
+ };
26
+
27
+ export { addToCleanupQueue, runCleanup, type CleanupFunction };
package/src/config.ts CHANGED
@@ -1,89 +1,62 @@
1
1
  import type { Container } from "./traversal.js";
2
2
 
3
3
  /**
4
- * Configuration options for the testing library.
4
+ * Global configuration for the testing harness, controlling query behavior,
5
+ * error formatting, and the timeouts used by async utilities and actionability
6
+ * checks.
5
7
  */
6
- export type Config = {
7
- /**
8
- * Whether to show role suggestions in error messages when elements are not found.
9
- * @default true
10
- */
11
- showSuggestions: boolean;
8
+ type Config = {
9
+ throwSuggestions: boolean;
12
10
 
13
- /**
14
- * Custom error factory for query failures.
15
- * Allows customizing how errors are constructed.
16
- */
17
- getElementError: (message: string, container: Container) => Error;
11
+ /** Builds the error thrown when a query fails, given a message and optional container. */
12
+ getElementError: (message: string, container?: Container) => Error;
18
13
 
19
- /**
20
- * Default timeout in milliseconds for async utilities (waitFor, findBy* queries).
21
- * @default 1000
22
- */
14
+ /** Default timeout in milliseconds for async utilities such as waitFor. */
23
15
  asyncUtilTimeout: number;
24
- };
25
16
 
26
- const defaultGetElementError = (message: string, _container: Container): Error => {
27
- return new Error(message);
17
+ /** Timeout in milliseconds for waiting on a widget to become actionable. */
18
+ actionabilityTimeout: number;
28
19
  };
29
20
 
21
+ /**
22
+ * Function form of a configuration update: receives the current config and
23
+ * returns the fields to override.
24
+ */
25
+ type ConfigFn = (existingConfig: Config) => Partial<Config>;
26
+
30
27
  const defaultConfig: Config = {
31
- showSuggestions: true,
28
+ throwSuggestions: false,
32
29
  getElementError: defaultGetElementError,
33
30
  asyncUtilTimeout: 1000,
31
+ actionabilityTimeout: 500,
34
32
  };
35
33
 
36
- let currentConfig: Config = { ...defaultConfig };
34
+ const currentConfig: Config = { ...defaultConfig };
35
+
36
+ function defaultGetElementError(message: string): Error {
37
+ return new GtkxElementError(message);
38
+ }
37
39
 
38
40
  /**
39
- * Returns the current testing library configuration.
40
- *
41
- * @returns The current configuration object
42
- *
43
- * @example
44
- * ```tsx
45
- * import { getConfig } from "@gtkx/testing";
46
- *
47
- * const config = getConfig();
48
- * console.log(config.showSuggestions);
49
- * ```
41
+ * Returns the current global testing configuration.
50
42
  */
51
- export const getConfig = (): Readonly<Config> => {
43
+ const getConfig = (): Config => {
52
44
  return currentConfig;
53
45
  };
54
46
 
55
47
  /**
56
- * Configures the testing library behavior.
57
- *
58
- * Accepts either a partial configuration object or a function that receives
59
- * the current configuration and returns updates.
48
+ * Merges the given overrides into the global testing configuration.
60
49
  *
61
- * @param newConfig - Partial configuration or updater function
62
- *
63
- * @example
64
- * ```tsx
65
- * import { configure } from "@gtkx/testing";
66
- *
67
- * // Disable role suggestions
68
- * configure({ showSuggestions: false });
69
- *
70
- * // Use updater function
71
- * configure((current) => ({
72
- * showSuggestions: !current.showSuggestions,
73
- * }));
74
- * ```
50
+ * @param newConfig Either a partial config object or a function that receives
51
+ * the current config and returns the fields to override.
75
52
  */
76
- export const configure = (newConfig: Partial<Config> | ((current: Config) => Partial<Config>)): void => {
53
+ const configure = (newConfig: Partial<Config> | ConfigFn): void => {
77
54
  const updates = typeof newConfig === "function" ? newConfig(currentConfig) : newConfig;
78
- currentConfig = { ...currentConfig, ...updates };
55
+ Object.assign(currentConfig, updates);
79
56
  };
80
57
 
81
- /**
82
- * Resets configuration to defaults.
83
- * Primarily used for testing.
84
- *
85
- * @internal
86
- */
87
- export const resetConfig = (): void => {
88
- currentConfig = { ...defaultConfig };
89
- };
58
+ class GtkxElementError extends Error {
59
+ override name = "GtkxElementError";
60
+ }
61
+
62
+ export { getConfig, configure, type Config, type ConfigFn };