@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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/index.cjs +2439 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1391 -0
  6. package/dist/index.d.mts +1391 -0
  7. package/dist/index.mjs +2386 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +48 -0
  10. package/src/components/AccordionDriver.ts +109 -0
  11. package/src/components/AlertDriver.ts +74 -0
  12. package/src/components/AutoCompleteDriver.ts +151 -0
  13. package/src/components/AvatarDriver.ts +51 -0
  14. package/src/components/AvatarGroupDriver.ts +79 -0
  15. package/src/components/BadgeDriver.ts +43 -0
  16. package/src/components/BottomNavigationActionDriver.ts +23 -0
  17. package/src/components/BottomNavigationDriver.ts +138 -0
  18. package/src/components/ButtonDriver.ts +16 -0
  19. package/src/components/CheckboxDriver.ts +98 -0
  20. package/src/components/ChipDriver.ts +53 -0
  21. package/src/components/DialogDriver.ts +122 -0
  22. package/src/components/DrawerDriver.ts +90 -0
  23. package/src/components/FabDriver.ts +11 -0
  24. package/src/components/InputDriver.ts +112 -0
  25. package/src/components/ListDriver.ts +42 -0
  26. package/src/components/ListItemDriver.ts +34 -0
  27. package/src/components/MenuDriver.ts +65 -0
  28. package/src/components/MenuItemDriver.ts +15 -0
  29. package/src/components/OverlayDriver.ts +98 -0
  30. package/src/components/PaginationDriver.ts +117 -0
  31. package/src/components/ProgressDriver.ts +78 -0
  32. package/src/components/RadioDriver.ts +63 -0
  33. package/src/components/RadioGroupDriver.ts +129 -0
  34. package/src/components/RatingDriver.ts +121 -0
  35. package/src/components/SelectDriver.ts +223 -0
  36. package/src/components/SliderDriver.ts +109 -0
  37. package/src/components/SnackbarDriver.ts +68 -0
  38. package/src/components/SpeedDialDriver.ts +83 -0
  39. package/src/components/StepperDriver.ts +109 -0
  40. package/src/components/SwitchDriver.ts +62 -0
  41. package/src/components/TabDriver.ts +39 -0
  42. package/src/components/TableCellDriver.ts +14 -0
  43. package/src/components/TableDriver.ts +148 -0
  44. package/src/components/TablePaginationDriver.ts +110 -0
  45. package/src/components/TableRowDriver.ts +79 -0
  46. package/src/components/TabsDriver.ts +133 -0
  47. package/src/components/TextFieldDriver.ts +155 -0
  48. package/src/components/ToggleButtonDriver.ts +21 -0
  49. package/src/components/ToggleButtonGroupDriver.ts +75 -0
  50. package/src/components/TooltipDriver.ts +82 -0
  51. package/src/errors/MenuItemDisabledError.ts +17 -0
  52. package/src/errors/MenuItemNotFoundError.ts +17 -0
  53. package/src/index.ts +52 -0
