@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,14 @@
1
+ import { ComponentDriver } from '@atomic-testing/core';
2
+
3
+ /**
4
+ * Driver for a single Material UI v9 TableCell (`<td>`/`<th>`, `.MuiTableCell-root`).
5
+ *
6
+ * A cell's content is plain text, so it relies on the inherited `getText()`/`exists()`;
7
+ * it exists as a distinct type so {@link TableRowDriver} can expose typed cell drivers.
8
+ * @see https://mui.com/material-ui/react-table/
9
+ */
10
+ export class TableCellDriver extends ComponentDriver {
11
+ get driverName(): string {
12
+ return 'MuiV9TableCellDriver';
13
+ }
14
+ }
@@ -0,0 +1,148 @@
1
+ import {
2
+ byCssSelector,
3
+ IComponentDriverOption,
4
+ Interactor,
5
+ ListComponentDriver,
6
+ ListComponentDriverSpecificOption,
7
+ locatorUtil,
8
+ Optional,
9
+ PartLocator,
10
+ } from '@atomic-testing/core';
11
+
12
+ import { TableRowDriver } from './TableRowDriver';
13
+
14
+ /** Reported sort state of a column, mirroring the `aria-sort` values MUI emits. */
15
+ export type TableSortDirection = 'ascending' | 'descending';
16
+
17
+ // Body rows are the iterated items; the header row is addressed separately.
18
+ const headerRowLocator: PartLocator = byCssSelector('.MuiTableHead-root .MuiTableRow-root');
19
+
20
+ /**
21
+ * Body rows are located under `.MuiTableBody-root`, so header rows are never
22
+ * mistaken for data rows.
23
+ */
24
+ export const defaultTableDriverOption: ListComponentDriverSpecificOption<TableRowDriver> = {
25
+ itemClass: TableRowDriver,
26
+ itemLocator: byCssSelector('.MuiTableBody-root .MuiTableRow-root'),
27
+ };
28
+
29
+ type TableDriverOption<ItemT extends TableRowDriver> = ListComponentDriverSpecificOption<ItemT> &
30
+ Partial<IComponentDriverOption<any>>;
31
+
32
+ // The nth header cell (1-based for nth-of-type); header cells are `<th>` siblings.
33
+ function headerCellAt(columnIndex: number): PartLocator {
34
+ return byCssSelector(`.MuiTableHead-root .MuiTableRow-root .MuiTableCell-root:nth-of-type(${columnIndex + 1})`);
35
+ }
36
+
37
+ /**
38
+ * Driver for the Material UI v9 Table component.
39
+ *
40
+ * A {@link ListComponentDriver} over the data rows (`.MuiTableBody-root > .MuiTableRow-root`),
41
+ * exposing per-row {@link TableRowDriver}s plus header reads and column sort state.
42
+ * Locators key off MUI's structural classes (`MuiTableHead/Body/Row/Cell-root`),
43
+ * which are stable across v9. Sort state is read from the header cell's `aria-sort`
44
+ * and driven by clicking its `TableSortLabel`.
45
+ * @see https://mui.com/material-ui/react-table/
46
+ */
47
+ export class TableDriver<ItemT extends TableRowDriver = TableRowDriver> extends ListComponentDriver<ItemT> {
48
+ constructor(locator: PartLocator, interactor: Interactor, option: Partial<TableDriverOption<ItemT>> = {}) {
49
+ super(locator, interactor, {
50
+ ...defaultTableDriverOption,
51
+ ...option,
52
+ } as TableDriverOption<ItemT>);
53
+ }
54
+
55
+ /**
56
+ * The number of data (body) rows.
57
+ */
58
+ async getRowCount(): Promise<number> {
59
+ return this.getItemCount();
60
+ }
61
+
62
+ /**
63
+ * The data-row driver at the given zero-based index, or `null` when out of range.
64
+ */
65
+ async getRow(index: number): Promise<ItemT | null> {
66
+ return this.getItemByIndex(index);
67
+ }
68
+
69
+ /**
70
+ * The header row as a {@link TableRowDriver}, or `null` when the table has no header.
71
+ */
72
+ async getHeaderRow(): Promise<TableRowDriver | null> {
73
+ const locator = locatorUtil.append(this.locator, headerRowLocator);
74
+ if (!(await this.interactor.exists(locator))) {
75
+ return null;
76
+ }
77
+ return new TableRowDriver(locator, this.interactor);
78
+ }
79
+
80
+ /**
81
+ * The number of columns, derived from the header cell count (0 when no header).
82
+ */
83
+ async getColumnCount(): Promise<number> {
84
+ const header = await this.getHeaderRow();
85
+ return header == null ? 0 : header.getCellCount();
86
+ }
87
+
88
+ /**
89
+ * The header cell texts, in column order (empty when no header).
90
+ */
91
+ async getHeaderTexts(): Promise<string[]> {
92
+ const header = await this.getHeaderRow();
93
+ return header == null ? [] : header.getCellTexts();
94
+ }
95
+
96
+ /**
97
+ * The text of the data cell at the given row/column, or `undefined` when either
98
+ * index is out of range.
99
+ */
100
+ async getCellText(rowIndex: number, columnIndex: number): Promise<Optional<string>> {
101
+ const row = await this.getRow(rowIndex);
102
+ if (row == null) {
103
+ return undefined;
104
+ }
105
+ const cell = await row.getCell(columnIndex);
106
+ if (cell == null) {
107
+ return undefined;
108
+ }
109
+ return (await cell.getText())?.trim();
110
+ }
111
+
112
+ /**
113
+ * The sort direction applied to the column at `columnIndex`, read from the header
114
+ * cell's `aria-sort`, or `undefined` when that column is not the sorted one.
115
+ */
116
+ async getSortDirection(columnIndex: number): Promise<Optional<TableSortDirection>> {
117
+ const cell = headerCellAt(columnIndex);
118
+ const fullLocator = locatorUtil.append(this.locator, cell);
119
+ if (!(await this.interactor.exists(fullLocator))) {
120
+ return undefined;
121
+ }
122
+ const ariaSort = await this.interactor.getAttribute(fullLocator, 'aria-sort');
123
+ return ariaSort === 'ascending' || ariaSort === 'descending' ? ariaSort : undefined;
124
+ }
125
+
126
+ /**
127
+ * Toggle/apply sorting on the column at `columnIndex` by clicking its
128
+ * `TableSortLabel`.
129
+ * @returns `false` when the column has no sort control.
130
+ */
131
+ async sortByColumn(columnIndex: number): Promise<boolean> {
132
+ const sortLabel = locatorUtil.append(
133
+ this.locator,
134
+ byCssSelector(
135
+ `.MuiTableHead-root .MuiTableRow-root .MuiTableCell-root:nth-of-type(${columnIndex + 1}) .MuiTableSortLabel-root`
136
+ )
137
+ );
138
+ if (!(await this.interactor.exists(sortLabel))) {
139
+ return false;
140
+ }
141
+ await this.interactor.click(sortLabel);
142
+ return true;
143
+ }
144
+
145
+ override get driverName(): string {
146
+ return 'MuiV9TableDriver';
147
+ }
148
+ }
@@ -0,0 +1,110 @@
1
+ import { byAttribute, byCssSelector, ComponentDriver, locatorUtil, Optional, PartLocator } from '@atomic-testing/core';
2
+
3
+ import { SelectDriver } from './SelectDriver';
4
+
5
+ const previousButtonLocator = byAttribute('aria-label', 'Go to previous page');
6
+ const nextButtonLocator = byAttribute('aria-label', 'Go to next page');
7
+ const displayedRowsLocator = byCssSelector('.MuiTablePagination-displayedRows');
8
+
9
+ /**
10
+ * Driver for the Material UI v9 TablePagination component.
11
+ *
12
+ * TablePagination is a composite: a "rows per page" MUI Select (a portal-backed
13
+ * `role="combobox"`), an aria-labelled previous/next pair that disables at the
14
+ * bounds, and a `.MuiTablePagination-displayedRows` label ("1–5 of 13"). The
15
+ * rows-per-page control is delegated to {@link SelectDriver} rather than
16
+ * reimplemented — the driver's own root contains exactly one combobox/input, so
17
+ * SelectDriver scoped to that root resolves it unambiguously.
18
+ * @see https://mui.com/material-ui/react-pagination/#table-pagination
19
+ */
20
+ export class TablePaginationDriver extends ComponentDriver {
21
+ // The driver root contains exactly one combobox/input, so a SelectDriver scoped
22
+ // to it resolves the rows-per-page control unambiguously. Constructed on demand
23
+ // (stateless) to avoid overriding the base constructor.
24
+ private get rowsPerPageSelect(): SelectDriver {
25
+ return new SelectDriver(this.locator, this.interactor);
26
+ }
27
+
28
+ /**
29
+ * The current rows-per-page value (read from the select's hidden input), or
30
+ * `-1` when it cannot be parsed.
31
+ */
32
+ async getRowsPerPage(): Promise<number> {
33
+ const value = await this.rowsPerPageSelect.getValue();
34
+ const parsed = Number.parseInt(value ?? '', 10);
35
+ return Number.isNaN(parsed) ? -1 : parsed;
36
+ }
37
+
38
+ /**
39
+ * Choose a rows-per-page value by opening the select and picking the option.
40
+ * @returns `false` when no such option exists.
41
+ */
42
+ async setRowsPerPage(rowsPerPage: number): Promise<boolean> {
43
+ return this.rowsPerPageSelect.setValue(String(rowsPerPage));
44
+ }
45
+
46
+ /**
47
+ * The raw "displayed rows" label (e.g. "1–5 of 13"), or `undefined` when absent.
48
+ * Returned verbatim because its exact format (separator, "of") is locale-defined.
49
+ */
50
+ async getDisplayedRowsText(): Promise<Optional<string>> {
51
+ const locator = locatorUtil.append(this.locator, displayedRowsLocator);
52
+ if (!(await this.interactor.exists(locator))) {
53
+ return undefined;
54
+ }
55
+ return (await this.interactor.getText(locator))?.trim();
56
+ }
57
+
58
+ /** Whether the previous-page control is disabled (i.e. on the first page). */
59
+ async isPreviousDisabled(): Promise<boolean> {
60
+ return this.isNavDisabled(previousButtonLocator);
61
+ }
62
+
63
+ /** Whether the next-page control is disabled (i.e. on the last page). */
64
+ async isNextDisabled(): Promise<boolean> {
65
+ return this.isNavDisabled(nextButtonLocator);
66
+ }
67
+
68
+ /**
69
+ * An absent control counts as disabled — both to stay consistent with
70
+ * {@link clickNavButton} (which reports a no-op for a missing control) and
71
+ * because Playwright's `isDisabled` throws on a zero-match locator rather than
72
+ * returning false the way jsdom does.
73
+ */
74
+ private async isNavDisabled(navLocator: PartLocator): Promise<boolean> {
75
+ const locator = locatorUtil.append(this.locator, navLocator);
76
+ if (!(await this.interactor.exists(locator))) {
77
+ return true;
78
+ }
79
+ return this.interactor.isDisabled(locator);
80
+ }
81
+
82
+ /**
83
+ * Advance to the next page unless the control is disabled (last page).
84
+ * @returns whether the click was performed.
85
+ */
86
+ async nextPage(): Promise<boolean> {
87
+ return this.clickNavButton(nextButtonLocator);
88
+ }
89
+
90
+ /**
91
+ * Go back to the previous page unless the control is disabled (first page).
92
+ * @returns whether the click was performed.
93
+ */
94
+ async previousPage(): Promise<boolean> {
95
+ return this.clickNavButton(previousButtonLocator);
96
+ }
97
+
98
+ private async clickNavButton(navLocator: PartLocator): Promise<boolean> {
99
+ const locator = locatorUtil.append(this.locator, navLocator);
100
+ if (!(await this.interactor.exists(locator)) || (await this.interactor.isDisabled(locator))) {
101
+ return false;
102
+ }
103
+ await this.interactor.click(locator);
104
+ return true;
105
+ }
106
+
107
+ override get driverName(): string {
108
+ return 'MuiV9TablePaginationDriver';
109
+ }
110
+ }
@@ -0,0 +1,79 @@
1
+ import {
2
+ byCssSelector,
3
+ IComponentDriverOption,
4
+ Interactor,
5
+ ListComponentDriver,
6
+ ListComponentDriverSpecificOption,
7
+ PartLocator,
8
+ } from '@atomic-testing/core';
9
+
10
+ import { TableCellDriver } from './TableCellDriver';
11
+
12
+ /**
13
+ * Cells are located by `.MuiTableCell-root`, covering both `<td>` body cells and
14
+ * `<th>` header cells.
15
+ */
16
+ export const defaultTableRowDriverOption: ListComponentDriverSpecificOption<TableCellDriver> = {
17
+ itemClass: TableCellDriver,
18
+ itemLocator: byCssSelector('.MuiTableCell-root'),
19
+ };
20
+
21
+ type TableRowDriverOption<ItemT extends TableCellDriver> = ListComponentDriverSpecificOption<ItemT> &
22
+ Partial<IComponentDriverOption<any>>;
23
+
24
+ /**
25
+ * Driver for a single Material UI v9 TableRow (`.MuiTableRow-root`).
26
+ *
27
+ * A {@link ListComponentDriver} over the row's cells, exposing per-cell
28
+ * {@link TableCellDriver}s (via `getItems`/`getItemByIndex`) plus convenience reads
29
+ * of the cell texts.
30
+ *
31
+ * Cells are addressed positionally via `:nth-of-type`, which counts per element type.
32
+ * This assumes a row's cells are homogeneous (all `<td>`, or all `<th>` for a header
33
+ * row) — the common MUI rendering. A row mixing a leading `<th scope="row">` with
34
+ * `<td>` data cells would desynchronize the index (the `<td>`s start at type-index 1
35
+ * after the `<th>`); such rows are out of scope.
36
+ * @see https://mui.com/material-ui/react-table/
37
+ */
38
+ export class TableRowDriver<ItemT extends TableCellDriver = TableCellDriver> extends ListComponentDriver<ItemT> {
39
+ constructor(locator: PartLocator, interactor: Interactor, option: Partial<TableRowDriverOption<ItemT>> = {}) {
40
+ // A row's item shape is fixed (cells driven by TableCellDriver). The defaults are
41
+ // applied LAST so they win over any inherited `itemLocator`/`itemClass`: when a
42
+ // TableDriver builds its row drivers it forwards its own commutable option, whose
43
+ // (row-level) item locator would otherwise shadow the cell locator here.
44
+ super(locator, interactor, {
45
+ ...option,
46
+ ...defaultTableRowDriverOption,
47
+ } as TableRowDriverOption<ItemT>);
48
+ }
49
+
50
+ /**
51
+ * The number of cells in the row.
52
+ */
53
+ async getCellCount(): Promise<number> {
54
+ return this.getItemCount();
55
+ }
56
+
57
+ /**
58
+ * The cell driver at the given zero-based column index, or `null` when out of range.
59
+ */
60
+ async getCell(index: number): Promise<ItemT | null> {
61
+ return this.getItemByIndex(index);
62
+ }
63
+
64
+ /**
65
+ * The text of every cell, in column order.
66
+ */
67
+ async getCellTexts(): Promise<string[]> {
68
+ const cells = await this.getItems();
69
+ const texts: string[] = [];
70
+ for (const cell of cells) {
71
+ texts.push((await cell.getText())?.trim() ?? '');
72
+ }
73
+ return texts;
74
+ }
75
+
76
+ override get driverName(): string {
77
+ return 'MuiV9TableRowDriver';
78
+ }
79
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ byRole,
3
+ IComponentDriverOption,
4
+ Interactor,
5
+ ListComponentDriver,
6
+ ListComponentDriverSpecificOption,
7
+ listHelper,
8
+ Nullable,
9
+ PartLocator,
10
+ } from '@atomic-testing/core';
11
+
12
+ import { TabDriver } from './TabDriver';
13
+
14
+ /**
15
+ * Tabs are located by their accessible `role="tab"` children rather than by MUI
16
+ * class names, so the driver is resilient to MUI styling/version changes.
17
+ */
18
+ export const defaultTabsDriverOption: ListComponentDriverSpecificOption<TabDriver> = {
19
+ itemClass: TabDriver,
20
+ itemLocator: byRole('tab'),
21
+ };
22
+
23
+ type TabsDriverOption<ItemT extends TabDriver> = ListComponentDriverSpecificOption<ItemT> &
24
+ Partial<IComponentDriverOption<any>>;
25
+
26
+ /**
27
+ * Driver for the Material UI v9 Tabs component.
28
+ *
29
+ * `<Tabs>` renders a `role="tablist"` whose `role="tab"` buttons carry the
30
+ * selected (`aria-selected`) and disabled state. This driver is a
31
+ * {@link ListComponentDriver} over those tabs, exposing both group-level helpers
32
+ * (selected index/label, select by index/label) and per-tab {@link TabDriver}
33
+ * instances via `getItems`/`getItemByIndex`/`getItemByLabel`.
34
+ * @see https://mui.com/material-ui/react-tabs/
35
+ */
36
+ export class TabsDriver<ItemT extends TabDriver = TabDriver> extends ListComponentDriver<ItemT> {
37
+ constructor(locator: PartLocator, interactor: Interactor, option: Partial<TabsDriverOption<ItemT>> = {}) {
38
+ // A tab group's item shape is fixed (role="tab" buttons driven by TabDriver),
39
+ // so the defaults are merged in rather than relying on a default parameter:
40
+ // the test engine always passes an option object for a scene part, which would
41
+ // otherwise shadow a default-valued parameter and leave itemLocator unset.
42
+ super(locator, interactor, {
43
+ ...defaultTabsDriverOption,
44
+ ...option,
45
+ } as TabsDriverOption<ItemT>);
46
+ }
47
+
48
+ /**
49
+ * The visible label of every tab, in DOM order.
50
+ */
51
+ async getTabLabels(): Promise<string[]> {
52
+ const labels: string[] = [];
53
+ for await (const tab of listHelper.getListItemIterator(this, this.getItemLocator(), TabDriver)) {
54
+ const text = await tab.getText();
55
+ labels.push(text?.trim() ?? '');
56
+ }
57
+ return labels;
58
+ }
59
+
60
+ /**
61
+ * The number of tabs in the group.
62
+ */
63
+ async getTabCount(): Promise<number> {
64
+ return this.getItemCount();
65
+ }
66
+
67
+ /**
68
+ * Zero-based index of the selected tab, or `-1` when no tab is selected
69
+ * (e.g. `<Tabs value={false}>`).
70
+ */
71
+ async getSelectedIndex(): Promise<number> {
72
+ let index = 0;
73
+ for await (const tab of listHelper.getListItemIterator(this, this.getItemLocator(), TabDriver)) {
74
+ if (await tab.isSelected()) {
75
+ return index;
76
+ }
77
+ index++;
78
+ }
79
+ return -1;
80
+ }
81
+
82
+ /**
83
+ * Label of the selected tab, or `null` when no tab is selected. Returns `null`
84
+ * (not `undefined`) to match the sibling `SelectDriver.getSelectedLabel` contract.
85
+ */
86
+ async getSelectedLabel(): Promise<Nullable<string>> {
87
+ for await (const tab of listHelper.getListItemIterator(this, this.getItemLocator(), TabDriver)) {
88
+ if (await tab.isSelected()) {
89
+ return (await tab.getText())?.trim() ?? null;
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Select the tab at the given zero-based index.
97
+ * @returns `false` when the index is out of range.
98
+ */
99
+ async selectByIndex(index: number): Promise<boolean> {
100
+ const tab = await this.getItemByIndex(index);
101
+ if (tab == null) {
102
+ return false;
103
+ }
104
+ await tab.click();
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * Select the first tab whose visible label equals `label`.
110
+ * @returns `false` when no tab matches.
111
+ */
112
+ async selectByLabel(label: string): Promise<boolean> {
113
+ const tab = await this.getItemByLabel(label);
114
+ if (tab == null) {
115
+ return false;
116
+ }
117
+ await tab.click();
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Whether the tab at the given index is disabled.
123
+ * @returns `false` when the index is out of range.
124
+ */
125
+ async isTabDisabled(index: number): Promise<boolean> {
126
+ const tab = await this.getItemByIndex(index);
127
+ return tab == null ? false : tab.isDisabled();
128
+ }
129
+
130
+ override get driverName(): string {
131
+ return 'MuiV9TabsDriver';
132
+ }
133
+ }
@@ -0,0 +1,155 @@
1
+ import { HTMLElementDriver, HTMLTextInputDriver } from '@atomic-testing/component-driver-html';
2
+ import {
3
+ byCssSelector,
4
+ byRole,
5
+ byTagName,
6
+ ComponentDriver,
7
+ IComponentDriverOption,
8
+ IInputDriver,
9
+ Interactor,
10
+ Optional,
11
+ PartLocator,
12
+ ScenePart,
13
+ } from '@atomic-testing/core';
14
+
15
+ import { SelectDriver } from './SelectDriver';
16
+
17
+ export const parts = {
18
+ label: {
19
+ // In v9 the label is a direct child `.MuiInputLabel-root`: a `<label>` for the
20
+ // text/multiline variants, but a `<div>` for the select variant (where the
21
+ // control is a combobox associated via aria-labelledby, not a focusable input).
22
+ // Matching the class instead of the `<label>` tag covers both.
23
+ locator: byCssSelector('>.MuiInputLabel-root'),
24
+ driver: HTMLElementDriver,
25
+ },
26
+ helperText: {
27
+ locator: byCssSelector('>p'),
28
+ driver: HTMLElementDriver,
29
+ },
30
+ singlelineInput: {
31
+ locator: byCssSelector('input:not([aria-hidden])'),
32
+ driver: HTMLTextInputDriver,
33
+ },
34
+ multilineInput: {
35
+ locator: byCssSelector('textarea:not([aria-hidden])'),
36
+ driver: HTMLTextInputDriver,
37
+ },
38
+ selectInput: {
39
+ // Target the input wrapper specifically: in v9 the select variant's label is
40
+ // also a direct child `<div>`, so a bare `>div` is ambiguous (it matches the
41
+ // label first, breaking class reads like the readonly state). `.MuiInputBase-root`
42
+ // pins it to the actual input root.
43
+ locator: byCssSelector('>.MuiInputBase-root'),
44
+ driver: SelectDriver,
45
+ },
46
+ // Used to detect the presence of select input
47
+ richSelectInputDetect: {
48
+ locator: byRole('combobox'),
49
+ driver: HTMLElementDriver,
50
+ },
51
+ nativeSelectInputDetect: {
52
+ locator: byTagName('SELECT'),
53
+ driver: HTMLElementDriver,
54
+ },
55
+ } satisfies ScenePart;
56
+
57
+ type TextFieldInputType = 'singleLine' | 'multiline' | 'select';
58
+
59
+ /**
60
+ * A driver for the Material UI v9 TextField component with single line or multiline text input.
61
+ */
62
+ export class TextFieldDriver extends ComponentDriver<typeof parts> implements IInputDriver<string | null> {
63
+ constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
64
+ super(locator, interactor, {
65
+ ...option,
66
+ parts,
67
+ });
68
+ }
69
+
70
+ private async getInputType(): Promise<TextFieldInputType> {
71
+ const result = await Promise.all([
72
+ this.parts.singlelineInput.exists(),
73
+ this.parts.richSelectInputDetect.exists(),
74
+ this.parts.nativeSelectInputDetect.exists(),
75
+ this.parts.multilineInput.exists(),
76
+ ]).then(([singlelineExists, richSelectExists, nativeSelectExists, multilineExists]) => {
77
+ if (singlelineExists) {
78
+ return 'singleLine';
79
+ }
80
+ if (richSelectExists || nativeSelectExists) {
81
+ return 'select';
82
+ }
83
+ if (multilineExists) {
84
+ return 'multiline';
85
+ }
86
+
87
+ throw new Error('Unable to determine input type in TextFieldInput');
88
+ });
89
+
90
+ return result;
91
+ }
92
+
93
+ async getValue(): Promise<string | null> {
94
+ const inputType = await this.getInputType();
95
+ switch (inputType) {
96
+ case 'singleLine':
97
+ return this.parts.singlelineInput.getValue();
98
+ case 'select':
99
+ return this.parts.selectInput.getValue();
100
+ case 'multiline':
101
+ return this.parts.multilineInput.getValue();
102
+ }
103
+ }
104
+
105
+ async setValue(value: string | null): Promise<boolean> {
106
+ const inputType = await this.getInputType();
107
+ switch (inputType) {
108
+ case 'singleLine':
109
+ return this.parts.singlelineInput.setValue(value);
110
+ case 'select':
111
+ return this.parts.selectInput.setValue(value);
112
+ case 'multiline':
113
+ return this.parts.multilineInput.setValue(value);
114
+ }
115
+ }
116
+
117
+ async getLabel(): Promise<Optional<string>> {
118
+ return this.parts.label.getText();
119
+ }
120
+
121
+ async getHelperText(): Promise<Optional<string>> {
122
+ const helperTextExists = await this.interactor.exists(this.parts.helperText.locator);
123
+ if (helperTextExists) {
124
+ return this.parts.helperText.getText();
125
+ }
126
+ }
127
+
128
+ async isDisabled(): Promise<boolean> {
129
+ const inputType = await this.getInputType();
130
+ switch (inputType) {
131
+ case 'singleLine':
132
+ return this.parts.singlelineInput.isDisabled();
133
+ case 'select':
134
+ return this.parts.selectInput.isDisabled();
135
+ case 'multiline':
136
+ return this.parts.multilineInput.isDisabled();
137
+ }
138
+ }
139
+
140
+ async isReadonly(): Promise<boolean> {
141
+ const inputType = await this.getInputType();
142
+ switch (inputType) {
143
+ case 'singleLine':
144
+ return this.parts.singlelineInput.isReadonly();
145
+ case 'select':
146
+ return this.interactor.hasCssClass(this.parts.selectInput.locator, 'MuiInputBase-readOnly');
147
+ case 'multiline':
148
+ return this.parts.multilineInput.isReadonly();
149
+ }
150
+ }
151
+
152
+ get driverName(): string {
153
+ return 'MuiV9TextFieldDriver';
154
+ }
155
+ }
@@ -0,0 +1,21 @@
1
+ import { IToggleDriver } from '@atomic-testing/core';
2
+
3
+ import { ButtonDriver } from './ButtonDriver';
4
+
5
+ export class ToggleButtonDriver extends ButtonDriver implements IToggleDriver {
6
+ async isSelected(): Promise<boolean> {
7
+ const val = await this.interactor.getAttribute(this.locator, 'aria-pressed');
8
+ return val === 'true';
9
+ }
10
+
11
+ async setSelected(targetState: boolean): Promise<void> {
12
+ const currentState = await this.isSelected();
13
+ if (currentState !== targetState) {
14
+ await this.interactor.click(this.locator);
15
+ }
16
+ }
17
+
18
+ override get driverName() {
19
+ return 'MuiV9ToggleButtonDriver';
20
+ }
21
+ }