@wordpress/ui 0.10.0 → 0.11.1-next.v.202604091042.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/CHANGELOG.md +27 -0
- package/CONTRIBUTING.md +25 -0
- package/README.md +22 -2
- package/build/alert-dialog/context.cjs +6 -1
- package/build/alert-dialog/context.cjs.map +2 -2
- package/build/alert-dialog/popup.cjs +105 -33
- package/build/alert-dialog/popup.cjs.map +4 -4
- package/build/alert-dialog/root.cjs +106 -6
- package/build/alert-dialog/root.cjs.map +2 -2
- package/build/alert-dialog/trigger.cjs +4 -14
- package/build/alert-dialog/trigger.cjs.map +3 -3
- package/build/alert-dialog/types.cjs.map +1 -1
- package/build/button/button.cjs +16 -6
- package/build/button/button.cjs.map +3 -3
- package/build/card/content.cjs +3 -3
- package/build/card/content.cjs.map +1 -1
- package/build/card/full-bleed.cjs +3 -3
- package/build/card/full-bleed.cjs.map +1 -1
- package/build/card/header.cjs +3 -3
- package/build/card/header.cjs.map +1 -1
- package/build/card/root.cjs +3 -3
- package/build/card/root.cjs.map +1 -1
- package/build/card/title.cjs +3 -3
- package/build/card/title.cjs.map +1 -1
- package/build/collapsible-card/header.cjs +3 -3
- package/build/collapsible-card/header.cjs.map +2 -2
- package/build/empty-state/title.cjs.map +2 -2
- package/build/form/primitives/field/description.cjs +17 -4
- package/build/form/primitives/field/description.cjs.map +3 -3
- package/build/form/primitives/field/details.cjs +3 -3
- package/build/form/primitives/field/details.cjs.map +2 -2
- package/build/form/primitives/field/label.cjs +3 -3
- package/build/form/primitives/field/label.cjs.map +2 -2
- package/build/form/primitives/fieldset/description.cjs +20 -4
- package/build/form/primitives/fieldset/description.cjs.map +3 -3
- package/build/form/primitives/fieldset/details.cjs +3 -3
- package/build/form/primitives/fieldset/details.cjs.map +2 -2
- package/build/form/primitives/fieldset/legend.cjs +3 -3
- package/build/form/primitives/fieldset/legend.cjs.map +2 -2
- package/build/form/primitives/input/input.cjs +23 -7
- package/build/form/primitives/input/input.cjs.map +3 -3
- package/build/form/primitives/input-layout/input-layout.cjs +10 -0
- package/build/form/primitives/input-layout/input-layout.cjs.map +3 -3
- package/build/form/primitives/select/trigger.cjs +3 -3
- package/build/form/primitives/select/trigger.cjs.map +2 -2
- package/build/form/primitives/textarea/textarea.cjs +20 -1
- package/build/form/primitives/textarea/textarea.cjs.map +3 -3
- package/build/index.cjs +3 -0
- package/build/index.cjs.map +2 -2
- package/build/link/link.cjs +16 -6
- package/build/link/link.cjs.map +3 -3
- package/build/popover/arrow.cjs +94 -0
- package/build/popover/arrow.cjs.map +7 -0
- package/build/popover/close.cjs +45 -0
- package/build/popover/close.cjs.map +7 -0
- package/build/popover/context.cjs +76 -0
- package/build/popover/context.cjs.map +7 -0
- package/build/popover/description.cjs +70 -0
- package/build/popover/description.cjs.map +7 -0
- package/build/popover/index.cjs +49 -0
- package/build/popover/index.cjs.map +7 -0
- package/build/popover/popup.cjs +138 -0
- package/build/popover/popup.cjs.map +7 -0
- package/build/popover/root.cjs +35 -0
- package/build/popover/root.cjs.map +7 -0
- package/build/popover/title.cjs +56 -0
- package/build/popover/title.cjs.map +7 -0
- package/build/popover/trigger.cjs +38 -0
- package/build/popover/trigger.cjs.map +7 -0
- package/build/popover/types.cjs +19 -0
- package/build/popover/types.cjs.map +7 -0
- package/build/text/text.cjs +20 -5
- package/build/text/text.cjs.map +3 -3
- package/build/utils/use-deprioritized-initial-focus.cjs.map +2 -2
- package/build-module/alert-dialog/context.mjs +6 -1
- package/build-module/alert-dialog/context.mjs.map +2 -2
- package/build-module/alert-dialog/popup.mjs +107 -33
- package/build-module/alert-dialog/popup.mjs.map +4 -4
- package/build-module/alert-dialog/root.mjs +113 -7
- package/build-module/alert-dialog/root.mjs.map +2 -2
- package/build-module/alert-dialog/trigger.mjs +4 -4
- package/build-module/alert-dialog/trigger.mjs.map +3 -3
- package/build-module/button/button.mjs +16 -6
- package/build-module/button/button.mjs.map +3 -3
- package/build-module/card/content.mjs +3 -3
- package/build-module/card/content.mjs.map +1 -1
- package/build-module/card/full-bleed.mjs +3 -3
- package/build-module/card/full-bleed.mjs.map +1 -1
- package/build-module/card/header.mjs +3 -3
- package/build-module/card/header.mjs.map +1 -1
- package/build-module/card/root.mjs +3 -3
- package/build-module/card/root.mjs.map +1 -1
- package/build-module/card/title.mjs +3 -3
- package/build-module/card/title.mjs.map +1 -1
- package/build-module/collapsible-card/header.mjs +3 -3
- package/build-module/collapsible-card/header.mjs.map +2 -2
- package/build-module/empty-state/title.mjs.map +2 -2
- package/build-module/form/primitives/field/description.mjs +17 -4
- package/build-module/form/primitives/field/description.mjs.map +3 -3
- package/build-module/form/primitives/field/details.mjs +3 -3
- package/build-module/form/primitives/field/details.mjs.map +2 -2
- package/build-module/form/primitives/field/label.mjs +3 -3
- package/build-module/form/primitives/field/label.mjs.map +2 -2
- package/build-module/form/primitives/fieldset/description.mjs +20 -4
- package/build-module/form/primitives/fieldset/description.mjs.map +3 -3
- package/build-module/form/primitives/fieldset/details.mjs +3 -3
- package/build-module/form/primitives/fieldset/details.mjs.map +2 -2
- package/build-module/form/primitives/fieldset/legend.mjs +3 -3
- package/build-module/form/primitives/fieldset/legend.mjs.map +2 -2
- package/build-module/form/primitives/input/input.mjs +23 -7
- package/build-module/form/primitives/input/input.mjs.map +3 -3
- package/build-module/form/primitives/input-layout/input-layout.mjs +10 -0
- package/build-module/form/primitives/input-layout/input-layout.mjs.map +3 -3
- package/build-module/form/primitives/select/trigger.mjs +3 -3
- package/build-module/form/primitives/select/trigger.mjs.map +2 -2
- package/build-module/form/primitives/textarea/textarea.mjs +20 -1
- package/build-module/form/primitives/textarea/textarea.mjs.map +3 -3
- package/build-module/index.mjs +2 -0
- package/build-module/index.mjs.map +2 -2
- package/build-module/link/link.mjs +16 -6
- package/build-module/link/link.mjs.map +3 -3
- package/build-module/popover/arrow.mjs +59 -0
- package/build-module/popover/arrow.mjs.map +7 -0
- package/build-module/popover/close.mjs +20 -0
- package/build-module/popover/close.mjs.map +7 -0
- package/build-module/popover/context.mjs +57 -0
- package/build-module/popover/context.mjs.map +7 -0
- package/build-module/popover/description.mjs +35 -0
- package/build-module/popover/description.mjs.map +7 -0
- package/build-module/popover/index.mjs +18 -0
- package/build-module/popover/index.mjs.map +7 -0
- package/build-module/popover/popup.mjs +105 -0
- package/build-module/popover/popup.mjs.map +7 -0
- package/build-module/popover/root.mjs +10 -0
- package/build-module/popover/root.mjs.map +7 -0
- package/build-module/popover/title.mjs +31 -0
- package/build-module/popover/title.mjs.map +7 -0
- package/build-module/popover/trigger.mjs +13 -0
- package/build-module/popover/trigger.mjs.map +7 -0
- package/build-module/popover/types.mjs +1 -0
- package/build-module/popover/types.mjs.map +7 -0
- package/build-module/text/text.mjs +20 -5
- package/build-module/text/text.mjs.map +3 -3
- package/build-module/utils/use-deprioritized-initial-focus.mjs.map +2 -2
- package/build-types/alert-dialog/context.d.ts +6 -3
- package/build-types/alert-dialog/context.d.ts.map +1 -1
- package/build-types/alert-dialog/popup.d.ts.map +1 -1
- package/build-types/alert-dialog/root.d.ts +2 -8
- package/build-types/alert-dialog/root.d.ts.map +1 -1
- package/build-types/alert-dialog/stories/index.story.d.ts +18 -6
- package/build-types/alert-dialog/stories/index.story.d.ts.map +1 -1
- package/build-types/alert-dialog/trigger.d.ts +2 -1
- package/build-types/alert-dialog/trigger.d.ts.map +1 -1
- package/build-types/alert-dialog/types.d.ts +57 -26
- package/build-types/alert-dialog/types.d.ts.map +1 -1
- package/build-types/button/button.d.ts.map +1 -1
- package/build-types/card/stories/index.story.d.ts.map +1 -1
- package/build-types/empty-state/title.d.ts.map +1 -1
- package/build-types/form/primitives/field/description.d.ts.map +1 -1
- package/build-types/form/primitives/fieldset/description.d.ts.map +1 -1
- package/build-types/form/primitives/input/input.d.ts.map +1 -1
- package/build-types/form/primitives/input-layout/input-layout.d.ts.map +1 -1
- package/build-types/form/primitives/textarea/textarea.d.ts.map +1 -1
- package/build-types/form/stories/shared.d.ts.map +1 -1
- package/build-types/index.d.ts +1 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/link/link.d.ts.map +1 -1
- package/build-types/popover/arrow.d.ts +10 -0
- package/build-types/popover/arrow.d.ts.map +1 -0
- package/build-types/popover/close.d.ts +11 -0
- package/build-types/popover/close.d.ts.map +1 -0
- package/build-types/popover/context.d.ts +22 -0
- package/build-types/popover/context.d.ts.map +1 -0
- package/build-types/popover/description.d.ts +10 -0
- package/build-types/popover/description.d.ts.map +1 -0
- package/build-types/popover/index.d.ts +9 -0
- package/build-types/popover/index.d.ts.map +1 -0
- package/build-types/popover/popup.d.ts +11 -0
- package/build-types/popover/popup.d.ts.map +1 -0
- package/build-types/popover/root.d.ts +37 -0
- package/build-types/popover/root.d.ts.map +1 -0
- package/build-types/popover/stories/index.story.d.ts +211 -0
- package/build-types/popover/stories/index.story.d.ts.map +1 -0
- package/build-types/popover/stories/utils.d.ts +25 -0
- package/build-types/popover/stories/utils.d.ts.map +1 -0
- package/build-types/popover/test/index.test.d.ts +2 -0
- package/build-types/popover/test/index.test.d.ts.map +1 -0
- package/build-types/popover/title.d.ts +20 -0
- package/build-types/popover/title.d.ts.map +1 -0
- package/build-types/popover/trigger.d.ts +10 -0
- package/build-types/popover/trigger.d.ts.map +1 -0
- package/build-types/popover/types.d.ts +83 -0
- package/build-types/popover/types.d.ts.map +1 -0
- package/build-types/text/stories/index.story.d.ts +4 -0
- package/build-types/text/stories/index.story.d.ts.map +1 -1
- package/build-types/text/text.d.ts.map +1 -1
- package/build-types/utils/use-deprioritized-initial-focus.d.ts +6 -5
- package/build-types/utils/use-deprioritized-initial-focus.d.ts.map +1 -1
- package/package.json +11 -11
- package/src/alert-dialog/context.tsx +12 -4
- package/src/alert-dialog/popup.tsx +91 -33
- package/src/alert-dialog/root.tsx +191 -13
- package/src/alert-dialog/stories/index.story.tsx +116 -65
- package/src/alert-dialog/style.module.css +11 -0
- package/src/alert-dialog/test/index.test.tsx +1265 -347
- package/src/alert-dialog/trigger.tsx +2 -2
- package/src/alert-dialog/types.ts +59 -28
- package/src/button/button.tsx +2 -0
- package/src/button/style.module.css +4 -0
- package/src/card/stories/index.story.tsx +0 -1
- package/src/card/style.module.css +1 -1
- package/src/card/test/index.test.tsx +0 -1
- package/src/empty-state/title.tsx +0 -1
- package/src/form/primitives/field/description.tsx +6 -1
- package/src/form/primitives/fieldset/description.tsx +9 -1
- package/src/form/primitives/input/input.tsx +6 -1
- package/src/form/primitives/input/style.module.css +4 -0
- package/src/form/primitives/input-layout/input-layout.tsx +2 -0
- package/src/form/primitives/textarea/textarea.tsx +10 -1
- package/src/form/stories/shared.tsx +4 -2
- package/src/index.ts +1 -0
- package/src/link/link.tsx +2 -0
- package/src/link/style.module.css +10 -0
- package/src/popover/arrow.tsx +49 -0
- package/src/popover/close.tsx +24 -0
- package/src/popover/context.tsx +100 -0
- package/src/popover/description.tsx +34 -0
- package/src/popover/index.ts +9 -0
- package/src/popover/popup.tsx +106 -0
- package/src/popover/root.tsx +41 -0
- package/src/popover/stories/index.story.tsx +1315 -0
- package/src/popover/stories/utils.tsx +91 -0
- package/src/popover/style.module.css +64 -0
- package/src/popover/test/index.test.tsx +727 -0
- package/src/popover/title.tsx +50 -0
- package/src/popover/trigger.tsx +17 -0
- package/src/popover/types.ts +113 -0
- package/src/text/stories/index.story.tsx +4 -2
- package/src/text/style.module.css +26 -0
- package/src/text/test/index.test.tsx +1 -4
- package/src/text/text.tsx +8 -1
- package/src/utils/css/field.module.css +4 -1
- package/src/utils/css/focus.module.css +7 -5
- package/src/utils/css/global-css-defense.module.css +117 -0
- package/src/utils/use-deprioritized-initial-focus.ts +5 -4
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { speak } from '@wordpress/a11y';
|
|
2
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
2
3
|
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import { createRef
|
|
4
|
+
import { createRef } from '@wordpress/element';
|
|
4
5
|
|
|
5
6
|
import * as AlertDialog from '..';
|
|
7
|
+
import type { ConfirmResult } from '../types';
|
|
8
|
+
|
|
9
|
+
jest.mock( '@wordpress/a11y', () => ( {
|
|
10
|
+
speak: jest.fn(),
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
function createDeferred() {
|
|
14
|
+
let resolve!: ( value?: ConfirmResult ) => void;
|
|
15
|
+
let reject!: ( reason?: unknown ) => void;
|
|
16
|
+
const promise = new Promise< ConfirmResult >( ( res, rej ) => {
|
|
17
|
+
resolve = res;
|
|
18
|
+
reject = rej;
|
|
19
|
+
} );
|
|
20
|
+
return { promise, resolve, reject };
|
|
21
|
+
}
|
|
6
22
|
|
|
7
23
|
describe( 'AlertDialog', () => {
|
|
8
24
|
it( 'forwards ref', () => {
|
|
@@ -14,12 +30,8 @@ describe( 'AlertDialog', () => {
|
|
|
14
30
|
<AlertDialog.Trigger ref={ triggerRef }>
|
|
15
31
|
Open
|
|
16
32
|
</AlertDialog.Trigger>
|
|
17
|
-
<AlertDialog.Popup
|
|
18
|
-
|
|
19
|
-
title="Test Title"
|
|
20
|
-
onConfirm={ jest.fn() }
|
|
21
|
-
>
|
|
22
|
-
Test message content
|
|
33
|
+
<AlertDialog.Popup ref={ popupRef } title="Test Title">
|
|
34
|
+
Content
|
|
23
35
|
</AlertDialog.Popup>
|
|
24
36
|
</AlertDialog.Root>
|
|
25
37
|
);
|
|
@@ -28,10 +40,10 @@ describe( 'AlertDialog', () => {
|
|
|
28
40
|
expect( popupRef.current ).toBeInstanceOf( HTMLDivElement );
|
|
29
41
|
} );
|
|
30
42
|
|
|
31
|
-
it( 'renders with title,
|
|
43
|
+
it( 'renders with title, children, and default buttons', async () => {
|
|
32
44
|
render(
|
|
33
45
|
<AlertDialog.Root open onOpenChange={ jest.fn() }>
|
|
34
|
-
<AlertDialog.Popup title="Test Title"
|
|
46
|
+
<AlertDialog.Popup title="Test Title">
|
|
35
47
|
Test message content
|
|
36
48
|
</AlertDialog.Popup>
|
|
37
49
|
</AlertDialog.Root>
|
|
@@ -51,34 +63,27 @@ describe( 'AlertDialog', () => {
|
|
|
51
63
|
).toBeVisible();
|
|
52
64
|
} );
|
|
53
65
|
|
|
54
|
-
it( 'renders
|
|
66
|
+
it( 'renders description when provided', async () => {
|
|
55
67
|
render(
|
|
56
68
|
<AlertDialog.Root open onOpenChange={ jest.fn() }>
|
|
57
69
|
<AlertDialog.Popup
|
|
58
|
-
title="
|
|
59
|
-
|
|
70
|
+
title="Test Title"
|
|
71
|
+
description="This is a description"
|
|
60
72
|
>
|
|
61
|
-
|
|
73
|
+
Body content
|
|
62
74
|
</AlertDialog.Popup>
|
|
63
75
|
</AlertDialog.Root>
|
|
64
76
|
);
|
|
65
77
|
|
|
66
78
|
await waitFor( () => {
|
|
67
|
-
expect( screen.
|
|
79
|
+
expect( screen.getByText( 'This is a description' ) ).toBeVisible();
|
|
68
80
|
} );
|
|
69
81
|
} );
|
|
70
82
|
|
|
71
|
-
it( 'renders with role="alertdialog" for
|
|
83
|
+
it( 'renders with role="alertdialog" for default intent', async () => {
|
|
72
84
|
render(
|
|
73
|
-
<AlertDialog.Root
|
|
74
|
-
|
|
75
|
-
open
|
|
76
|
-
onOpenChange={ jest.fn() }
|
|
77
|
-
>
|
|
78
|
-
<AlertDialog.Popup
|
|
79
|
-
title="Irreversible Dialog"
|
|
80
|
-
onConfirm={ jest.fn() }
|
|
81
|
-
>
|
|
85
|
+
<AlertDialog.Root open onOpenChange={ jest.fn() }>
|
|
86
|
+
<AlertDialog.Popup title="Default Dialog">
|
|
82
87
|
Content
|
|
83
88
|
</AlertDialog.Popup>
|
|
84
89
|
</AlertDialog.Root>
|
|
@@ -89,449 +94,1362 @@ describe( 'AlertDialog', () => {
|
|
|
89
94
|
} );
|
|
90
95
|
} );
|
|
91
96
|
|
|
92
|
-
it( '
|
|
93
|
-
const onConfirm = jest.fn();
|
|
94
|
-
const onOpenChange = jest.fn();
|
|
95
|
-
|
|
97
|
+
it( 'renders with role="alertdialog" for irreversible intent', async () => {
|
|
96
98
|
render(
|
|
97
|
-
<AlertDialog.Root open onOpenChange={
|
|
99
|
+
<AlertDialog.Root open onOpenChange={ jest.fn() }>
|
|
98
100
|
<AlertDialog.Popup
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
intent="irreversible"
|
|
102
|
+
title="Irreversible Dialog"
|
|
101
103
|
>
|
|
102
|
-
|
|
104
|
+
Content
|
|
103
105
|
</AlertDialog.Popup>
|
|
104
106
|
</AlertDialog.Root>
|
|
105
107
|
);
|
|
106
108
|
|
|
107
109
|
await waitFor( () => {
|
|
108
|
-
expect(
|
|
109
|
-
screen.getByRole( 'button', { name: 'OK' } )
|
|
110
|
-
).toBeVisible();
|
|
110
|
+
expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
|
|
111
111
|
} );
|
|
112
|
-
|
|
113
|
-
await userEvent.click( screen.getByRole( 'button', { name: 'OK' } ) );
|
|
114
|
-
|
|
115
|
-
expect( onConfirm ).toHaveBeenCalledTimes( 1 );
|
|
116
|
-
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
117
|
-
false,
|
|
118
|
-
expect.objectContaining( { reason: 'close-press' } )
|
|
119
|
-
);
|
|
120
112
|
} );
|
|
121
113
|
|
|
122
|
-
it( '
|
|
123
|
-
const onConfirm = jest.fn();
|
|
124
|
-
const onOpenChange = jest.fn();
|
|
125
|
-
|
|
114
|
+
it( 'uses custom button labels', async () => {
|
|
126
115
|
render(
|
|
127
|
-
<AlertDialog.Root open onOpenChange={
|
|
116
|
+
<AlertDialog.Root open onOpenChange={ jest.fn() }>
|
|
128
117
|
<AlertDialog.Popup
|
|
129
|
-
title="
|
|
130
|
-
|
|
118
|
+
title="Custom Labels"
|
|
119
|
+
confirmButtonText="Yes, do it"
|
|
120
|
+
cancelButtonText="No, go back"
|
|
131
121
|
>
|
|
132
|
-
|
|
122
|
+
Content
|
|
133
123
|
</AlertDialog.Popup>
|
|
134
124
|
</AlertDialog.Root>
|
|
135
125
|
);
|
|
136
126
|
|
|
137
127
|
await waitFor( () => {
|
|
138
128
|
expect(
|
|
139
|
-
screen.getByRole( 'button', { name: '
|
|
129
|
+
screen.getByRole( 'button', { name: 'Yes, do it' } )
|
|
140
130
|
).toBeVisible();
|
|
141
131
|
} );
|
|
142
132
|
|
|
143
|
-
|
|
144
|
-
screen.getByRole( 'button', { name: '
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
148
|
-
false,
|
|
149
|
-
expect.objectContaining( { reason: 'close-press' } )
|
|
150
|
-
);
|
|
151
|
-
expect( onConfirm ).not.toHaveBeenCalled();
|
|
133
|
+
expect(
|
|
134
|
+
screen.getByRole( 'button', { name: 'No, go back' } )
|
|
135
|
+
).toBeVisible();
|
|
152
136
|
} );
|
|
153
137
|
|
|
154
|
-
it( '
|
|
155
|
-
const onOpenChange = jest.fn();
|
|
156
|
-
|
|
138
|
+
it( 'opens dialog when Trigger is clicked', async () => {
|
|
157
139
|
render(
|
|
158
|
-
<AlertDialog.Root
|
|
159
|
-
<AlertDialog.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
>
|
|
163
|
-
Content
|
|
140
|
+
<AlertDialog.Root>
|
|
141
|
+
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
|
|
142
|
+
<AlertDialog.Popup title="Trigger Test">
|
|
143
|
+
Dialog content
|
|
164
144
|
</AlertDialog.Popup>
|
|
165
145
|
</AlertDialog.Root>
|
|
166
146
|
);
|
|
167
147
|
|
|
148
|
+
expect(
|
|
149
|
+
screen.queryByText( 'Dialog content' )
|
|
150
|
+
).not.toBeInTheDocument();
|
|
151
|
+
|
|
152
|
+
await userEvent.click( screen.getByRole( 'button', { name: 'Open' } ) );
|
|
153
|
+
|
|
168
154
|
await waitFor( () => {
|
|
169
|
-
expect( screen.getByText( '
|
|
155
|
+
expect( screen.getByText( 'Trigger Test' ) ).toBeVisible();
|
|
170
156
|
} );
|
|
171
157
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
175
|
-
false,
|
|
176
|
-
expect.objectContaining( { reason: 'escape-key' } )
|
|
177
|
-
);
|
|
158
|
+
expect( screen.getByText( 'Dialog content' ) ).toBeVisible();
|
|
178
159
|
} );
|
|
179
160
|
|
|
180
|
-
|
|
181
|
-
|
|
161
|
+
describe( 'sync confirm flow', () => {
|
|
162
|
+
it( 'calls onConfirm and closes on confirm click', async () => {
|
|
163
|
+
const onConfirm = jest.fn();
|
|
164
|
+
const onOpenChange = jest.fn();
|
|
182
165
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
onConfirm={
|
|
166
|
+
render(
|
|
167
|
+
<AlertDialog.Root
|
|
168
|
+
open
|
|
169
|
+
onOpenChange={ onOpenChange }
|
|
170
|
+
onConfirm={ onConfirm }
|
|
188
171
|
>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
await waitFor( () => {
|
|
195
|
-
expect( screen.getByText( 'Default Dialog' ) ).toBeVisible();
|
|
196
|
-
} );
|
|
197
|
-
|
|
198
|
-
await userEvent.click( document.body );
|
|
172
|
+
<AlertDialog.Popup title="Sync Test">
|
|
173
|
+
Content
|
|
174
|
+
</AlertDialog.Popup>
|
|
175
|
+
</AlertDialog.Root>
|
|
176
|
+
);
|
|
199
177
|
|
|
200
|
-
|
|
201
|
-
|
|
178
|
+
await waitFor( () => {
|
|
179
|
+
expect(
|
|
180
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
181
|
+
).toBeVisible();
|
|
182
|
+
} );
|
|
202
183
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
intent="irreversible"
|
|
207
|
-
open
|
|
208
|
-
onOpenChange={ jest.fn() }
|
|
209
|
-
>
|
|
210
|
-
<AlertDialog.Popup
|
|
211
|
-
title="Irreversible Dialog"
|
|
212
|
-
onConfirm={ jest.fn() }
|
|
213
|
-
>
|
|
214
|
-
Irreversible message content
|
|
215
|
-
</AlertDialog.Popup>
|
|
216
|
-
</AlertDialog.Root>
|
|
217
|
-
);
|
|
184
|
+
await userEvent.click(
|
|
185
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
186
|
+
);
|
|
218
187
|
|
|
219
|
-
|
|
220
|
-
|
|
188
|
+
expect( onConfirm ).toHaveBeenCalledTimes( 1 );
|
|
189
|
+
await waitFor( () => {
|
|
190
|
+
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
191
|
+
false,
|
|
192
|
+
expect.objectContaining( {
|
|
193
|
+
reason: 'imperative-action',
|
|
194
|
+
} )
|
|
195
|
+
);
|
|
196
|
+
} );
|
|
221
197
|
} );
|
|
222
198
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
).toBeVisible();
|
|
226
|
-
expect(
|
|
227
|
-
screen.queryByRole( 'button', { name: 'Close' } )
|
|
228
|
-
).not.toBeInTheDocument();
|
|
229
|
-
expect( screen.getByRole( 'button', { name: 'OK' } ) ).toBeVisible();
|
|
230
|
-
expect(
|
|
231
|
-
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
232
|
-
).toBeVisible();
|
|
233
|
-
} );
|
|
234
|
-
|
|
235
|
-
it( 'calls onOpenChange on escape key for irreversible intent', async () => {
|
|
236
|
-
const onOpenChange = jest.fn();
|
|
199
|
+
it( 'provides well-formed event details on confirm close', async () => {
|
|
200
|
+
const onOpenChange = jest.fn();
|
|
237
201
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
onOpenChange={ onOpenChange }
|
|
243
|
-
>
|
|
244
|
-
<AlertDialog.Popup
|
|
245
|
-
title="Irreversible Dialog"
|
|
202
|
+
render(
|
|
203
|
+
<AlertDialog.Root
|
|
204
|
+
open
|
|
205
|
+
onOpenChange={ onOpenChange }
|
|
246
206
|
onConfirm={ jest.fn() }
|
|
247
207
|
>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
208
|
+
<AlertDialog.Popup title="Details Test">
|
|
209
|
+
Content
|
|
210
|
+
</AlertDialog.Popup>
|
|
211
|
+
</AlertDialog.Root>
|
|
212
|
+
);
|
|
252
213
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
214
|
+
await waitFor( () => {
|
|
215
|
+
expect(
|
|
216
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
217
|
+
).toBeVisible();
|
|
218
|
+
} );
|
|
256
219
|
|
|
257
|
-
|
|
220
|
+
await userEvent.click(
|
|
221
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
222
|
+
);
|
|
258
223
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
224
|
+
await waitFor( () => {
|
|
225
|
+
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
226
|
+
false,
|
|
227
|
+
expect.objectContaining( {
|
|
228
|
+
reason: 'imperative-action',
|
|
229
|
+
} )
|
|
230
|
+
);
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
const details = onOpenChange.mock.calls.find(
|
|
234
|
+
( [ open ]: [ boolean ] ) => ! open
|
|
235
|
+
)?.[ 1 ];
|
|
236
|
+
|
|
237
|
+
expect( details ).toBeDefined();
|
|
238
|
+
expect( typeof details.cancel ).toBe( 'function' );
|
|
239
|
+
expect( typeof details.allowPropagation ).toBe( 'function' );
|
|
240
|
+
expect( typeof details.preventUnmountOnClose ).toBe( 'function' );
|
|
241
|
+
expect( details.event ).toBeInstanceOf( Event );
|
|
242
|
+
} );
|
|
264
243
|
|
|
265
|
-
|
|
266
|
-
|
|
244
|
+
it( 'closes without onConfirm when no handler is provided', async () => {
|
|
245
|
+
const onOpenChange = jest.fn();
|
|
267
246
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
title="Irreversible Dialog"
|
|
276
|
-
onConfirm={ jest.fn() }
|
|
277
|
-
>
|
|
278
|
-
Content
|
|
279
|
-
</AlertDialog.Popup>
|
|
280
|
-
</AlertDialog.Root>
|
|
281
|
-
);
|
|
247
|
+
render(
|
|
248
|
+
<AlertDialog.Root open onOpenChange={ onOpenChange }>
|
|
249
|
+
<AlertDialog.Popup title="No Handler">
|
|
250
|
+
Content
|
|
251
|
+
</AlertDialog.Popup>
|
|
252
|
+
</AlertDialog.Root>
|
|
253
|
+
);
|
|
282
254
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
255
|
+
await waitFor( () => {
|
|
256
|
+
expect(
|
|
257
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
258
|
+
).toBeVisible();
|
|
259
|
+
} );
|
|
286
260
|
|
|
287
|
-
|
|
261
|
+
await userEvent.click(
|
|
262
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
263
|
+
);
|
|
288
264
|
|
|
289
|
-
|
|
265
|
+
await waitFor( () => {
|
|
266
|
+
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
267
|
+
false,
|
|
268
|
+
expect.objectContaining( {
|
|
269
|
+
reason: 'imperative-action',
|
|
270
|
+
} )
|
|
271
|
+
);
|
|
272
|
+
} );
|
|
273
|
+
} );
|
|
290
274
|
} );
|
|
291
275
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
276
|
+
describe( 'cancel and dismiss', () => {
|
|
277
|
+
it( 'closes on cancel click without calling onConfirm', async () => {
|
|
278
|
+
const onConfirm = jest.fn();
|
|
279
|
+
const onOpenChange = jest.fn();
|
|
295
280
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
onOpenChange={ onOpenChange }
|
|
301
|
-
>
|
|
302
|
-
<AlertDialog.Popup
|
|
303
|
-
title="Irreversible Dialog"
|
|
281
|
+
render(
|
|
282
|
+
<AlertDialog.Root
|
|
283
|
+
open
|
|
284
|
+
onOpenChange={ onOpenChange }
|
|
304
285
|
onConfirm={ onConfirm }
|
|
305
286
|
>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
287
|
+
<AlertDialog.Popup title="Cancel Test">
|
|
288
|
+
Content
|
|
289
|
+
</AlertDialog.Popup>
|
|
290
|
+
</AlertDialog.Root>
|
|
291
|
+
);
|
|
310
292
|
|
|
311
|
-
|
|
312
|
-
|
|
293
|
+
await waitFor( () => {
|
|
294
|
+
expect(
|
|
295
|
+
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
296
|
+
).toBeVisible();
|
|
297
|
+
} );
|
|
298
|
+
|
|
299
|
+
await userEvent.click(
|
|
313
300
|
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
314
|
-
)
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
304
|
+
false,
|
|
305
|
+
expect.objectContaining( { reason: 'close-press' } )
|
|
306
|
+
);
|
|
307
|
+
expect( onConfirm ).not.toHaveBeenCalled();
|
|
315
308
|
} );
|
|
316
309
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
);
|
|
310
|
+
it( 'closes on escape key', async () => {
|
|
311
|
+
const onOpenChange = jest.fn();
|
|
320
312
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
313
|
+
render(
|
|
314
|
+
<AlertDialog.Root open onOpenChange={ onOpenChange }>
|
|
315
|
+
<AlertDialog.Popup title="Escape Test">
|
|
316
|
+
Content
|
|
317
|
+
</AlertDialog.Popup>
|
|
318
|
+
</AlertDialog.Root>
|
|
319
|
+
);
|
|
327
320
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
321
|
+
await waitFor( () => {
|
|
322
|
+
expect( screen.getByText( 'Escape Test' ) ).toBeVisible();
|
|
323
|
+
} );
|
|
331
324
|
|
|
332
|
-
|
|
333
|
-
<AlertDialog.Root
|
|
334
|
-
intent="irreversible"
|
|
335
|
-
open
|
|
336
|
-
onOpenChange={ onOpenChange }
|
|
337
|
-
>
|
|
338
|
-
<AlertDialog.Popup
|
|
339
|
-
title="Irreversible Dialog"
|
|
340
|
-
onConfirm={ onConfirm }
|
|
341
|
-
>
|
|
342
|
-
Content
|
|
343
|
-
</AlertDialog.Popup>
|
|
344
|
-
</AlertDialog.Root>
|
|
345
|
-
);
|
|
325
|
+
await userEvent.keyboard( '{Escape}' );
|
|
346
326
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
)
|
|
327
|
+
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
328
|
+
false,
|
|
329
|
+
expect.objectContaining( { reason: 'escape-key' } )
|
|
330
|
+
);
|
|
351
331
|
} );
|
|
352
332
|
|
|
353
|
-
|
|
333
|
+
it( 'does not close on backdrop click', async () => {
|
|
334
|
+
const onOpenChange = jest.fn();
|
|
354
335
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
it( 'disables both buttons when loading', async () => {
|
|
363
|
-
render(
|
|
364
|
-
<AlertDialog.Root open onOpenChange={ jest.fn() }>
|
|
365
|
-
<AlertDialog.Popup
|
|
366
|
-
title="Loading Test"
|
|
367
|
-
onConfirm={ jest.fn() }
|
|
368
|
-
loading
|
|
369
|
-
>
|
|
370
|
-
Content
|
|
371
|
-
</AlertDialog.Popup>
|
|
372
|
-
</AlertDialog.Root>
|
|
373
|
-
);
|
|
336
|
+
render(
|
|
337
|
+
<AlertDialog.Root open onOpenChange={ onOpenChange }>
|
|
338
|
+
<AlertDialog.Popup title="Backdrop Test">
|
|
339
|
+
Content
|
|
340
|
+
</AlertDialog.Popup>
|
|
341
|
+
</AlertDialog.Root>
|
|
342
|
+
);
|
|
374
343
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
).toBeVisible();
|
|
379
|
-
} );
|
|
344
|
+
await waitFor( () => {
|
|
345
|
+
expect( screen.getByText( 'Backdrop Test' ) ).toBeVisible();
|
|
346
|
+
} );
|
|
380
347
|
|
|
381
|
-
|
|
382
|
-
'aria-disabled',
|
|
383
|
-
'true'
|
|
384
|
-
);
|
|
348
|
+
await userEvent.click( document.body );
|
|
385
349
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
).toHaveAttribute( 'aria-disabled', 'true' );
|
|
350
|
+
expect( onOpenChange ).not.toHaveBeenCalled();
|
|
351
|
+
} );
|
|
389
352
|
} );
|
|
390
353
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
<AlertDialog.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
354
|
+
describe( 'irreversible intent', () => {
|
|
355
|
+
it( 'renders title and buttons', async () => {
|
|
356
|
+
render(
|
|
357
|
+
<AlertDialog.Root open onOpenChange={ jest.fn() }>
|
|
358
|
+
<AlertDialog.Popup
|
|
359
|
+
intent="irreversible"
|
|
360
|
+
title="Irreversible Dialog"
|
|
361
|
+
>
|
|
362
|
+
Irreversible message content
|
|
363
|
+
</AlertDialog.Popup>
|
|
364
|
+
</AlertDialog.Root>
|
|
365
|
+
);
|
|
403
366
|
|
|
404
|
-
|
|
367
|
+
await waitFor( () => {
|
|
368
|
+
expect(
|
|
369
|
+
screen.getByText( 'Irreversible Dialog' )
|
|
370
|
+
).toBeVisible();
|
|
371
|
+
} );
|
|
372
|
+
|
|
373
|
+
expect(
|
|
374
|
+
screen.getByText( 'Irreversible message content' )
|
|
375
|
+
).toBeVisible();
|
|
405
376
|
expect(
|
|
406
377
|
screen.getByRole( 'button', { name: 'OK' } )
|
|
407
378
|
).toBeVisible();
|
|
379
|
+
expect(
|
|
380
|
+
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
381
|
+
).toBeVisible();
|
|
408
382
|
} );
|
|
409
383
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
384
|
+
it( 'closes on escape key', async () => {
|
|
385
|
+
const onOpenChange = jest.fn();
|
|
413
386
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
387
|
+
render(
|
|
388
|
+
<AlertDialog.Root open onOpenChange={ onOpenChange }>
|
|
389
|
+
<AlertDialog.Popup
|
|
390
|
+
intent="irreversible"
|
|
391
|
+
title="Irreversible Dialog"
|
|
392
|
+
>
|
|
393
|
+
Content
|
|
394
|
+
</AlertDialog.Popup>
|
|
395
|
+
</AlertDialog.Root>
|
|
396
|
+
);
|
|
418
397
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
onConfirm={ jest.fn() }
|
|
425
|
-
confirmButtonText="Yes, do it"
|
|
426
|
-
cancelButtonText="No, go back"
|
|
427
|
-
>
|
|
428
|
-
Custom message
|
|
429
|
-
</AlertDialog.Popup>
|
|
430
|
-
</AlertDialog.Root>
|
|
431
|
-
);
|
|
398
|
+
await waitFor( () => {
|
|
399
|
+
expect(
|
|
400
|
+
screen.getByText( 'Irreversible Dialog' )
|
|
401
|
+
).toBeVisible();
|
|
402
|
+
} );
|
|
432
403
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
404
|
+
await userEvent.keyboard( '{Escape}' );
|
|
405
|
+
|
|
406
|
+
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
407
|
+
false,
|
|
408
|
+
expect.objectContaining( { reason: 'escape-key' } )
|
|
409
|
+
);
|
|
437
410
|
} );
|
|
438
411
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
412
|
+
it( 'does not close on backdrop click', async () => {
|
|
413
|
+
const onOpenChange = jest.fn();
|
|
414
|
+
|
|
415
|
+
render(
|
|
416
|
+
<AlertDialog.Root open onOpenChange={ onOpenChange }>
|
|
417
|
+
<AlertDialog.Popup
|
|
418
|
+
intent="irreversible"
|
|
419
|
+
title="Irreversible Dialog"
|
|
420
|
+
>
|
|
421
|
+
Content
|
|
422
|
+
</AlertDialog.Popup>
|
|
423
|
+
</AlertDialog.Root>
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
await waitFor( () => {
|
|
427
|
+
expect(
|
|
428
|
+
screen.getByText( 'Irreversible Dialog' )
|
|
429
|
+
).toBeVisible();
|
|
430
|
+
} );
|
|
431
|
+
|
|
432
|
+
await userEvent.click( document.body );
|
|
433
|
+
|
|
434
|
+
expect( onOpenChange ).not.toHaveBeenCalled();
|
|
435
|
+
} );
|
|
442
436
|
} );
|
|
443
437
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const
|
|
447
|
-
const [ isLoading, setIsLoading ] = useState( false );
|
|
438
|
+
describe( 'async confirm flow', () => {
|
|
439
|
+
it( 'disables buttons while confirm is pending', async () => {
|
|
440
|
+
const deferred = createDeferred();
|
|
448
441
|
|
|
449
|
-
|
|
442
|
+
render(
|
|
450
443
|
<AlertDialog.Root
|
|
451
|
-
open
|
|
452
|
-
onOpenChange={ (
|
|
453
|
-
|
|
454
|
-
setIsOpen( open );
|
|
455
|
-
}
|
|
456
|
-
} }
|
|
444
|
+
open
|
|
445
|
+
onOpenChange={ jest.fn() }
|
|
446
|
+
onConfirm={ () => deferred.promise }
|
|
457
447
|
>
|
|
458
|
-
<AlertDialog.Popup
|
|
459
|
-
title="Async Test"
|
|
460
|
-
loading={ isLoading }
|
|
461
|
-
onConfirm={ () => setIsLoading( true ) }
|
|
462
|
-
>
|
|
448
|
+
<AlertDialog.Popup title="Async Test">
|
|
463
449
|
Content
|
|
464
450
|
</AlertDialog.Popup>
|
|
465
451
|
</AlertDialog.Root>
|
|
466
452
|
);
|
|
467
|
-
}
|
|
468
453
|
|
|
469
|
-
|
|
454
|
+
await waitFor( () => {
|
|
455
|
+
expect(
|
|
456
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
457
|
+
).toBeVisible();
|
|
458
|
+
} );
|
|
470
459
|
|
|
471
|
-
|
|
472
|
-
|
|
460
|
+
await userEvent.click(
|
|
461
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
await waitFor( () => {
|
|
465
|
+
expect(
|
|
466
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
467
|
+
).toHaveAttribute( 'aria-disabled', 'true' );
|
|
468
|
+
} );
|
|
469
|
+
|
|
470
|
+
expect(
|
|
471
|
+
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
472
|
+
).toHaveAttribute( 'aria-disabled', 'true' );
|
|
473
|
+
|
|
474
|
+
await act( async () => {
|
|
475
|
+
deferred.resolve();
|
|
476
|
+
} );
|
|
473
477
|
} );
|
|
474
478
|
|
|
475
|
-
|
|
479
|
+
it( 'closes dialog when async confirm resolves', async () => {
|
|
480
|
+
const deferred = createDeferred();
|
|
481
|
+
const onOpenChange = jest.fn();
|
|
476
482
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
483
|
+
render(
|
|
484
|
+
<AlertDialog.Root
|
|
485
|
+
open
|
|
486
|
+
onOpenChange={ onOpenChange }
|
|
487
|
+
onConfirm={ () => deferred.promise }
|
|
488
|
+
>
|
|
489
|
+
<AlertDialog.Popup title="Async Resolve">
|
|
490
|
+
Content
|
|
491
|
+
</AlertDialog.Popup>
|
|
492
|
+
</AlertDialog.Root>
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
await waitFor( () => {
|
|
496
|
+
expect(
|
|
497
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
498
|
+
).toBeVisible();
|
|
499
|
+
} );
|
|
500
|
+
|
|
501
|
+
await userEvent.click(
|
|
502
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
await act( async () => {
|
|
506
|
+
deferred.resolve();
|
|
507
|
+
} );
|
|
508
|
+
|
|
509
|
+
await waitFor( () => {
|
|
510
|
+
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
511
|
+
false,
|
|
512
|
+
expect.objectContaining( {
|
|
513
|
+
reason: 'imperative-action',
|
|
514
|
+
} )
|
|
515
|
+
);
|
|
516
|
+
} );
|
|
517
|
+
} );
|
|
518
|
+
|
|
519
|
+
it( 're-enables buttons when async confirm rejects (task failure)', async () => {
|
|
520
|
+
const deferred = createDeferred();
|
|
521
|
+
const consoleSpy = jest
|
|
522
|
+
.spyOn( console, 'error' )
|
|
523
|
+
.mockImplementation( () => {} );
|
|
524
|
+
|
|
525
|
+
render(
|
|
526
|
+
<AlertDialog.Root
|
|
527
|
+
open
|
|
528
|
+
onOpenChange={ jest.fn() }
|
|
529
|
+
onConfirm={ () => deferred.promise }
|
|
530
|
+
>
|
|
531
|
+
<AlertDialog.Popup title="Async Reject">
|
|
532
|
+
Content
|
|
533
|
+
</AlertDialog.Popup>
|
|
534
|
+
</AlertDialog.Root>
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
await waitFor( () => {
|
|
538
|
+
expect(
|
|
539
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
540
|
+
).toBeVisible();
|
|
541
|
+
} );
|
|
542
|
+
|
|
543
|
+
await userEvent.click(
|
|
544
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
await waitFor( () => {
|
|
548
|
+
expect(
|
|
549
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
550
|
+
).toHaveAttribute( 'aria-disabled', 'true' );
|
|
551
|
+
} );
|
|
552
|
+
|
|
553
|
+
await act( async () => {
|
|
554
|
+
deferred.reject( new Error( 'Task failed' ) );
|
|
555
|
+
} );
|
|
556
|
+
|
|
557
|
+
// The error is caught and logged via console.error in Root.
|
|
558
|
+
await waitFor( () => {
|
|
559
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
560
|
+
expect.objectContaining( { message: 'Task failed' } )
|
|
561
|
+
);
|
|
562
|
+
} );
|
|
563
|
+
|
|
564
|
+
await waitFor( () => {
|
|
565
|
+
expect(
|
|
566
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
567
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
568
|
+
} );
|
|
569
|
+
|
|
570
|
+
expect(
|
|
571
|
+
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
572
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
573
|
+
|
|
574
|
+
expect( screen.getByText( 'Async Reject' ) ).toBeVisible();
|
|
575
|
+
|
|
576
|
+
// Throws do NOT render a visible error message.
|
|
577
|
+
expect(
|
|
578
|
+
screen.queryByText( 'Task failed' )
|
|
579
|
+
).not.toBeInTheDocument();
|
|
580
|
+
|
|
581
|
+
consoleSpy.mockRestore();
|
|
582
|
+
} );
|
|
583
|
+
|
|
584
|
+
it( 'keeps dialog open when confirm returns { close: false }', async () => {
|
|
585
|
+
const onOpenChange = jest.fn();
|
|
586
|
+
|
|
587
|
+
render(
|
|
588
|
+
<AlertDialog.Root
|
|
589
|
+
open
|
|
590
|
+
onOpenChange={ onOpenChange }
|
|
591
|
+
onConfirm={ () => ( { close: false } ) }
|
|
592
|
+
>
|
|
593
|
+
<AlertDialog.Popup title="Keep Open">
|
|
594
|
+
Content
|
|
595
|
+
</AlertDialog.Popup>
|
|
596
|
+
</AlertDialog.Root>
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
await waitFor( () => {
|
|
600
|
+
expect(
|
|
601
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
602
|
+
).toBeVisible();
|
|
603
|
+
} );
|
|
604
|
+
|
|
605
|
+
await userEvent.click(
|
|
606
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
await waitFor( () => {
|
|
610
|
+
expect(
|
|
611
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
612
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
613
|
+
} );
|
|
614
|
+
|
|
615
|
+
expect( onOpenChange ).not.toHaveBeenCalledWith(
|
|
616
|
+
false,
|
|
617
|
+
expect.anything()
|
|
618
|
+
);
|
|
619
|
+
expect( screen.getByText( 'Keep Open' ) ).toBeVisible();
|
|
620
|
+
} );
|
|
621
|
+
|
|
622
|
+
it( 'keeps dialog open when async confirm returns { close: false }', async () => {
|
|
623
|
+
const deferred = createDeferred();
|
|
624
|
+
const onOpenChange = jest.fn();
|
|
625
|
+
|
|
626
|
+
render(
|
|
627
|
+
<AlertDialog.Root
|
|
628
|
+
open
|
|
629
|
+
onOpenChange={ onOpenChange }
|
|
630
|
+
onConfirm={ () => deferred.promise }
|
|
631
|
+
>
|
|
632
|
+
<AlertDialog.Popup title="Async Keep Open">
|
|
633
|
+
Content
|
|
634
|
+
</AlertDialog.Popup>
|
|
635
|
+
</AlertDialog.Root>
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
await waitFor( () => {
|
|
639
|
+
expect(
|
|
640
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
641
|
+
).toBeVisible();
|
|
642
|
+
} );
|
|
643
|
+
|
|
644
|
+
await userEvent.click(
|
|
645
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
await waitFor( () => {
|
|
649
|
+
expect(
|
|
650
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
651
|
+
).toHaveAttribute( 'aria-disabled', 'true' );
|
|
652
|
+
} );
|
|
653
|
+
|
|
654
|
+
await act( async () => {
|
|
655
|
+
deferred.resolve( { close: false } );
|
|
656
|
+
} );
|
|
657
|
+
|
|
658
|
+
await waitFor( () => {
|
|
659
|
+
expect(
|
|
660
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
661
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
662
|
+
} );
|
|
663
|
+
|
|
664
|
+
expect( onOpenChange ).not.toHaveBeenCalledWith(
|
|
665
|
+
false,
|
|
666
|
+
expect.anything()
|
|
667
|
+
);
|
|
668
|
+
} );
|
|
669
|
+
|
|
670
|
+
it( 'blocks dismiss while pending by default', async () => {
|
|
671
|
+
const deferred = createDeferred();
|
|
672
|
+
const onOpenChange = jest.fn();
|
|
673
|
+
|
|
674
|
+
render(
|
|
675
|
+
<AlertDialog.Root
|
|
676
|
+
open
|
|
677
|
+
onOpenChange={ onOpenChange }
|
|
678
|
+
onConfirm={ () => deferred.promise }
|
|
679
|
+
>
|
|
680
|
+
<AlertDialog.Popup title="Block Dismiss">
|
|
681
|
+
Content
|
|
682
|
+
</AlertDialog.Popup>
|
|
683
|
+
</AlertDialog.Root>
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
await waitFor( () => {
|
|
687
|
+
expect(
|
|
688
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
689
|
+
).toBeVisible();
|
|
690
|
+
} );
|
|
691
|
+
|
|
692
|
+
await userEvent.click(
|
|
693
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
await waitFor( () => {
|
|
697
|
+
expect(
|
|
698
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
699
|
+
).toHaveAttribute( 'aria-disabled', 'true' );
|
|
700
|
+
} );
|
|
701
|
+
|
|
702
|
+
await userEvent.keyboard( '{Escape}' );
|
|
703
|
+
|
|
704
|
+
expect( onOpenChange ).not.toHaveBeenCalledWith(
|
|
705
|
+
false,
|
|
706
|
+
expect.anything()
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
await act( async () => {
|
|
710
|
+
deferred.resolve();
|
|
711
|
+
} );
|
|
712
|
+
} );
|
|
713
|
+
|
|
714
|
+
it( 'ignores duplicate confirm clicks while pending', async () => {
|
|
715
|
+
const onConfirm = jest.fn(
|
|
716
|
+
() =>
|
|
717
|
+
new Promise< void >( () => {
|
|
718
|
+
// Never resolves
|
|
719
|
+
} )
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
render(
|
|
723
|
+
<AlertDialog.Root
|
|
724
|
+
open
|
|
725
|
+
onOpenChange={ jest.fn() }
|
|
726
|
+
onConfirm={ onConfirm }
|
|
727
|
+
>
|
|
728
|
+
<AlertDialog.Popup title="Double Click">
|
|
729
|
+
Content
|
|
730
|
+
</AlertDialog.Popup>
|
|
731
|
+
</AlertDialog.Root>
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
await waitFor( () => {
|
|
735
|
+
expect(
|
|
736
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
737
|
+
).toBeVisible();
|
|
738
|
+
} );
|
|
739
|
+
|
|
740
|
+
await userEvent.click(
|
|
741
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
742
|
+
);
|
|
743
|
+
await userEvent.click(
|
|
744
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
expect( onConfirm ).toHaveBeenCalledTimes( 1 );
|
|
748
|
+
} );
|
|
482
749
|
} );
|
|
483
750
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
751
|
+
describe( 'uncontrolled mode', () => {
|
|
752
|
+
it( 'renders dialog open when defaultOpen is true', async () => {
|
|
753
|
+
render(
|
|
754
|
+
<AlertDialog.Root defaultOpen>
|
|
755
|
+
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
|
|
756
|
+
<AlertDialog.Popup title="Default Open">
|
|
757
|
+
Dialog content
|
|
758
|
+
</AlertDialog.Popup>
|
|
759
|
+
</AlertDialog.Root>
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
await waitFor( () => {
|
|
763
|
+
expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
|
|
764
|
+
} );
|
|
765
|
+
expect( screen.getByText( 'Default Open' ) ).toBeVisible();
|
|
766
|
+
expect( screen.getByText( 'Dialog content' ) ).toBeVisible();
|
|
767
|
+
} );
|
|
768
|
+
|
|
769
|
+
it( 'allows closing and reopening after defaultOpen', async () => {
|
|
770
|
+
render(
|
|
771
|
+
<AlertDialog.Root defaultOpen>
|
|
772
|
+
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
|
|
773
|
+
<AlertDialog.Popup title="Reopen Test">
|
|
774
|
+
Content
|
|
775
|
+
</AlertDialog.Popup>
|
|
776
|
+
</AlertDialog.Root>
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
await waitFor( () => {
|
|
780
|
+
expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
|
|
781
|
+
} );
|
|
782
|
+
|
|
783
|
+
await userEvent.click(
|
|
784
|
+
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
await waitFor( () => {
|
|
788
|
+
expect(
|
|
789
|
+
screen.queryByRole( 'alertdialog' )
|
|
790
|
+
).not.toBeInTheDocument();
|
|
791
|
+
} );
|
|
792
|
+
|
|
793
|
+
await userEvent.click(
|
|
794
|
+
screen.getByRole( 'button', { name: 'Open' } )
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
await waitFor( () => {
|
|
798
|
+
expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
|
|
799
|
+
} );
|
|
800
|
+
} );
|
|
801
|
+
|
|
802
|
+
it( 'opens and closes via cancel', async () => {
|
|
803
|
+
const onConfirm = jest.fn();
|
|
804
|
+
|
|
805
|
+
render(
|
|
806
|
+
<AlertDialog.Root onConfirm={ onConfirm }>
|
|
807
|
+
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
|
|
808
|
+
<AlertDialog.Popup title="Uncontrolled">
|
|
809
|
+
Content
|
|
810
|
+
</AlertDialog.Popup>
|
|
811
|
+
</AlertDialog.Root>
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
expect( screen.queryByText( 'Content' ) ).not.toBeInTheDocument();
|
|
815
|
+
|
|
816
|
+
await userEvent.click(
|
|
817
|
+
screen.getByRole( 'button', { name: 'Open' } )
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
await waitFor( () => {
|
|
821
|
+
expect( screen.getByText( 'Uncontrolled' ) ).toBeVisible();
|
|
822
|
+
} );
|
|
823
|
+
|
|
824
|
+
await userEvent.click(
|
|
825
|
+
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
await waitFor( () => {
|
|
829
|
+
expect(
|
|
830
|
+
screen.queryByText( 'Uncontrolled' )
|
|
831
|
+
).not.toBeInTheDocument();
|
|
832
|
+
} );
|
|
833
|
+
} );
|
|
834
|
+
|
|
835
|
+
it( 'closes and unmounts dialog via confirm click', async () => {
|
|
836
|
+
const onConfirm = jest.fn();
|
|
837
|
+
|
|
838
|
+
render(
|
|
839
|
+
<AlertDialog.Root onConfirm={ onConfirm }>
|
|
840
|
+
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
|
|
841
|
+
<AlertDialog.Popup title="Uncontrolled Confirm">
|
|
842
|
+
Content
|
|
843
|
+
</AlertDialog.Popup>
|
|
844
|
+
</AlertDialog.Root>
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
await userEvent.click(
|
|
848
|
+
screen.getByRole( 'button', { name: 'Open' } )
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
await waitFor( () => {
|
|
852
|
+
expect(
|
|
853
|
+
screen.getByText( 'Uncontrolled Confirm' )
|
|
854
|
+
).toBeVisible();
|
|
855
|
+
} );
|
|
856
|
+
|
|
857
|
+
await userEvent.click(
|
|
858
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
expect( onConfirm ).toHaveBeenCalledTimes( 1 );
|
|
862
|
+
|
|
863
|
+
await waitFor( () => {
|
|
864
|
+
expect(
|
|
865
|
+
screen.queryByRole( 'alertdialog' )
|
|
866
|
+
).not.toBeInTheDocument();
|
|
867
|
+
} );
|
|
868
|
+
} );
|
|
869
|
+
} );
|
|
870
|
+
|
|
871
|
+
describe( 'edge cases', () => {
|
|
872
|
+
it( 'does not error when unmounted during pending', async () => {
|
|
873
|
+
const deferred = createDeferred();
|
|
874
|
+
|
|
875
|
+
const { unmount } = render(
|
|
876
|
+
<AlertDialog.Root
|
|
877
|
+
open
|
|
878
|
+
onOpenChange={ jest.fn() }
|
|
879
|
+
onConfirm={ () => deferred.promise }
|
|
880
|
+
>
|
|
881
|
+
<AlertDialog.Popup title="Unmount Test">
|
|
882
|
+
Content
|
|
883
|
+
</AlertDialog.Popup>
|
|
884
|
+
</AlertDialog.Root>
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
await waitFor( () => {
|
|
888
|
+
expect(
|
|
889
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
890
|
+
).toBeVisible();
|
|
891
|
+
} );
|
|
892
|
+
|
|
893
|
+
await userEvent.click(
|
|
894
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
await waitFor( () => {
|
|
898
|
+
expect(
|
|
899
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
900
|
+
).toHaveAttribute( 'aria-disabled', 'true' );
|
|
901
|
+
} );
|
|
902
|
+
|
|
903
|
+
// Unmount while pending — should not throw
|
|
904
|
+
unmount();
|
|
905
|
+
|
|
906
|
+
// Resolve the deferred — should be a no-op after unmount
|
|
907
|
+
await act( async () => {
|
|
908
|
+
deferred.resolve();
|
|
909
|
+
} );
|
|
910
|
+
} );
|
|
911
|
+
|
|
912
|
+
it( 'controlled mode: recovers to idle when consumer keeps dialog open after confirm', async () => {
|
|
913
|
+
const onConfirm = jest.fn();
|
|
914
|
+
|
|
915
|
+
render(
|
|
916
|
+
<AlertDialog.Root
|
|
917
|
+
open
|
|
918
|
+
onOpenChange={ jest.fn() }
|
|
919
|
+
onConfirm={ onConfirm }
|
|
920
|
+
>
|
|
921
|
+
<AlertDialog.Popup title="Deadlock Test">
|
|
922
|
+
Content
|
|
923
|
+
</AlertDialog.Popup>
|
|
924
|
+
</AlertDialog.Root>
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
await waitFor( () => {
|
|
928
|
+
expect(
|
|
929
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
930
|
+
).toBeVisible();
|
|
931
|
+
} );
|
|
932
|
+
|
|
933
|
+
await userEvent.click(
|
|
934
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
expect( onConfirm ).toHaveBeenCalledTimes( 1 );
|
|
938
|
+
|
|
939
|
+
// Consumer passes open={true} and does NOT update it in
|
|
940
|
+
// onOpenChange, so phase would be stuck at 'closing'.
|
|
941
|
+
// The safety-net useEffect should recover phase to 'idle'.
|
|
942
|
+
await waitFor( () => {
|
|
943
|
+
expect(
|
|
944
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
945
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
946
|
+
} );
|
|
947
|
+
|
|
948
|
+
expect(
|
|
949
|
+
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
950
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
951
|
+
} );
|
|
952
|
+
|
|
953
|
+
it( 'recovers when onConfirm throws synchronously', async () => {
|
|
954
|
+
const onConfirm = jest.fn( () => {
|
|
955
|
+
throw new Error( 'Sync error' );
|
|
956
|
+
} );
|
|
957
|
+
const onOpenChange = jest.fn();
|
|
958
|
+
const consoleSpy = jest
|
|
959
|
+
.spyOn( console, 'error' )
|
|
960
|
+
.mockImplementation( () => {} );
|
|
487
961
|
|
|
488
|
-
|
|
962
|
+
render(
|
|
489
963
|
<AlertDialog.Root
|
|
490
|
-
open
|
|
491
|
-
onOpenChange={
|
|
964
|
+
open
|
|
965
|
+
onOpenChange={ onOpenChange }
|
|
966
|
+
onConfirm={ onConfirm }
|
|
492
967
|
>
|
|
968
|
+
<AlertDialog.Popup title="Throw Test">
|
|
969
|
+
Content
|
|
970
|
+
</AlertDialog.Popup>
|
|
971
|
+
</AlertDialog.Root>
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
await waitFor( () => {
|
|
975
|
+
expect(
|
|
976
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
977
|
+
).toBeVisible();
|
|
978
|
+
} );
|
|
979
|
+
|
|
980
|
+
await userEvent.click(
|
|
981
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
// The error is caught and logged via console.error in Root.
|
|
985
|
+
await waitFor( () => {
|
|
986
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
987
|
+
expect.objectContaining( { message: 'Sync error' } )
|
|
988
|
+
);
|
|
989
|
+
} );
|
|
990
|
+
|
|
991
|
+
// Dialog stays open and buttons return to idle
|
|
992
|
+
await waitFor( () => {
|
|
993
|
+
expect(
|
|
994
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
995
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
996
|
+
} );
|
|
997
|
+
|
|
998
|
+
expect( onOpenChange ).not.toHaveBeenCalledWith(
|
|
999
|
+
false,
|
|
1000
|
+
expect.anything()
|
|
1001
|
+
);
|
|
1002
|
+
expect( screen.getByText( 'Throw Test' ) ).toBeVisible();
|
|
1003
|
+
|
|
1004
|
+
// Throws do NOT render a visible error message.
|
|
1005
|
+
expect(
|
|
1006
|
+
screen.queryByText( 'Sync error' )
|
|
1007
|
+
).not.toBeInTheDocument();
|
|
1008
|
+
|
|
1009
|
+
consoleSpy.mockRestore();
|
|
1010
|
+
} );
|
|
1011
|
+
|
|
1012
|
+
it( 'sets aria-describedby when description is provided', async () => {
|
|
1013
|
+
render(
|
|
1014
|
+
<AlertDialog.Root open onOpenChange={ jest.fn() }>
|
|
493
1015
|
<AlertDialog.Popup
|
|
494
|
-
title="
|
|
495
|
-
|
|
496
|
-
onConfirm={ jest.fn() }
|
|
1016
|
+
title="Describedby Test"
|
|
1017
|
+
description="A helpful description"
|
|
497
1018
|
>
|
|
498
1019
|
Content
|
|
499
1020
|
</AlertDialog.Popup>
|
|
500
1021
|
</AlertDialog.Root>
|
|
501
1022
|
);
|
|
502
|
-
}
|
|
503
1023
|
|
|
504
|
-
|
|
1024
|
+
await waitFor( () => {
|
|
1025
|
+
expect( screen.getByRole( 'alertdialog' ) ).toBeVisible();
|
|
1026
|
+
} );
|
|
505
1027
|
|
|
506
|
-
|
|
507
|
-
expect(
|
|
1028
|
+
const dialog = screen.getByRole( 'alertdialog' );
|
|
1029
|
+
expect( dialog ).toHaveAccessibleDescription(
|
|
1030
|
+
'A helpful description'
|
|
1031
|
+
);
|
|
508
1032
|
} );
|
|
509
1033
|
|
|
510
|
-
|
|
1034
|
+
it( 'allows re-confirm after { close: false, error }', async () => {
|
|
1035
|
+
const deferred = createDeferred();
|
|
1036
|
+
const onOpenChange = jest.fn();
|
|
1037
|
+
|
|
1038
|
+
render(
|
|
1039
|
+
<AlertDialog.Root
|
|
1040
|
+
open
|
|
1041
|
+
onOpenChange={ onOpenChange }
|
|
1042
|
+
onConfirm={ () => deferred.promise }
|
|
1043
|
+
>
|
|
1044
|
+
<AlertDialog.Popup title="Error Retry">
|
|
1045
|
+
Content
|
|
1046
|
+
</AlertDialog.Popup>
|
|
1047
|
+
</AlertDialog.Root>
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
await waitFor( () => {
|
|
1051
|
+
expect(
|
|
1052
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1053
|
+
).toBeVisible();
|
|
1054
|
+
} );
|
|
1055
|
+
|
|
1056
|
+
// First confirm — returns error
|
|
1057
|
+
await userEvent.click(
|
|
1058
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
await act( async () => {
|
|
1062
|
+
deferred.resolve( {
|
|
1063
|
+
close: false,
|
|
1064
|
+
error: 'Validation failed',
|
|
1065
|
+
} );
|
|
1066
|
+
} );
|
|
1067
|
+
|
|
1068
|
+
await waitFor( () => {
|
|
1069
|
+
expect( screen.getByText( 'Validation failed' ) ).toBeVisible();
|
|
1070
|
+
} );
|
|
1071
|
+
|
|
1072
|
+
// Buttons are re-enabled, user can retry
|
|
1073
|
+
expect(
|
|
1074
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1075
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
1076
|
+
} );
|
|
1077
|
+
|
|
1078
|
+
it( 'allows re-confirm after { close: false }', async () => {
|
|
1079
|
+
let callCount = 0;
|
|
1080
|
+
const onConfirm = jest.fn( (): { close: boolean } | undefined => {
|
|
1081
|
+
callCount++;
|
|
1082
|
+
if ( callCount === 1 ) {
|
|
1083
|
+
return { close: false };
|
|
1084
|
+
}
|
|
1085
|
+
return undefined;
|
|
1086
|
+
} );
|
|
1087
|
+
const onOpenChange = jest.fn();
|
|
1088
|
+
|
|
1089
|
+
render(
|
|
1090
|
+
<AlertDialog.Root
|
|
1091
|
+
open
|
|
1092
|
+
onOpenChange={ onOpenChange }
|
|
1093
|
+
onConfirm={ onConfirm }
|
|
1094
|
+
>
|
|
1095
|
+
<AlertDialog.Popup title="Retry Test">
|
|
1096
|
+
Content
|
|
1097
|
+
</AlertDialog.Popup>
|
|
1098
|
+
</AlertDialog.Root>
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
await waitFor( () => {
|
|
1102
|
+
expect(
|
|
1103
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1104
|
+
).toBeVisible();
|
|
1105
|
+
} );
|
|
1106
|
+
|
|
1107
|
+
// First confirm — returns { close: false }
|
|
1108
|
+
await userEvent.click(
|
|
1109
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1110
|
+
);
|
|
1111
|
+
|
|
1112
|
+
await waitFor( () => {
|
|
1113
|
+
expect(
|
|
1114
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1115
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
1116
|
+
} );
|
|
1117
|
+
|
|
1118
|
+
expect( onOpenChange ).not.toHaveBeenCalledWith(
|
|
1119
|
+
false,
|
|
1120
|
+
expect.anything()
|
|
1121
|
+
);
|
|
511
1122
|
|
|
512
|
-
|
|
1123
|
+
// Second confirm — returns void → should close
|
|
1124
|
+
await userEvent.click(
|
|
1125
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
await waitFor( () => {
|
|
1129
|
+
expect( onOpenChange ).toHaveBeenCalledWith(
|
|
1130
|
+
false,
|
|
1131
|
+
expect.objectContaining( {
|
|
1132
|
+
reason: 'imperative-action',
|
|
1133
|
+
} )
|
|
1134
|
+
);
|
|
1135
|
+
} );
|
|
1136
|
+
|
|
1137
|
+
expect( onConfirm ).toHaveBeenCalledTimes( 2 );
|
|
1138
|
+
} );
|
|
513
1139
|
} );
|
|
514
1140
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
<AlertDialog.Popup title="Trigger Test" onConfirm={ jest.fn() }>
|
|
520
|
-
Dialog content
|
|
521
|
-
</AlertDialog.Popup>
|
|
522
|
-
</AlertDialog.Root>
|
|
523
|
-
);
|
|
1141
|
+
describe( 'error handling', () => {
|
|
1142
|
+
beforeEach( () => {
|
|
1143
|
+
( speak as jest.Mock ).mockClear();
|
|
1144
|
+
} );
|
|
524
1145
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
1146
|
+
it( 'displays error message when onConfirm returns { close: false, error }', async () => {
|
|
1147
|
+
render(
|
|
1148
|
+
<AlertDialog.Root
|
|
1149
|
+
open
|
|
1150
|
+
onOpenChange={ jest.fn() }
|
|
1151
|
+
onConfirm={ () => ( {
|
|
1152
|
+
close: false,
|
|
1153
|
+
error: 'Something went wrong.',
|
|
1154
|
+
} ) }
|
|
1155
|
+
>
|
|
1156
|
+
<AlertDialog.Popup title="Error Test">
|
|
1157
|
+
Content
|
|
1158
|
+
</AlertDialog.Popup>
|
|
1159
|
+
</AlertDialog.Root>
|
|
1160
|
+
);
|
|
528
1161
|
|
|
529
|
-
|
|
1162
|
+
await waitFor( () => {
|
|
1163
|
+
expect(
|
|
1164
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1165
|
+
).toBeVisible();
|
|
1166
|
+
} );
|
|
530
1167
|
|
|
531
|
-
|
|
532
|
-
|
|
1168
|
+
await userEvent.click(
|
|
1169
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
await waitFor( () => {
|
|
1173
|
+
expect(
|
|
1174
|
+
screen.getByText( 'Something went wrong.' )
|
|
1175
|
+
).toBeVisible();
|
|
1176
|
+
} );
|
|
533
1177
|
} );
|
|
534
1178
|
|
|
535
|
-
|
|
1179
|
+
it( 'displays error message from async onConfirm', async () => {
|
|
1180
|
+
const deferred = createDeferred();
|
|
1181
|
+
|
|
1182
|
+
render(
|
|
1183
|
+
<AlertDialog.Root
|
|
1184
|
+
open
|
|
1185
|
+
onOpenChange={ jest.fn() }
|
|
1186
|
+
onConfirm={ () => deferred.promise }
|
|
1187
|
+
>
|
|
1188
|
+
<AlertDialog.Popup title="Async Error">
|
|
1189
|
+
Content
|
|
1190
|
+
</AlertDialog.Popup>
|
|
1191
|
+
</AlertDialog.Root>
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
await waitFor( () => {
|
|
1195
|
+
expect(
|
|
1196
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1197
|
+
).toBeVisible();
|
|
1198
|
+
} );
|
|
1199
|
+
|
|
1200
|
+
await userEvent.click(
|
|
1201
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
await act( async () => {
|
|
1205
|
+
deferred.resolve( {
|
|
1206
|
+
close: false,
|
|
1207
|
+
error: 'Server error occurred.',
|
|
1208
|
+
} );
|
|
1209
|
+
} );
|
|
1210
|
+
|
|
1211
|
+
await waitFor( () => {
|
|
1212
|
+
expect(
|
|
1213
|
+
screen.getByText( 'Server error occurred.' )
|
|
1214
|
+
).toBeVisible();
|
|
1215
|
+
} );
|
|
1216
|
+
|
|
1217
|
+
// Buttons return to idle
|
|
1218
|
+
expect(
|
|
1219
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1220
|
+
).not.toHaveAttribute( 'aria-disabled', 'true' );
|
|
1221
|
+
} );
|
|
1222
|
+
|
|
1223
|
+
it( 'stays open when error is returned without explicit close: false', async () => {
|
|
1224
|
+
const onOpenChange = jest.fn();
|
|
1225
|
+
|
|
1226
|
+
render(
|
|
1227
|
+
<AlertDialog.Root
|
|
1228
|
+
open
|
|
1229
|
+
onOpenChange={ onOpenChange }
|
|
1230
|
+
onConfirm={ () => ( { error: 'Implicit stay open.' } ) }
|
|
1231
|
+
>
|
|
1232
|
+
<AlertDialog.Popup title="Implicit Close">
|
|
1233
|
+
Content
|
|
1234
|
+
</AlertDialog.Popup>
|
|
1235
|
+
</AlertDialog.Root>
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
await waitFor( () => {
|
|
1239
|
+
expect(
|
|
1240
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1241
|
+
).toBeVisible();
|
|
1242
|
+
} );
|
|
1243
|
+
|
|
1244
|
+
await userEvent.click(
|
|
1245
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1246
|
+
);
|
|
1247
|
+
|
|
1248
|
+
await waitFor( () => {
|
|
1249
|
+
expect(
|
|
1250
|
+
screen.getByText( 'Implicit stay open.' )
|
|
1251
|
+
).toBeVisible();
|
|
1252
|
+
} );
|
|
1253
|
+
|
|
1254
|
+
// Dialog stays open — onOpenChange(false) was not called
|
|
1255
|
+
expect( onOpenChange ).not.toHaveBeenCalledWith(
|
|
1256
|
+
false,
|
|
1257
|
+
expect.anything()
|
|
1258
|
+
);
|
|
1259
|
+
} );
|
|
1260
|
+
|
|
1261
|
+
it( 'clears error message on next confirm attempt', async () => {
|
|
1262
|
+
let callCount = 0;
|
|
1263
|
+
const onConfirm = jest.fn( (): ConfirmResult => {
|
|
1264
|
+
callCount++;
|
|
1265
|
+
if ( callCount === 1 ) {
|
|
1266
|
+
return {
|
|
1267
|
+
close: false,
|
|
1268
|
+
error: 'First attempt failed.',
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
return undefined;
|
|
1272
|
+
} );
|
|
1273
|
+
|
|
1274
|
+
render(
|
|
1275
|
+
<AlertDialog.Root
|
|
1276
|
+
open
|
|
1277
|
+
onOpenChange={ jest.fn() }
|
|
1278
|
+
onConfirm={ onConfirm }
|
|
1279
|
+
>
|
|
1280
|
+
<AlertDialog.Popup title="Clear Error">
|
|
1281
|
+
Content
|
|
1282
|
+
</AlertDialog.Popup>
|
|
1283
|
+
</AlertDialog.Root>
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
await waitFor( () => {
|
|
1287
|
+
expect(
|
|
1288
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1289
|
+
).toBeVisible();
|
|
1290
|
+
} );
|
|
1291
|
+
|
|
1292
|
+
// First confirm — shows error
|
|
1293
|
+
await userEvent.click(
|
|
1294
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
await waitFor( () => {
|
|
1298
|
+
expect(
|
|
1299
|
+
screen.getByText( 'First attempt failed.' )
|
|
1300
|
+
).toBeVisible();
|
|
1301
|
+
} );
|
|
1302
|
+
|
|
1303
|
+
// Second confirm — error should be cleared
|
|
1304
|
+
await userEvent.click(
|
|
1305
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
await waitFor( () => {
|
|
1309
|
+
expect(
|
|
1310
|
+
screen.queryByText( 'First attempt failed.' )
|
|
1311
|
+
).not.toBeInTheDocument();
|
|
1312
|
+
} );
|
|
1313
|
+
} );
|
|
1314
|
+
|
|
1315
|
+
it( 'clears error message when dialog reopens', async () => {
|
|
1316
|
+
render(
|
|
1317
|
+
<AlertDialog.Root
|
|
1318
|
+
onConfirm={ () => ( {
|
|
1319
|
+
close: false,
|
|
1320
|
+
error: 'Persistent error.',
|
|
1321
|
+
} ) }
|
|
1322
|
+
>
|
|
1323
|
+
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
|
|
1324
|
+
<AlertDialog.Popup title="Reopen Clear">
|
|
1325
|
+
Content
|
|
1326
|
+
</AlertDialog.Popup>
|
|
1327
|
+
</AlertDialog.Root>
|
|
1328
|
+
);
|
|
1329
|
+
|
|
1330
|
+
// Open dialog
|
|
1331
|
+
await userEvent.click(
|
|
1332
|
+
screen.getByRole( 'button', { name: 'Open' } )
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
await waitFor( () => {
|
|
1336
|
+
expect(
|
|
1337
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1338
|
+
).toBeVisible();
|
|
1339
|
+
} );
|
|
1340
|
+
|
|
1341
|
+
// Trigger error
|
|
1342
|
+
await userEvent.click(
|
|
1343
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1344
|
+
);
|
|
1345
|
+
|
|
1346
|
+
await waitFor( () => {
|
|
1347
|
+
expect( screen.getByText( 'Persistent error.' ) ).toBeVisible();
|
|
1348
|
+
} );
|
|
1349
|
+
|
|
1350
|
+
// Close via cancel
|
|
1351
|
+
await userEvent.click(
|
|
1352
|
+
screen.getByRole( 'button', { name: 'Cancel' } )
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
await waitFor( () => {
|
|
1356
|
+
expect(
|
|
1357
|
+
screen.queryByRole( 'alertdialog' )
|
|
1358
|
+
).not.toBeInTheDocument();
|
|
1359
|
+
} );
|
|
1360
|
+
|
|
1361
|
+
// Reopen — error should be gone
|
|
1362
|
+
await userEvent.click(
|
|
1363
|
+
screen.getByRole( 'button', { name: 'Open' } )
|
|
1364
|
+
);
|
|
1365
|
+
|
|
1366
|
+
await waitFor( () => {
|
|
1367
|
+
expect(
|
|
1368
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1369
|
+
).toBeVisible();
|
|
1370
|
+
} );
|
|
1371
|
+
|
|
1372
|
+
expect(
|
|
1373
|
+
screen.queryByText( 'Persistent error.' )
|
|
1374
|
+
).not.toBeInTheDocument();
|
|
1375
|
+
} );
|
|
1376
|
+
|
|
1377
|
+
it( 'announces error message to screen readers via speak()', async () => {
|
|
1378
|
+
render(
|
|
1379
|
+
<AlertDialog.Root
|
|
1380
|
+
open
|
|
1381
|
+
onOpenChange={ jest.fn() }
|
|
1382
|
+
onConfirm={ () => ( {
|
|
1383
|
+
close: false,
|
|
1384
|
+
error: 'Announced error.',
|
|
1385
|
+
} ) }
|
|
1386
|
+
>
|
|
1387
|
+
<AlertDialog.Popup title="Speak Test">
|
|
1388
|
+
Content
|
|
1389
|
+
</AlertDialog.Popup>
|
|
1390
|
+
</AlertDialog.Root>
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
await waitFor( () => {
|
|
1394
|
+
expect(
|
|
1395
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1396
|
+
).toBeVisible();
|
|
1397
|
+
} );
|
|
1398
|
+
|
|
1399
|
+
await userEvent.click(
|
|
1400
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1403
|
+
await waitFor( () => {
|
|
1404
|
+
expect( speak ).toHaveBeenCalledWith(
|
|
1405
|
+
'Announced error.',
|
|
1406
|
+
'assertive'
|
|
1407
|
+
);
|
|
1408
|
+
} );
|
|
1409
|
+
} );
|
|
1410
|
+
|
|
1411
|
+
it( 'does not show error message when onConfirm throws', async () => {
|
|
1412
|
+
const consoleSpy = jest
|
|
1413
|
+
.spyOn( console, 'error' )
|
|
1414
|
+
.mockImplementation( () => {} );
|
|
1415
|
+
|
|
1416
|
+
render(
|
|
1417
|
+
<AlertDialog.Root
|
|
1418
|
+
open
|
|
1419
|
+
onOpenChange={ jest.fn() }
|
|
1420
|
+
onConfirm={ () => {
|
|
1421
|
+
throw new Error( 'Unhandled throw' );
|
|
1422
|
+
} }
|
|
1423
|
+
>
|
|
1424
|
+
<AlertDialog.Popup title="No Error Display">
|
|
1425
|
+
Content
|
|
1426
|
+
</AlertDialog.Popup>
|
|
1427
|
+
</AlertDialog.Root>
|
|
1428
|
+
);
|
|
1429
|
+
|
|
1430
|
+
await waitFor( () => {
|
|
1431
|
+
expect(
|
|
1432
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1433
|
+
).toBeVisible();
|
|
1434
|
+
} );
|
|
1435
|
+
|
|
1436
|
+
await userEvent.click(
|
|
1437
|
+
screen.getByRole( 'button', { name: 'OK' } )
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1440
|
+
await waitFor( () => {
|
|
1441
|
+
expect( consoleSpy ).toHaveBeenCalledWith(
|
|
1442
|
+
expect.objectContaining( { message: 'Unhandled throw' } )
|
|
1443
|
+
);
|
|
1444
|
+
} );
|
|
1445
|
+
|
|
1446
|
+
// No error message rendered — throws don't trigger the error UI
|
|
1447
|
+
expect(
|
|
1448
|
+
screen.queryByText( 'Unhandled throw' )
|
|
1449
|
+
).not.toBeInTheDocument();
|
|
1450
|
+
expect( speak ).not.toHaveBeenCalled();
|
|
1451
|
+
|
|
1452
|
+
consoleSpy.mockRestore();
|
|
1453
|
+
} );
|
|
536
1454
|
} );
|
|
537
1455
|
} );
|