@@ -0,0 +1,151 @@
1
+ import { HTMLButtonDriver, HTMLElementDriver, HTMLTextInputDriver } from '@atomic-testing/component-driver-html';
2
+ import {
3
+ byCssClass,
4
+ byLinkedElement,
5
+ byRole,
6
+ ComponentDriver,
7
+ IComponentDriverOption,
8
+ IInputDriver,
9
+ Interactor,
10
+ listHelper,
11
+ locatorUtil,
12
+ PartLocator,
13
+ ScenePart,
14
+ } from '@atomic-testing/core';
15
+
16
+ export const parts = {
17
+ input: {
18
+ locator: byRole('combobox'),
19
+ driver: HTMLTextInputDriver,
20
+ },
21
+ dropdown: {
22
+ locator: byLinkedElement('Root')
23
+ .onLinkedElement(byRole('combobox'))
24
+ .extractAttribute('aria-controls')
25
+ .toMatchMyAttribute('id'),
26
+ driver: HTMLElementDriver,
27
+ },
28
+ } satisfies ScenePart;
29
+
30
+ const optionLocator = byRole('option');
31
+
32
+ // In the "no options" and "loading" states MUI renders no listbox, so the popup
33
+ // cannot be reached through the `dropdown` part (which keys off the combobox's
34
+ // `aria-controls`). These nodes live in the popper (portaled outside the driver
35
+ // subtree), so they are matched by their global class. Only one Autocomplete
36
+ // popup is open at a time, so a global match is unambiguous in practice.
37
+ const noOptionsLocator = byCssClass('MuiAutocomplete-noOptions');
38
+ const loadingLocator = byCssClass('MuiAutocomplete-loading');
39
+
40
+ /**
41
+ * The match type of the autocomplete, default to 'exact'
42
+ * 'exact': The value must match exactly to one of the options
43
+ * 'first-available': The value will be set to the first available option
44
+ */
45
+ export type AutoCompleteMatchType = 'exact' | 'first-available';
46
+
47
+ export interface AutoCompleteDriverSpecificOption {
48
+ matchType: AutoCompleteMatchType;
49
+ }
50
+
51
+ export interface AutoCompleteDriverOption extends IComponentDriverOption, AutoCompleteDriverSpecificOption {}
52
+
53
+ export const defaultAutoCompleteDriverOption: AutoCompleteDriverSpecificOption = {
54
+ matchType: 'exact',
55
+ };
56
+
57
+ export class AutoCompleteDriver extends ComponentDriver<typeof parts> implements IInputDriver<string | null> {
58
+ private _option: Partial<AutoCompleteDriverOption> = {};
59
+ constructor(locator: PartLocator, interactor: Interactor, option?: Partial<AutoCompleteDriverOption>) {
60
+ super(locator, interactor, {
61
+ ...option,
62
+ parts,
63
+ });
64
+
65
+ this._option = option ?? {};
66
+ }
67
+
68
+ /**
69
+ * Get the display of the autocomplete
70
+ */
71
+ async getValue(): Promise<string | null> {
72
+ const value = await this.parts.input.getValue();
73
+ return value ?? null;
74
+ }
75
+
76
+ /**
77
+ * Set the value of the autocomplete, how selection happens
78
+ * depends on the option assigned to AutoCompleteDriver
79
+ * By default, when the option has matchType set to exact, only option with matching text would be selected
80
+ * When the option has matchType set to first-available, the first option would be selected regardless of the text
81
+ *
82
+ * Option of auto complete can be set at the time of part definition, for example
83
+ * ```
84
+ * {
85
+ * myAutoComplete: {
86
+ * locator: byCssSelector('my-auto-complete'),
87
+ * driver: AutoCompleteDriver,
88
+ * option: {
89
+ * matchType: 'first-available',
90
+ * },
91
+ * },
92
+ * }
93
+ * ```
94
+ *
95
+ * @param value
96
+ * @returns
97
+ */
98
+ async setValue(value: string | null): Promise<boolean> {
99
+ await this.parts.input.setValue(value ?? '');
100
+
101
+ if (value === null) {
102
+ return true;
103
+ }
104
+
105
+ const option = locatorUtil.append(this.parts.dropdown.locator, optionLocator);
106
+ let index = 0;
107
+ const matchType: AutoCompleteMatchType = this._option?.matchType ?? defaultAutoCompleteDriverOption.matchType;
108
+ for await (const optionDriver of listHelper.getListItemIterator(this, option, HTMLButtonDriver)) {
109
+ const optionValue = await optionDriver.getText();
110
+ const isMatched =
111
+ (matchType === 'exact' && optionValue?.trim() === value) || (matchType === 'first-available' && index === 0);
112
+ if (isMatched) {
113
+ await optionDriver.click();
114
+ return true;
115
+ }
116
+
117
+ index++;
118
+ }
119
+
120
+ return false;
121
+ }
122
+
123
+ async isDisabled(): Promise<boolean> {
124
+ return this.parts.input.isDisabled();
125
+ }
126
+
127
+ async isReadonly(): Promise<boolean> {
128
+ return this.parts.input.isReadonly();
129
+ }
130
+
131
+ /**
132
+ * Whether the popup is currently showing its loading indicator (the
133
+ * `loadingText`). Only meaningful while the popup is open — open it first
134
+ * (e.g. by typing into the input), since MUI renders nothing otherwise.
135
+ */
136
+ async isLoading(): Promise<boolean> {
137
+ return this.interactor.exists(loadingLocator);
138
+ }
139
+
140
+ /**
141
+ * Whether the popup is currently showing its "no options" message. Same
142
+ * open-the-popup-first caveat as {@link AutoCompleteDriver.isLoading}.
143
+ */
144
+ async hasNoOptions(): Promise<boolean> {
145
+ return this.interactor.exists(noOptionsLocator);
146
+ }
147
+
148
+ get driverName(): string {
149
+ return 'MuiV9AutoCompleteDriver';
150
+ }
151
+ }
@@ -0,0 +1,51 @@
1
+ import { byCssSelector, ComponentDriver, locatorUtil, Optional, PartLocator } from '@atomic-testing/core';
2
+
3
+ const imageLocator = byCssSelector('img.MuiAvatar-img');
4
+
5
+ /**
6
+ * Driver for the Material UI v9 Avatar component.
7
+ *
8
+ * An Avatar renders a `.MuiAvatar-root`; with a `src` it contains an
9
+ * `img.MuiAvatar-img`, otherwise it shows letter initials (or an icon). The
10
+ * driver reads the image's `alt`, the presence of the image, and the letter
11
+ * initials accordingly.
12
+ * @see https://mui.com/material-ui/react-avatar/
13
+ */
14
+ export class AvatarDriver extends ComponentDriver {
15
+ private get imageLocator(): PartLocator {
16
+ return locatorUtil.append(this.locator, imageLocator);
17
+ }
18
+
19
+ /**
20
+ * Whether the avatar renders an image (vs letter/icon fallback).
21
+ */
22
+ async hasImage(): Promise<boolean> {
23
+ return this.interactor.exists(this.imageLocator);
24
+ }
25
+
26
+ /**
27
+ * The image's alt text, or `undefined` when the avatar has no image.
28
+ */
29
+ async getAltText(): Promise<Optional<string>> {
30
+ if (!(await this.hasImage())) {
31
+ return undefined;
32
+ }
33
+ return (await this.interactor.getAttribute(this.imageLocator, 'alt')) ?? undefined;
34
+ }
35
+
36
+ /**
37
+ * The letter initials of a text avatar, or `undefined` for an image or icon
38
+ * avatar (which render no text).
39
+ */
40
+ async getInitials(): Promise<Optional<string>> {
41
+ if (await this.hasImage()) {
42
+ return undefined;
43
+ }
44
+ const text = (await this.getText())?.trim();
45
+ return text ? text : undefined;
46
+ }
47
+
48
+ override get driverName(): string {
49
+ return 'MuiV9AvatarDriver';
50
+ }
51
+ }
@@ -0,0 +1,79 @@
1
+ import {
2
+ byCssSelector,
3
+ IComponentDriverOption,
4
+ Interactor,
5
+ ListComponentDriver,
6
+ ListComponentDriverSpecificOption,
7
+ listHelper,
8
+ Optional,
9
+ PartLocator,
10
+ } from '@atomic-testing/core';
11
+
12
+ import { AvatarDriver } from './AvatarDriver';
13
+
14
+ // Every avatar inside the group (including the surplus "+N" avatar) carries the
15
+ // MuiAvatarGroup-avatar class and is a direct-child sibling, so they enumerate
16
+ // positionally.
17
+ const avatarItemLocator = byCssSelector('.MuiAvatarGroup-avatar');
18
+
19
+ // The surplus avatar's only distinguishing feature is its "+N" text.
20
+ const surplusPattern = /^\+\d+$/;
21
+
22
+ export const defaultAvatarGroupDriverOption: ListComponentDriverSpecificOption<AvatarDriver> = {
23
+ itemClass: AvatarDriver,
24
+ itemLocator: avatarItemLocator,
25
+ };
26
+
27
+ type AvatarGroupDriverOption<ItemT extends AvatarDriver> = ListComponentDriverSpecificOption<ItemT> &
28
+ Partial<IComponentDriverOption<any>>;
29
+
30
+ /**
31
+ * Driver for the Material UI v9 AvatarGroup component.
32
+ *
33
+ * AvatarGroup renders up to `max` avatars plus a surplus "+N" avatar when there
34
+ * are more. This is a {@link ListComponentDriver} over the rendered avatars
35
+ * (surplus included via `getItems`), with helpers to count the real avatars and
36
+ * read the surplus label.
37
+ * @see https://mui.com/material-ui/react-avatar/#grouped
38
+ */
39
+ export class AvatarGroupDriver<ItemT extends AvatarDriver = AvatarDriver> extends ListComponentDriver<ItemT> {
40
+ constructor(locator: PartLocator, interactor: Interactor, option: Partial<AvatarGroupDriverOption<ItemT>> = {}) {
41
+ // Merge defaults so the driver works as a bare scene part (see TabsDriver).
42
+ super(locator, interactor, {
43
+ ...defaultAvatarGroupDriverOption,
44
+ ...option,
45
+ } as AvatarGroupDriverOption<ItemT>);
46
+ }
47
+
48
+ /**
49
+ * The number of real avatars shown, excluding the surplus "+N" indicator.
50
+ */
51
+ async getVisibleCount(): Promise<number> {
52
+ let count = 0;
53
+ for await (const item of listHelper.getListItemIterator(this, this.getItemLocator(), AvatarDriver)) {
54
+ const text = (await item.getText())?.trim();
55
+ if (text == null || !surplusPattern.test(text)) {
56
+ count++;
57
+ }
58
+ }
59
+ return count;
60
+ }
61
+
62
+ /**
63
+ * The surplus indicator's label (e.g. "+3"), or `undefined` when every avatar
64
+ * fits within `max` and no surplus is shown.
65
+ */
66
+ async getSurplusLabel(): Promise<Optional<string>> {
67
+ for await (const item of listHelper.getListItemIterator(this, this.getItemLocator(), AvatarDriver)) {
68
+ const text = (await item.getText())?.trim();
69
+ if (text != null && surplusPattern.test(text)) {
70
+ return text;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ override get driverName(): string {
77
+ return 'MuiV9AvatarGroupDriver';
78
+ }
79
+ }
@@ -0,0 +1,43 @@
1
+ import { HTMLElementDriver } from '@atomic-testing/component-driver-html';
2
+ import {
3
+ byCssClass,
4
+ ComponentDriver,
5
+ IComponentDriverOption,
6
+ Interactor,
7
+ PartLocator,
8
+ ScenePart,
9
+ } from '@atomic-testing/core';
10
+
11
+ export const parts = {
12
+ contentDisplay: {
13
+ locator: byCssClass('MuiBadge-badge'),
14
+ driver: HTMLElementDriver,
15
+ },
16
+ } satisfies ScenePart;
17
+
18
+ /**
19
+ * Driver for Material UI v9 Badge component.
20
+ * @see https://mui.com/material-ui/react-badge/
21
+ */
22
+ export class BadgeDriver extends ComponentDriver<typeof parts> {
23
+ constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
24
+ super(locator, interactor, {
25
+ ...option,
26
+ parts: parts,
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Get the content of the badge.
32
+ * @returns The content of the badge.
33
+ */
34
+ async getContent(): Promise<string | null> {
35
+ await this.enforcePartExistence('contentDisplay');
36
+ const content = await this.parts.contentDisplay.getText();
37
+ return content ?? null;
38
+ }
39
+
40
+ override get driverName(): string {
41
+ return 'MuiV9BadgeDriver';
42
+ }
43
+ }
@@ -0,0 +1,23 @@
1
+ import { HTMLButtonDriver } from '@atomic-testing/component-driver-html';
2
+
3
+ /**
4
+ * Driver for a single Material UI v9 BottomNavigationAction.
5
+ *
6
+ * Each action renders as a `<button>` (no explicit ARIA role); MUI marks the
7
+ * active one with the `Mui-selected` state class, so selection is read from the
8
+ * class while `getText`/`click`/`isDisabled` come from {@link HTMLButtonDriver}
9
+ * and the base `ComponentDriver`.
10
+ * @see https://mui.com/material-ui/react-bottom-navigation/
11
+ */
12
+ export class BottomNavigationActionDriver extends HTMLButtonDriver {
13
+ /**
14
+ * Whether this action is selected (MUI applies the `Mui-selected` state class).
15
+ */
16
+ async isSelected(): Promise<boolean> {
17
+ return this.interactor.hasCssClass(this.locator, 'Mui-selected');
18
+ }
19
+
20
+ override get driverName(): string {
21
+ return 'MuiV9BottomNavigationActionDriver';
22
+ }
23
+ }
@@ -0,0 +1,138 @@
1
+ import {
2
+ byCssSelector,
3
+ IComponentDriverOption,
4
+ Interactor,
5
+ ListComponentDriver,
6
+ ListComponentDriverSpecificOption,
7
+ listHelper,
8
+ Nullable,
9
+ Optional,
10
+ PartLocator,
11
+ } from '@atomic-testing/core';
12
+
13
+ import { BottomNavigationActionDriver } from './BottomNavigationActionDriver';
14
+
15
+ export interface BottomNavigationActionInfo {
16
+ /** The action's visible label. */
17
+ label: Optional<string>;
18
+ /** Whether the action is currently selected. */
19
+ selected: boolean;
20
+ }
21
+
22
+ /**
23
+ * BottomNavigation actions are direct-child `<button>` siblings of the root (no
24
+ * ARIA role). They are located by their `MuiBottomNavigationAction-root` class
25
+ * rather than a bare `button` tag, so positional enumeration is not thrown off by
26
+ * any incidental nested button inside an action.
27
+ */
28
+ export const defaultBottomNavigationDriverOption: ListComponentDriverSpecificOption<BottomNavigationActionDriver> = {
29
+ itemClass: BottomNavigationActionDriver,
30
+ itemLocator: byCssSelector('.MuiBottomNavigationAction-root'),
31
+ };
32
+
33
+ type BottomNavigationDriverOption<ItemT extends BottomNavigationActionDriver> =
34
+ ListComponentDriverSpecificOption<ItemT> & Partial<IComponentDriverOption<any>>;
35
+
36
+ /**
37
+ * Driver for the Material UI v9 BottomNavigation component.
38
+ *
39
+ * A {@link ListComponentDriver} over the action buttons, exposing the selected
40
+ * index/label, selection by index/label, and per-action info, plus per-action
41
+ * {@link BottomNavigationActionDriver} instances via `getItems`/`getItemByIndex`/
42
+ * `getItemByLabel`.
43
+ * @see https://mui.com/material-ui/react-bottom-navigation/
44
+ */
45
+ export class BottomNavigationDriver<
46
+ ItemT extends BottomNavigationActionDriver = BottomNavigationActionDriver,
47
+ > extends ListComponentDriver<ItemT> {
48
+ constructor(locator: PartLocator, interactor: Interactor, option: Partial<BottomNavigationDriverOption<ItemT>> = {}) {
49
+ // Merge defaults so the driver works as a bare scene part (see TabsDriver).
50
+ super(locator, interactor, {
51
+ ...defaultBottomNavigationDriverOption,
52
+ ...option,
53
+ } as BottomNavigationDriverOption<ItemT>);
54
+ }
55
+
56
+ /**
57
+ * Every action with its label and selected state, in order.
58
+ */
59
+ async getActions(): Promise<BottomNavigationActionInfo[]> {
60
+ const actions: BottomNavigationActionInfo[] = [];
61
+ for await (const item of listHelper.getListItemIterator(
62
+ this,
63
+ this.getItemLocator(),
64
+ BottomNavigationActionDriver
65
+ )) {
66
+ actions.push({
67
+ label: (await item.getText())?.trim(),
68
+ selected: await item.isSelected(),
69
+ });
70
+ }
71
+ return actions;
72
+ }
73
+
74
+ /**
75
+ * Zero-based index of the selected action, or `-1` when none is selected.
76
+ */
77
+ async getSelectedIndex(): Promise<number> {
78
+ let index = 0;
79
+ for await (const item of listHelper.getListItemIterator(
80
+ this,
81
+ this.getItemLocator(),
82
+ BottomNavigationActionDriver
83
+ )) {
84
+ if (await item.isSelected()) {
85
+ return index;
86
+ }
87
+ index++;
88
+ }
89
+ return -1;
90
+ }
91
+
92
+ /**
93
+ * Label of the selected action, or `null` when none is selected. Returns `null`
94
+ * (not `undefined`) to match the sibling `TabsDriver.getSelectedLabel` contract.
95
+ */
96
+ async getSelectedLabel(): Promise<Nullable<string>> {
97
+ for await (const item of listHelper.getListItemIterator(
98
+ this,
99
+ this.getItemLocator(),
100
+ BottomNavigationActionDriver
101
+ )) {
102
+ if (await item.isSelected()) {
103
+ return (await item.getText())?.trim() ?? null;
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Select the action at the given zero-based index.
111
+ * @returns `false` when the index is out of range.
112
+ */
113
+ async selectByIndex(index: number): Promise<boolean> {
114
+ const item = await this.getItemByIndex(index);
115
+ if (item == null) {
116
+ return false;
117
+ }
118
+ await item.click();
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * Select the first action whose visible label equals `label`.
124
+ * @returns `false` when no action matches.
125
+ */
126
+ async selectByLabel(label: string): Promise<boolean> {
127
+ const item = await this.getItemByLabel(label);
128
+ if (item == null) {
129
+ return false;
130
+ }
131
+ await item.click();
132
+ return true;
133
+ }
134
+
135
+ override get driverName(): string {
136
+ return 'MuiV9BottomNavigationDriver';
137
+ }
138
+ }
@@ -0,0 +1,16 @@
1
+ import { HTMLButtonDriver } from '@atomic-testing/component-driver-html';
2
+
3
+ /**
4
+ * Driver for Material UI v9 Button component.
5
+ * @see https://mui.com/material-ui/react-button/
6
+ */
7
+ export class ButtonDriver extends HTMLButtonDriver {
8
+ async getValue(): Promise<string | null> {
9
+ const val = await this.interactor.getAttribute(this.locator, 'value');
10
+ return val ?? null;
11
+ }
12
+
13
+ override get driverName(): string {
14
+ return 'MuiV9ButtonDriver';
15
+ }
16
+ }
@@ -0,0 +1,98 @@
1
+ import { HTMLCheckboxDriver } from '@atomic-testing/component-driver-html';
2
+ import {
3
+ byCssSelector,
4
+ byTagName,
5
+ ComponentDriver,
6
+ IComponentDriverOption,
7
+ IFormFieldDriver,
8
+ Interactor,
9
+ IToggleDriver,
10
+ locatorUtil,
11
+ Optional,
12
+ PartLocator,
13
+ ScenePart,
14
+ ScenePartDriver,
15
+ } from '@atomic-testing/core';
16
+
17
+ export const checkboxPart = {
18
+ checkbox: {
19
+ locator: byTagName('input'),
20
+ driver: HTMLCheckboxDriver,
21
+ },
22
+ } satisfies ScenePart;
23
+
24
+ export type CheckboxScenePart = typeof checkboxPart;
25
+ export type CheckboxScenePartDriver = ScenePartDriver<CheckboxScenePart>;
26
+
27
+ export class CheckboxDriver
28
+ extends ComponentDriver<CheckboxScenePart>
29
+ implements IFormFieldDriver<string | null>, IToggleDriver
30
+ {
31
+ constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
32
+ super(locator, interactor, {
33
+ ...option,
34
+ parts: checkboxPart,
35
+ });
36
+ }
37
+ isSelected(): Promise<boolean> {
38
+ return this.parts.checkbox.isSelected();
39
+ }
40
+ async setSelected(selected: boolean): Promise<void> {
41
+ const isIndeterminate = await this.isIndeterminate();
42
+ if (isIndeterminate && selected === false) {
43
+ // if the checkbox is indeterminate and we want to set it to false, we need to click it twice
44
+ // this is done through setting it to true first, then to false
45
+ await this.parts.checkbox.setSelected(true);
46
+ }
47
+
48
+ await this.parts.checkbox.setSelected(selected);
49
+ }
50
+
51
+ getValue(): Promise<string | null> {
52
+ return this.parts.checkbox.getValue();
53
+ }
54
+
55
+ async isIndeterminate(): Promise<boolean> {
56
+ const indeterminate = await this.interactor.getAttribute(this.parts.checkbox.locator, 'data-indeterminate');
57
+ return indeterminate === 'true';
58
+ }
59
+
60
+ isDisabled(): Promise<boolean> {
61
+ return this.parts.checkbox.isDisabled();
62
+ }
63
+
64
+ isReadonly(): Promise<boolean> {
65
+ return this.parts.checkbox.isReadonly();
66
+ }
67
+
68
+ /**
69
+ * Get the text of the label associated with the checkbox, or `undefined` when the checkbox
70
+ * is rendered without one (e.g. a bare `<Checkbox>` outside of a `FormControlLabel`).
71
+ *
72
+ * MUI's `FormControlLabel` does not expose a `for`/`id` or `aria-labelledby` association; it
73
+ * labels the control implicitly by wrapping it in a `<label>` and rendering the text as a
74
+ * sibling. The label therefore lives outside this driver's own subtree, so we re-root at the
75
+ * enclosing `<label>` — matched against this checkbox via `:has()`, while preserving the
76
+ * surrounding scope — and read its text. This resolves to a single CSS selector, so it behaves
77
+ * identically across every interactor (DOM/React/Vue and Playwright).
78
+ */
79
+ async getLabel(): Promise<Optional<string>> {
80
+ const labelLocator = this.getEnclosingLabelLocator();
81
+ const hasLabel = await this.interactor.exists(labelLocator);
82
+ return hasLabel ? this.interactor.getText(labelLocator) : undefined;
83
+ }
84
+
85
+ /**
86
+ * Build a locator for the `<label>` that encloses this checkbox, scoped to the same ancestor
87
+ * context as the checkbox itself so that sibling checkboxes are never mismatched.
88
+ */
89
+ private getEnclosingLabelLocator(): PartLocator {
90
+ const chain = locatorUtil.toChain(this.locator);
91
+ const selfSelector = chain[chain.length - 1].selector;
92
+ return locatorUtil.append(chain.slice(0, -1), byCssSelector(`label:has(${selfSelector})`));
93
+ }
94
+
95
+ get driverName(): string {
96
+ return 'MuiV9CheckboxDriver';
97
+ }
98
+ }
@@ -0,0 +1,53 @@
1
+ import { HTMLElementDriver } from '@atomic-testing/component-driver-html';
2
+ import {
3
+ byCssClass,
4
+ byDataTestId,
5
+ ComponentDriver,
6
+ IComponentDriverOption,
7
+ Interactor,
8
+ PartLocator,
9
+ ScenePart,
10
+ } from '@atomic-testing/core';
11
+
12
+ export const parts = {
13
+ contentDisplay: {
14
+ locator: byCssClass('MuiChip-label'),
15
+ driver: HTMLElementDriver,
16
+ },
17
+ removeButton: {
18
+ locator: byDataTestId('CancelIcon'),
19
+ driver: HTMLElementDriver,
20
+ },
21
+ } satisfies ScenePart;
22
+
23
+ /**
24
+ * Driver for Material UI v9 Chip component.
25
+ * @see https://mui.com/material-ui/react-chip/
26
+ */
27
+ export class ChipDriver extends ComponentDriver<typeof parts> {
28
+ constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
29
+ super(locator, interactor, {
30
+ ...option,
31
+ parts: parts,
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Get the label content of the chip.
37
+ * @returns The label text content of the chip.
38
+ */
39
+ async getLabel(): Promise<string | null> {
40
+ await this.enforcePartExistence('contentDisplay');
41
+ const content = await this.parts.contentDisplay.getText();
42
+ return content ?? null;
43
+ }
44
+
45
+ async clickRemove(): Promise<void> {
46
+ await this.enforcePartExistence('removeButton');
47
+ await this.parts.removeButton.click();
48
+ }
49
+
50
+ override get driverName(): string {
51
+ return 'MuiV9ChipDriver';
52
+ }
53
+ }