@atomic-testing/component-driver-mui-v9 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/index.cjs +2439 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1391 -0
- package/dist/index.d.mts +1391 -0
- package/dist/index.mjs +2386 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +48 -0
- package/src/components/AccordionDriver.ts +109 -0
- package/src/components/AlertDriver.ts +74 -0
- package/src/components/AutoCompleteDriver.ts +151 -0
- package/src/components/AvatarDriver.ts +51 -0
- package/src/components/AvatarGroupDriver.ts +79 -0
- package/src/components/BadgeDriver.ts +43 -0
- package/src/components/BottomNavigationActionDriver.ts +23 -0
- package/src/components/BottomNavigationDriver.ts +138 -0
- package/src/components/ButtonDriver.ts +16 -0
- package/src/components/CheckboxDriver.ts +98 -0
- package/src/components/ChipDriver.ts +53 -0
- package/src/components/DialogDriver.ts +122 -0
- package/src/components/DrawerDriver.ts +90 -0
- package/src/components/FabDriver.ts +11 -0
- package/src/components/InputDriver.ts +112 -0
- package/src/components/ListDriver.ts +42 -0
- package/src/components/ListItemDriver.ts +34 -0
- package/src/components/MenuDriver.ts +65 -0
- package/src/components/MenuItemDriver.ts +15 -0
- package/src/components/OverlayDriver.ts +98 -0
- package/src/components/PaginationDriver.ts +117 -0
- package/src/components/ProgressDriver.ts +78 -0
- package/src/components/RadioDriver.ts +63 -0
- package/src/components/RadioGroupDriver.ts +129 -0
- package/src/components/RatingDriver.ts +121 -0
- package/src/components/SelectDriver.ts +223 -0
- package/src/components/SliderDriver.ts +109 -0
- package/src/components/SnackbarDriver.ts +68 -0
- package/src/components/SpeedDialDriver.ts +83 -0
- package/src/components/StepperDriver.ts +109 -0
- package/src/components/SwitchDriver.ts +62 -0
- package/src/components/TabDriver.ts +39 -0
- package/src/components/TableCellDriver.ts +14 -0
- package/src/components/TableDriver.ts +148 -0
- package/src/components/TablePaginationDriver.ts +110 -0
- package/src/components/TableRowDriver.ts +79 -0
- package/src/components/TabsDriver.ts +133 -0
- package/src/components/TextFieldDriver.ts +155 -0
- package/src/components/ToggleButtonDriver.ts +21 -0
- package/src/components/ToggleButtonGroupDriver.ts +75 -0
- package/src/components/TooltipDriver.ts +82 -0
- package/src/errors/MenuItemDisabledError.ts +17 -0
- package/src/errors/MenuItemNotFoundError.ts +17 -0
- package/src/index.ts +52 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2386 @@
|
|
|
1
|
+
import { HTMLButtonDriver, HTMLCheckboxDriver, HTMLElementDriver, HTMLRadioButtonGroupDriver, HTMLSelectDriver, HTMLTextInputDriver } from "@atomic-testing/component-driver-html";
|
|
2
|
+
import { ComponentDriver, ContainerDriver, ErrorBase, ListComponentDriver, byAttribute, byCssClass, byCssSelector, byDataTestId, byInputType, byLinkedElement, byRole, byTagName, byValue, escapeUtil, listHelper, locatorUtil, timingUtil } from "@atomic-testing/core";
|
|
3
|
+
//#region src/components/AccordionDriver.ts
|
|
4
|
+
const parts$13 = {
|
|
5
|
+
/**
|
|
6
|
+
* The clickable area to expand/collapse the accordion.
|
|
7
|
+
*/
|
|
8
|
+
disclosure: {
|
|
9
|
+
locator: byCssClass("MuiAccordionSummary-root"),
|
|
10
|
+
driver: HTMLElementDriver
|
|
11
|
+
},
|
|
12
|
+
summary: {
|
|
13
|
+
locator: byCssClass("MuiAccordionSummary-content"),
|
|
14
|
+
driver: HTMLElementDriver
|
|
15
|
+
},
|
|
16
|
+
content: {
|
|
17
|
+
locator: byRole("region"),
|
|
18
|
+
driver: HTMLElementDriver
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Driver for Material UI v9 Accordion component.
|
|
23
|
+
* @see https://mui.com/material-ui/react-accordion/
|
|
24
|
+
*/
|
|
25
|
+
var AccordionDriver = class extends ComponentDriver {
|
|
26
|
+
constructor(locator, interactor, option) {
|
|
27
|
+
super(locator, interactor, {
|
|
28
|
+
...option,
|
|
29
|
+
parts: parts$13
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get the title/summary of the accordion.
|
|
34
|
+
* @returns The title/summary of the accordion.
|
|
35
|
+
*/
|
|
36
|
+
async getSummary() {
|
|
37
|
+
return await this.parts.summary.getText() ?? null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether the accordion is expanded.
|
|
41
|
+
* @returns True if the accordion is expanded, false if collapsed.
|
|
42
|
+
*/
|
|
43
|
+
async isExpanded() {
|
|
44
|
+
await this.enforcePartExistence("disclosure");
|
|
45
|
+
return await this.parts.disclosure.getAttribute("aria-expanded") === "true";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether the accordion is disabled.
|
|
49
|
+
* @returns True if the accordion is disabled, false if enabled.
|
|
50
|
+
*/
|
|
51
|
+
async isDisabled() {
|
|
52
|
+
await this.enforcePartExistence("disclosure");
|
|
53
|
+
return await this.parts.disclosure.getAttribute("disabled") != null;
|
|
54
|
+
}
|
|
55
|
+
async click() {
|
|
56
|
+
await this.parts.disclosure.click();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Expand the accordion.
|
|
60
|
+
*/
|
|
61
|
+
async expand() {
|
|
62
|
+
if (!await this.isExpanded()) {
|
|
63
|
+
await this.parts.disclosure.click();
|
|
64
|
+
await timingUtil.waitUntil({
|
|
65
|
+
probeFn: () => this.isExpanded(),
|
|
66
|
+
terminateCondition: true,
|
|
67
|
+
timeoutMs: 1e3
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Collapse the accordion.
|
|
73
|
+
*/
|
|
74
|
+
async collapse() {
|
|
75
|
+
if (await this.isExpanded()) {
|
|
76
|
+
await this.parts.disclosure.click();
|
|
77
|
+
await timingUtil.waitUntil({
|
|
78
|
+
probeFn: () => this.isExpanded(),
|
|
79
|
+
terminateCondition: false,
|
|
80
|
+
timeoutMs: 1e3
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
get driverName() {
|
|
85
|
+
return "MuiV9AccordionDriver";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/components/AlertDriver.ts
|
|
90
|
+
const parts$12 = {
|
|
91
|
+
title: {
|
|
92
|
+
locator: byCssClass("MuiAlertTitle-root"),
|
|
93
|
+
driver: HTMLElementDriver
|
|
94
|
+
},
|
|
95
|
+
message: {
|
|
96
|
+
locator: byCssClass("MuiAlert-message"),
|
|
97
|
+
driver: HTMLElementDriver
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const alertSeverityEvaluators = [
|
|
101
|
+
{
|
|
102
|
+
value: "error",
|
|
103
|
+
pattern: /MuiAlert-.*Error/
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
value: "warning",
|
|
107
|
+
pattern: /MuiAlert-.*Warning/
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
value: "info",
|
|
111
|
+
pattern: /MuiAlert-.*Info/
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
value: "success",
|
|
115
|
+
pattern: /MuiAlert-.*Success/
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
/**
|
|
119
|
+
* Driver for Material UI v9 Alert component.
|
|
120
|
+
* @see https://mui.com/material-ui/react-alert/
|
|
121
|
+
*/
|
|
122
|
+
var AlertDriver = class extends ContainerDriver {
|
|
123
|
+
constructor(locator, interactor, option) {
|
|
124
|
+
super(locator, interactor, {
|
|
125
|
+
...option,
|
|
126
|
+
parts: parts$12,
|
|
127
|
+
content: option?.content ?? {}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async getTitle() {
|
|
131
|
+
return await this.parts.title.getText() ?? null;
|
|
132
|
+
}
|
|
133
|
+
async getMessage() {
|
|
134
|
+
return await this.parts.message.getText() ?? null;
|
|
135
|
+
}
|
|
136
|
+
async getSeverity() {
|
|
137
|
+
const cssClassString = await this.interactor.getAttribute(this.locator, "class");
|
|
138
|
+
if (cssClassString != null) {
|
|
139
|
+
const cssClasses = cssClassString.split(/\s+/);
|
|
140
|
+
for (const cssClassName of cssClasses) for (const evaluator of alertSeverityEvaluators) if (evaluator.pattern.test(cssClassName)) return evaluator.value;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
get driverName() {
|
|
145
|
+
return "MuiV9AlertDriver";
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/components/AvatarDriver.ts
|
|
150
|
+
const imageLocator = byCssSelector("img.MuiAvatar-img");
|
|
151
|
+
/**
|
|
152
|
+
* Driver for the Material UI v9 Avatar component.
|
|
153
|
+
*
|
|
154
|
+
* An Avatar renders a `.MuiAvatar-root`; with a `src` it contains an
|
|
155
|
+
* `img.MuiAvatar-img`, otherwise it shows letter initials (or an icon). The
|
|
156
|
+
* driver reads the image's `alt`, the presence of the image, and the letter
|
|
157
|
+
* initials accordingly.
|
|
158
|
+
* @see https://mui.com/material-ui/react-avatar/
|
|
159
|
+
*/
|
|
160
|
+
var AvatarDriver = class extends ComponentDriver {
|
|
161
|
+
get imageLocator() {
|
|
162
|
+
return locatorUtil.append(this.locator, imageLocator);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Whether the avatar renders an image (vs letter/icon fallback).
|
|
166
|
+
*/
|
|
167
|
+
async hasImage() {
|
|
168
|
+
return this.interactor.exists(this.imageLocator);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The image's alt text, or `undefined` when the avatar has no image.
|
|
172
|
+
*/
|
|
173
|
+
async getAltText() {
|
|
174
|
+
if (!await this.hasImage()) return;
|
|
175
|
+
return await this.interactor.getAttribute(this.imageLocator, "alt") ?? void 0;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The letter initials of a text avatar, or `undefined` for an image or icon
|
|
179
|
+
* avatar (which render no text).
|
|
180
|
+
*/
|
|
181
|
+
async getInitials() {
|
|
182
|
+
if (await this.hasImage()) return;
|
|
183
|
+
const text = (await this.getText())?.trim();
|
|
184
|
+
return text ? text : void 0;
|
|
185
|
+
}
|
|
186
|
+
get driverName() {
|
|
187
|
+
return "MuiV9AvatarDriver";
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/components/AvatarGroupDriver.ts
|
|
192
|
+
const avatarItemLocator = byCssSelector(".MuiAvatarGroup-avatar");
|
|
193
|
+
const surplusPattern = /^\+\d+$/;
|
|
194
|
+
const defaultAvatarGroupDriverOption = {
|
|
195
|
+
itemClass: AvatarDriver,
|
|
196
|
+
itemLocator: avatarItemLocator
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Driver for the Material UI v9 AvatarGroup component.
|
|
200
|
+
*
|
|
201
|
+
* AvatarGroup renders up to `max` avatars plus a surplus "+N" avatar when there
|
|
202
|
+
* are more. This is a {@link ListComponentDriver} over the rendered avatars
|
|
203
|
+
* (surplus included via `getItems`), with helpers to count the real avatars and
|
|
204
|
+
* read the surplus label.
|
|
205
|
+
* @see https://mui.com/material-ui/react-avatar/#grouped
|
|
206
|
+
*/
|
|
207
|
+
var AvatarGroupDriver = class extends ListComponentDriver {
|
|
208
|
+
constructor(locator, interactor, option = {}) {
|
|
209
|
+
super(locator, interactor, {
|
|
210
|
+
...defaultAvatarGroupDriverOption,
|
|
211
|
+
...option
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The number of real avatars shown, excluding the surplus "+N" indicator.
|
|
216
|
+
*/
|
|
217
|
+
async getVisibleCount() {
|
|
218
|
+
let count = 0;
|
|
219
|
+
for await (const item of listHelper.getListItemIterator(this, this.getItemLocator(), AvatarDriver)) {
|
|
220
|
+
const text = (await item.getText())?.trim();
|
|
221
|
+
if (text == null || !surplusPattern.test(text)) count++;
|
|
222
|
+
}
|
|
223
|
+
return count;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The surplus indicator's label (e.g. "+3"), or `undefined` when every avatar
|
|
227
|
+
* fits within `max` and no surplus is shown.
|
|
228
|
+
*/
|
|
229
|
+
async getSurplusLabel() {
|
|
230
|
+
for await (const item of listHelper.getListItemIterator(this, this.getItemLocator(), AvatarDriver)) {
|
|
231
|
+
const text = (await item.getText())?.trim();
|
|
232
|
+
if (text != null && surplusPattern.test(text)) return text;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
get driverName() {
|
|
236
|
+
return "MuiV9AvatarGroupDriver";
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/components/AutoCompleteDriver.ts
|
|
241
|
+
const parts$11 = {
|
|
242
|
+
input: {
|
|
243
|
+
locator: byRole("combobox"),
|
|
244
|
+
driver: HTMLTextInputDriver
|
|
245
|
+
},
|
|
246
|
+
dropdown: {
|
|
247
|
+
locator: byLinkedElement("Root").onLinkedElement(byRole("combobox")).extractAttribute("aria-controls").toMatchMyAttribute("id"),
|
|
248
|
+
driver: HTMLElementDriver
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const optionLocator$1 = byRole("option");
|
|
252
|
+
const noOptionsLocator = byCssClass("MuiAutocomplete-noOptions");
|
|
253
|
+
const loadingLocator = byCssClass("MuiAutocomplete-loading");
|
|
254
|
+
const defaultAutoCompleteDriverOption = { matchType: "exact" };
|
|
255
|
+
var AutoCompleteDriver = class extends ComponentDriver {
|
|
256
|
+
constructor(locator, interactor, option) {
|
|
257
|
+
super(locator, interactor, {
|
|
258
|
+
...option,
|
|
259
|
+
parts: parts$11
|
|
260
|
+
});
|
|
261
|
+
this._option = {};
|
|
262
|
+
this._option = option ?? {};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get the display of the autocomplete
|
|
266
|
+
*/
|
|
267
|
+
async getValue() {
|
|
268
|
+
return await this.parts.input.getValue() ?? null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Set the value of the autocomplete, how selection happens
|
|
272
|
+
* depends on the option assigned to AutoCompleteDriver
|
|
273
|
+
* By default, when the option has matchType set to exact, only option with matching text would be selected
|
|
274
|
+
* When the option has matchType set to first-available, the first option would be selected regardless of the text
|
|
275
|
+
*
|
|
276
|
+
* Option of auto complete can be set at the time of part definition, for example
|
|
277
|
+
* ```
|
|
278
|
+
* {
|
|
279
|
+
* myAutoComplete: {
|
|
280
|
+
* locator: byCssSelector('my-auto-complete'),
|
|
281
|
+
* driver: AutoCompleteDriver,
|
|
282
|
+
* option: {
|
|
283
|
+
* matchType: 'first-available',
|
|
284
|
+
* },
|
|
285
|
+
* },
|
|
286
|
+
* }
|
|
287
|
+
* ```
|
|
288
|
+
*
|
|
289
|
+
* @param value
|
|
290
|
+
* @returns
|
|
291
|
+
*/
|
|
292
|
+
async setValue(value) {
|
|
293
|
+
await this.parts.input.setValue(value ?? "");
|
|
294
|
+
if (value === null) return true;
|
|
295
|
+
const option = locatorUtil.append(this.parts.dropdown.locator, optionLocator$1);
|
|
296
|
+
let index = 0;
|
|
297
|
+
const matchType = this._option?.matchType ?? defaultAutoCompleteDriverOption.matchType;
|
|
298
|
+
for await (const optionDriver of listHelper.getListItemIterator(this, option, HTMLButtonDriver)) {
|
|
299
|
+
const optionValue = await optionDriver.getText();
|
|
300
|
+
if (matchType === "exact" && optionValue?.trim() === value || matchType === "first-available" && index === 0) {
|
|
301
|
+
await optionDriver.click();
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
index++;
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
async isDisabled() {
|
|
309
|
+
return this.parts.input.isDisabled();
|
|
310
|
+
}
|
|
311
|
+
async isReadonly() {
|
|
312
|
+
return this.parts.input.isReadonly();
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Whether the popup is currently showing its loading indicator (the
|
|
316
|
+
* `loadingText`). Only meaningful while the popup is open — open it first
|
|
317
|
+
* (e.g. by typing into the input), since MUI renders nothing otherwise.
|
|
318
|
+
*/
|
|
319
|
+
async isLoading() {
|
|
320
|
+
return this.interactor.exists(loadingLocator);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Whether the popup is currently showing its "no options" message. Same
|
|
324
|
+
* open-the-popup-first caveat as {@link AutoCompleteDriver.isLoading}.
|
|
325
|
+
*/
|
|
326
|
+
async hasNoOptions() {
|
|
327
|
+
return this.interactor.exists(noOptionsLocator);
|
|
328
|
+
}
|
|
329
|
+
get driverName() {
|
|
330
|
+
return "MuiV9AutoCompleteDriver";
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/components/BadgeDriver.ts
|
|
335
|
+
const parts$10 = { contentDisplay: {
|
|
336
|
+
locator: byCssClass("MuiBadge-badge"),
|
|
337
|
+
driver: HTMLElementDriver
|
|
338
|
+
} };
|
|
339
|
+
/**
|
|
340
|
+
* Driver for Material UI v9 Badge component.
|
|
341
|
+
* @see https://mui.com/material-ui/react-badge/
|
|
342
|
+
*/
|
|
343
|
+
var BadgeDriver = class extends ComponentDriver {
|
|
344
|
+
constructor(locator, interactor, option) {
|
|
345
|
+
super(locator, interactor, {
|
|
346
|
+
...option,
|
|
347
|
+
parts: parts$10
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Get the content of the badge.
|
|
352
|
+
* @returns The content of the badge.
|
|
353
|
+
*/
|
|
354
|
+
async getContent() {
|
|
355
|
+
await this.enforcePartExistence("contentDisplay");
|
|
356
|
+
return await this.parts.contentDisplay.getText() ?? null;
|
|
357
|
+
}
|
|
358
|
+
get driverName() {
|
|
359
|
+
return "MuiV9BadgeDriver";
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/components/BottomNavigationActionDriver.ts
|
|
364
|
+
/**
|
|
365
|
+
* Driver for a single Material UI v9 BottomNavigationAction.
|
|
366
|
+
*
|
|
367
|
+
* Each action renders as a `<button>` (no explicit ARIA role); MUI marks the
|
|
368
|
+
* active one with the `Mui-selected` state class, so selection is read from the
|
|
369
|
+
* class while `getText`/`click`/`isDisabled` come from {@link HTMLButtonDriver}
|
|
370
|
+
* and the base `ComponentDriver`.
|
|
371
|
+
* @see https://mui.com/material-ui/react-bottom-navigation/
|
|
372
|
+
*/
|
|
373
|
+
var BottomNavigationActionDriver = class extends HTMLButtonDriver {
|
|
374
|
+
/**
|
|
375
|
+
* Whether this action is selected (MUI applies the `Mui-selected` state class).
|
|
376
|
+
*/
|
|
377
|
+
async isSelected() {
|
|
378
|
+
return this.interactor.hasCssClass(this.locator, "Mui-selected");
|
|
379
|
+
}
|
|
380
|
+
get driverName() {
|
|
381
|
+
return "MuiV9BottomNavigationActionDriver";
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/components/BottomNavigationDriver.ts
|
|
386
|
+
/**
|
|
387
|
+
* BottomNavigation actions are direct-child `<button>` siblings of the root (no
|
|
388
|
+
* ARIA role). They are located by their `MuiBottomNavigationAction-root` class
|
|
389
|
+
* rather than a bare `button` tag, so positional enumeration is not thrown off by
|
|
390
|
+
* any incidental nested button inside an action.
|
|
391
|
+
*/
|
|
392
|
+
const defaultBottomNavigationDriverOption = {
|
|
393
|
+
itemClass: BottomNavigationActionDriver,
|
|
394
|
+
itemLocator: byCssSelector(".MuiBottomNavigationAction-root")
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Driver for the Material UI v9 BottomNavigation component.
|
|
398
|
+
*
|
|
399
|
+
* A {@link ListComponentDriver} over the action buttons, exposing the selected
|
|
400
|
+
* index/label, selection by index/label, and per-action info, plus per-action
|
|
401
|
+
* {@link BottomNavigationActionDriver} instances via `getItems`/`getItemByIndex`/
|
|
402
|
+
* `getItemByLabel`.
|
|
403
|
+
* @see https://mui.com/material-ui/react-bottom-navigation/
|
|
404
|
+
*/
|
|
405
|
+
var BottomNavigationDriver = class extends ListComponentDriver {
|
|
406
|
+
constructor(locator, interactor, option = {}) {
|
|
407
|
+
super(locator, interactor, {
|
|
408
|
+
...defaultBottomNavigationDriverOption,
|
|
409
|
+
...option
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Every action with its label and selected state, in order.
|
|
414
|
+
*/
|
|
415
|
+
async getActions() {
|
|
416
|
+
const actions = [];
|
|
417
|
+
for await (const item of listHelper.getListItemIterator(this, this.getItemLocator(), BottomNavigationActionDriver)) actions.push({
|
|
418
|
+
label: (await item.getText())?.trim(),
|
|
419
|
+
selected: await item.isSelected()
|
|
420
|
+
});
|
|
421
|
+
return actions;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Zero-based index of the selected action, or `-1` when none is selected.
|
|
425
|
+
*/
|
|
426
|
+
async getSelectedIndex() {
|
|
427
|
+
let index = 0;
|
|
428
|
+
for await (const item of listHelper.getListItemIterator(this, this.getItemLocator(), BottomNavigationActionDriver)) {
|
|
429
|
+
if (await item.isSelected()) return index;
|
|
430
|
+
index++;
|
|
431
|
+
}
|
|
432
|
+
return -1;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Label of the selected action, or `null` when none is selected. Returns `null`
|
|
436
|
+
* (not `undefined`) to match the sibling `TabsDriver.getSelectedLabel` contract.
|
|
437
|
+
*/
|
|
438
|
+
async getSelectedLabel() {
|
|
439
|
+
for await (const item of listHelper.getListItemIterator(this, this.getItemLocator(), BottomNavigationActionDriver)) if (await item.isSelected()) return (await item.getText())?.trim() ?? null;
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Select the action at the given zero-based index.
|
|
444
|
+
* @returns `false` when the index is out of range.
|
|
445
|
+
*/
|
|
446
|
+
async selectByIndex(index) {
|
|
447
|
+
const item = await this.getItemByIndex(index);
|
|
448
|
+
if (item == null) return false;
|
|
449
|
+
await item.click();
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Select the first action whose visible label equals `label`.
|
|
454
|
+
* @returns `false` when no action matches.
|
|
455
|
+
*/
|
|
456
|
+
async selectByLabel(label) {
|
|
457
|
+
const item = await this.getItemByLabel(label);
|
|
458
|
+
if (item == null) return false;
|
|
459
|
+
await item.click();
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
get driverName() {
|
|
463
|
+
return "MuiV9BottomNavigationDriver";
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/components/ButtonDriver.ts
|
|
468
|
+
/**
|
|
469
|
+
* Driver for Material UI v9 Button component.
|
|
470
|
+
* @see https://mui.com/material-ui/react-button/
|
|
471
|
+
*/
|
|
472
|
+
var ButtonDriver = class extends HTMLButtonDriver {
|
|
473
|
+
async getValue() {
|
|
474
|
+
return await this.interactor.getAttribute(this.locator, "value") ?? null;
|
|
475
|
+
}
|
|
476
|
+
get driverName() {
|
|
477
|
+
return "MuiV9ButtonDriver";
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/components/CheckboxDriver.ts
|
|
482
|
+
const checkboxPart = { checkbox: {
|
|
483
|
+
locator: byTagName("input"),
|
|
484
|
+
driver: HTMLCheckboxDriver
|
|
485
|
+
} };
|
|
486
|
+
var CheckboxDriver = class extends ComponentDriver {
|
|
487
|
+
constructor(locator, interactor, option) {
|
|
488
|
+
super(locator, interactor, {
|
|
489
|
+
...option,
|
|
490
|
+
parts: checkboxPart
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
isSelected() {
|
|
494
|
+
return this.parts.checkbox.isSelected();
|
|
495
|
+
}
|
|
496
|
+
async setSelected(selected) {
|
|
497
|
+
if (await this.isIndeterminate() && selected === false) await this.parts.checkbox.setSelected(true);
|
|
498
|
+
await this.parts.checkbox.setSelected(selected);
|
|
499
|
+
}
|
|
500
|
+
getValue() {
|
|
501
|
+
return this.parts.checkbox.getValue();
|
|
502
|
+
}
|
|
503
|
+
async isIndeterminate() {
|
|
504
|
+
return await this.interactor.getAttribute(this.parts.checkbox.locator, "data-indeterminate") === "true";
|
|
505
|
+
}
|
|
506
|
+
isDisabled() {
|
|
507
|
+
return this.parts.checkbox.isDisabled();
|
|
508
|
+
}
|
|
509
|
+
isReadonly() {
|
|
510
|
+
return this.parts.checkbox.isReadonly();
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Get the text of the label associated with the checkbox, or `undefined` when the checkbox
|
|
514
|
+
* is rendered without one (e.g. a bare `<Checkbox>` outside of a `FormControlLabel`).
|
|
515
|
+
*
|
|
516
|
+
* MUI's `FormControlLabel` does not expose a `for`/`id` or `aria-labelledby` association; it
|
|
517
|
+
* labels the control implicitly by wrapping it in a `<label>` and rendering the text as a
|
|
518
|
+
* sibling. The label therefore lives outside this driver's own subtree, so we re-root at the
|
|
519
|
+
* enclosing `<label>` — matched against this checkbox via `:has()`, while preserving the
|
|
520
|
+
* surrounding scope — and read its text. This resolves to a single CSS selector, so it behaves
|
|
521
|
+
* identically across every interactor (DOM/React/Vue and Playwright).
|
|
522
|
+
*/
|
|
523
|
+
async getLabel() {
|
|
524
|
+
const labelLocator = this.getEnclosingLabelLocator();
|
|
525
|
+
return await this.interactor.exists(labelLocator) ? this.interactor.getText(labelLocator) : void 0;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Build a locator for the `<label>` that encloses this checkbox, scoped to the same ancestor
|
|
529
|
+
* context as the checkbox itself so that sibling checkboxes are never mismatched.
|
|
530
|
+
*/
|
|
531
|
+
getEnclosingLabelLocator() {
|
|
532
|
+
const chain = locatorUtil.toChain(this.locator);
|
|
533
|
+
const selfSelector = chain[chain.length - 1].selector;
|
|
534
|
+
return locatorUtil.append(chain.slice(0, -1), byCssSelector(`label:has(${selfSelector})`));
|
|
535
|
+
}
|
|
536
|
+
get driverName() {
|
|
537
|
+
return "MuiV9CheckboxDriver";
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/components/ChipDriver.ts
|
|
542
|
+
const parts$9 = {
|
|
543
|
+
contentDisplay: {
|
|
544
|
+
locator: byCssClass("MuiChip-label"),
|
|
545
|
+
driver: HTMLElementDriver
|
|
546
|
+
},
|
|
547
|
+
removeButton: {
|
|
548
|
+
locator: byDataTestId("CancelIcon"),
|
|
549
|
+
driver: HTMLElementDriver
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
/**
|
|
553
|
+
* Driver for Material UI v9 Chip component.
|
|
554
|
+
* @see https://mui.com/material-ui/react-chip/
|
|
555
|
+
*/
|
|
556
|
+
var ChipDriver = class extends ComponentDriver {
|
|
557
|
+
constructor(locator, interactor, option) {
|
|
558
|
+
super(locator, interactor, {
|
|
559
|
+
...option,
|
|
560
|
+
parts: parts$9
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get the label content of the chip.
|
|
565
|
+
* @returns The label text content of the chip.
|
|
566
|
+
*/
|
|
567
|
+
async getLabel() {
|
|
568
|
+
await this.enforcePartExistence("contentDisplay");
|
|
569
|
+
return await this.parts.contentDisplay.getText() ?? null;
|
|
570
|
+
}
|
|
571
|
+
async clickRemove() {
|
|
572
|
+
await this.enforcePartExistence("removeButton");
|
|
573
|
+
await this.parts.removeButton.click();
|
|
574
|
+
}
|
|
575
|
+
get driverName() {
|
|
576
|
+
return "MuiV9ChipDriver";
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region src/components/DialogDriver.ts
|
|
581
|
+
const parts$8 = {
|
|
582
|
+
title: {
|
|
583
|
+
locator: byCssClass("MuiDialogTitle-root"),
|
|
584
|
+
driver: HTMLElementDriver
|
|
585
|
+
},
|
|
586
|
+
dialogContainer: {
|
|
587
|
+
locator: byRole("presentation"),
|
|
588
|
+
driver: HTMLElementDriver
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const dialogRootLocator = byRole("presentation", "Root");
|
|
592
|
+
const defaultTransitionDuration$1 = 250;
|
|
593
|
+
var DialogDriver = class extends ContainerDriver {
|
|
594
|
+
constructor(locator, interactor, option) {
|
|
595
|
+
super(locator, interactor, {
|
|
596
|
+
...option,
|
|
597
|
+
parts: parts$8,
|
|
598
|
+
content: option?.content ?? {}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
overriddenParentLocator() {
|
|
602
|
+
return dialogRootLocator;
|
|
603
|
+
}
|
|
604
|
+
overrideLocatorRelativePosition() {
|
|
605
|
+
return "Same";
|
|
606
|
+
}
|
|
607
|
+
async getTitle() {
|
|
608
|
+
await this.enforcePartExistence("title");
|
|
609
|
+
return await this.parts.title.getText() ?? null;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Dismiss the dialog by clicking outside its content, then wait for it to close.
|
|
613
|
+
*
|
|
614
|
+
* MUI's "backdrop click" is handled on the `.MuiDialog-container` surface (which
|
|
615
|
+
* overlays the visual `.MuiBackdrop-root`), firing `onClose` only when the click
|
|
616
|
+
* target is the container itself. The click therefore lands on the container near
|
|
617
|
+
* its top-left corner to avoid the centered paper. Whether it actually closes
|
|
618
|
+
* depends on the consumer's `onClose` handling (MUI reports a `"backdropClick"`
|
|
619
|
+
* reason); the returned boolean reflects the observed close, not merely the click.
|
|
620
|
+
*
|
|
621
|
+
* @param timeoutMs How long to wait for the close transition to finish
|
|
622
|
+
* @returns true if the dialog closed
|
|
623
|
+
*/
|
|
624
|
+
async closeByBackdropClick(timeoutMs = defaultTransitionDuration$1) {
|
|
625
|
+
await this.enforcePartExistence("dialogContainer");
|
|
626
|
+
const cornerClick = { position: {
|
|
627
|
+
x: 5,
|
|
628
|
+
y: 5
|
|
629
|
+
} };
|
|
630
|
+
await this.parts.dialogContainer.mouseDown(cornerClick);
|
|
631
|
+
await this.parts.dialogContainer.mouseUp(cornerClick);
|
|
632
|
+
await this.parts.dialogContainer.click(cornerClick);
|
|
633
|
+
return this.waitForClose(timeoutMs);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Wait for dialog to open
|
|
637
|
+
* @param timeoutMs
|
|
638
|
+
* @returns true open has performed successfully
|
|
639
|
+
*/
|
|
640
|
+
async waitForOpen(timeoutMs = defaultTransitionDuration$1) {
|
|
641
|
+
return await this.interactor.waitUntil({
|
|
642
|
+
probeFn: () => this.isOpen(),
|
|
643
|
+
terminateCondition: true,
|
|
644
|
+
timeoutMs
|
|
645
|
+
}) === true;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Wait for dialog to close
|
|
649
|
+
* @param timeoutMs
|
|
650
|
+
* @returns true open has performed successfully
|
|
651
|
+
*/
|
|
652
|
+
async waitForClose(timeoutMs = defaultTransitionDuration$1) {
|
|
653
|
+
return await this.interactor.waitUntil({
|
|
654
|
+
probeFn: () => this.isOpen(),
|
|
655
|
+
terminateCondition: false,
|
|
656
|
+
timeoutMs
|
|
657
|
+
}) === false;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Check if the dialog box is open. Caution, because of animation, upon an open/close action is performed
|
|
661
|
+
* use waitForOpen() or waitForClose() before using isOpen() would result a more accurate open state of the dialog
|
|
662
|
+
* @returns true if dialog box is open
|
|
663
|
+
*/
|
|
664
|
+
async isOpen() {
|
|
665
|
+
if (!await this.exists()) return false;
|
|
666
|
+
return await this.interactor.isVisible(this.parts.dialogContainer.locator);
|
|
667
|
+
}
|
|
668
|
+
get driverName() {
|
|
669
|
+
return "MuiV9DialogDriver";
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/components/OverlayDriver.ts
|
|
674
|
+
const backdropLocator = byCssClass("MuiBackdrop-root");
|
|
675
|
+
const defaultTransitionDuration = 250;
|
|
676
|
+
/**
|
|
677
|
+
* Shared base for MUI portal-rendered overlays (Drawer today; Dialog, Menu,
|
|
678
|
+
* Popover and SpeedDial are prospective consumers). It owns the open/close
|
|
679
|
+
* lifecycle that {@link DialogDriver} first proved out: `isOpen` derived from the
|
|
680
|
+
* visible surface, `waitForOpen`/`waitForClose` spanning the transition, and
|
|
681
|
+
* `closeByBackdrop`.
|
|
682
|
+
*
|
|
683
|
+
* Subclasses supply {@link getSurfaceLocator} (the element whose visibility means
|
|
684
|
+
* "open") and, when the overlay is portal-rendered, override
|
|
685
|
+
* `overriddenParentLocator()`/`overrideLocatorRelativePosition()` to re-root.
|
|
686
|
+
*
|
|
687
|
+
* `closeByEscape` is intentionally absent: keyboard dismissal needs a key-press
|
|
688
|
+
* primitive the `Interactor` interface does not yet expose, which would have to be
|
|
689
|
+
* added to every interactor (DOM/React/Vue/Playwright). It is deferred rather than
|
|
690
|
+
* partially implemented.
|
|
691
|
+
*/
|
|
692
|
+
var OverlayDriver = class extends ContainerDriver {
|
|
693
|
+
/**
|
|
694
|
+
* Locator of the dismissible backdrop. Defaults to MUI's `.MuiBackdrop-root`.
|
|
695
|
+
*/
|
|
696
|
+
getBackdropLocator() {
|
|
697
|
+
return locatorUtil.append(this.locator, backdropLocator);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Whether the overlay is mounted and its surface is visible. Because of open/close
|
|
701
|
+
* transitions, prefer `waitForOpen()`/`waitForClose()` immediately after an action.
|
|
702
|
+
*/
|
|
703
|
+
async isOpen() {
|
|
704
|
+
if (!await this.exists()) return false;
|
|
705
|
+
return this.interactor.isVisible(this.getSurfaceLocator());
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Wait until the overlay is open.
|
|
709
|
+
* @returns true once open within the timeout.
|
|
710
|
+
*/
|
|
711
|
+
async waitForOpen(timeoutMs = defaultTransitionDuration) {
|
|
712
|
+
return await this.interactor.waitUntil({
|
|
713
|
+
probeFn: () => this.isOpen(),
|
|
714
|
+
terminateCondition: true,
|
|
715
|
+
timeoutMs
|
|
716
|
+
}) === true;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Wait until the overlay is closed.
|
|
720
|
+
* @returns true once closed within the timeout.
|
|
721
|
+
*/
|
|
722
|
+
async waitForClose(timeoutMs = defaultTransitionDuration) {
|
|
723
|
+
if (await this.interactor.waitUntil({
|
|
724
|
+
probeFn: () => this.isOpen(),
|
|
725
|
+
terminateCondition: false,
|
|
726
|
+
timeoutMs
|
|
727
|
+
}) === false) return true;
|
|
728
|
+
return !await this.isOpen();
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Dismiss by clicking the backdrop, then wait for the close transition. Whether
|
|
732
|
+
* it actually closes depends on the consumer honoring the backdrop dismissal; the
|
|
733
|
+
* returned boolean reflects the observed close, not merely the click.
|
|
734
|
+
*/
|
|
735
|
+
async closeByBackdrop(timeoutMs = defaultTransitionDuration) {
|
|
736
|
+
const backdrop = this.getBackdropLocator();
|
|
737
|
+
if (await this.interactor.exists(backdrop)) await this.interactor.click(backdrop);
|
|
738
|
+
return this.waitForClose(timeoutMs);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
//#endregion
|
|
742
|
+
//#region src/components/DrawerDriver.ts
|
|
743
|
+
const drawerParts = { paper: {
|
|
744
|
+
locator: byCssClass("MuiDrawer-paper"),
|
|
745
|
+
driver: HTMLElementDriver
|
|
746
|
+
} };
|
|
747
|
+
const anchorClassByAnchor = {
|
|
748
|
+
left: "MuiDrawer-anchorLeft",
|
|
749
|
+
right: "MuiDrawer-anchorRight",
|
|
750
|
+
top: "MuiDrawer-anchorTop",
|
|
751
|
+
bottom: "MuiDrawer-anchorBottom"
|
|
752
|
+
};
|
|
753
|
+
const drawerRootLocator = byRole("presentation", "Root");
|
|
754
|
+
/**
|
|
755
|
+
* Driver for the Material UI v9 Drawer (and SwipeableDrawer) component.
|
|
756
|
+
*
|
|
757
|
+
* A temporary Drawer is a portal-rendered Modal: its root `role="presentation"`
|
|
758
|
+
* (`.MuiDrawer-root`) carries the anchor class and holds a `.MuiBackdrop-root` plus
|
|
759
|
+
* the `.MuiDrawer-paper` panel. Open/close/backdrop behavior is inherited from
|
|
760
|
+
* {@link OverlayDriver}; this driver adds the anchor read and the portal re-rooting.
|
|
761
|
+
* @see https://mui.com/material-ui/react-drawer/
|
|
762
|
+
*/
|
|
763
|
+
var DrawerDriver = class extends OverlayDriver {
|
|
764
|
+
constructor(locator, interactor, option) {
|
|
765
|
+
super(locator, interactor, {
|
|
766
|
+
...option,
|
|
767
|
+
parts: drawerParts,
|
|
768
|
+
content: option?.content ?? {}
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
overriddenParentLocator() {
|
|
772
|
+
return drawerRootLocator;
|
|
773
|
+
}
|
|
774
|
+
overrideLocatorRelativePosition() {
|
|
775
|
+
return "Same";
|
|
776
|
+
}
|
|
777
|
+
getSurfaceLocator() {
|
|
778
|
+
return this.parts.paper.locator;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* The side the drawer is anchored to, read from the portal root's anchor class, or
|
|
782
|
+
* `undefined` when the drawer is closed/unmounted.
|
|
783
|
+
*
|
|
784
|
+
* The anchor class sits on the `role="presentation"` root itself, so it is read
|
|
785
|
+
* directly off {@link drawerRootLocator} rather than through a part (parts resolve
|
|
786
|
+
* as descendants of that root, but the class is on the root).
|
|
787
|
+
*/
|
|
788
|
+
async getAnchor() {
|
|
789
|
+
if (!await this.interactor.exists(drawerRootLocator)) return;
|
|
790
|
+
for (const anchor of Object.keys(anchorClassByAnchor)) if (await this.interactor.hasCssClass(drawerRootLocator, anchorClassByAnchor[anchor])) return anchor;
|
|
791
|
+
}
|
|
792
|
+
get driverName() {
|
|
793
|
+
return "MuiV9DrawerDriver";
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
//#endregion
|
|
797
|
+
//#region src/components/FabDriver.ts
|
|
798
|
+
/**
|
|
799
|
+
* Driver for Material UI v9 Floating Action Button component.
|
|
800
|
+
* @see https://mui.com/material-ui/react-floating-action-button/
|
|
801
|
+
*/
|
|
802
|
+
var FabDriver = class extends ButtonDriver {
|
|
803
|
+
get driverName() {
|
|
804
|
+
return "MuiV9FabDriver";
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
//#endregion
|
|
808
|
+
//#region src/components/InputDriver.ts
|
|
809
|
+
const parts$7 = {
|
|
810
|
+
singlelineInput: {
|
|
811
|
+
locator: byCssSelector("input:not([aria-hidden])"),
|
|
812
|
+
driver: HTMLTextInputDriver
|
|
813
|
+
},
|
|
814
|
+
multilineInput: {
|
|
815
|
+
locator: byCssSelector("textarea:not([aria-hidden])"),
|
|
816
|
+
driver: HTMLTextInputDriver
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
/**
|
|
820
|
+
* A driver for the Material UI v9 Input, FilledInput, OutlinedInput, and StandardInput components.
|
|
821
|
+
*/
|
|
822
|
+
var InputDriver = class extends ComponentDriver {
|
|
823
|
+
constructor(locator, interactor, option) {
|
|
824
|
+
super(locator, interactor, {
|
|
825
|
+
...option,
|
|
826
|
+
parts: parts$7
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
async getInputType() {
|
|
830
|
+
if (await this.interactor.exists(this.parts.singlelineInput.locator)) return "singleLine";
|
|
831
|
+
if (await this.interactor.exists(this.parts.multilineInput.locator)) return "multiline";
|
|
832
|
+
throw new Error("Unable to determine input type in TextFieldInput");
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Retrieve the current value of the input element, handling both single line
|
|
836
|
+
* and multiline configurations.
|
|
837
|
+
*/
|
|
838
|
+
async getValue() {
|
|
839
|
+
switch (await this.getInputType()) {
|
|
840
|
+
case "singleLine": return this.parts.singlelineInput.getValue();
|
|
841
|
+
case "multiline": return this.parts.multilineInput.getValue();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Set the value of the underlying input element.
|
|
846
|
+
*
|
|
847
|
+
* @param value The text to assign to the input.
|
|
848
|
+
*/
|
|
849
|
+
async setValue(value) {
|
|
850
|
+
switch (await this.getInputType()) {
|
|
851
|
+
case "singleLine": return this.parts.singlelineInput.setValue(value);
|
|
852
|
+
case "multiline": return this.parts.multilineInput.setValue(value);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Determine whether the input element is disabled.
|
|
857
|
+
*/
|
|
858
|
+
async isDisabled() {
|
|
859
|
+
switch (await this.getInputType()) {
|
|
860
|
+
case "singleLine": return this.parts.singlelineInput.isDisabled();
|
|
861
|
+
case "multiline": return this.parts.multilineInput.isDisabled();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Determine whether the input element is read only.
|
|
866
|
+
*/
|
|
867
|
+
async isReadonly() {
|
|
868
|
+
switch (await this.getInputType()) {
|
|
869
|
+
case "singleLine": return this.parts.singlelineInput.isReadonly();
|
|
870
|
+
case "multiline": return this.parts.multilineInput.isReadonly();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Identifier for this driver.
|
|
875
|
+
*/
|
|
876
|
+
get driverName() {
|
|
877
|
+
return "MuiV9InputDriver";
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/errors/MenuItemDisabledError.ts
|
|
882
|
+
const MenuItemDisabledErrorId = "MenuItemDisabledError";
|
|
883
|
+
function getErrorMessage$1(label) {
|
|
884
|
+
return `The menu item with label: ${label} is disabled`;
|
|
885
|
+
}
|
|
886
|
+
var MenuItemDisabledError = class extends ErrorBase {
|
|
887
|
+
constructor(label, driver) {
|
|
888
|
+
super(getErrorMessage$1(label), driver);
|
|
889
|
+
this.label = label;
|
|
890
|
+
this.driver = driver;
|
|
891
|
+
this.name = MenuItemDisabledErrorId;
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
//#endregion
|
|
895
|
+
//#region src/components/ListItemDriver.ts
|
|
896
|
+
/**
|
|
897
|
+
* @internal
|
|
898
|
+
*/
|
|
899
|
+
var ListItemDriver = class extends ComponentDriver {
|
|
900
|
+
async label() {
|
|
901
|
+
return (await this.getText())?.trim() || null;
|
|
902
|
+
}
|
|
903
|
+
async isSelected() {
|
|
904
|
+
return await this.interactor.hasCssClass(this.locator, "Mui-selected");
|
|
905
|
+
}
|
|
906
|
+
async isDisabled() {
|
|
907
|
+
return await this.interactor.getAttribute(this.locator, "aria-disabled") === "true";
|
|
908
|
+
}
|
|
909
|
+
async click() {
|
|
910
|
+
if (await this.isDisabled()) throw new MenuItemDisabledError(await this.label() ?? "", this);
|
|
911
|
+
await this.interactor.click(this.locator);
|
|
912
|
+
}
|
|
913
|
+
get driverName() {
|
|
914
|
+
return "MuiV9ListItemDriver";
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region src/components/ListDriver.ts
|
|
919
|
+
const defaultListDriverOption = {
|
|
920
|
+
itemClass: ListItemDriver,
|
|
921
|
+
itemLocator: byRole("option")
|
|
922
|
+
};
|
|
923
|
+
var ListDriver = class extends ListComponentDriver {
|
|
924
|
+
constructor(locator, interactor, option = { ...defaultListDriverOption }) {
|
|
925
|
+
super(locator, interactor, option);
|
|
926
|
+
}
|
|
927
|
+
async getSelected() {
|
|
928
|
+
for await (const item of listHelper.getListItemIterator(this, this.getItemLocator(), ListItemDriver)) if (await item.isSelected()) return item;
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
get driverName() {
|
|
932
|
+
return "MuiV9ListDriver";
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
//#endregion
|
|
936
|
+
//#region src/errors/MenuItemNotFoundError.ts
|
|
937
|
+
const MenuItemNotFoundErrorId = "MenuItemNotFoundError";
|
|
938
|
+
function getErrorMessage(label) {
|
|
939
|
+
return `Cannot find menu item with label: ${label}`;
|
|
940
|
+
}
|
|
941
|
+
var MenuItemNotFoundError = class extends ErrorBase {
|
|
942
|
+
constructor(label, driver) {
|
|
943
|
+
super(getErrorMessage(label), driver);
|
|
944
|
+
this.label = label;
|
|
945
|
+
this.driver = driver;
|
|
946
|
+
this.name = MenuItemNotFoundErrorId;
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
//#endregion
|
|
950
|
+
//#region src/components/MenuItemDriver.ts
|
|
951
|
+
/**
|
|
952
|
+
* @internal
|
|
953
|
+
*/
|
|
954
|
+
var MenuItemDriver = class extends ListItemDriver {
|
|
955
|
+
async value() {
|
|
956
|
+
return await this.interactor.getAttribute(this.locator, "data-value") ?? null;
|
|
957
|
+
}
|
|
958
|
+
get driverName() {
|
|
959
|
+
return "MuiV9MenuItemDriver";
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
//#endregion
|
|
963
|
+
//#region src/components/MenuDriver.ts
|
|
964
|
+
const parts$6 = { menu: {
|
|
965
|
+
locator: byRole("menu"),
|
|
966
|
+
driver: HTMLElementDriver
|
|
967
|
+
} };
|
|
968
|
+
const menuRootLocator = byRole("presentation", "Root");
|
|
969
|
+
const menuItemLocator = byRole("menuitem");
|
|
970
|
+
var MenuDriver = class extends ComponentDriver {
|
|
971
|
+
constructor(locator, interactor, option) {
|
|
972
|
+
super(locator, interactor, {
|
|
973
|
+
...option,
|
|
974
|
+
parts: parts$6
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
overriddenParentLocator() {
|
|
978
|
+
return menuRootLocator;
|
|
979
|
+
}
|
|
980
|
+
overrideLocatorRelativePosition() {
|
|
981
|
+
return "Same";
|
|
982
|
+
}
|
|
983
|
+
async getMenuItemByLabel(label) {
|
|
984
|
+
for await (const item of listHelper.getListItemIterator(this, menuItemLocator, MenuItemDriver)) if (await item.label() === label) return item;
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
async selectByLabel(label) {
|
|
988
|
+
const item = await this.getMenuItemByLabel(label);
|
|
989
|
+
if (item) await item.click();
|
|
990
|
+
else throw new MenuItemNotFoundError(label, this);
|
|
991
|
+
}
|
|
992
|
+
get driverName() {
|
|
993
|
+
return "MuiV9MenuDriver";
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/components/PaginationDriver.ts
|
|
998
|
+
const pageItemLocator = byCssSelector(".MuiPaginationItem-page");
|
|
999
|
+
const selectedPageLocator = byCssSelector("[aria-current]");
|
|
1000
|
+
const firstButtonLocator = byAttribute("aria-label", "Go to first page");
|
|
1001
|
+
const previousButtonLocator$1 = byAttribute("aria-label", "Go to previous page");
|
|
1002
|
+
const nextButtonLocator$1 = byAttribute("aria-label", "Go to next page");
|
|
1003
|
+
const lastButtonLocator = byAttribute("aria-label", "Go to last page");
|
|
1004
|
+
/**
|
|
1005
|
+
* Driver for the Material UI v9 Pagination component.
|
|
1006
|
+
*
|
|
1007
|
+
* Pagination renders a `<nav>` whose `MuiPaginationItem` buttons are each wrapped
|
|
1008
|
+
* in their own `<li>` (so they are not siblings — positional `:nth-of-type`
|
|
1009
|
+
* iteration does not apply). Page controls are read by their accessible name
|
|
1010
|
+
* instead: every page button's aria-label ends in its page number ("Go to page N",
|
|
1011
|
+
* or "page N" once selected), and first/previous/next/last carry stable
|
|
1012
|
+
* aria-labels and become `disabled` at the bounds.
|
|
1013
|
+
* @see https://mui.com/material-ui/react-pagination/
|
|
1014
|
+
*/
|
|
1015
|
+
var PaginationDriver = class extends ComponentDriver {
|
|
1016
|
+
get pageItemsLocator() {
|
|
1017
|
+
return locatorUtil.append(this.locator, pageItemLocator);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* The selected page number, or `-1` when no page is marked current.
|
|
1021
|
+
*/
|
|
1022
|
+
async getSelectedPage() {
|
|
1023
|
+
const locator = locatorUtil.append(this.locator, selectedPageLocator);
|
|
1024
|
+
if (!await this.interactor.exists(locator)) return -1;
|
|
1025
|
+
const text = await this.interactor.getText(locator);
|
|
1026
|
+
const page = Number.parseInt(text?.trim() ?? "", 10);
|
|
1027
|
+
return Number.isNaN(page) ? -1 : page;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Total number of pages, taken from the highest numbered page control. MUI
|
|
1031
|
+
* always renders the upper boundary page (boundaryCount >= 1), so this is exact
|
|
1032
|
+
* even when middle pages collapse into an ellipsis.
|
|
1033
|
+
*/
|
|
1034
|
+
async getPageCount() {
|
|
1035
|
+
const labels = await this.interactor.getAttribute(this.pageItemsLocator, "aria-label", true);
|
|
1036
|
+
let max = 0;
|
|
1037
|
+
for (const label of labels) {
|
|
1038
|
+
const match = label?.match(/(\d+)\s*$/);
|
|
1039
|
+
if (match != null) {
|
|
1040
|
+
const page = Number.parseInt(match[1], 10);
|
|
1041
|
+
if (page > max) max = page;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return max;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Click the numbered control for `page`. A no-op (returns `true`) when `page` is
|
|
1048
|
+
* already selected.
|
|
1049
|
+
* @returns `false` when that page is not currently rendered (e.g. hidden behind
|
|
1050
|
+
* an ellipsis) or is disabled.
|
|
1051
|
+
*/
|
|
1052
|
+
async goToPage(page) {
|
|
1053
|
+
if (await this.getSelectedPage() === page) return true;
|
|
1054
|
+
const locator = locatorUtil.append(this.locator, byAttribute("aria-label", `Go to page ${page}`));
|
|
1055
|
+
if (!await this.interactor.exists(locator) || await this.interactor.isDisabled(locator)) return false;
|
|
1056
|
+
await this.interactor.click(locator);
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Click a navigation control unless it is absent or disabled (at a bound).
|
|
1061
|
+
* @returns whether the click was performed.
|
|
1062
|
+
*/
|
|
1063
|
+
async clickNavButton(navLocator) {
|
|
1064
|
+
const locator = locatorUtil.append(this.locator, navLocator);
|
|
1065
|
+
if (!await this.interactor.exists(locator) || await this.interactor.isDisabled(locator)) return false;
|
|
1066
|
+
await this.interactor.click(locator);
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
/** Go to the next page. Returns `false` when on the last page or the control is hidden. */
|
|
1070
|
+
async next() {
|
|
1071
|
+
return this.clickNavButton(nextButtonLocator$1);
|
|
1072
|
+
}
|
|
1073
|
+
/** Go to the previous page. Returns `false` when on the first page or the control is hidden. */
|
|
1074
|
+
async previous() {
|
|
1075
|
+
return this.clickNavButton(previousButtonLocator$1);
|
|
1076
|
+
}
|
|
1077
|
+
/** Jump to the first page. Returns `false` when already there or the control is hidden (needs `showFirstButton`). */
|
|
1078
|
+
async first() {
|
|
1079
|
+
return this.clickNavButton(firstButtonLocator);
|
|
1080
|
+
}
|
|
1081
|
+
/** Jump to the last page. Returns `false` when already there or the control is hidden (needs `showLastButton`). */
|
|
1082
|
+
async last() {
|
|
1083
|
+
return this.clickNavButton(lastButtonLocator);
|
|
1084
|
+
}
|
|
1085
|
+
get driverName() {
|
|
1086
|
+
return "MuiV9PaginationDriver";
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
//#endregion
|
|
1090
|
+
//#region src/components/ProgressDriver.ts
|
|
1091
|
+
const parts$5 = { choices: {
|
|
1092
|
+
locator: byInputType("radio"),
|
|
1093
|
+
driver: HTMLRadioButtonGroupDriver
|
|
1094
|
+
} };
|
|
1095
|
+
var ProgressDriver = class extends ComponentDriver {
|
|
1096
|
+
constructor(locator, interactor, option) {
|
|
1097
|
+
super(locator, interactor, {
|
|
1098
|
+
...option,
|
|
1099
|
+
parts: parts$5
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
async getValue() {
|
|
1103
|
+
const rawValue = await this.getAttribute("aria-valuenow");
|
|
1104
|
+
const numValue = Number(rawValue);
|
|
1105
|
+
if (rawValue == null || isNaN(numValue)) return null;
|
|
1106
|
+
return Number(rawValue);
|
|
1107
|
+
}
|
|
1108
|
+
async getType() {
|
|
1109
|
+
if ((await this.getAttribute("class"))?.includes("MuiCircularProgress-root")) return "circular";
|
|
1110
|
+
return "linear";
|
|
1111
|
+
}
|
|
1112
|
+
async isDeterminate() {
|
|
1113
|
+
return await this.getValue() != null;
|
|
1114
|
+
}
|
|
1115
|
+
async setValue(value) {
|
|
1116
|
+
const currentValue = await this.getValue();
|
|
1117
|
+
if (value === currentValue) return true;
|
|
1118
|
+
const valueToClick = value == null ? currentValue : value;
|
|
1119
|
+
const targetLocator = locatorUtil.append(this.parts.choices.locator, byValue(valueToClick.toString(), "Same"));
|
|
1120
|
+
const targetExists = await this.interactor.exists(targetLocator);
|
|
1121
|
+
if (targetExists) {
|
|
1122
|
+
const id = await this.interactor.getAttribute(targetLocator, "id");
|
|
1123
|
+
const labelLocator = locatorUtil.append(this.locator, byCssSelector(`label[for="${id}"]`));
|
|
1124
|
+
await this.interactor.click(labelLocator);
|
|
1125
|
+
}
|
|
1126
|
+
return targetExists;
|
|
1127
|
+
}
|
|
1128
|
+
get driverName() {
|
|
1129
|
+
return "MuiV9ProgressDriver";
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region src/components/RadioDriver.ts
|
|
1134
|
+
const inputLocator = byCssSelector("input");
|
|
1135
|
+
const checkedInputLocator = byCssSelector("input:checked");
|
|
1136
|
+
/**
|
|
1137
|
+
* Driver for a single Material UI v9 Radio option (a `FormControlLabel` wrapping a
|
|
1138
|
+
* `Radio`). Its label text is the `FormControlLabel`'s text; selected/value/disabled
|
|
1139
|
+
* state is read from the underlying radio `<input>`.
|
|
1140
|
+
*
|
|
1141
|
+
* Used as the item driver of {@link RadioGroupDriver}, but also usable on its own
|
|
1142
|
+
* when a single radio option is addressed directly. It declares no parts (so it
|
|
1143
|
+
* composes as a list item) and reads the input via descendant locators.
|
|
1144
|
+
* @see https://mui.com/material-ui/react-radio-button/
|
|
1145
|
+
*/
|
|
1146
|
+
var RadioDriver = class extends ComponentDriver {
|
|
1147
|
+
get input() {
|
|
1148
|
+
return locatorUtil.append(this.locator, inputLocator);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* The option's visible label, or `undefined` when it renders without text.
|
|
1152
|
+
*/
|
|
1153
|
+
async getLabel() {
|
|
1154
|
+
return (await this.getText())?.trim() || void 0;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* The option's `value` attribute.
|
|
1158
|
+
*/
|
|
1159
|
+
async getValue() {
|
|
1160
|
+
return await this.interactor.getAttribute(this.input, "value") ?? null;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Whether this option is the selected one in its group.
|
|
1164
|
+
*/
|
|
1165
|
+
isSelected() {
|
|
1166
|
+
return this.interactor.exists(locatorUtil.append(this.locator, checkedInputLocator));
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Whether this option is disabled.
|
|
1170
|
+
*/
|
|
1171
|
+
isDisabled() {
|
|
1172
|
+
return this.interactor.isDisabled(this.input);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Select this option by clicking its radio input. No-op effect when already selected.
|
|
1176
|
+
*/
|
|
1177
|
+
async select() {
|
|
1178
|
+
await this.interactor.click(this.input);
|
|
1179
|
+
}
|
|
1180
|
+
get driverName() {
|
|
1181
|
+
return "MuiV9RadioDriver";
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
//#endregion
|
|
1185
|
+
//#region src/components/RadioGroupDriver.ts
|
|
1186
|
+
/**
|
|
1187
|
+
* Radio options are located by their `FormControlLabel` root, which wraps the
|
|
1188
|
+
* radio `<input>` and renders the label text — the unit a {@link RadioDriver}
|
|
1189
|
+
* drives.
|
|
1190
|
+
*/
|
|
1191
|
+
const defaultRadioGroupDriverOption = {
|
|
1192
|
+
itemClass: RadioDriver,
|
|
1193
|
+
itemLocator: byCssSelector(".MuiFormControlLabel-root")
|
|
1194
|
+
};
|
|
1195
|
+
/**
|
|
1196
|
+
* Driver for the Material UI v9 RadioGroup component.
|
|
1197
|
+
*
|
|
1198
|
+
* `<RadioGroup>` renders a `role="radiogroup"` whose options are `FormControlLabel`s
|
|
1199
|
+
* wrapping a radio `<input>`. This driver is a {@link ListComponentDriver} over those
|
|
1200
|
+
* options, so per-option {@link RadioDriver} instances are available via
|
|
1201
|
+
* `getItems`/`getItemByIndex`/`getItemByLabel`, on top of the group-level value and
|
|
1202
|
+
* label helpers. The selected value is read from the checked `<input>` and selection
|
|
1203
|
+
* is made by value or label.
|
|
1204
|
+
* @see https://mui.com/material-ui/react-radio-button/
|
|
1205
|
+
*/
|
|
1206
|
+
var RadioGroupDriver = class extends ListComponentDriver {
|
|
1207
|
+
constructor(locator, interactor, option = {}) {
|
|
1208
|
+
super(locator, interactor, {
|
|
1209
|
+
...defaultRadioGroupDriverOption,
|
|
1210
|
+
...option
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* The `value` of the selected option, or `null` when none is selected.
|
|
1215
|
+
*/
|
|
1216
|
+
async getValue() {
|
|
1217
|
+
const checked = locatorUtil.append(this.locator, byCssSelector("input:checked"));
|
|
1218
|
+
return await this.interactor.getAttribute(checked, "value") ?? null;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Select the option whose radio `<input>` has the given `value`.
|
|
1222
|
+
* @returns `false` when no option has that value, or when `value` is `null`.
|
|
1223
|
+
*/
|
|
1224
|
+
async setValue(value) {
|
|
1225
|
+
if (value == null) return false;
|
|
1226
|
+
const input = locatorUtil.append(this.locator, byCssSelector(`input[value="${escapeUtil.escapeValue(value)}"]`));
|
|
1227
|
+
if (!await this.interactor.exists(input)) return false;
|
|
1228
|
+
await this.interactor.click(input);
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* The visible label of every option, in DOM order.
|
|
1233
|
+
*/
|
|
1234
|
+
async getOptions() {
|
|
1235
|
+
const items = await this.getItems();
|
|
1236
|
+
const labels = [];
|
|
1237
|
+
for (const item of items) labels.push(await item.getLabel() ?? "");
|
|
1238
|
+
return labels;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* The label of the selected option, or `null` when none is selected.
|
|
1242
|
+
*/
|
|
1243
|
+
async getSelectedLabel() {
|
|
1244
|
+
const items = await this.getItems();
|
|
1245
|
+
for (const item of items) if (await item.isSelected()) return await item.getLabel() ?? null;
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Select the first option whose visible label equals `label`.
|
|
1250
|
+
* @returns `false` when no option matches.
|
|
1251
|
+
*/
|
|
1252
|
+
async selectByLabel(label) {
|
|
1253
|
+
const option = await this.getItemByLabel(label);
|
|
1254
|
+
if (option == null) return false;
|
|
1255
|
+
await option.select();
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Whether the option with the given label is disabled.
|
|
1260
|
+
* @returns `false` when no option matches.
|
|
1261
|
+
*/
|
|
1262
|
+
async isOptionDisabled(label) {
|
|
1263
|
+
const option = await this.getItemByLabel(label);
|
|
1264
|
+
return option == null ? false : option.isDisabled();
|
|
1265
|
+
}
|
|
1266
|
+
get driverName() {
|
|
1267
|
+
return "MuiV9RadioGroupDriver";
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
//#endregion
|
|
1271
|
+
//#region src/components/RatingDriver.ts
|
|
1272
|
+
const parts$4 = { choices: {
|
|
1273
|
+
locator: byInputType("radio"),
|
|
1274
|
+
driver: HTMLRadioButtonGroupDriver
|
|
1275
|
+
} };
|
|
1276
|
+
/**
|
|
1277
|
+
* MUI marks the disabled/read-only state with a class on the Rating root span
|
|
1278
|
+
* (`Mui-disabled` / `Mui-readOnly`), not on the visually-hidden radio inputs that
|
|
1279
|
+
* the composed {@link HTMLRadioButtonGroupDriver} can see — so these states are
|
|
1280
|
+
* only observable from the root element.
|
|
1281
|
+
*/
|
|
1282
|
+
const disabledClassName = "Mui-disabled";
|
|
1283
|
+
const readOnlyClassName = "Mui-readOnly";
|
|
1284
|
+
const filledIconClassName = "MuiRating-iconFilled";
|
|
1285
|
+
var RatingDriver = class extends ComponentDriver {
|
|
1286
|
+
constructor(locator, interactor, option) {
|
|
1287
|
+
super(locator, interactor, {
|
|
1288
|
+
...option,
|
|
1289
|
+
parts: parts$4
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
async getValue() {
|
|
1293
|
+
if (await this.interactor.hasCssClass(this.locator, readOnlyClassName)) return this.getReadOnlyValue();
|
|
1294
|
+
await this.enforcePartExistence("choices");
|
|
1295
|
+
const value = await this.parts.choices.getValue();
|
|
1296
|
+
if (value == null || value === "") return null;
|
|
1297
|
+
return parseFloat(value);
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Read the value of a read-only Rating.
|
|
1301
|
+
*
|
|
1302
|
+
* Primary source is the root's `aria-label`, which MUI populates with the accessible
|
|
1303
|
+
* name (e.g. `"2.5 Stars"`) — accessibility-first and precision-accurate. When a
|
|
1304
|
+
* caller supplies a custom, non-numeric `aria-label`, fall back to counting the
|
|
1305
|
+
* filled star icons; that count is exact for whole-star ratings.
|
|
1306
|
+
*/
|
|
1307
|
+
async getReadOnlyValue() {
|
|
1308
|
+
const label = await this.interactor.getAttribute(this.locator, "aria-label");
|
|
1309
|
+
if (label != null) {
|
|
1310
|
+
const parsed = parseFloat(label);
|
|
1311
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
1312
|
+
}
|
|
1313
|
+
const filledLocator = locatorUtil.append(this.locator, byCssClass(filledIconClassName));
|
|
1314
|
+
const filledIcons = await this.interactor.getAttribute(filledLocator, "class", true);
|
|
1315
|
+
return filledIcons.length > 0 ? filledIcons.length : null;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Whether the Rating is disabled. `disabled` is reflected as the `Mui-disabled`
|
|
1319
|
+
* class on the root span (the composed radio-group driver exposes no `isDisabled`).
|
|
1320
|
+
*/
|
|
1321
|
+
async isDisabled() {
|
|
1322
|
+
return this.interactor.hasCssClass(this.locator, disabledClassName);
|
|
1323
|
+
}
|
|
1324
|
+
async setValue(value) {
|
|
1325
|
+
const currentValue = await this.getValue();
|
|
1326
|
+
if (value === currentValue) return true;
|
|
1327
|
+
const valueToClick = value ?? currentValue;
|
|
1328
|
+
if (valueToClick == null) return true;
|
|
1329
|
+
const targetLocator = locatorUtil.append(this.parts.choices.locator, byValue(valueToClick.toString(), "Same"));
|
|
1330
|
+
const targetExists = await this.interactor.exists(targetLocator);
|
|
1331
|
+
if (targetExists) {
|
|
1332
|
+
const id = await this.interactor.getAttribute(targetLocator, "id");
|
|
1333
|
+
const labelLocator = locatorUtil.append(this.locator, byCssSelector(`label[for="${id}"]`));
|
|
1334
|
+
await this.interactor.click(labelLocator);
|
|
1335
|
+
}
|
|
1336
|
+
return targetExists;
|
|
1337
|
+
}
|
|
1338
|
+
get driverName() {
|
|
1339
|
+
return "MuiV9RatingDriver";
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
//#endregion
|
|
1343
|
+
//#region src/components/SelectDriver.ts
|
|
1344
|
+
const selectPart = {
|
|
1345
|
+
trigger: {
|
|
1346
|
+
locator: byRole("combobox"),
|
|
1347
|
+
driver: HTMLButtonDriver
|
|
1348
|
+
},
|
|
1349
|
+
dropdown: {
|
|
1350
|
+
locator: byCssSelector("[role=presentation] [role=listbox]", "Root"),
|
|
1351
|
+
driver: HTMLElementDriver
|
|
1352
|
+
},
|
|
1353
|
+
input: {
|
|
1354
|
+
locator: byTagName("input"),
|
|
1355
|
+
driver: HTMLTextInputDriver
|
|
1356
|
+
},
|
|
1357
|
+
nativeSelect: {
|
|
1358
|
+
locator: byTagName("select"),
|
|
1359
|
+
driver: HTMLSelectDriver
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
const optionLocator = byRole("option");
|
|
1363
|
+
var SelectDriver = class extends ComponentDriver {
|
|
1364
|
+
constructor(locator, interactor, option) {
|
|
1365
|
+
super(locator, interactor, {
|
|
1366
|
+
...option,
|
|
1367
|
+
parts: selectPart
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
async isNative() {
|
|
1371
|
+
const nativeSelectExists = await this.interactor.exists(this.parts.nativeSelect.locator);
|
|
1372
|
+
return Promise.resolve(nativeSelectExists);
|
|
1373
|
+
}
|
|
1374
|
+
async getValue() {
|
|
1375
|
+
if (await this.isNative()) return await this.parts.nativeSelect.getValue();
|
|
1376
|
+
await this.enforcePartExistence("input");
|
|
1377
|
+
return await this.parts.input.getValue() ?? null;
|
|
1378
|
+
}
|
|
1379
|
+
async setValue(value) {
|
|
1380
|
+
let success = false;
|
|
1381
|
+
if (await this.isNative()) {
|
|
1382
|
+
success = await this.parts.nativeSelect.setValue(value);
|
|
1383
|
+
return success;
|
|
1384
|
+
}
|
|
1385
|
+
await this.openDropdown();
|
|
1386
|
+
await this.enforcePartExistence("dropdown");
|
|
1387
|
+
const optionSelector = byAttribute("data-value", value);
|
|
1388
|
+
const optionLocator = locatorUtil.append(this.parts.dropdown.locator, optionSelector);
|
|
1389
|
+
if (await this.interactor.exists(optionLocator)) {
|
|
1390
|
+
await this.interactor.click(optionLocator);
|
|
1391
|
+
success = true;
|
|
1392
|
+
}
|
|
1393
|
+
return success;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Select menu item by its label, if it exists
|
|
1397
|
+
* Limitation, this method will not work if the dropdown is a native select.
|
|
1398
|
+
* @param label
|
|
1399
|
+
* @returns
|
|
1400
|
+
*/
|
|
1401
|
+
async getMenuItemByLabel(label, option) {
|
|
1402
|
+
if (!option?.skipDropdownCheck) await this.openDropdown();
|
|
1403
|
+
for await (const item of listHelper.getListItemIterator(this, optionLocator, MenuItemDriver)) if (await item.label() === label) return item;
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Selects an option by its label
|
|
1408
|
+
* @param label
|
|
1409
|
+
* @returns
|
|
1410
|
+
*/
|
|
1411
|
+
async selectByLabel(label) {
|
|
1412
|
+
if (await this.isNative()) {
|
|
1413
|
+
await this.parts.nativeSelect.selectByLabel(label);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
await this.enforcePartExistence("trigger");
|
|
1417
|
+
await this.parts.trigger.click();
|
|
1418
|
+
await this.enforcePartExistence("dropdown");
|
|
1419
|
+
const item = await this.getMenuItemByLabel(label, { skipDropdownCheck: true });
|
|
1420
|
+
if (item) await item.click();
|
|
1421
|
+
else throw new MenuItemNotFoundError(label, this);
|
|
1422
|
+
}
|
|
1423
|
+
async getSelectedLabel() {
|
|
1424
|
+
if (await this.isNative()) return await this.parts.nativeSelect.getSelectedLabel();
|
|
1425
|
+
await this.enforcePartExistence("trigger");
|
|
1426
|
+
return await this.parts.trigger.getText() ?? null;
|
|
1427
|
+
}
|
|
1428
|
+
async exists() {
|
|
1429
|
+
if (await this.interactor.exists(this.parts.trigger.locator)) return true;
|
|
1430
|
+
return await this.interactor.exists(this.parts.nativeSelect.locator);
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Check if the dropdown is open, or if it is a native select, it is always open because there is no known way check its open state
|
|
1434
|
+
* @returns For native dropdown it is always true. For custom dropdown, it is true if the dropdown is open.
|
|
1435
|
+
*/
|
|
1436
|
+
async isDropdownOpen() {
|
|
1437
|
+
if (await this.isNative()) return true;
|
|
1438
|
+
else return this.parts.dropdown.exists();
|
|
1439
|
+
}
|
|
1440
|
+
async openDropdown() {
|
|
1441
|
+
if (await this.isDropdownOpen()) return;
|
|
1442
|
+
await this.parts.trigger.click();
|
|
1443
|
+
}
|
|
1444
|
+
async closeDropdown() {
|
|
1445
|
+
if (!await this.isDropdownOpen()) return;
|
|
1446
|
+
await this.parts.trigger.click();
|
|
1447
|
+
}
|
|
1448
|
+
async isDisabled() {
|
|
1449
|
+
if (await this.isNative()) return this.parts.nativeSelect.isDisabled();
|
|
1450
|
+
else {
|
|
1451
|
+
await this.enforcePartExistence("trigger");
|
|
1452
|
+
return await this.interactor.getAttribute(this.parts.trigger.locator, "aria-disabled") === "true";
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async isReadonly() {
|
|
1456
|
+
if (await this.isNative()) return this.parts.nativeSelect.isReadonly();
|
|
1457
|
+
else return false;
|
|
1458
|
+
}
|
|
1459
|
+
get driverName() {
|
|
1460
|
+
return "MuiV9SelectDriver";
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
//#endregion
|
|
1464
|
+
//#region src/components/SliderDriver.ts
|
|
1465
|
+
const parts$3 = { input: {
|
|
1466
|
+
locator: byCssSelector("input[type=\"range\"][data-index=\"0\"]"),
|
|
1467
|
+
driver: HTMLTextInputDriver
|
|
1468
|
+
} };
|
|
1469
|
+
var SliderDriver = class extends ComponentDriver {
|
|
1470
|
+
constructor(locator, interactor, option) {
|
|
1471
|
+
super(locator, interactor, {
|
|
1472
|
+
...option,
|
|
1473
|
+
parts: parts$3
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Return the first occurrence of the Slider input
|
|
1478
|
+
* @returns
|
|
1479
|
+
*/
|
|
1480
|
+
async getValue() {
|
|
1481
|
+
return (await this.getRangeValues(1))[0];
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Set slider's range value. Do not use as it will throw an error
|
|
1485
|
+
* @param values
|
|
1486
|
+
* @see https://github.com/atomic-testing/atomic-testing/issues/73
|
|
1487
|
+
*/
|
|
1488
|
+
async setValue(value) {
|
|
1489
|
+
return await this.setRangeValues([value]);
|
|
1490
|
+
}
|
|
1491
|
+
async getRangeValues(count) {
|
|
1492
|
+
await this.enforcePartExistence("input");
|
|
1493
|
+
const result = [];
|
|
1494
|
+
let index = 0;
|
|
1495
|
+
let done = false;
|
|
1496
|
+
while (!done) {
|
|
1497
|
+
const locator = locatorUtil.append(this.locator, this.getInputLocator(index));
|
|
1498
|
+
if (await this.interactor.exists(locator)) {
|
|
1499
|
+
index++;
|
|
1500
|
+
done = count != null && index >= count;
|
|
1501
|
+
const value = await this.interactor.getAttribute(locator, "value");
|
|
1502
|
+
result.push(parseFloat(value));
|
|
1503
|
+
} else done = true;
|
|
1504
|
+
}
|
|
1505
|
+
return result;
|
|
1506
|
+
}
|
|
1507
|
+
getInputLocator(index) {
|
|
1508
|
+
return byCssSelector(`input[type="range"][data-index="${index}"]`);
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Set slider's range values. Do not use as it will throw an error
|
|
1512
|
+
* @param values
|
|
1513
|
+
* @see https://github.com/atomic-testing/atomic-testing/issues/73
|
|
1514
|
+
*/
|
|
1515
|
+
async setRangeValues(_values) {
|
|
1516
|
+
await this.enforcePartExistence("input");
|
|
1517
|
+
throw new Error("setRangeValue is not supported.");
|
|
1518
|
+
}
|
|
1519
|
+
async isDisabled() {
|
|
1520
|
+
await this.enforcePartExistence("input");
|
|
1521
|
+
return await this.parts.input.isDisabled();
|
|
1522
|
+
}
|
|
1523
|
+
get driverName() {
|
|
1524
|
+
return "MuiV9SliderDriver";
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
//#endregion
|
|
1528
|
+
//#region src/components/SnackbarDriver.ts
|
|
1529
|
+
const parts$2 = {
|
|
1530
|
+
contentDisplay: {
|
|
1531
|
+
locator: byCssClass("MuiSnackbarContent-message"),
|
|
1532
|
+
driver: HTMLElementDriver
|
|
1533
|
+
},
|
|
1534
|
+
actionArea: {
|
|
1535
|
+
locator: byCssClass("MuiSnackbarContent-action"),
|
|
1536
|
+
driver: HTMLElementDriver
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
/**
|
|
1540
|
+
* Driver for Material UI v9 Snackbar component.
|
|
1541
|
+
* @see https://mui.com/material-ui/react-snackbar/
|
|
1542
|
+
*/
|
|
1543
|
+
var SnackbarDriver = class extends ComponentDriver {
|
|
1544
|
+
constructor(locator, interactor, option) {
|
|
1545
|
+
super(locator, interactor, {
|
|
1546
|
+
...option,
|
|
1547
|
+
parts: parts$2
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Get the label content of the snackbar.
|
|
1552
|
+
* @returns The label text content of the snackbar.
|
|
1553
|
+
*/
|
|
1554
|
+
async getLabel() {
|
|
1555
|
+
await this.enforcePartExistence("contentDisplay");
|
|
1556
|
+
return await this.parts.contentDisplay.getText() ?? null;
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Get a driver instance of a component in the action area of the snackbar.
|
|
1560
|
+
* @param locator
|
|
1561
|
+
* @param driverClass
|
|
1562
|
+
* @returns
|
|
1563
|
+
*/
|
|
1564
|
+
async getActionComponent(locator, driverClass) {
|
|
1565
|
+
await this.enforcePartExistence("actionArea");
|
|
1566
|
+
const componentLocator = locatorUtil.append(this.parts.actionArea.locator, locator);
|
|
1567
|
+
if (await this.interactor.exists(componentLocator)) return new driverClass(componentLocator, this.interactor, this.commutableOption);
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
get driverName() {
|
|
1571
|
+
return "MuiV9SnackbarDriver";
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
//#endregion
|
|
1575
|
+
//#region src/components/SpeedDialDriver.ts
|
|
1576
|
+
const fabLocator = byCssSelector(".MuiSpeedDial-fab");
|
|
1577
|
+
const actionLocator = byCssSelector(".MuiSpeedDialAction-fab");
|
|
1578
|
+
/**
|
|
1579
|
+
* Driver for the Material UI v9 SpeedDial component.
|
|
1580
|
+
*
|
|
1581
|
+
* SpeedDial renders a trigger FAB (`.MuiSpeedDial-fab`, `aria-expanded` reflects
|
|
1582
|
+
* open state) and a set of action FABs (`.MuiSpeedDialAction-fab`, each
|
|
1583
|
+
* aria-labelled with its name). The actions stay mounted but are only revealed
|
|
1584
|
+
* when open, so open state is read from the FAB's `aria-expanded` rather than
|
|
1585
|
+
* action presence.
|
|
1586
|
+
* @see https://mui.com/material-ui/react-speed-dial/
|
|
1587
|
+
*/
|
|
1588
|
+
var SpeedDialDriver = class extends ComponentDriver {
|
|
1589
|
+
get fab() {
|
|
1590
|
+
return locatorUtil.append(this.locator, fabLocator);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Whether the speed dial is open (its FAB reports `aria-expanded="true"`).
|
|
1594
|
+
*/
|
|
1595
|
+
async isOpen() {
|
|
1596
|
+
return await this.interactor.getAttribute(this.fab, "aria-expanded") === "true";
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Open the speed dial by hovering its FAB. No-op when already open.
|
|
1600
|
+
*
|
|
1601
|
+
* Hover (`onMouseEnter`) is the trigger that opens consistently across MUI
|
|
1602
|
+
* versions and browsers — clicking only focuses-then-opens in Chromium, and
|
|
1603
|
+
* focus opens in v7 but not v5, whereas hover opens everywhere.
|
|
1604
|
+
*/
|
|
1605
|
+
async open() {
|
|
1606
|
+
if (!await this.isOpen()) await this.interactor.hover(this.fab);
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Close the speed dial by moving the pointer off its FAB. No-op when already closed.
|
|
1610
|
+
*/
|
|
1611
|
+
async close() {
|
|
1612
|
+
if (await this.isOpen()) await this.interactor.mouseLeave(this.fab);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* The labels of every action, in order (read from each action FAB's aria-label).
|
|
1616
|
+
*/
|
|
1617
|
+
async getActionLabels() {
|
|
1618
|
+
return (await this.interactor.getAttribute(locatorUtil.append(this.locator, actionLocator), "aria-label", true)).filter((label) => label != null);
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Trigger the action with the given label, opening the dial first if needed.
|
|
1622
|
+
* @returns `false` when no action has that label.
|
|
1623
|
+
*/
|
|
1624
|
+
async triggerActionByLabel(label) {
|
|
1625
|
+
await this.open();
|
|
1626
|
+
const actionByLabel = byCssSelector(`.MuiSpeedDialAction-fab[aria-label="${escapeUtil.escapeValue(label)}"]`);
|
|
1627
|
+
const locator = locatorUtil.append(this.locator, actionByLabel);
|
|
1628
|
+
if (!await this.interactor.exists(locator)) return false;
|
|
1629
|
+
await this.interactor.click(locator);
|
|
1630
|
+
return true;
|
|
1631
|
+
}
|
|
1632
|
+
get driverName() {
|
|
1633
|
+
return "MuiV9SpeedDialDriver";
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
//#endregion
|
|
1637
|
+
//#region src/components/StepperDriver.ts
|
|
1638
|
+
const stepLabelLocator = byCssSelector(".MuiStepLabel-label");
|
|
1639
|
+
/**
|
|
1640
|
+
* Locator for the label of the step at `index`. In Material UI v9 the connector
|
|
1641
|
+
* is rendered *inside* each `.MuiStep-root` (in v7 it was an interleaved sibling),
|
|
1642
|
+
* so the step elements are now consecutive siblings and the step at `index` is the
|
|
1643
|
+
* `index+1`-th element of its type; nth-of-type addresses it directly.
|
|
1644
|
+
*/
|
|
1645
|
+
function stepLabelAt(index) {
|
|
1646
|
+
return byCssSelector(`.MuiStep-root:nth-of-type(${index + 1}) .MuiStepLabel-label`);
|
|
1647
|
+
}
|
|
1648
|
+
function stepButtonAt(index) {
|
|
1649
|
+
return byCssSelector(`.MuiStep-root:nth-of-type(${index + 1}) .MuiStepButton-root`);
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Driver for the Material UI v9 Stepper component.
|
|
1653
|
+
*
|
|
1654
|
+
* Each step's state lives on its `.MuiStepLabel-label` (`Mui-active`,
|
|
1655
|
+
* `Mui-completed`, `Mui-disabled`); reading every label's class in one pass gives
|
|
1656
|
+
* the step states and count in document order. Steps are addressed positionally
|
|
1657
|
+
* for label text and clicks via {@link stepLabelAt}/{@link stepButtonAt}.
|
|
1658
|
+
*
|
|
1659
|
+
* Supported layouts: the default horizontal stepper and the vertical orientation.
|
|
1660
|
+
* In v9 every `.MuiStep-root` is a consecutive sibling (the connector lives inside
|
|
1661
|
+
* the step), and every step renders a text label, so positional addressing lines up
|
|
1662
|
+
* with the document-order class list. The unsupported case is icon-only steps, which
|
|
1663
|
+
* render no `.MuiStepLabel-label`; under those the document-order class list has fewer
|
|
1664
|
+
* entries than steps and the two desynchronize. Robustly supporting that needs an
|
|
1665
|
+
* interactor primitive to address the n-th of non-sibling matches
|
|
1666
|
+
* (CSS `:nth-child(of S)` is unsupported in jsdom), which is out of this driver's scope.
|
|
1667
|
+
* @see https://mui.com/material-ui/react-stepper/
|
|
1668
|
+
*/
|
|
1669
|
+
var StepperDriver = class extends ComponentDriver {
|
|
1670
|
+
async getStepClassList() {
|
|
1671
|
+
return this.interactor.getAttribute(locatorUtil.append(this.locator, stepLabelLocator), "class", true);
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* The number of steps.
|
|
1675
|
+
*/
|
|
1676
|
+
async getStepCount() {
|
|
1677
|
+
return (await this.getStepClassList()).length;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Zero-based index of the active step, or `-1` when none is active.
|
|
1681
|
+
*/
|
|
1682
|
+
async getActiveStepIndex() {
|
|
1683
|
+
return (await this.getStepClassList()).findIndex((c) => c.split(/\s+/).includes("Mui-active"));
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Every step with its label and active/completed/disabled state, in order.
|
|
1687
|
+
*/
|
|
1688
|
+
async getSteps() {
|
|
1689
|
+
const classes = await this.getStepClassList();
|
|
1690
|
+
const steps = [];
|
|
1691
|
+
for (let index = 0; index < classes.length; index++) {
|
|
1692
|
+
const stateClasses = classes[index].split(/\s+/);
|
|
1693
|
+
const label = await this.interactor.getText(locatorUtil.append(this.locator, stepLabelAt(index)));
|
|
1694
|
+
steps.push({
|
|
1695
|
+
label: label?.trim(),
|
|
1696
|
+
active: stateClasses.includes("Mui-active"),
|
|
1697
|
+
completed: stateClasses.includes("Mui-completed"),
|
|
1698
|
+
disabled: stateClasses.includes("Mui-disabled")
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
return steps;
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Navigate to the step at `index` by clicking its control (requires a clickable
|
|
1705
|
+
* `StepButton`, e.g. a non-linear stepper).
|
|
1706
|
+
* @returns `false` when the step is out of range, has no button, or is disabled.
|
|
1707
|
+
*/
|
|
1708
|
+
async goToStep(index) {
|
|
1709
|
+
if (index < 0 || index >= await this.getStepCount()) return false;
|
|
1710
|
+
const button = locatorUtil.append(this.locator, stepButtonAt(index));
|
|
1711
|
+
if (!await this.interactor.exists(button) || await this.interactor.isDisabled(button)) return false;
|
|
1712
|
+
await this.interactor.click(button);
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1715
|
+
get driverName() {
|
|
1716
|
+
return "MuiV9StepperDriver";
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
//#endregion
|
|
1720
|
+
//#region src/components/SwitchDriver.ts
|
|
1721
|
+
const parts$1 = { input: {
|
|
1722
|
+
locator: byAttribute("type", "checkbox"),
|
|
1723
|
+
driver: HTMLCheckboxDriver
|
|
1724
|
+
} };
|
|
1725
|
+
var SwitchDriver = class extends ComponentDriver {
|
|
1726
|
+
constructor(locator, interactor, option) {
|
|
1727
|
+
super(locator, interactor, {
|
|
1728
|
+
...option,
|
|
1729
|
+
parts: parts$1
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
async exists() {
|
|
1733
|
+
return this.interactor.exists(this.parts.input.locator);
|
|
1734
|
+
}
|
|
1735
|
+
async isSelected() {
|
|
1736
|
+
await this.enforcePartExistence("input");
|
|
1737
|
+
return this.parts.input.isSelected();
|
|
1738
|
+
}
|
|
1739
|
+
async setSelected(selected) {
|
|
1740
|
+
await this.enforcePartExistence("input");
|
|
1741
|
+
await this.parts.input.setSelected(selected);
|
|
1742
|
+
}
|
|
1743
|
+
async getValue() {
|
|
1744
|
+
await this.enforcePartExistence("input");
|
|
1745
|
+
return this.parts.input.getValue();
|
|
1746
|
+
}
|
|
1747
|
+
async isDisabled() {
|
|
1748
|
+
await this.enforcePartExistence("input");
|
|
1749
|
+
return this.parts.input.isDisabled();
|
|
1750
|
+
}
|
|
1751
|
+
async isReadonly() {
|
|
1752
|
+
return Promise.resolve(false);
|
|
1753
|
+
}
|
|
1754
|
+
get driverName() {
|
|
1755
|
+
return "MuiV9SwitchDriver";
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
//#endregion
|
|
1759
|
+
//#region src/components/TableCellDriver.ts
|
|
1760
|
+
/**
|
|
1761
|
+
* Driver for a single Material UI v9 TableCell (`<td>`/`<th>`, `.MuiTableCell-root`).
|
|
1762
|
+
*
|
|
1763
|
+
* A cell's content is plain text, so it relies on the inherited `getText()`/`exists()`;
|
|
1764
|
+
* it exists as a distinct type so {@link TableRowDriver} can expose typed cell drivers.
|
|
1765
|
+
* @see https://mui.com/material-ui/react-table/
|
|
1766
|
+
*/
|
|
1767
|
+
var TableCellDriver = class extends ComponentDriver {
|
|
1768
|
+
get driverName() {
|
|
1769
|
+
return "MuiV9TableCellDriver";
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
//#endregion
|
|
1773
|
+
//#region src/components/TableRowDriver.ts
|
|
1774
|
+
/**
|
|
1775
|
+
* Cells are located by `.MuiTableCell-root`, covering both `<td>` body cells and
|
|
1776
|
+
* `<th>` header cells.
|
|
1777
|
+
*/
|
|
1778
|
+
const defaultTableRowDriverOption = {
|
|
1779
|
+
itemClass: TableCellDriver,
|
|
1780
|
+
itemLocator: byCssSelector(".MuiTableCell-root")
|
|
1781
|
+
};
|
|
1782
|
+
/**
|
|
1783
|
+
* Driver for a single Material UI v9 TableRow (`.MuiTableRow-root`).
|
|
1784
|
+
*
|
|
1785
|
+
* A {@link ListComponentDriver} over the row's cells, exposing per-cell
|
|
1786
|
+
* {@link TableCellDriver}s (via `getItems`/`getItemByIndex`) plus convenience reads
|
|
1787
|
+
* of the cell texts.
|
|
1788
|
+
*
|
|
1789
|
+
* Cells are addressed positionally via `:nth-of-type`, which counts per element type.
|
|
1790
|
+
* This assumes a row's cells are homogeneous (all `<td>`, or all `<th>` for a header
|
|
1791
|
+
* row) — the common MUI rendering. A row mixing a leading `<th scope="row">` with
|
|
1792
|
+
* `<td>` data cells would desynchronize the index (the `<td>`s start at type-index 1
|
|
1793
|
+
* after the `<th>`); such rows are out of scope.
|
|
1794
|
+
* @see https://mui.com/material-ui/react-table/
|
|
1795
|
+
*/
|
|
1796
|
+
var TableRowDriver = class extends ListComponentDriver {
|
|
1797
|
+
constructor(locator, interactor, option = {}) {
|
|
1798
|
+
super(locator, interactor, {
|
|
1799
|
+
...option,
|
|
1800
|
+
...defaultTableRowDriverOption
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* The number of cells in the row.
|
|
1805
|
+
*/
|
|
1806
|
+
async getCellCount() {
|
|
1807
|
+
return this.getItemCount();
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* The cell driver at the given zero-based column index, or `null` when out of range.
|
|
1811
|
+
*/
|
|
1812
|
+
async getCell(index) {
|
|
1813
|
+
return this.getItemByIndex(index);
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* The text of every cell, in column order.
|
|
1817
|
+
*/
|
|
1818
|
+
async getCellTexts() {
|
|
1819
|
+
const cells = await this.getItems();
|
|
1820
|
+
const texts = [];
|
|
1821
|
+
for (const cell of cells) texts.push((await cell.getText())?.trim() ?? "");
|
|
1822
|
+
return texts;
|
|
1823
|
+
}
|
|
1824
|
+
get driverName() {
|
|
1825
|
+
return "MuiV9TableRowDriver";
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
//#endregion
|
|
1829
|
+
//#region src/components/TableDriver.ts
|
|
1830
|
+
const headerRowLocator = byCssSelector(".MuiTableHead-root .MuiTableRow-root");
|
|
1831
|
+
/**
|
|
1832
|
+
* Body rows are located under `.MuiTableBody-root`, so header rows are never
|
|
1833
|
+
* mistaken for data rows.
|
|
1834
|
+
*/
|
|
1835
|
+
const defaultTableDriverOption = {
|
|
1836
|
+
itemClass: TableRowDriver,
|
|
1837
|
+
itemLocator: byCssSelector(".MuiTableBody-root .MuiTableRow-root")
|
|
1838
|
+
};
|
|
1839
|
+
function headerCellAt(columnIndex) {
|
|
1840
|
+
return byCssSelector(`.MuiTableHead-root .MuiTableRow-root .MuiTableCell-root:nth-of-type(${columnIndex + 1})`);
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Driver for the Material UI v9 Table component.
|
|
1844
|
+
*
|
|
1845
|
+
* A {@link ListComponentDriver} over the data rows (`.MuiTableBody-root > .MuiTableRow-root`),
|
|
1846
|
+
* exposing per-row {@link TableRowDriver}s plus header reads and column sort state.
|
|
1847
|
+
* Locators key off MUI's structural classes (`MuiTableHead/Body/Row/Cell-root`),
|
|
1848
|
+
* which are stable across v9. Sort state is read from the header cell's `aria-sort`
|
|
1849
|
+
* and driven by clicking its `TableSortLabel`.
|
|
1850
|
+
* @see https://mui.com/material-ui/react-table/
|
|
1851
|
+
*/
|
|
1852
|
+
var TableDriver = class extends ListComponentDriver {
|
|
1853
|
+
constructor(locator, interactor, option = {}) {
|
|
1854
|
+
super(locator, interactor, {
|
|
1855
|
+
...defaultTableDriverOption,
|
|
1856
|
+
...option
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* The number of data (body) rows.
|
|
1861
|
+
*/
|
|
1862
|
+
async getRowCount() {
|
|
1863
|
+
return this.getItemCount();
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* The data-row driver at the given zero-based index, or `null` when out of range.
|
|
1867
|
+
*/
|
|
1868
|
+
async getRow(index) {
|
|
1869
|
+
return this.getItemByIndex(index);
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* The header row as a {@link TableRowDriver}, or `null` when the table has no header.
|
|
1873
|
+
*/
|
|
1874
|
+
async getHeaderRow() {
|
|
1875
|
+
const locator = locatorUtil.append(this.locator, headerRowLocator);
|
|
1876
|
+
if (!await this.interactor.exists(locator)) return null;
|
|
1877
|
+
return new TableRowDriver(locator, this.interactor);
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* The number of columns, derived from the header cell count (0 when no header).
|
|
1881
|
+
*/
|
|
1882
|
+
async getColumnCount() {
|
|
1883
|
+
const header = await this.getHeaderRow();
|
|
1884
|
+
return header == null ? 0 : header.getCellCount();
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* The header cell texts, in column order (empty when no header).
|
|
1888
|
+
*/
|
|
1889
|
+
async getHeaderTexts() {
|
|
1890
|
+
const header = await this.getHeaderRow();
|
|
1891
|
+
return header == null ? [] : header.getCellTexts();
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* The text of the data cell at the given row/column, or `undefined` when either
|
|
1895
|
+
* index is out of range.
|
|
1896
|
+
*/
|
|
1897
|
+
async getCellText(rowIndex, columnIndex) {
|
|
1898
|
+
const row = await this.getRow(rowIndex);
|
|
1899
|
+
if (row == null) return;
|
|
1900
|
+
const cell = await row.getCell(columnIndex);
|
|
1901
|
+
if (cell == null) return;
|
|
1902
|
+
return (await cell.getText())?.trim();
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* The sort direction applied to the column at `columnIndex`, read from the header
|
|
1906
|
+
* cell's `aria-sort`, or `undefined` when that column is not the sorted one.
|
|
1907
|
+
*/
|
|
1908
|
+
async getSortDirection(columnIndex) {
|
|
1909
|
+
const cell = headerCellAt(columnIndex);
|
|
1910
|
+
const fullLocator = locatorUtil.append(this.locator, cell);
|
|
1911
|
+
if (!await this.interactor.exists(fullLocator)) return;
|
|
1912
|
+
const ariaSort = await this.interactor.getAttribute(fullLocator, "aria-sort");
|
|
1913
|
+
return ariaSort === "ascending" || ariaSort === "descending" ? ariaSort : void 0;
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Toggle/apply sorting on the column at `columnIndex` by clicking its
|
|
1917
|
+
* `TableSortLabel`.
|
|
1918
|
+
* @returns `false` when the column has no sort control.
|
|
1919
|
+
*/
|
|
1920
|
+
async sortByColumn(columnIndex) {
|
|
1921
|
+
const sortLabel = locatorUtil.append(this.locator, byCssSelector(`.MuiTableHead-root .MuiTableRow-root .MuiTableCell-root:nth-of-type(${columnIndex + 1}) .MuiTableSortLabel-root`));
|
|
1922
|
+
if (!await this.interactor.exists(sortLabel)) return false;
|
|
1923
|
+
await this.interactor.click(sortLabel);
|
|
1924
|
+
return true;
|
|
1925
|
+
}
|
|
1926
|
+
get driverName() {
|
|
1927
|
+
return "MuiV9TableDriver";
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
//#endregion
|
|
1931
|
+
//#region src/components/TablePaginationDriver.ts
|
|
1932
|
+
const previousButtonLocator = byAttribute("aria-label", "Go to previous page");
|
|
1933
|
+
const nextButtonLocator = byAttribute("aria-label", "Go to next page");
|
|
1934
|
+
const displayedRowsLocator = byCssSelector(".MuiTablePagination-displayedRows");
|
|
1935
|
+
/**
|
|
1936
|
+
* Driver for the Material UI v9 TablePagination component.
|
|
1937
|
+
*
|
|
1938
|
+
* TablePagination is a composite: a "rows per page" MUI Select (a portal-backed
|
|
1939
|
+
* `role="combobox"`), an aria-labelled previous/next pair that disables at the
|
|
1940
|
+
* bounds, and a `.MuiTablePagination-displayedRows` label ("1–5 of 13"). The
|
|
1941
|
+
* rows-per-page control is delegated to {@link SelectDriver} rather than
|
|
1942
|
+
* reimplemented — the driver's own root contains exactly one combobox/input, so
|
|
1943
|
+
* SelectDriver scoped to that root resolves it unambiguously.
|
|
1944
|
+
* @see https://mui.com/material-ui/react-pagination/#table-pagination
|
|
1945
|
+
*/
|
|
1946
|
+
var TablePaginationDriver = class extends ComponentDriver {
|
|
1947
|
+
get rowsPerPageSelect() {
|
|
1948
|
+
return new SelectDriver(this.locator, this.interactor);
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* The current rows-per-page value (read from the select's hidden input), or
|
|
1952
|
+
* `-1` when it cannot be parsed.
|
|
1953
|
+
*/
|
|
1954
|
+
async getRowsPerPage() {
|
|
1955
|
+
const value = await this.rowsPerPageSelect.getValue();
|
|
1956
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
1957
|
+
return Number.isNaN(parsed) ? -1 : parsed;
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Choose a rows-per-page value by opening the select and picking the option.
|
|
1961
|
+
* @returns `false` when no such option exists.
|
|
1962
|
+
*/
|
|
1963
|
+
async setRowsPerPage(rowsPerPage) {
|
|
1964
|
+
return this.rowsPerPageSelect.setValue(String(rowsPerPage));
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* The raw "displayed rows" label (e.g. "1–5 of 13"), or `undefined` when absent.
|
|
1968
|
+
* Returned verbatim because its exact format (separator, "of") is locale-defined.
|
|
1969
|
+
*/
|
|
1970
|
+
async getDisplayedRowsText() {
|
|
1971
|
+
const locator = locatorUtil.append(this.locator, displayedRowsLocator);
|
|
1972
|
+
if (!await this.interactor.exists(locator)) return;
|
|
1973
|
+
return (await this.interactor.getText(locator))?.trim();
|
|
1974
|
+
}
|
|
1975
|
+
/** Whether the previous-page control is disabled (i.e. on the first page). */
|
|
1976
|
+
async isPreviousDisabled() {
|
|
1977
|
+
return this.isNavDisabled(previousButtonLocator);
|
|
1978
|
+
}
|
|
1979
|
+
/** Whether the next-page control is disabled (i.e. on the last page). */
|
|
1980
|
+
async isNextDisabled() {
|
|
1981
|
+
return this.isNavDisabled(nextButtonLocator);
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* An absent control counts as disabled — both to stay consistent with
|
|
1985
|
+
* {@link clickNavButton} (which reports a no-op for a missing control) and
|
|
1986
|
+
* because Playwright's `isDisabled` throws on a zero-match locator rather than
|
|
1987
|
+
* returning false the way jsdom does.
|
|
1988
|
+
*/
|
|
1989
|
+
async isNavDisabled(navLocator) {
|
|
1990
|
+
const locator = locatorUtil.append(this.locator, navLocator);
|
|
1991
|
+
if (!await this.interactor.exists(locator)) return true;
|
|
1992
|
+
return this.interactor.isDisabled(locator);
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Advance to the next page unless the control is disabled (last page).
|
|
1996
|
+
* @returns whether the click was performed.
|
|
1997
|
+
*/
|
|
1998
|
+
async nextPage() {
|
|
1999
|
+
return this.clickNavButton(nextButtonLocator);
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Go back to the previous page unless the control is disabled (first page).
|
|
2003
|
+
* @returns whether the click was performed.
|
|
2004
|
+
*/
|
|
2005
|
+
async previousPage() {
|
|
2006
|
+
return this.clickNavButton(previousButtonLocator);
|
|
2007
|
+
}
|
|
2008
|
+
async clickNavButton(navLocator) {
|
|
2009
|
+
const locator = locatorUtil.append(this.locator, navLocator);
|
|
2010
|
+
if (!await this.interactor.exists(locator) || await this.interactor.isDisabled(locator)) return false;
|
|
2011
|
+
await this.interactor.click(locator);
|
|
2012
|
+
return true;
|
|
2013
|
+
}
|
|
2014
|
+
get driverName() {
|
|
2015
|
+
return "MuiV9TablePaginationDriver";
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
//#endregion
|
|
2019
|
+
//#region src/components/TabDriver.ts
|
|
2020
|
+
/**
|
|
2021
|
+
* Driver for a single Material UI v9 Tab.
|
|
2022
|
+
*
|
|
2023
|
+
* A `<Tab>` renders as a `<button role="tab">`; MUI marks the active one with
|
|
2024
|
+
* `aria-selected="true"` and renders a real `disabled` button when disabled, so
|
|
2025
|
+
* selection and disabled state are read straight off the accessible attributes
|
|
2026
|
+
* (`isDisabled` is inherited from {@link HTMLButtonDriver}; `getText`/`click` from
|
|
2027
|
+
* the base `ComponentDriver`).
|
|
2028
|
+
*
|
|
2029
|
+
* Unlike a toggle button this is intentionally not an `IToggleDriver`: a tab can
|
|
2030
|
+
* be selected but not toggled off (selecting another tab deselects it), so only
|
|
2031
|
+
* `isSelected`/`select` are exposed.
|
|
2032
|
+
* @see https://mui.com/material-ui/react-tabs/
|
|
2033
|
+
*/
|
|
2034
|
+
var TabDriver = class extends HTMLButtonDriver {
|
|
2035
|
+
/**
|
|
2036
|
+
* Whether this tab is the selected one, i.e. MUI set `aria-selected="true"`.
|
|
2037
|
+
*/
|
|
2038
|
+
async isSelected() {
|
|
2039
|
+
return await this.interactor.getAttribute(this.locator, "aria-selected") === "true";
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Activate this tab by clicking it, unless it is already selected. A selected
|
|
2043
|
+
* tab cannot be toggled off, so this is a no-op when already active.
|
|
2044
|
+
*/
|
|
2045
|
+
async select() {
|
|
2046
|
+
if (!await this.isSelected()) await this.interactor.click(this.locator);
|
|
2047
|
+
}
|
|
2048
|
+
get driverName() {
|
|
2049
|
+
return "MuiV9TabDriver";
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
//#endregion
|
|
2053
|
+
//#region src/components/TabsDriver.ts
|
|
2054
|
+
/**
|
|
2055
|
+
* Tabs are located by their accessible `role="tab"` children rather than by MUI
|
|
2056
|
+
* class names, so the driver is resilient to MUI styling/version changes.
|
|
2057
|
+
*/
|
|
2058
|
+
const defaultTabsDriverOption = {
|
|
2059
|
+
itemClass: TabDriver,
|
|
2060
|
+
itemLocator: byRole("tab")
|
|
2061
|
+
};
|
|
2062
|
+
/**
|
|
2063
|
+
* Driver for the Material UI v9 Tabs component.
|
|
2064
|
+
*
|
|
2065
|
+
* `<Tabs>` renders a `role="tablist"` whose `role="tab"` buttons carry the
|
|
2066
|
+
* selected (`aria-selected`) and disabled state. This driver is a
|
|
2067
|
+
* {@link ListComponentDriver} over those tabs, exposing both group-level helpers
|
|
2068
|
+
* (selected index/label, select by index/label) and per-tab {@link TabDriver}
|
|
2069
|
+
* instances via `getItems`/`getItemByIndex`/`getItemByLabel`.
|
|
2070
|
+
* @see https://mui.com/material-ui/react-tabs/
|
|
2071
|
+
*/
|
|
2072
|
+
var TabsDriver = class extends ListComponentDriver {
|
|
2073
|
+
constructor(locator, interactor, option = {}) {
|
|
2074
|
+
super(locator, interactor, {
|
|
2075
|
+
...defaultTabsDriverOption,
|
|
2076
|
+
...option
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* The visible label of every tab, in DOM order.
|
|
2081
|
+
*/
|
|
2082
|
+
async getTabLabels() {
|
|
2083
|
+
const labels = [];
|
|
2084
|
+
for await (const tab of listHelper.getListItemIterator(this, this.getItemLocator(), TabDriver)) {
|
|
2085
|
+
const text = await tab.getText();
|
|
2086
|
+
labels.push(text?.trim() ?? "");
|
|
2087
|
+
}
|
|
2088
|
+
return labels;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* The number of tabs in the group.
|
|
2092
|
+
*/
|
|
2093
|
+
async getTabCount() {
|
|
2094
|
+
return this.getItemCount();
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Zero-based index of the selected tab, or `-1` when no tab is selected
|
|
2098
|
+
* (e.g. `<Tabs value={false}>`).
|
|
2099
|
+
*/
|
|
2100
|
+
async getSelectedIndex() {
|
|
2101
|
+
let index = 0;
|
|
2102
|
+
for await (const tab of listHelper.getListItemIterator(this, this.getItemLocator(), TabDriver)) {
|
|
2103
|
+
if (await tab.isSelected()) return index;
|
|
2104
|
+
index++;
|
|
2105
|
+
}
|
|
2106
|
+
return -1;
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Label of the selected tab, or `null` when no tab is selected. Returns `null`
|
|
2110
|
+
* (not `undefined`) to match the sibling `SelectDriver.getSelectedLabel` contract.
|
|
2111
|
+
*/
|
|
2112
|
+
async getSelectedLabel() {
|
|
2113
|
+
for await (const tab of listHelper.getListItemIterator(this, this.getItemLocator(), TabDriver)) if (await tab.isSelected()) return (await tab.getText())?.trim() ?? null;
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Select the tab at the given zero-based index.
|
|
2118
|
+
* @returns `false` when the index is out of range.
|
|
2119
|
+
*/
|
|
2120
|
+
async selectByIndex(index) {
|
|
2121
|
+
const tab = await this.getItemByIndex(index);
|
|
2122
|
+
if (tab == null) return false;
|
|
2123
|
+
await tab.click();
|
|
2124
|
+
return true;
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Select the first tab whose visible label equals `label`.
|
|
2128
|
+
* @returns `false` when no tab matches.
|
|
2129
|
+
*/
|
|
2130
|
+
async selectByLabel(label) {
|
|
2131
|
+
const tab = await this.getItemByLabel(label);
|
|
2132
|
+
if (tab == null) return false;
|
|
2133
|
+
await tab.click();
|
|
2134
|
+
return true;
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Whether the tab at the given index is disabled.
|
|
2138
|
+
* @returns `false` when the index is out of range.
|
|
2139
|
+
*/
|
|
2140
|
+
async isTabDisabled(index) {
|
|
2141
|
+
const tab = await this.getItemByIndex(index);
|
|
2142
|
+
return tab == null ? false : tab.isDisabled();
|
|
2143
|
+
}
|
|
2144
|
+
get driverName() {
|
|
2145
|
+
return "MuiV9TabsDriver";
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
//#endregion
|
|
2149
|
+
//#region src/components/TextFieldDriver.ts
|
|
2150
|
+
const parts = {
|
|
2151
|
+
label: {
|
|
2152
|
+
locator: byCssSelector(">.MuiInputLabel-root"),
|
|
2153
|
+
driver: HTMLElementDriver
|
|
2154
|
+
},
|
|
2155
|
+
helperText: {
|
|
2156
|
+
locator: byCssSelector(">p"),
|
|
2157
|
+
driver: HTMLElementDriver
|
|
2158
|
+
},
|
|
2159
|
+
singlelineInput: {
|
|
2160
|
+
locator: byCssSelector("input:not([aria-hidden])"),
|
|
2161
|
+
driver: HTMLTextInputDriver
|
|
2162
|
+
},
|
|
2163
|
+
multilineInput: {
|
|
2164
|
+
locator: byCssSelector("textarea:not([aria-hidden])"),
|
|
2165
|
+
driver: HTMLTextInputDriver
|
|
2166
|
+
},
|
|
2167
|
+
selectInput: {
|
|
2168
|
+
locator: byCssSelector(">.MuiInputBase-root"),
|
|
2169
|
+
driver: SelectDriver
|
|
2170
|
+
},
|
|
2171
|
+
richSelectInputDetect: {
|
|
2172
|
+
locator: byRole("combobox"),
|
|
2173
|
+
driver: HTMLElementDriver
|
|
2174
|
+
},
|
|
2175
|
+
nativeSelectInputDetect: {
|
|
2176
|
+
locator: byTagName("SELECT"),
|
|
2177
|
+
driver: HTMLElementDriver
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
/**
|
|
2181
|
+
* A driver for the Material UI v9 TextField component with single line or multiline text input.
|
|
2182
|
+
*/
|
|
2183
|
+
var TextFieldDriver = class extends ComponentDriver {
|
|
2184
|
+
constructor(locator, interactor, option) {
|
|
2185
|
+
super(locator, interactor, {
|
|
2186
|
+
...option,
|
|
2187
|
+
parts
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
async getInputType() {
|
|
2191
|
+
return await Promise.all([
|
|
2192
|
+
this.parts.singlelineInput.exists(),
|
|
2193
|
+
this.parts.richSelectInputDetect.exists(),
|
|
2194
|
+
this.parts.nativeSelectInputDetect.exists(),
|
|
2195
|
+
this.parts.multilineInput.exists()
|
|
2196
|
+
]).then(([singlelineExists, richSelectExists, nativeSelectExists, multilineExists]) => {
|
|
2197
|
+
if (singlelineExists) return "singleLine";
|
|
2198
|
+
if (richSelectExists || nativeSelectExists) return "select";
|
|
2199
|
+
if (multilineExists) return "multiline";
|
|
2200
|
+
throw new Error("Unable to determine input type in TextFieldInput");
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
async getValue() {
|
|
2204
|
+
switch (await this.getInputType()) {
|
|
2205
|
+
case "singleLine": return this.parts.singlelineInput.getValue();
|
|
2206
|
+
case "select": return this.parts.selectInput.getValue();
|
|
2207
|
+
case "multiline": return this.parts.multilineInput.getValue();
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
async setValue(value) {
|
|
2211
|
+
switch (await this.getInputType()) {
|
|
2212
|
+
case "singleLine": return this.parts.singlelineInput.setValue(value);
|
|
2213
|
+
case "select": return this.parts.selectInput.setValue(value);
|
|
2214
|
+
case "multiline": return this.parts.multilineInput.setValue(value);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
async getLabel() {
|
|
2218
|
+
return this.parts.label.getText();
|
|
2219
|
+
}
|
|
2220
|
+
async getHelperText() {
|
|
2221
|
+
if (await this.interactor.exists(this.parts.helperText.locator)) return this.parts.helperText.getText();
|
|
2222
|
+
}
|
|
2223
|
+
async isDisabled() {
|
|
2224
|
+
switch (await this.getInputType()) {
|
|
2225
|
+
case "singleLine": return this.parts.singlelineInput.isDisabled();
|
|
2226
|
+
case "select": return this.parts.selectInput.isDisabled();
|
|
2227
|
+
case "multiline": return this.parts.multilineInput.isDisabled();
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
async isReadonly() {
|
|
2231
|
+
switch (await this.getInputType()) {
|
|
2232
|
+
case "singleLine": return this.parts.singlelineInput.isReadonly();
|
|
2233
|
+
case "select": return this.interactor.hasCssClass(this.parts.selectInput.locator, "MuiInputBase-readOnly");
|
|
2234
|
+
case "multiline": return this.parts.multilineInput.isReadonly();
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
get driverName() {
|
|
2238
|
+
return "MuiV9TextFieldDriver";
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
//#endregion
|
|
2242
|
+
//#region src/components/ToggleButtonDriver.ts
|
|
2243
|
+
var ToggleButtonDriver = class extends ButtonDriver {
|
|
2244
|
+
async isSelected() {
|
|
2245
|
+
return await this.interactor.getAttribute(this.locator, "aria-pressed") === "true";
|
|
2246
|
+
}
|
|
2247
|
+
async setSelected(targetState) {
|
|
2248
|
+
if (await this.isSelected() !== targetState) await this.interactor.click(this.locator);
|
|
2249
|
+
}
|
|
2250
|
+
get driverName() {
|
|
2251
|
+
return "MuiV9ToggleButtonDriver";
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
//#endregion
|
|
2255
|
+
//#region src/components/ToggleButtonGroupDriver.ts
|
|
2256
|
+
const toggleButtonLocator = byTagName("button");
|
|
2257
|
+
var ToggleButtonGroupDriver = class extends ComponentDriver {
|
|
2258
|
+
constructor(..._args) {
|
|
2259
|
+
super(..._args);
|
|
2260
|
+
this.itemLocator = locatorUtil.append(this.locator, toggleButtonLocator);
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Get all the selected toggle buttons' values.
|
|
2264
|
+
* @returns
|
|
2265
|
+
*/
|
|
2266
|
+
async getValue() {
|
|
2267
|
+
const result = [];
|
|
2268
|
+
for await (const itemDriver of listHelper.getListItemIterator(this, this.itemLocator, ToggleButtonDriver)) if (await itemDriver.isSelected()) {
|
|
2269
|
+
const value = await itemDriver.getValue();
|
|
2270
|
+
if (value != null) result.push(value);
|
|
2271
|
+
}
|
|
2272
|
+
return result;
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Toggle all the toggle buttons such that only those with value in the given array are selected.
|
|
2276
|
+
* @param value Always true
|
|
2277
|
+
* @returns
|
|
2278
|
+
*/
|
|
2279
|
+
async setValue(value) {
|
|
2280
|
+
const valueSet = new Set(value);
|
|
2281
|
+
for await (const itemDriver of listHelper.getListItemIterator(this, this.itemLocator, ToggleButtonDriver)) {
|
|
2282
|
+
const value = await itemDriver.getValue();
|
|
2283
|
+
await itemDriver.setSelected(valueSet.has(value));
|
|
2284
|
+
}
|
|
2285
|
+
return true;
|
|
2286
|
+
}
|
|
2287
|
+
get driverName() {
|
|
2288
|
+
return "MuiV9ToggleButtonGroupDriver";
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
/**
|
|
2292
|
+
* A toggle button group driver that only allows a single selection.
|
|
2293
|
+
*
|
|
2294
|
+
* INTENTIONAL @ts-ignore comments below: This class intentionally narrows the return type
|
|
2295
|
+
* from `readonly string[]` to `string | null` for exclusive selection mode. TypeScript
|
|
2296
|
+
* correctly flags this as a type incompatibility because the subclass changes the interface
|
|
2297
|
+
* contract. However, this design is intentional as exclusive and multi-select toggle groups
|
|
2298
|
+
* have fundamentally different value semantics, and we want the type system to reflect
|
|
2299
|
+
* `string | null` for exclusive mode rather than forcing consumers to work with arrays.
|
|
2300
|
+
*/
|
|
2301
|
+
var ExclusiveToggleButtonGroupDriver = class extends ToggleButtonGroupDriver {
|
|
2302
|
+
async getValue() {
|
|
2303
|
+
return (await super.getValue())?.[0] ?? null;
|
|
2304
|
+
}
|
|
2305
|
+
async setValue(value) {
|
|
2306
|
+
if (value === null) return super.setValue([]);
|
|
2307
|
+
else return super.setValue([value]);
|
|
2308
|
+
}
|
|
2309
|
+
get driverName() {
|
|
2310
|
+
return "MuiV9ExclusiveToggleButtonGroupDriver";
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
//#endregion
|
|
2314
|
+
//#region src/components/TooltipDriver.ts
|
|
2315
|
+
const tooltipLocator = byRole("tooltip", "Root");
|
|
2316
|
+
const defaultRevealTimeout = 250;
|
|
2317
|
+
/**
|
|
2318
|
+
* Driver for the Material UI v9 Tooltip component.
|
|
2319
|
+
*
|
|
2320
|
+
* The driver is rooted at the **trigger** element. MUI shows the tooltip on
|
|
2321
|
+
* hover/focus and renders it into a portal (a `role="tooltip"` popper) outside the
|
|
2322
|
+
* trigger's subtree, so the text is read from the document root via
|
|
2323
|
+
* {@link tooltipLocator} after revealing it. The tooltip unmounts when not shown,
|
|
2324
|
+
* so presence of that element doubles as the visible state.
|
|
2325
|
+
*
|
|
2326
|
+
* Note: hover reveal depends on the tooltip's `enterDelay`; with a non-zero delay
|
|
2327
|
+
* the reveal is timer-bound. {@link getTitle} waits for the tooltip to appear.
|
|
2328
|
+
* @see https://mui.com/material-ui/react-tooltip/
|
|
2329
|
+
*/
|
|
2330
|
+
var TooltipDriver = class extends ComponentDriver {
|
|
2331
|
+
/**
|
|
2332
|
+
* Reveal the tooltip by hovering its trigger, waiting until it is shown (or the
|
|
2333
|
+
* timeout elapses, e.g. when the trigger has no tooltip).
|
|
2334
|
+
*/
|
|
2335
|
+
async show(timeoutMs = defaultRevealTimeout) {
|
|
2336
|
+
await this.interactor.hover(this.locator);
|
|
2337
|
+
await this.interactor.waitUntil({
|
|
2338
|
+
probeFn: () => this.isVisible(),
|
|
2339
|
+
terminateCondition: true,
|
|
2340
|
+
timeoutMs
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Hide the tooltip by moving the pointer off its trigger, waiting until it is gone.
|
|
2345
|
+
*/
|
|
2346
|
+
async hide(timeoutMs = defaultRevealTimeout) {
|
|
2347
|
+
await this.interactor.mouseLeave(this.locator);
|
|
2348
|
+
await this.interactor.waitUntil({
|
|
2349
|
+
probeFn: () => this.isVisible(),
|
|
2350
|
+
terminateCondition: false,
|
|
2351
|
+
timeoutMs
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Whether a tooltip is currently shown.
|
|
2356
|
+
*/
|
|
2357
|
+
async isVisible() {
|
|
2358
|
+
if (!await this.interactor.exists(tooltipLocator)) return false;
|
|
2359
|
+
return this.interactor.isVisible(tooltipLocator);
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Reveal the tooltip, read its text, then restore the un-hovered state. Returns
|
|
2363
|
+
* `undefined` when the trigger has no tooltip (none appears within the timeout).
|
|
2364
|
+
*
|
|
2365
|
+
* The hover is undone before returning so a subsequent read on a *different*
|
|
2366
|
+
* trigger isn't shadowed by this still-open tooltip: MUI renders all tooltips into
|
|
2367
|
+
* one portal and provides no per-trigger DOM link, and jsdom's synthetic hover does
|
|
2368
|
+
* not fire `mouseout` on the previously-hovered sibling.
|
|
2369
|
+
*
|
|
2370
|
+
* @param timeoutMs How long to wait for the tooltip to appear after hovering.
|
|
2371
|
+
*/
|
|
2372
|
+
async getTitle(timeoutMs = defaultRevealTimeout) {
|
|
2373
|
+
await this.show(timeoutMs);
|
|
2374
|
+
if (!await this.interactor.exists(tooltipLocator)) return;
|
|
2375
|
+
const text = (await this.interactor.getText(tooltipLocator))?.trim();
|
|
2376
|
+
await this.hide(timeoutMs);
|
|
2377
|
+
return text;
|
|
2378
|
+
}
|
|
2379
|
+
get driverName() {
|
|
2380
|
+
return "MuiV9TooltipDriver";
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
//#endregion
|
|
2384
|
+
export { AccordionDriver, AlertDriver, AutoCompleteDriver, AvatarDriver, AvatarGroupDriver, BadgeDriver, BottomNavigationActionDriver, BottomNavigationDriver, ButtonDriver, CheckboxDriver, ChipDriver, DialogDriver, DrawerDriver, ExclusiveToggleButtonGroupDriver, FabDriver, InputDriver, ListDriver, ListItemDriver, MenuDriver, MenuItemDisabledError, MenuItemDisabledErrorId, MenuItemDriver, MenuItemNotFoundError, MenuItemNotFoundErrorId, OverlayDriver, PaginationDriver, ProgressDriver, RadioDriver, RadioGroupDriver, RatingDriver, SelectDriver, SliderDriver, SnackbarDriver, SpeedDialDriver, StepperDriver, SwitchDriver, TabDriver, TableCellDriver, TableDriver, TablePaginationDriver, TableRowDriver, TabsDriver, TextFieldDriver, ToggleButtonDriver, ToggleButtonGroupDriver, TooltipDriver, defaultAutoCompleteDriverOption, defaultAvatarGroupDriverOption, defaultRadioGroupDriverOption, defaultTableDriverOption, defaultTableRowDriverOption, defaultTabsDriverOption, drawerParts };
|
|
2385
|
+
|
|
2386
|
+
//# sourceMappingURL=index.mjs.map
|