@gtkx/testing 1.0.0-rc.1 → 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 (182) hide show
  1. package/dist/act.d.ts +6 -6
  2. package/dist/act.d.ts.map +1 -1
  3. package/dist/act.js +34 -26
  4. package/dist/act.js.map +1 -1
  5. package/dist/bound-queries.d.ts +4 -3
  6. package/dist/bound-queries.d.ts.map +1 -1
  7. package/dist/bound-queries.js.map +1 -1
  8. package/dist/build-queries.d.ts +5 -5
  9. package/dist/build-queries.d.ts.map +1 -1
  10. package/dist/build-queries.js +21 -9
  11. package/dist/build-queries.js.map +1 -1
  12. package/dist/cleanup-registry.d.ts +4 -3
  13. package/dist/cleanup-registry.d.ts.map +1 -1
  14. package/dist/cleanup-registry.js +13 -3
  15. package/dist/cleanup-registry.js.map +1 -1
  16. package/dist/config.d.ts +5 -4
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +11 -9
  19. package/dist/config.js.map +1 -1
  20. package/dist/editable.d.ts +6 -5
  21. package/dist/editable.d.ts.map +1 -1
  22. package/dist/editable.js +7 -5
  23. package/dist/editable.js.map +1 -1
  24. package/dist/errors.d.ts +7 -6
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +64 -48
  27. package/dist/errors.js.map +1 -1
  28. package/dist/fire-event.d.ts +2 -1
  29. package/dist/fire-event.d.ts.map +1 -1
  30. package/dist/fire-event.js +2 -1
  31. package/dist/fire-event.js.map +1 -1
  32. package/dist/frame-sync.d.ts +2 -1
  33. package/dist/frame-sync.d.ts.map +1 -1
  34. package/dist/frame-sync.js +28 -21
  35. package/dist/frame-sync.js.map +1 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/matchers.d.ts +38 -34
  41. package/dist/matchers.d.ts.map +1 -1
  42. package/dist/matchers.js +72 -58
  43. package/dist/matchers.js.map +1 -1
  44. package/dist/pretty-widget.d.ts +6 -4
  45. package/dist/pretty-widget.d.ts.map +1 -1
  46. package/dist/pretty-widget.js +64 -24
  47. package/dist/pretty-widget.js.map +1 -1
  48. package/dist/queries.d.ts +143 -86
  49. package/dist/queries.d.ts.map +1 -1
  50. package/dist/queries.js +231 -147
  51. package/dist/queries.js.map +1 -1
  52. package/dist/register-test-hooks.js +2 -1
  53. package/dist/register-test-hooks.js.map +1 -1
  54. package/dist/render-hook.d.ts +3 -2
  55. package/dist/render-hook.d.ts.map +1 -1
  56. package/dist/render-hook.js +2 -1
  57. package/dist/render-hook.js.map +1 -1
  58. package/dist/render.d.ts +3 -2
  59. package/dist/render.d.ts.map +1 -1
  60. package/dist/render.js +24 -18
  61. package/dist/render.js.map +1 -1
  62. package/dist/role-helpers.d.ts +6 -5
  63. package/dist/role-helpers.d.ts.map +1 -1
  64. package/dist/role-helpers.js +16 -12
  65. package/dist/role-helpers.js.map +1 -1
  66. package/dist/screen.d.ts +4 -3
  67. package/dist/screen.d.ts.map +1 -1
  68. package/dist/screen.js +13 -11
  69. package/dist/screen.js.map +1 -1
  70. package/dist/screenshot.d.ts +4 -3
  71. package/dist/screenshot.d.ts.map +1 -1
  72. package/dist/screenshot.js +43 -36
  73. package/dist/screenshot.js.map +1 -1
  74. package/dist/suggestions.d.ts +11 -6
  75. package/dist/suggestions.d.ts.map +1 -1
  76. package/dist/suggestions.js +10 -5
  77. package/dist/suggestions.js.map +1 -1
  78. package/dist/traversal.d.ts +7 -7
  79. package/dist/traversal.d.ts.map +1 -1
  80. package/dist/traversal.js +17 -11
  81. package/dist/traversal.js.map +1 -1
  82. package/dist/types.d.ts +39 -24
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js.map +1 -1
  85. package/dist/user-event/adjustment.d.ts +5 -3
  86. package/dist/user-event/adjustment.d.ts.map +1 -1
  87. package/dist/user-event/adjustment.js +6 -4
  88. package/dist/user-event/adjustment.js.map +1 -1
  89. package/dist/user-event/click.d.ts +6 -5
  90. package/dist/user-event/click.d.ts.map +1 -1
  91. package/dist/user-event/click.js +29 -21
  92. package/dist/user-event/click.js.map +1 -1
  93. package/dist/user-event/controller.d.ts +6 -5
  94. package/dist/user-event/controller.d.ts.map +1 -1
  95. package/dist/user-event/controller.js +9 -6
  96. package/dist/user-event/controller.js.map +1 -1
  97. package/dist/user-event/dispatch.d.ts +3 -3
  98. package/dist/user-event/dispatch.d.ts.map +1 -1
  99. package/dist/user-event/dispatch.js +3 -2
  100. package/dist/user-event/dispatch.js.map +1 -1
  101. package/dist/user-event/event-wrapper.d.ts +2 -1
  102. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  103. package/dist/user-event/event-wrapper.js +27 -16
  104. package/dist/user-event/event-wrapper.js.map +1 -1
  105. package/dist/user-event/gesture.d.ts +18 -14
  106. package/dist/user-event/gesture.d.ts.map +1 -1
  107. package/dist/user-event/gesture.js +70 -37
  108. package/dist/user-event/gesture.js.map +1 -1
  109. package/dist/user-event/index.d.ts +17 -10
  110. package/dist/user-event/index.d.ts.map +1 -1
  111. package/dist/user-event/index.js +7 -3
  112. package/dist/user-event/index.js.map +1 -1
  113. package/dist/user-event/keyboard.d.ts +4 -3
  114. package/dist/user-event/keyboard.d.ts.map +1 -1
  115. package/dist/user-event/keyboard.js +89 -58
  116. package/dist/user-event/keyboard.js.map +1 -1
  117. package/dist/user-event/pointer.d.ts +7 -3
  118. package/dist/user-event/pointer.d.ts.map +1 -1
  119. package/dist/user-event/pointer.js +4 -1
  120. package/dist/user-event/pointer.js.map +1 -1
  121. package/dist/user-event/selection.d.ts +3 -2
  122. package/dist/user-event/selection.d.ts.map +1 -1
  123. package/dist/user-event/selection.js +26 -27
  124. package/dist/user-event/selection.js.map +1 -1
  125. package/dist/user-event/state.d.ts +3 -2
  126. package/dist/user-event/state.d.ts.map +1 -1
  127. package/dist/user-event/state.js +2 -1
  128. package/dist/user-event/state.js.map +1 -1
  129. package/dist/user-event/text.d.ts +8 -7
  130. package/dist/user-event/text.d.ts.map +1 -1
  131. package/dist/user-event/text.js +39 -33
  132. package/dist/user-event/text.js.map +1 -1
  133. package/dist/wait-for.d.ts +5 -5
  134. package/dist/wait-for.d.ts.map +1 -1
  135. package/dist/wait-for.js +45 -30
  136. package/dist/wait-for.js.map +1 -1
  137. package/dist/widget-accessible-properties.d.ts +26 -25
  138. package/dist/widget-accessible-properties.d.ts.map +1 -1
  139. package/dist/widget-accessible-properties.js +106 -61
  140. package/dist/widget-accessible-properties.js.map +1 -1
  141. package/dist/within.d.ts +2 -1
  142. package/dist/within.d.ts.map +1 -1
  143. package/dist/within.js +2 -1
  144. package/dist/within.js.map +1 -1
  145. package/package.json +11 -9
  146. package/src/act.ts +44 -35
  147. package/src/bound-queries.ts +5 -4
  148. package/src/build-queries.ts +84 -43
  149. package/src/cleanup-registry.ts +18 -5
  150. package/src/config.ts +16 -12
  151. package/src/editable.ts +13 -6
  152. package/src/errors.ts +106 -45
  153. package/src/fire-event.ts +3 -1
  154. package/src/frame-sync.ts +38 -21
  155. package/src/index.ts +2 -1
  156. package/src/matchers.ts +158 -107
  157. package/src/pretty-widget.ts +114 -42
  158. package/src/queries.ts +422 -233
  159. package/src/register-test-hooks.ts +4 -1
  160. package/src/render-hook.tsx +10 -4
  161. package/src/render.tsx +58 -33
  162. package/src/role-helpers.ts +29 -16
  163. package/src/screen.ts +17 -12
  164. package/src/screenshot.ts +61 -38
  165. package/src/suggestions.ts +29 -10
  166. package/src/traversal.ts +35 -15
  167. package/src/types.ts +65 -32
  168. package/src/user-event/adjustment.ts +23 -10
  169. package/src/user-event/click.ts +49 -20
  170. package/src/user-event/controller.ts +21 -7
  171. package/src/user-event/dispatch.ts +4 -2
  172. package/src/user-event/event-wrapper.ts +42 -16
  173. package/src/user-event/gesture.ts +135 -50
  174. package/src/user-event/index.ts +19 -12
  175. package/src/user-event/keyboard.ts +147 -61
  176. package/src/user-event/pointer.ts +20 -8
  177. package/src/user-event/selection.ts +69 -32
  178. package/src/user-event/state.ts +4 -2
  179. package/src/user-event/text.ts +73 -38
  180. package/src/wait-for.ts +77 -36
  181. package/src/widget-accessible-properties.ts +218 -74
  182. package/src/within.ts +7 -4
