@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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HTMLButtonDriver,
|
|
3
|
+
HTMLElementDriver,
|
|
4
|
+
HTMLSelectDriver,
|
|
5
|
+
HTMLTextInputDriver,
|
|
6
|
+
} from '@atomic-testing/component-driver-html';
|
|
7
|
+
import {
|
|
8
|
+
byAttribute,
|
|
9
|
+
byCssSelector,
|
|
10
|
+
byRole,
|
|
11
|
+
byTagName,
|
|
12
|
+
ComponentDriver,
|
|
13
|
+
IComponentDriverOption,
|
|
14
|
+
IInputDriver,
|
|
15
|
+
Interactor,
|
|
16
|
+
listHelper,
|
|
17
|
+
locatorUtil,
|
|
18
|
+
Nullable,
|
|
19
|
+
PartLocator,
|
|
20
|
+
ScenePart,
|
|
21
|
+
ScenePartDriver,
|
|
22
|
+
} from '@atomic-testing/core';
|
|
23
|
+
|
|
24
|
+
import { MenuItemNotFoundError } from '../errors/MenuItemNotFoundError';
|
|
25
|
+
import { MenuItemDriver } from './MenuItemDriver';
|
|
26
|
+
|
|
27
|
+
export const selectPart = {
|
|
28
|
+
trigger: {
|
|
29
|
+
locator: byRole('combobox'), // Starting in 5.12 and beyond, the role has changed from 'button' to 'combobox'
|
|
30
|
+
driver: HTMLButtonDriver,
|
|
31
|
+
},
|
|
32
|
+
dropdown: {
|
|
33
|
+
locator: byCssSelector('[role=presentation] [role=listbox]', 'Root'),
|
|
34
|
+
driver: HTMLElementDriver,
|
|
35
|
+
},
|
|
36
|
+
input: {
|
|
37
|
+
locator: byTagName('input'),
|
|
38
|
+
driver: HTMLTextInputDriver,
|
|
39
|
+
},
|
|
40
|
+
nativeSelect: {
|
|
41
|
+
locator: byTagName('select'),
|
|
42
|
+
driver: HTMLSelectDriver,
|
|
43
|
+
},
|
|
44
|
+
} satisfies ScenePart;
|
|
45
|
+
|
|
46
|
+
export type SelectScenePart = typeof selectPart;
|
|
47
|
+
export type SelectScenePartDriver = ScenePartDriver<SelectScenePart>;
|
|
48
|
+
export interface MenuItemGetOption {
|
|
49
|
+
/**
|
|
50
|
+
* When true, the driver will not check if the dropdown is open, which helps speed the process up.
|
|
51
|
+
*/
|
|
52
|
+
skipDropdownCheck?: boolean;
|
|
53
|
+
}
|
|
54
|
+
const optionLocator = byRole('option');
|
|
55
|
+
|
|
56
|
+
export class SelectDriver extends ComponentDriver<SelectScenePart> implements IInputDriver<string | null> {
|
|
57
|
+
constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
|
|
58
|
+
super(locator, interactor, {
|
|
59
|
+
...option,
|
|
60
|
+
parts: selectPart,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async isNative(): Promise<boolean> {
|
|
64
|
+
const nativeSelectExists = await this.interactor.exists(this.parts.nativeSelect.locator);
|
|
65
|
+
return Promise.resolve(nativeSelectExists);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getValue(): Promise<string | null> {
|
|
69
|
+
const isNative = await this.isNative();
|
|
70
|
+
if (isNative) {
|
|
71
|
+
const val = (await this.parts.nativeSelect.getValue()) as Nullable<string>;
|
|
72
|
+
return val;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await this.enforcePartExistence('input');
|
|
76
|
+
const value = await this.parts.input.getValue();
|
|
77
|
+
return value ?? null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async setValue(value: string | null): Promise<boolean> {
|
|
81
|
+
let success = false;
|
|
82
|
+
const isNative = await this.isNative();
|
|
83
|
+
if (isNative) {
|
|
84
|
+
success = await this.parts.nativeSelect.setValue(value);
|
|
85
|
+
return success;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await this.openDropdown();
|
|
89
|
+
await this.enforcePartExistence('dropdown');
|
|
90
|
+
const optionSelector = byAttribute('data-value', value!);
|
|
91
|
+
const optionLocator = locatorUtil.append(this.parts.dropdown.locator, optionSelector);
|
|
92
|
+
const optionExists = await this.interactor.exists(optionLocator);
|
|
93
|
+
|
|
94
|
+
if (optionExists) {
|
|
95
|
+
await this.interactor.click(optionLocator);
|
|
96
|
+
success = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return success;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Select menu item by its label, if it exists
|
|
104
|
+
* Limitation, this method will not work if the dropdown is a native select.
|
|
105
|
+
* @param label
|
|
106
|
+
* @returns
|
|
107
|
+
*/
|
|
108
|
+
async getMenuItemByLabel(label: string, option?: MenuItemGetOption): Promise<MenuItemDriver | null> {
|
|
109
|
+
if (!option?.skipDropdownCheck) {
|
|
110
|
+
await this.openDropdown();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// TODO: Add native select support
|
|
114
|
+
|
|
115
|
+
for await (const item of listHelper.getListItemIterator(this, optionLocator, MenuItemDriver)) {
|
|
116
|
+
const itemLabel = await item.label();
|
|
117
|
+
if (itemLabel === label) {
|
|
118
|
+
return item;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Selects an option by its label
|
|
126
|
+
* @param label
|
|
127
|
+
* @returns
|
|
128
|
+
*/
|
|
129
|
+
async selectByLabel(label: string): Promise<void> {
|
|
130
|
+
const isNative = await this.isNative();
|
|
131
|
+
if (isNative) {
|
|
132
|
+
await this.parts.nativeSelect.selectByLabel(label);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await this.enforcePartExistence('trigger');
|
|
137
|
+
await this.parts.trigger.click();
|
|
138
|
+
|
|
139
|
+
await this.enforcePartExistence('dropdown');
|
|
140
|
+
const item = await this.getMenuItemByLabel(label, { skipDropdownCheck: true });
|
|
141
|
+
|
|
142
|
+
if (item) {
|
|
143
|
+
await item.click();
|
|
144
|
+
} else {
|
|
145
|
+
throw new MenuItemNotFoundError(label, this);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getSelectedLabel(): Promise<string | null> {
|
|
150
|
+
const isNative = await this.isNative();
|
|
151
|
+
if (isNative) {
|
|
152
|
+
return await this.parts.nativeSelect.getSelectedLabel();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await this.enforcePartExistence('trigger');
|
|
156
|
+
const label = await this.parts.trigger.getText();
|
|
157
|
+
return label ?? null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
override async exists(): Promise<boolean> {
|
|
161
|
+
const triggerExists = await this.interactor.exists(this.parts.trigger.locator);
|
|
162
|
+
if (triggerExists) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const nativeExists = await this.interactor.exists(this.parts.nativeSelect.locator);
|
|
167
|
+
return nativeExists;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 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
|
|
172
|
+
* @returns For native dropdown it is always true. For custom dropdown, it is true if the dropdown is open.
|
|
173
|
+
*/
|
|
174
|
+
async isDropdownOpen(): Promise<boolean> {
|
|
175
|
+
const isNative = await this.isNative();
|
|
176
|
+
if (isNative) {
|
|
177
|
+
return true;
|
|
178
|
+
} else {
|
|
179
|
+
return this.parts.dropdown.exists();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async openDropdown(): Promise<void> {
|
|
184
|
+
const isOpen = await this.isDropdownOpen();
|
|
185
|
+
if (isOpen) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await this.parts.trigger.click();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async closeDropdown(): Promise<void> {
|
|
192
|
+
const isOpen = await this.isDropdownOpen();
|
|
193
|
+
if (!isOpen) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
await this.parts.trigger.click();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async isDisabled(): Promise<boolean> {
|
|
200
|
+
const isNative = await this.isNative();
|
|
201
|
+
if (isNative) {
|
|
202
|
+
return this.parts.nativeSelect.isDisabled();
|
|
203
|
+
} else {
|
|
204
|
+
await this.enforcePartExistence('trigger');
|
|
205
|
+
const isDisabled = await this.interactor.getAttribute(this.parts.trigger.locator, 'aria-disabled');
|
|
206
|
+
return isDisabled === 'true';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async isReadonly(): Promise<boolean> {
|
|
211
|
+
const isNative = await this.isNative();
|
|
212
|
+
if (isNative) {
|
|
213
|
+
return this.parts.nativeSelect.isReadonly();
|
|
214
|
+
} else {
|
|
215
|
+
// Cannot determine readonly state of a select input.
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get driverName(): string {
|
|
221
|
+
return 'MuiV9SelectDriver';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { HTMLTextInputDriver } from '@atomic-testing/component-driver-html';
|
|
2
|
+
import {
|
|
3
|
+
byCssSelector,
|
|
4
|
+
ComponentDriver,
|
|
5
|
+
IComponentDriverOption,
|
|
6
|
+
IInputDriver,
|
|
7
|
+
Interactor,
|
|
8
|
+
locatorUtil,
|
|
9
|
+
PartLocator,
|
|
10
|
+
ScenePart,
|
|
11
|
+
ScenePartDriver,
|
|
12
|
+
} from '@atomic-testing/core';
|
|
13
|
+
|
|
14
|
+
export const parts = {
|
|
15
|
+
input: {
|
|
16
|
+
locator: byCssSelector('input[type="range"][data-index="0"]'),
|
|
17
|
+
driver: HTMLTextInputDriver,
|
|
18
|
+
},
|
|
19
|
+
} satisfies ScenePart;
|
|
20
|
+
|
|
21
|
+
export type SelectScenePart = typeof parts;
|
|
22
|
+
export type SelectScenePartDriver = ScenePartDriver<SelectScenePart>;
|
|
23
|
+
|
|
24
|
+
export class SliderDriver extends ComponentDriver<SelectScenePart> implements IInputDriver<number> {
|
|
25
|
+
constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
|
|
26
|
+
super(locator, interactor, {
|
|
27
|
+
...option,
|
|
28
|
+
parts: parts,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Return the first occurrence of the Slider input
|
|
34
|
+
* @returns
|
|
35
|
+
*/
|
|
36
|
+
async getValue(): Promise<number> {
|
|
37
|
+
const values = await this.getRangeValues(1);
|
|
38
|
+
return values[0]!;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Set slider's range value. Do not use as it will throw an error
|
|
43
|
+
* @param values
|
|
44
|
+
* @see https://github.com/atomic-testing/atomic-testing/issues/73
|
|
45
|
+
*/
|
|
46
|
+
async setValue(value: number): Promise<boolean> {
|
|
47
|
+
const success = await this.setRangeValues([value]);
|
|
48
|
+
return success;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getRangeValues(count?: number): Promise<readonly number[]> {
|
|
52
|
+
await this.enforcePartExistence('input');
|
|
53
|
+
const result: number[] = [];
|
|
54
|
+
|
|
55
|
+
let index = 0;
|
|
56
|
+
let done = false;
|
|
57
|
+
while (!done) {
|
|
58
|
+
const locator = locatorUtil.append(this.locator, this.getInputLocator(index));
|
|
59
|
+
const exists = await this.interactor.exists(locator);
|
|
60
|
+
if (exists) {
|
|
61
|
+
index++;
|
|
62
|
+
done = count != null && index >= count;
|
|
63
|
+
const value = await this.interactor.getAttribute(locator, 'value');
|
|
64
|
+
result.push(parseFloat(value!));
|
|
65
|
+
} else {
|
|
66
|
+
done = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private getInputLocator(index: number): PartLocator {
|
|
73
|
+
return byCssSelector(`input[type="range"][data-index="${index}"]`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set slider's range values. Do not use as it will throw an error
|
|
78
|
+
* @param values
|
|
79
|
+
* @see https://github.com/atomic-testing/atomic-testing/issues/73
|
|
80
|
+
*/
|
|
81
|
+
async setRangeValues(_values: readonly number[]): Promise<boolean> {
|
|
82
|
+
await this.enforcePartExistence('input');
|
|
83
|
+
throw new Error('setRangeValue is not supported.');
|
|
84
|
+
// for (let index = 0; index < values.length; index++) {
|
|
85
|
+
// const locator = locatorUtil.append(this.locator, this.getInputLocator(index));
|
|
86
|
+
// const exists = await this.interactor.exists(locator);
|
|
87
|
+
// if (exists) {
|
|
88
|
+
// // @ts-ignore
|
|
89
|
+
// await this.interactor.changeValue(locator, values[index].toString());
|
|
90
|
+
// // const driver = new HTMLTextInputDriver(locator, this.interactor);
|
|
91
|
+
// // await driver.setValue(values[index].toString());
|
|
92
|
+
// } else {
|
|
93
|
+
// return false;
|
|
94
|
+
// }
|
|
95
|
+
// }
|
|
96
|
+
|
|
97
|
+
// return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async isDisabled(): Promise<boolean> {
|
|
101
|
+
await this.enforcePartExistence('input');
|
|
102
|
+
const disabled = await this.parts.input.isDisabled();
|
|
103
|
+
return disabled;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get driverName(): string {
|
|
107
|
+
return 'MuiV9SliderDriver';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { HTMLElementDriver } from '@atomic-testing/component-driver-html';
|
|
2
|
+
import {
|
|
3
|
+
byCssClass,
|
|
4
|
+
ComponentDriver,
|
|
5
|
+
IComponentDriverOption,
|
|
6
|
+
Interactor,
|
|
7
|
+
locatorUtil,
|
|
8
|
+
PartLocator,
|
|
9
|
+
ScenePart,
|
|
10
|
+
ComponentDriverCtor,
|
|
11
|
+
} from '@atomic-testing/core';
|
|
12
|
+
|
|
13
|
+
export const parts = {
|
|
14
|
+
contentDisplay: {
|
|
15
|
+
locator: byCssClass('MuiSnackbarContent-message'),
|
|
16
|
+
driver: HTMLElementDriver,
|
|
17
|
+
},
|
|
18
|
+
actionArea: {
|
|
19
|
+
locator: byCssClass('MuiSnackbarContent-action'),
|
|
20
|
+
driver: HTMLElementDriver,
|
|
21
|
+
},
|
|
22
|
+
} satisfies ScenePart;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Driver for Material UI v9 Snackbar component.
|
|
26
|
+
* @see https://mui.com/material-ui/react-snackbar/
|
|
27
|
+
*/
|
|
28
|
+
export class SnackbarDriver extends ComponentDriver<typeof parts> {
|
|
29
|
+
constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
|
|
30
|
+
super(locator, interactor, {
|
|
31
|
+
...option,
|
|
32
|
+
parts: parts,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the label content of the snackbar.
|
|
38
|
+
* @returns The label text content of the snackbar.
|
|
39
|
+
*/
|
|
40
|
+
async getLabel(): Promise<string | null> {
|
|
41
|
+
await this.enforcePartExistence('contentDisplay');
|
|
42
|
+
const content = await this.parts.contentDisplay.getText();
|
|
43
|
+
return content ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get a driver instance of a component in the action area of the snackbar.
|
|
48
|
+
* @param locator
|
|
49
|
+
* @param driverClass
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
async getActionComponent<ItemClass extends ComponentDriver>(
|
|
53
|
+
locator: PartLocator,
|
|
54
|
+
driverClass: ComponentDriverCtor<ItemClass>
|
|
55
|
+
): Promise<ItemClass | null> {
|
|
56
|
+
await this.enforcePartExistence('actionArea');
|
|
57
|
+
const componentLocator = locatorUtil.append(this.parts.actionArea.locator, locator);
|
|
58
|
+
const exists = await this.interactor.exists(componentLocator);
|
|
59
|
+
if (exists) {
|
|
60
|
+
return new driverClass(componentLocator, this.interactor, this.commutableOption);
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override get driverName(): string {
|
|
66
|
+
return 'MuiV9SnackbarDriver';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { byCssSelector, ComponentDriver, escapeUtil, locatorUtil, PartLocator } from '@atomic-testing/core';
|
|
2
|
+
|
|
3
|
+
// The trigger FAB carries the open state via aria-expanded; the action buttons
|
|
4
|
+
// carry their name via aria-label. Both are distinct MUI classes within the root.
|
|
5
|
+
const fabLocator = byCssSelector('.MuiSpeedDial-fab');
|
|
6
|
+
const actionLocator = byCssSelector('.MuiSpeedDialAction-fab');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Driver for the Material UI v9 SpeedDial component.
|
|
10
|
+
*
|
|
11
|
+
* SpeedDial renders a trigger FAB (`.MuiSpeedDial-fab`, `aria-expanded` reflects
|
|
12
|
+
* open state) and a set of action FABs (`.MuiSpeedDialAction-fab`, each
|
|
13
|
+
* aria-labelled with its name). The actions stay mounted but are only revealed
|
|
14
|
+
* when open, so open state is read from the FAB's `aria-expanded` rather than
|
|
15
|
+
* action presence.
|
|
16
|
+
* @see https://mui.com/material-ui/react-speed-dial/
|
|
17
|
+
*/
|
|
18
|
+
export class SpeedDialDriver extends ComponentDriver {
|
|
19
|
+
private get fab(): PartLocator {
|
|
20
|
+
return locatorUtil.append(this.locator, fabLocator);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether the speed dial is open (its FAB reports `aria-expanded="true"`).
|
|
25
|
+
*/
|
|
26
|
+
async isOpen(): Promise<boolean> {
|
|
27
|
+
const expanded = await this.interactor.getAttribute(this.fab, 'aria-expanded');
|
|
28
|
+
return expanded === 'true';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Open the speed dial by hovering its FAB. No-op when already open.
|
|
33
|
+
*
|
|
34
|
+
* Hover (`onMouseEnter`) is the trigger that opens consistently across MUI
|
|
35
|
+
* versions and browsers — clicking only focuses-then-opens in Chromium, and
|
|
36
|
+
* focus opens in v7 but not v5, whereas hover opens everywhere.
|
|
37
|
+
*/
|
|
38
|
+
async open(): Promise<void> {
|
|
39
|
+
if (!(await this.isOpen())) {
|
|
40
|
+
await this.interactor.hover(this.fab);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Close the speed dial by moving the pointer off its FAB. No-op when already closed.
|
|
46
|
+
*/
|
|
47
|
+
async close(): Promise<void> {
|
|
48
|
+
if (await this.isOpen()) {
|
|
49
|
+
await this.interactor.mouseLeave(this.fab);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The labels of every action, in order (read from each action FAB's aria-label).
|
|
55
|
+
*/
|
|
56
|
+
async getActionLabels(): Promise<string[]> {
|
|
57
|
+
const labels = await this.interactor.getAttribute(
|
|
58
|
+
locatorUtil.append(this.locator, actionLocator),
|
|
59
|
+
'aria-label',
|
|
60
|
+
true
|
|
61
|
+
);
|
|
62
|
+
return labels.filter((label): label is string => label != null);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Trigger the action with the given label, opening the dial first if needed.
|
|
67
|
+
* @returns `false` when no action has that label.
|
|
68
|
+
*/
|
|
69
|
+
async triggerActionByLabel(label: string): Promise<boolean> {
|
|
70
|
+
await this.open();
|
|
71
|
+
const actionByLabel = byCssSelector(`.MuiSpeedDialAction-fab[aria-label="${escapeUtil.escapeValue(label)}"]`);
|
|
72
|
+
const locator = locatorUtil.append(this.locator, actionByLabel);
|
|
73
|
+
if (!(await this.interactor.exists(locator))) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
await this.interactor.click(locator);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override get driverName(): string {
|
|
81
|
+
return 'MuiV9SpeedDialDriver';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { byCssSelector, ComponentDriver, locatorUtil, Optional, PartLocator } from '@atomic-testing/core';
|
|
2
|
+
|
|
3
|
+
export interface StepInfo {
|
|
4
|
+
/** The step's visible label. */
|
|
5
|
+
label: Optional<string>;
|
|
6
|
+
/** Whether this is the active step (MUI marks the label `Mui-active`). */
|
|
7
|
+
active: boolean;
|
|
8
|
+
/** Whether the step has been completed (`Mui-completed`). */
|
|
9
|
+
completed: boolean;
|
|
10
|
+
/** Whether the step is disabled (`Mui-disabled`). */
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The `.MuiStepLabel-label` span carries both the clean label text and the
|
|
15
|
+
// per-step state class, making it the single source of truth for step state.
|
|
16
|
+
const stepLabelLocator = byCssSelector('.MuiStepLabel-label');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Locator for the label of the step at `index`. In Material UI v9 the connector
|
|
20
|
+
* is rendered *inside* each `.MuiStep-root` (in v7 it was an interleaved sibling),
|
|
21
|
+
* so the step elements are now consecutive siblings and the step at `index` is the
|
|
22
|
+
* `index+1`-th element of its type; nth-of-type addresses it directly.
|
|
23
|
+
*/
|
|
24
|
+
function stepLabelAt(index: number): PartLocator {
|
|
25
|
+
return byCssSelector(`.MuiStep-root:nth-of-type(${index + 1}) .MuiStepLabel-label`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stepButtonAt(index: number): PartLocator {
|
|
29
|
+
return byCssSelector(`.MuiStep-root:nth-of-type(${index + 1}) .MuiStepButton-root`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Driver for the Material UI v9 Stepper component.
|
|
34
|
+
*
|
|
35
|
+
* Each step's state lives on its `.MuiStepLabel-label` (`Mui-active`,
|
|
36
|
+
* `Mui-completed`, `Mui-disabled`); reading every label's class in one pass gives
|
|
37
|
+
* the step states and count in document order. Steps are addressed positionally
|
|
38
|
+
* for label text and clicks via {@link stepLabelAt}/{@link stepButtonAt}.
|
|
39
|
+
*
|
|
40
|
+
* Supported layouts: the default horizontal stepper and the vertical orientation.
|
|
41
|
+
* In v9 every `.MuiStep-root` is a consecutive sibling (the connector lives inside
|
|
42
|
+
* the step), and every step renders a text label, so positional addressing lines up
|
|
43
|
+
* with the document-order class list. The unsupported case is icon-only steps, which
|
|
44
|
+
* render no `.MuiStepLabel-label`; under those the document-order class list has fewer
|
|
45
|
+
* entries than steps and the two desynchronize. Robustly supporting that needs an
|
|
46
|
+
* interactor primitive to address the n-th of non-sibling matches
|
|
47
|
+
* (CSS `:nth-child(of S)` is unsupported in jsdom), which is out of this driver's scope.
|
|
48
|
+
* @see https://mui.com/material-ui/react-stepper/
|
|
49
|
+
*/
|
|
50
|
+
export class StepperDriver extends ComponentDriver {
|
|
51
|
+
private async getStepClassList(): Promise<readonly string[]> {
|
|
52
|
+
return this.interactor.getAttribute(locatorUtil.append(this.locator, stepLabelLocator), 'class', true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The number of steps.
|
|
57
|
+
*/
|
|
58
|
+
async getStepCount(): Promise<number> {
|
|
59
|
+
return (await this.getStepClassList()).length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Zero-based index of the active step, or `-1` when none is active.
|
|
64
|
+
*/
|
|
65
|
+
async getActiveStepIndex(): Promise<number> {
|
|
66
|
+
const classes = await this.getStepClassList();
|
|
67
|
+
return classes.findIndex(c => c.split(/\s+/).includes('Mui-active'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Every step with its label and active/completed/disabled state, in order.
|
|
72
|
+
*/
|
|
73
|
+
async getSteps(): Promise<StepInfo[]> {
|
|
74
|
+
const classes = await this.getStepClassList();
|
|
75
|
+
const steps: StepInfo[] = [];
|
|
76
|
+
for (let index = 0; index < classes.length; index++) {
|
|
77
|
+
const stateClasses = classes[index].split(/\s+/);
|
|
78
|
+
const label = await this.interactor.getText(locatorUtil.append(this.locator, stepLabelAt(index)));
|
|
79
|
+
steps.push({
|
|
80
|
+
label: label?.trim(),
|
|
81
|
+
active: stateClasses.includes('Mui-active'),
|
|
82
|
+
completed: stateClasses.includes('Mui-completed'),
|
|
83
|
+
disabled: stateClasses.includes('Mui-disabled'),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return steps;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Navigate to the step at `index` by clicking its control (requires a clickable
|
|
91
|
+
* `StepButton`, e.g. a non-linear stepper).
|
|
92
|
+
* @returns `false` when the step is out of range, has no button, or is disabled.
|
|
93
|
+
*/
|
|
94
|
+
async goToStep(index: number): Promise<boolean> {
|
|
95
|
+
if (index < 0 || index >= (await this.getStepCount())) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const button = locatorUtil.append(this.locator, stepButtonAt(index));
|
|
99
|
+
if (!(await this.interactor.exists(button)) || (await this.interactor.isDisabled(button))) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
await this.interactor.click(button);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
override get driverName(): string {
|
|
107
|
+
return 'MuiV9StepperDriver';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { HTMLCheckboxDriver } from '@atomic-testing/component-driver-html';
|
|
2
|
+
import {
|
|
3
|
+
byAttribute,
|
|
4
|
+
ComponentDriver,
|
|
5
|
+
IComponentDriverOption,
|
|
6
|
+
IFormFieldDriver,
|
|
7
|
+
Interactor,
|
|
8
|
+
IToggleDriver,
|
|
9
|
+
PartLocator,
|
|
10
|
+
ScenePart,
|
|
11
|
+
} from '@atomic-testing/core';
|
|
12
|
+
|
|
13
|
+
export const parts = {
|
|
14
|
+
input: {
|
|
15
|
+
locator: byAttribute('type', 'checkbox'),
|
|
16
|
+
driver: HTMLCheckboxDriver,
|
|
17
|
+
},
|
|
18
|
+
} satisfies ScenePart;
|
|
19
|
+
|
|
20
|
+
export class SwitchDriver
|
|
21
|
+
extends ComponentDriver<typeof parts>
|
|
22
|
+
implements IFormFieldDriver<string | null>, IToggleDriver
|
|
23
|
+
{
|
|
24
|
+
constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
|
|
25
|
+
super(locator, interactor, {
|
|
26
|
+
...option,
|
|
27
|
+
parts,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override async exists(): Promise<boolean> {
|
|
32
|
+
return this.interactor.exists(this.parts.input.locator);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async isSelected(): Promise<boolean> {
|
|
36
|
+
await this.enforcePartExistence('input');
|
|
37
|
+
return this.parts.input.isSelected();
|
|
38
|
+
}
|
|
39
|
+
async setSelected(selected: boolean): Promise<void> {
|
|
40
|
+
await this.enforcePartExistence('input');
|
|
41
|
+
await this.parts.input.setSelected(selected);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getValue(): Promise<string | null> {
|
|
45
|
+
await this.enforcePartExistence('input');
|
|
46
|
+
return this.parts.input.getValue();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async isDisabled(): Promise<boolean> {
|
|
50
|
+
await this.enforcePartExistence('input');
|
|
51
|
+
return this.parts.input.isDisabled();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async isReadonly(): Promise<boolean> {
|
|
55
|
+
// MUI v5 does not have a readonly state for the switch
|
|
56
|
+
return Promise.resolve(false);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get driverName(): string {
|
|
60
|
+
return 'MuiV9SwitchDriver';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { HTMLButtonDriver } from '@atomic-testing/component-driver-html';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Driver for a single Material UI v9 Tab.
|
|
5
|
+
*
|
|
6
|
+
* A `<Tab>` renders as a `<button role="tab">`; MUI marks the active one with
|
|
7
|
+
* `aria-selected="true"` and renders a real `disabled` button when disabled, so
|
|
8
|
+
* selection and disabled state are read straight off the accessible attributes
|
|
9
|
+
* (`isDisabled` is inherited from {@link HTMLButtonDriver}; `getText`/`click` from
|
|
10
|
+
* the base `ComponentDriver`).
|
|
11
|
+
*
|
|
12
|
+
* Unlike a toggle button this is intentionally not an `IToggleDriver`: a tab can
|
|
13
|
+
* be selected but not toggled off (selecting another tab deselects it), so only
|
|
14
|
+
* `isSelected`/`select` are exposed.
|
|
15
|
+
* @see https://mui.com/material-ui/react-tabs/
|
|
16
|
+
*/
|
|
17
|
+
export class TabDriver extends HTMLButtonDriver {
|
|
18
|
+
/**
|
|
19
|
+
* Whether this tab is the selected one, i.e. MUI set `aria-selected="true"`.
|
|
20
|
+
*/
|
|
21
|
+
async isSelected(): Promise<boolean> {
|
|
22
|
+
const val = await this.interactor.getAttribute(this.locator, 'aria-selected');
|
|
23
|
+
return val === 'true';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Activate this tab by clicking it, unless it is already selected. A selected
|
|
28
|
+
* tab cannot be toggled off, so this is a no-op when already active.
|
|
29
|
+
*/
|
|
30
|
+
async select(): Promise<void> {
|
|
31
|
+
if (!(await this.isSelected())) {
|
|
32
|
+
await this.interactor.click(this.locator);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override get driverName(): string {
|
|
37
|
+
return 'MuiV9TabDriver';
|
|
38
|
+
}
|
|
39
|
+
}
|