@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,122 @@
1
+ import { HTMLElementDriver } from '@atomic-testing/component-driver-html';
2
+ import {
3
+ byCssClass,
4
+ byRole,
5
+ ContainerDriver,
6
+ IContainerDriverOption,
7
+ Interactor,
8
+ type LocatorRelativePosition,
9
+ Optional,
10
+ PartLocator,
11
+ ScenePart,
12
+ } from '@atomic-testing/core';
13
+
14
+ export const parts = {
15
+ title: {
16
+ locator: byCssClass('MuiDialogTitle-root'),
17
+ driver: HTMLElementDriver,
18
+ },
19
+ dialogContainer: {
20
+ locator: byRole('presentation'),
21
+ driver: HTMLElementDriver,
22
+ },
23
+ } satisfies ScenePart;
24
+
25
+ const dialogRootLocator: PartLocator = byRole('presentation', 'Root');
26
+
27
+ const defaultTransitionDuration = 250;
28
+
29
+ export class DialogDriver<ContentT extends ScenePart> extends ContainerDriver<ContentT, typeof parts> {
30
+ constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IContainerDriverOption>) {
31
+ super(locator, interactor, {
32
+ ...option,
33
+ parts: parts,
34
+ content: (option?.content ?? {}) as ContentT,
35
+ });
36
+ }
37
+
38
+ override overriddenParentLocator(): Optional<PartLocator> {
39
+ return dialogRootLocator;
40
+ }
41
+
42
+ override overrideLocatorRelativePosition(): Optional<LocatorRelativePosition> {
43
+ return 'Same';
44
+ }
45
+
46
+ async getTitle(): Promise<string | null> {
47
+ await this.enforcePartExistence('title');
48
+ const title = await this.parts.title.getText();
49
+ return title ?? null;
50
+ }
51
+
52
+ /**
53
+ * Dismiss the dialog by clicking outside its content, then wait for it to close.
54
+ *
55
+ * MUI's "backdrop click" is handled on the `.MuiDialog-container` surface (which
56
+ * overlays the visual `.MuiBackdrop-root`), firing `onClose` only when the click
57
+ * target is the container itself. The click therefore lands on the container near
58
+ * its top-left corner to avoid the centered paper. Whether it actually closes
59
+ * depends on the consumer's `onClose` handling (MUI reports a `"backdropClick"`
60
+ * reason); the returned boolean reflects the observed close, not merely the click.
61
+ *
62
+ * @param timeoutMs How long to wait for the close transition to finish
63
+ * @returns true if the dialog closed
64
+ */
65
+ async closeByBackdropClick(timeoutMs: number = defaultTransitionDuration): Promise<boolean> {
66
+ await this.enforcePartExistence('dialogContainer');
67
+ // MUI only dismisses when the same element receives mousedown and click, so
68
+ // drive the full press/release/click sequence on the container's empty corner
69
+ // (the click target must be the container, not the centered paper).
70
+ const cornerClick = { position: { x: 5, y: 5 } } as const;
71
+ await this.parts.dialogContainer.mouseDown(cornerClick);
72
+ await this.parts.dialogContainer.mouseUp(cornerClick);
73
+ await this.parts.dialogContainer.click(cornerClick);
74
+ return this.waitForClose(timeoutMs);
75
+ }
76
+
77
+ /**
78
+ * Wait for dialog to open
79
+ * @param timeoutMs
80
+ * @returns true open has performed successfully
81
+ */
82
+ async waitForOpen(timeoutMs: number = defaultTransitionDuration): Promise<boolean> {
83
+ const isOpened = await this.interactor.waitUntil({
84
+ probeFn: () => this.isOpen(),
85
+ terminateCondition: true,
86
+ timeoutMs,
87
+ });
88
+ return isOpened === true;
89
+ }
90
+
91
+ /**
92
+ * Wait for dialog to close
93
+ * @param timeoutMs
94
+ * @returns true open has performed successfully
95
+ */
96
+ async waitForClose(timeoutMs: number = defaultTransitionDuration): Promise<boolean> {
97
+ const isOpened = await this.interactor.waitUntil({
98
+ probeFn: () => this.isOpen(),
99
+ terminateCondition: false,
100
+ timeoutMs,
101
+ });
102
+ return isOpened === false;
103
+ }
104
+
105
+ /**
106
+ * Check if the dialog box is open. Caution, because of animation, upon an open/close action is performed
107
+ * use waitForOpen() or waitForClose() before using isOpen() would result a more accurate open state of the dialog
108
+ * @returns true if dialog box is open
109
+ */
110
+ async isOpen(): Promise<boolean> {
111
+ const exists = await this.exists();
112
+ if (!exists) {
113
+ return false;
114
+ }
115
+ const isVisible = await this.interactor.isVisible(this.parts.dialogContainer.locator);
116
+ return isVisible;
117
+ }
118
+
119
+ get driverName(): string {
120
+ return 'MuiV9DialogDriver';
121
+ }
122
+ }
@@ -0,0 +1,90 @@
1
+ import { HTMLElementDriver } from '@atomic-testing/component-driver-html';
2
+ import {
3
+ byCssClass,
4
+ byRole,
5
+ IContainerDriverOption,
6
+ Interactor,
7
+ type LocatorRelativePosition,
8
+ Optional,
9
+ PartLocator,
10
+ ScenePart,
11
+ } from '@atomic-testing/core';
12
+
13
+ import { OverlayDriver } from './OverlayDriver';
14
+
15
+ export const drawerParts = {
16
+ paper: {
17
+ locator: byCssClass('MuiDrawer-paper'),
18
+ driver: HTMLElementDriver,
19
+ },
20
+ } satisfies ScenePart;
21
+
22
+ export type DrawerAnchor = 'left' | 'right' | 'top' | 'bottom';
23
+
24
+ // In Material UI v9 the anchor class lives on the drawer root (`.MuiDrawer-root`,
25
+ // the role="presentation" element), not on the paper as in v7.
26
+ const anchorClassByAnchor: Record<DrawerAnchor, string> = {
27
+ left: 'MuiDrawer-anchorLeft',
28
+ right: 'MuiDrawer-anchorRight',
29
+ top: 'MuiDrawer-anchorTop',
30
+ bottom: 'MuiDrawer-anchorBottom',
31
+ };
32
+
33
+ // A temporary Drawer is a Modal rendered into a portal whose root carries
34
+ // role="presentation"; re-root to it like DialogDriver does.
35
+ const drawerRootLocator: PartLocator = byRole('presentation', 'Root');
36
+
37
+ /**
38
+ * Driver for the Material UI v9 Drawer (and SwipeableDrawer) component.
39
+ *
40
+ * A temporary Drawer is a portal-rendered Modal: its root `role="presentation"`
41
+ * (`.MuiDrawer-root`) carries the anchor class and holds a `.MuiBackdrop-root` plus
42
+ * the `.MuiDrawer-paper` panel. Open/close/backdrop behavior is inherited from
43
+ * {@link OverlayDriver}; this driver adds the anchor read and the portal re-rooting.
44
+ * @see https://mui.com/material-ui/react-drawer/
45
+ */
46
+ export class DrawerDriver<ContentT extends ScenePart> extends OverlayDriver<ContentT, typeof drawerParts> {
47
+ constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IContainerDriverOption>) {
48
+ super(locator, interactor, {
49
+ ...option,
50
+ parts: drawerParts,
51
+ content: (option?.content ?? {}) as ContentT,
52
+ });
53
+ }
54
+
55
+ override overriddenParentLocator(): Optional<PartLocator> {
56
+ return drawerRootLocator;
57
+ }
58
+
59
+ override overrideLocatorRelativePosition(): Optional<LocatorRelativePosition> {
60
+ return 'Same';
61
+ }
62
+
63
+ protected getSurfaceLocator(): PartLocator {
64
+ return this.parts.paper.locator;
65
+ }
66
+
67
+ /**
68
+ * The side the drawer is anchored to, read from the portal root's anchor class, or
69
+ * `undefined` when the drawer is closed/unmounted.
70
+ *
71
+ * The anchor class sits on the `role="presentation"` root itself, so it is read
72
+ * directly off {@link drawerRootLocator} rather than through a part (parts resolve
73
+ * as descendants of that root, but the class is on the root).
74
+ */
75
+ async getAnchor(): Promise<Optional<DrawerAnchor>> {
76
+ if (!(await this.interactor.exists(drawerRootLocator))) {
77
+ return undefined;
78
+ }
79
+ for (const anchor of Object.keys(anchorClassByAnchor) as DrawerAnchor[]) {
80
+ if (await this.interactor.hasCssClass(drawerRootLocator, anchorClassByAnchor[anchor])) {
81
+ return anchor;
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+
87
+ get driverName(): string {
88
+ return 'MuiV9DrawerDriver';
89
+ }
90
+ }
@@ -0,0 +1,11 @@
1
+ import { ButtonDriver } from './ButtonDriver';
2
+
3
+ /**
4
+ * Driver for Material UI v9 Floating Action Button component.
5
+ * @see https://mui.com/material-ui/react-floating-action-button/
6
+ */
7
+ export class FabDriver extends ButtonDriver {
8
+ override get driverName(): string {
9
+ return 'MuiV9FabDriver';
10
+ }
11
+ }
@@ -0,0 +1,112 @@
1
+ import { HTMLTextInputDriver } from '@atomic-testing/component-driver-html';
2
+ import {
3
+ byCssSelector,
4
+ ComponentDriver,
5
+ IComponentDriverOption,
6
+ IInputDriver,
7
+ Interactor,
8
+ PartLocator,
9
+ ScenePart,
10
+ } from '@atomic-testing/core';
11
+
12
+ export const parts = {
13
+ singlelineInput: {
14
+ locator: byCssSelector('input:not([aria-hidden])'),
15
+ driver: HTMLTextInputDriver,
16
+ },
17
+ multilineInput: {
18
+ locator: byCssSelector('textarea:not([aria-hidden])'),
19
+ driver: HTMLTextInputDriver,
20
+ },
21
+ } satisfies ScenePart;
22
+
23
+ type TextFieldInputType = 'singleLine' | 'multiline';
24
+
25
+ /**
26
+ * A driver for the Material UI v9 Input, FilledInput, OutlinedInput, and StandardInput components.
27
+ */
28
+ export class InputDriver extends ComponentDriver<typeof parts> implements IInputDriver<string | null> {
29
+ constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
30
+ super(locator, interactor, {
31
+ ...option,
32
+ parts,
33
+ });
34
+ }
35
+
36
+ private async getInputType(): Promise<TextFieldInputType> {
37
+ // TODO: Detection of both input types can be done in parallel.
38
+ const textInputExists = await this.interactor.exists(this.parts.singlelineInput.locator);
39
+ if (textInputExists) {
40
+ return 'singleLine';
41
+ }
42
+
43
+ const multilineExists = await this.interactor.exists(this.parts.multilineInput.locator);
44
+ if (multilineExists) {
45
+ return 'multiline';
46
+ }
47
+
48
+ throw new Error('Unable to determine input type in TextFieldInput');
49
+ }
50
+
51
+ /**
52
+ * Retrieve the current value of the input element, handling both single line
53
+ * and multiline configurations.
54
+ */
55
+ async getValue(): Promise<string | null> {
56
+ const inputType = await this.getInputType();
57
+ switch (inputType) {
58
+ case 'singleLine':
59
+ return this.parts.singlelineInput.getValue();
60
+ case 'multiline':
61
+ return this.parts.multilineInput.getValue();
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Set the value of the underlying input element.
67
+ *
68
+ * @param value The text to assign to the input.
69
+ */
70
+ async setValue(value: string | null): Promise<boolean> {
71
+ const inputType = await this.getInputType();
72
+ switch (inputType) {
73
+ case 'singleLine':
74
+ return this.parts.singlelineInput.setValue(value);
75
+ case 'multiline':
76
+ return this.parts.multilineInput.setValue(value);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Determine whether the input element is disabled.
82
+ */
83
+ async isDisabled(): Promise<boolean> {
84
+ const inputType = await this.getInputType();
85
+ switch (inputType) {
86
+ case 'singleLine':
87
+ return this.parts.singlelineInput.isDisabled();
88
+ case 'multiline':
89
+ return this.parts.multilineInput.isDisabled();
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Determine whether the input element is read only.
95
+ */
96
+ async isReadonly(): Promise<boolean> {
97
+ const inputType = await this.getInputType();
98
+ switch (inputType) {
99
+ case 'singleLine':
100
+ return this.parts.singlelineInput.isReadonly();
101
+ case 'multiline':
102
+ return this.parts.multilineInput.isReadonly();
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Identifier for this driver.
108
+ */
109
+ get driverName(): string {
110
+ return 'MuiV9InputDriver';
111
+ }
112
+ }
@@ -0,0 +1,42 @@
1
+ import {
2
+ byRole,
3
+ IComponentDriverOption,
4
+ Interactor,
5
+ ListComponentDriver,
6
+ ListComponentDriverSpecificOption,
7
+ listHelper,
8
+ PartLocator,
9
+ } from '@atomic-testing/core';
10
+
11
+ import { ListItemDriver } from './ListItemDriver';
12
+
13
+ export const defaultListDriverOption: ListComponentDriverSpecificOption<ListItemDriver> = {
14
+ itemClass: ListItemDriver,
15
+ itemLocator: byRole('option'),
16
+ };
17
+
18
+ type ListDriverOption<ItemT extends ListItemDriver> = ListComponentDriverSpecificOption<ItemT> &
19
+ Partial<IComponentDriverOption<any>>;
20
+
21
+ export class ListDriver<ItemT extends ListItemDriver = ListItemDriver> extends ListComponentDriver<ItemT> {
22
+ constructor(
23
+ locator: PartLocator,
24
+ interactor: Interactor,
25
+ option: ListDriverOption<ItemT> = { ...defaultListDriverOption } as ListDriverOption<ItemT>
26
+ ) {
27
+ super(locator, interactor, option);
28
+ }
29
+
30
+ async getSelected(): Promise<ListItemDriver | null> {
31
+ for await (const item of listHelper.getListItemIterator(this, this.getItemLocator(), ListItemDriver)) {
32
+ if (await item.isSelected()) {
33
+ return item;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
39
+ override get driverName(): string {
40
+ return 'MuiV9ListDriver';
41
+ }
42
+ }
@@ -0,0 +1,34 @@
1
+ import { ComponentDriver } from '@atomic-testing/core';
2
+
3
+ import { MenuItemDisabledError } from '../errors/MenuItemDisabledError';
4
+
5
+ /**
6
+ * @internal
7
+ */
8
+ export class ListItemDriver extends ComponentDriver {
9
+ async label(): Promise<string | null> {
10
+ const label = await this.getText();
11
+ return label?.trim() || null;
12
+ }
13
+
14
+ async isSelected(): Promise<boolean> {
15
+ return await this.interactor.hasCssClass(this.locator, 'Mui-selected');
16
+ }
17
+
18
+ async isDisabled(): Promise<boolean> {
19
+ const disabledVal = await this.interactor.getAttribute(this.locator, 'aria-disabled');
20
+ return disabledVal === 'true';
21
+ }
22
+
23
+ async click(): Promise<void> {
24
+ if (await this.isDisabled()) {
25
+ const label = await this.label();
26
+ throw new MenuItemDisabledError(label ?? '', this);
27
+ }
28
+ await this.interactor.click(this.locator);
29
+ }
30
+
31
+ get driverName(): string {
32
+ return 'MuiV9ListItemDriver';
33
+ }
34
+ }
@@ -0,0 +1,65 @@
1
+ import { HTMLElementDriver } from '@atomic-testing/component-driver-html';
2
+ import {
3
+ byRole,
4
+ ComponentDriver,
5
+ IComponentDriverOption,
6
+ Interactor,
7
+ listHelper,
8
+ type LocatorRelativePosition,
9
+ Optional,
10
+ PartLocator,
11
+ ScenePart,
12
+ } from '@atomic-testing/core';
13
+
14
+ import { MenuItemNotFoundError } from '../errors/MenuItemNotFoundError';
15
+ import { MenuItemDriver } from './MenuItemDriver';
16
+
17
+ export const parts = {
18
+ menu: {
19
+ locator: byRole('menu'),
20
+ driver: HTMLElementDriver,
21
+ },
22
+ } satisfies ScenePart;
23
+
24
+ const menuRootLocator: PartLocator = byRole('presentation', 'Root');
25
+ const menuItemLocator = byRole('menuitem');
26
+
27
+ export class MenuDriver extends ComponentDriver<typeof parts> {
28
+ constructor(locator: PartLocator, interactor: Interactor, option?: Partial<IComponentDriverOption>) {
29
+ super(locator, interactor, {
30
+ ...option,
31
+ parts,
32
+ });
33
+ }
34
+
35
+ override overriddenParentLocator(): Optional<PartLocator> {
36
+ return menuRootLocator;
37
+ }
38
+
39
+ override overrideLocatorRelativePosition(): Optional<LocatorRelativePosition> {
40
+ return 'Same';
41
+ }
42
+
43
+ async getMenuItemByLabel(label: string): Promise<MenuItemDriver | null> {
44
+ for await (const item of listHelper.getListItemIterator(this, menuItemLocator, MenuItemDriver)) {
45
+ const itemLabel = await item.label();
46
+ if (itemLabel === label) {
47
+ return item;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ async selectByLabel(label: string): Promise<void> {
54
+ const item = await this.getMenuItemByLabel(label);
55
+ if (item) {
56
+ await item.click();
57
+ } else {
58
+ throw new MenuItemNotFoundError(label, this);
59
+ }
60
+ }
61
+
62
+ get driverName(): string {
63
+ return 'MuiV9MenuDriver';
64
+ }
65
+ }
@@ -0,0 +1,15 @@
1
+ import { ListItemDriver } from './ListItemDriver';
2
+
3
+ /**
4
+ * @internal
5
+ */
6
+ export class MenuItemDriver extends ListItemDriver {
7
+ async value(): Promise<string | null> {
8
+ const value = await this.interactor.getAttribute(this.locator, 'data-value');
9
+ return value ?? null;
10
+ }
11
+
12
+ override get driverName(): string {
13
+ return 'MuiV9MenuItemDriver';
14
+ }
15
+ }
@@ -0,0 +1,98 @@
1
+ import { byCssClass, ContainerDriver, locatorUtil, PartLocator, ScenePart } from '@atomic-testing/core';
2
+
3
+ const backdropLocator = byCssClass('MuiBackdrop-root');
4
+ const defaultTransitionDuration = 250;
5
+
6
+ /**
7
+ * Shared base for MUI portal-rendered overlays (Drawer today; Dialog, Menu,
8
+ * Popover and SpeedDial are prospective consumers). It owns the open/close
9
+ * lifecycle that {@link DialogDriver} first proved out: `isOpen` derived from the
10
+ * visible surface, `waitForOpen`/`waitForClose` spanning the transition, and
11
+ * `closeByBackdrop`.
12
+ *
13
+ * Subclasses supply {@link getSurfaceLocator} (the element whose visibility means
14
+ * "open") and, when the overlay is portal-rendered, override
15
+ * `overriddenParentLocator()`/`overrideLocatorRelativePosition()` to re-root.
16
+ *
17
+ * `closeByEscape` is intentionally absent: keyboard dismissal needs a key-press
18
+ * primitive the `Interactor` interface does not yet expose, which would have to be
19
+ * added to every interactor (DOM/React/Vue/Playwright). It is deferred rather than
20
+ * partially implemented.
21
+ */
22
+ export abstract class OverlayDriver<ContentT extends ScenePart, T extends ScenePart = {}> extends ContainerDriver<
23
+ ContentT,
24
+ T
25
+ > {
26
+ /**
27
+ * Locator of the surface whose visibility reflects the open state (e.g. the
28
+ * drawer/dialog paper).
29
+ */
30
+ protected abstract getSurfaceLocator(): PartLocator;
31
+
32
+ /**
33
+ * Locator of the dismissible backdrop. Defaults to MUI's `.MuiBackdrop-root`.
34
+ */
35
+ protected getBackdropLocator(): PartLocator {
36
+ return locatorUtil.append(this.locator, backdropLocator);
37
+ }
38
+
39
+ /**
40
+ * Whether the overlay is mounted and its surface is visible. Because of open/close
41
+ * transitions, prefer `waitForOpen()`/`waitForClose()` immediately after an action.
42
+ */
43
+ async isOpen(): Promise<boolean> {
44
+ if (!(await this.exists())) {
45
+ return false;
46
+ }
47
+ return this.interactor.isVisible(this.getSurfaceLocator());
48
+ }
49
+
50
+ /**
51
+ * Wait until the overlay is open.
52
+ * @returns true once open within the timeout.
53
+ */
54
+ async waitForOpen(timeoutMs: number = defaultTransitionDuration): Promise<boolean> {
55
+ const result = await this.interactor.waitUntil({
56
+ probeFn: () => this.isOpen(),
57
+ terminateCondition: true,
58
+ timeoutMs,
59
+ });
60
+ return result === true;
61
+ }
62
+
63
+ /**
64
+ * Wait until the overlay is closed.
65
+ * @returns true once closed within the timeout.
66
+ */
67
+ async waitForClose(timeoutMs: number = defaultTransitionDuration): Promise<boolean> {
68
+ const result = await this.interactor.waitUntil({
69
+ probeFn: () => this.isOpen(),
70
+ terminateCondition: false,
71
+ timeoutMs,
72
+ });
73
+ if (result === false) {
74
+ return true;
75
+ }
76
+ // Under React's act() the close transition can commit only when the polling
77
+ // act block exits (seen with MUI v5 in jsdom), so the loop above observes the
78
+ // overlay as still open. A final fresh read reflects the now-committed state.
79
+ return !(await this.isOpen());
80
+ }
81
+
82
+ /**
83
+ * Dismiss by clicking the backdrop, then wait for the close transition. Whether
84
+ * it actually closes depends on the consumer honoring the backdrop dismissal; the
85
+ * returned boolean reflects the observed close, not merely the click.
86
+ */
87
+ async closeByBackdrop(timeoutMs: number = defaultTransitionDuration): Promise<boolean> {
88
+ const backdrop = this.getBackdropLocator();
89
+ if (await this.interactor.exists(backdrop)) {
90
+ // A single click suffices in both worlds: a real browser click is a full
91
+ // mouse sequence, and jsdom dispatches the click event MUI's backdrop
92
+ // onClick listens for. (A separate trailing click would race the dismissal
93
+ // and miss the unmounting backdrop in Playwright.)
94
+ await this.interactor.click(backdrop);
95
+ }
96
+ return this.waitForClose(timeoutMs);
97
+ }
98
+ }