package/src/frame-sync.ts CHANGED
@@ -1,34 +1,51 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
+ import * as GLib from "@gtkx/gi/glib";
2
3
 
3
4
  const CLOCK_STALL_FALLBACK_MS = 500;
4
5
 
5
- export const scheduleAfterLayout = (widget: Gtk.Widget | null, callback: () => void): void => {
6
- let done = false;
7
- let removeTick: (() => void) | null = null;
8
- let fallback: ReturnType<typeof setTimeout> | null = null;
9
-
10
- const finish = (): void => {
11
- if (done) return;
12
- done = true;
13
- if (fallback !== null) clearTimeout(fallback);
14
- removeTick?.();
15
- removeTick = null;
16
- fallback = null;
6
+ const once = (callback: () => void): (() => void) => {
7
+ let isCalled = false;
8
+
9
+ return () => {
10
+ if (isCalled) {
11
+ return;
12
+ }
13
+
14
+ isCalled = true;
17
15
  callback();
18
16
  };
17
+ };
19
18
 
20
- if (widget === null || widget.getFrameClock() === null || widget.getWidth() > 0) {
21
- queueMicrotask(finish);
22
- return;
23
- }
19
+ const runWhenSized = (widget: Gtk.Widget, finish: () => void): void => {
20
+ let tickId = 0;
21
+
22
+ const fallback = setTimeout(() => {
23
+ widget.removeTickCallback(tickId);
24
+ finish();
25
+ }, CLOCK_STALL_FALLBACK_MS);
24
26
 
25
- fallback = setTimeout(finish, CLOCK_STALL_FALLBACK_MS);
27
+ tickId = widget.addTickCallback(() => {
28
+ if (widget.getWidth() === 0) {
29
+ return GLib.SOURCE_CONTINUE;
30
+ }
26
31
 
27
- const tickId = widget.addTickCallback(() => {
28
- if (widget.getWidth() === 0) return true;
32
+ clearTimeout(fallback);
29
33
  finish();
30
- return false;
34
+
35
+ return GLib.SOURCE_REMOVE;
31
36
  });
37
+ };
38
+
39
+ const scheduleAfterLayout = (widget: Gtk.Widget | null, callback: () => void): void => {
40
+ const finish = once(callback);
41
+
42
+ if (widget?.getFrameClock() == null || widget.getWidth() > 0) {
43
+ queueMicrotask(finish);
32
44
 
33
- removeTick = () => widget.removeTickCallback(tickId);
45
+ return;
46
+ }
47
+
48
+ runWhenSized(widget, finish);
34
49
  };
50
+
51
+ export { scheduleAfterLayout };
package/src/index.ts CHANGED
@@ -48,8 +48,8 @@ export {
48
48
  queryByRole,
49
49
  queryByText,
50
50
  } from "./queries.js";
51
- export { cleanup, render } from "./render.js";
52
51
  export { renderHook } from "./render-hook.js";
52
+ export { cleanup, render } from "./render.js";
53
53
  export { formatRole, getRoles, logRoles, prettyRoles } from "./role-helpers.js";
54
54
  export { screen } from "./screen.js";
55
55
  export { captureAndSaveScreenshot, logScreenshotPath, screenshot } from "./screenshot.js";
@@ -79,6 +79,7 @@ export type {
79
79
  DropContent,
80
80
  DropOptions,
81
81
  PointerInput,
82
+ ScrollDelta,
82
83
  TabOptions,
83
84
  TypeOptions,
84
85
  UserEvent,
package/src/matchers.ts CHANGED
@@ -13,188 +13,239 @@ import {
13
13
  } from "./widget-accessible-properties.js";
14
14
 
15
15
  /** The expected value for a text matcher: an exact string or a regular expression. */
16
- export type TextExpectation = string | RegExp;
17
-
16
+ type TextExpectation = string | RegExp;
18
17
  type MatcherResult = { pass: boolean; message: () => string };
18
+ type TextMatcher = (received: unknown, expected?: TextExpectation) => MatcherResult;
19
+ type StateMatcher = (received: unknown) => MatcherResult;
20
+ type ValueMatcher = (received: unknown, expected: number) => MatcherResult;
21
+ type TextMatcherContext = { matcherName: string; widget: Gtk.Widget; actual: string | null };
19
22
 
20
- interface MatcherContext {
21
- isNot: boolean;
22
- }
23
+ type MatcherImplementations = {
24
+ toHaveDisplayValue: TextMatcher;
25
+ toHaveTextContent: TextMatcher;
26
+ toHaveAccessibleName: TextMatcher;
27
+ toHavePlaceholderText: TextMatcher;
28
+ toBeChecked: StateMatcher;
29
+ toBePressed: StateMatcher;
30
+ toBeExpanded: StateMatcher;
31
+ toBeSelected: StateMatcher;
32
+ toHaveValue: ValueMatcher;
33
+ };
34
+
35
+ type ExpectExtend = { extend: (m: MatcherImplementations) => void };
36
+
37
+ const toHaveDisplayValue: TextMatcher = textMatcher("toHaveDisplayValue", getWidgetDisplayValue, "exact");
38
+
39
+ const toHaveTextContent: TextMatcher = textMatcher(
40
+ "toHaveTextContent",
41
+ (widget) => getWidgetNodeText(widget) ?? getWidgetAccessibleName(widget),
42
+ "substring",
43
+ );
44
+
45
+ const toHaveAccessibleName: TextMatcher = textMatcher("toHaveAccessibleName", getWidgetAccessibleName, "exact");
46
+
47
+ const toHavePlaceholderText: TextMatcher = textMatcher(
48
+ "toHavePlaceholderText",
49
+ getWidgetPlaceholderText,
50
+ "exact",
51
+ );
52
+
53
+ const toBeChecked: StateMatcher = booleanStateMatcher("toBeChecked", "checked", getWidgetCheckedState);
54
+ const toBePressed: StateMatcher = booleanStateMatcher("toBePressed", "pressed", getWidgetPressedState);
55
+ const toBeExpanded: StateMatcher = booleanStateMatcher("toBeExpanded", "expanded", getWidgetExpandedState);
56
+ const toBeSelected: StateMatcher = booleanStateMatcher("toBeSelected", "selected", getWidgetSelectedState);
57
+
58
+ /** The widget assertion matchers keyed by name, suitable for passing to `expect.extend`. */
59
+ const matchers: MatcherImplementations = {
60
+ toHaveDisplayValue,
61
+ toHaveTextContent,
62
+ toHaveAccessibleName,
63
+ toHavePlaceholderText,
64
+ toBeChecked,
65
+ toBePressed,
66
+ toBeExpanded,
67
+ toBeSelected,
68
+ toHaveValue,
69
+ };
23
70
 
24
- type TextMatcher = (this: MatcherContext, received: unknown, expected?: TextExpectation) => MatcherResult;
25
- type StateMatcher = (this: MatcherContext, received: unknown) => MatcherResult;
26
- type ValueMatcher = (this: MatcherContext, received: unknown, expected: number) => MatcherResult;
71
+ const registration = { isRegistered: false };
27
72
 
28
73
  const asWidget = (received: unknown, matcherName: string): Gtk.Widget => {
29
74
  if (!(received instanceof Gtk.Widget)) {
30
75
  throw new TypeError(`${matcherName}: received value must be a Gtk.Widget, got ${typeof received}`);
31
76
  }
77
+
32
78
  return received;
33
79
  };
34
80
 
35
81
  const describeWidget = (widget: Gtk.Widget): string => {
36
82
  const role = Gtk.AccessibleRole[widget.getAccessibleRole()];
37
83
  const name = getWidgetAccessibleName(widget);
38
- return name !== null ? `<${role} name=${JSON.stringify(name)}>` : `<${role}>`;
84
+
85
+ return name === null ? `<${role}>` : `<${role} name=${JSON.stringify(name)}>`;
39
86
  };
40
87
 
41
- const matchesText = (actual: string, expected: TextExpectation, mode: "exact" | "substring"): boolean => {
88
+ const isTextMatch = (actual: string, expected: TextExpectation, mode: "exact" | "substring"): boolean => {
42
89
  if (expected instanceof RegExp) {
43
90
  expected.lastIndex = 0;
91
+
44
92
  return expected.test(actual);
45
93
  }
94
+
46
95
  return mode === "exact" ? actual === expected : actual.includes(expected);
47
96
  };
48
97
 
49
98
  const describeExpected = (expected: TextExpectation): string =>
50
99
  expected instanceof RegExp ? String(expected) : JSON.stringify(expected);
51
100
 
52
- const textMatcher = (
101
+ const negationPrefix = (isPass: boolean): string => (isPass ? "not " : "");
102
+
103
+ const nonEmptyResult = ({ matcherName, widget, actual }: TextMatcherContext): MatcherResult => {
104
+ const isPass = actual !== null && actual !== "";
105
+
106
+ return {
107
+ pass: isPass,
108
+ message: () =>
109
+ `expected widget ${negationPrefix(isPass)}to have a non-empty value for ${matcherName}, ` +
110
+ `but got ${JSON.stringify(actual)}\n${describeWidget(widget)}`,
111
+ };
112
+ };
113
+
114
+ const matchedResult = (context: TextMatcherContext, expected: TextExpectation, isPass: boolean): MatcherResult => ({
115
+ pass: isPass,
116
+ message: () =>
117
+ `expected widget ${negationPrefix(isPass)}${context.matcherName} ${describeExpected(expected)}, ` +
118
+ `but received ${JSON.stringify(context.actual)}\n${describeWidget(context.widget)}`,
119
+ });
120
+
121
+ function textMatcher(
53
122
  matcherName: string,
54
123
  read: (widget: Gtk.Widget) => string | null,
55
124
  mode: "exact" | "substring",
56
- ): TextMatcher =>
57
- function (this: MatcherContext, received: unknown, expected?: TextExpectation): MatcherResult {
125
+ ): TextMatcher {
126
+ return (received: unknown, expected?: TextExpectation): MatcherResult => {
58
127
  const widget = asWidget(received, matcherName);
59
128
  const actual = read(widget);
129
+ const context: TextMatcherContext = { matcherName, widget, actual };
60
130
 
61
131
  if (expected === undefined) {
62
- const pass = actual !== null && actual !== "";
63
- return {
64
- pass,
65
- message: () =>
66
- `expected widget ${this.isNot ? "not " : ""}to have a non-empty value for ${matcherName}, ` +
67
- `but got ${JSON.stringify(actual)}\n${describeWidget(widget)}`,
68
- };
132
+ return nonEmptyResult(context);
69
133
  }
70
134
 
71
- const pass = actual !== null && matchesText(actual, expected, mode);
72
- return {
73
- pass,
74
- message: () =>
75
- `expected widget ${this.isNot ? "not " : ""}${matcherName} ${describeExpected(expected)}, ` +
76
- `but received ${JSON.stringify(actual)}\n${describeWidget(widget)}`,
77
- };
135
+ return matchedResult(context, expected, actual !== null && isTextMatch(actual, expected, mode));
78
136
  };
137
+ }
79
138
 
80
- const booleanStateMatcher = (
139
+ function booleanStateMatcher(
81
140
  matcherName: string,
82
141
  stateName: string,
83
142
  read: (widget: Gtk.Widget) => boolean | null,
84
- ): StateMatcher =>
85
- function (this: MatcherContext, received: unknown): MatcherResult {
143
+ ): StateMatcher {
144
+ return (received: unknown): MatcherResult => {
86
145
  const widget = asWidget(received, matcherName);
87
146
  const state = read(widget);
147
+
88
148
  if (state === null) {
89
149
  throw new Error(
90
150
  `${matcherName}: widget does not expose a ${stateName} state ` +
91
- `(role ${Gtk.AccessibleRole[widget.getAccessibleRole()]})\n${describeWidget(widget)}`,
151
+ `(role ${Gtk.AccessibleRole[widget.getAccessibleRole()]})\n${describeWidget(widget)}`,
92
152
  );
93
153
  }
154
+
94
155
  return {
95
156
  pass: state,
96
- message: () => `expected widget ${this.isNot ? "not " : ""}to be ${stateName}\n${describeWidget(widget)}`,
157
+ message: () =>
158
+ `expected widget ${negationPrefix(state)}to be ${stateName}\n${describeWidget(widget)}`,
97
159
  };
98
160
  };
161
+ }
99
162
 
100
- export const toHaveDisplayValue: TextMatcher = textMatcher("toHaveDisplayValue", getWidgetDisplayValue, "exact");
101
- export const toHaveTextContent: TextMatcher = textMatcher(
102
- "toHaveTextContent",
103
- (widget) => getWidgetNodeText(widget) ?? getWidgetAccessibleName(widget),
104
- "substring",
105
- );
106
- export const toHaveAccessibleName: TextMatcher = textMatcher("toHaveAccessibleName", getWidgetAccessibleName, "exact");
107
- export const toHavePlaceholderText: TextMatcher = textMatcher(
108
- "toHavePlaceholderText",
109
- getWidgetPlaceholderText,
110
- "exact",
111
- );
112
-
113
- export const toBeChecked: StateMatcher = booleanStateMatcher("toBeChecked", "checked", getWidgetCheckedState);
114
- export const toBePressed: StateMatcher = booleanStateMatcher("toBePressed", "pressed", getWidgetPressedState);
115
- export const toBeExpanded: StateMatcher = booleanStateMatcher("toBeExpanded", "expanded", getWidgetExpandedState);
116
- export const toBeSelected: StateMatcher = booleanStateMatcher("toBeSelected", "selected", getWidgetSelectedState);
117
-
118
- export const toHaveValue: ValueMatcher = function (
119
- this: MatcherContext,
120
- received: unknown,
121
- expected: number,
122
- ): MatcherResult {
163
+ function toHaveValue(received: unknown, expected: number): MatcherResult {
123
164
  const widget = asWidget(received, "toHaveValue");
124
165
  const actual = getWidgetValue(widget).now;
166
+
125
167
  if (actual === null) {
126
168
  throw new Error(
127
- `toHaveValue: widget does not expose a numeric value ` +
128
- `(role ${Gtk.AccessibleRole[widget.getAccessibleRole()]})\n${describeWidget(widget)}`,
169
+ "toHaveValue: widget does not expose a numeric value " +
170
+ `(role ${Gtk.AccessibleRole[widget.getAccessibleRole()]})\n${describeWidget(widget)}`,
129
171
  );
130
172
  }
173
+
174
+ const isPass = actual === expected;
175
+
131
176
  return {
132
- pass: actual === expected,
177
+ pass: isPass,
133
178
  message: () =>
134
- `expected widget ${this.isNot ? "not " : ""}to have value ${expected}, but received ${actual}\n${describeWidget(widget)}`,
179
+ `expected widget ${negationPrefix(isPass)}to have value ${String(expected)}, ` +
180
+ `but received ${String(actual)}\n${describeWidget(widget)}`,
135
181
  };
136
- };
137
-
138
- interface MatcherImplementations {
139
- toHaveDisplayValue: TextMatcher;
140
- toHaveTextContent: TextMatcher;
141
- toHaveAccessibleName: TextMatcher;
142
- toHavePlaceholderText: TextMatcher;
143
- toBeChecked: StateMatcher;
144
- toBePressed: StateMatcher;
145
- toBeExpanded: StateMatcher;
146
- toBeSelected: StateMatcher;
147
- toHaveValue: ValueMatcher;
148
182
  }
149
183
 
150
- /** The widget assertion matchers keyed by name, suitable for passing to `expect.extend`. */
151
- export const matchers: MatcherImplementations = {
152
- toHaveDisplayValue,
153
- toHaveTextContent,
154
- toHaveAccessibleName,
155
- toHavePlaceholderText,
156
- toBeChecked,
157
- toBePressed,
158
- toBeExpanded,
159
- toBeSelected,
160
- toHaveValue,
161
- };
162
-
163
- type ExpectExtend = { extend: (m: MatcherImplementations) => void };
164
-
165
184
  const globalExpect = (): ExpectExtend | null => {
166
185
  const candidate: unknown = Reflect.get(globalThis, "expect");
186
+
167
187
  if (candidate && typeof (candidate as ExpectExtend).extend === "function") {
168
188
  return candidate as ExpectExtend;
169
189
  }
190
+
170
191
  return null;
171
192
  };
172
193
 
173
- let registered = false;
174
-
175
194
  /** Registers the widget matchers on the global `expect`, when one is available. Safe to call more than once. */
176
- export const registerMatchers = (): void => {
177
- if (registered) return;
195
+ const registerMatchers = (): void => {
196
+ if (registration.isRegistered) {
197
+ return;
198
+ }
199
+
178
200
  const expect = globalExpect();
179
- if (!expect) return;
201
+
202
+ if (!expect) {
203
+ return;
204
+ }
205
+
180
206
  expect.extend(matchers);
181
- registered = true;
207
+ registration.isRegistered = true;
182
208
  };
183
209
 
184
- interface WidgetMatchers<R = void> {
185
- toHaveDisplayValue(expected?: TextExpectation): R;
186
- toHaveTextContent(expected?: TextExpectation): R;
187
- toHaveAccessibleName(expected?: TextExpectation): R;
188
- toHavePlaceholderText(expected?: TextExpectation): R;
189
- toBeChecked(): R;
190
- toBePressed(): R;
191
- toBeExpanded(): R;
192
- toBeSelected(): R;
193
- toHaveValue(expected: number): R;
194
- }
195
-
196
210
  declare module "@vitest/expect" {
197
- // biome-ignore lint/suspicious/noExplicitAny: must match the `Assertion<T>` signature it augments for declaration merging
198
- interface Assertion<T = any> extends WidgetMatchers {}
199
- interface AsymmetricMatchersContaining extends WidgetMatchers {}
211
+ /* eslint-disable @typescript-eslint/consistent-type-definitions -- declaration merging requires interfaces */
212
+ interface Assertion {
213
+ toHaveDisplayValue(expected?: TextExpectation): void;
214
+ toHaveTextContent(expected?: TextExpectation): void;
215
+ toHaveAccessibleName(expected?: TextExpectation): void;
216
+ toHavePlaceholderText(expected?: TextExpectation): void;
217
+ toBeChecked(): void;
218
+ toBePressed(): void;
219
+ toBeExpanded(): void;
220
+ toBeSelected(): void;
221
+ toHaveValue(expected: number): void;
222
+ }
223
+
224
+ interface AsymmetricMatchersContaining {
225
+ toHaveDisplayValue(expected?: TextExpectation): void;
226
+ toHaveTextContent(expected?: TextExpectation): void;
227
+ toHaveAccessibleName(expected?: TextExpectation): void;
228
+ toHavePlaceholderText(expected?: TextExpectation): void;
229
+ toBeChecked(): void;
230
+ toBePressed(): void;
231
+ toBeExpanded(): void;
232
+ toBeSelected(): void;
233
+ toHaveValue(expected: number): void;
234
+ }
235
+ /* eslint-enable @typescript-eslint/consistent-type-definitions */
200
236
  }
237
+
238
+ export {
239
+ toHaveDisplayValue,
240
+ toHaveTextContent,
241
+ toHaveAccessibleName,
242
+ toHavePlaceholderText,
243
+ toBeChecked,
244
+ toBePressed,
245
+ toBeExpanded,
246
+ toBeSelected,
247
+ matchers,
248
+ toHaveValue,
249
+ registerMatchers,
250
+ type TextExpectation,
251
+ };