@transferwise/components 46.167.0 → 46.168.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/README.md +1 -1
- package/build/Link/Link.js.map +1 -1
- package/build/Link/Link.mjs.map +1 -1
- package/build/Loader/Loader.js.map +1 -1
- package/build/Loader/Loader.mjs.map +1 -1
- package/build/Prompt/PrimitivePrompt/PrimitivePrompt.js +2 -1
- package/build/Prompt/PrimitivePrompt/PrimitivePrompt.js.map +1 -1
- package/build/Prompt/PrimitivePrompt/PrimitivePrompt.mjs +2 -1
- package/build/Prompt/PrimitivePrompt/PrimitivePrompt.mjs.map +1 -1
- package/build/Prompt/Snackbar/Snackbar.js +205 -0
- package/build/Prompt/Snackbar/Snackbar.js.map +1 -0
- package/build/Prompt/Snackbar/Snackbar.messages.js +14 -0
- package/build/Prompt/Snackbar/Snackbar.messages.js.map +1 -0
- package/build/Prompt/Snackbar/Snackbar.messages.mjs +10 -0
- package/build/Prompt/Snackbar/Snackbar.messages.mjs.map +1 -0
- package/build/Prompt/Snackbar/Snackbar.mjs +203 -0
- package/build/Prompt/Snackbar/Snackbar.mjs.map +1 -0
- package/build/Prompt/Snackbar/SnackbarContext.js.map +1 -0
- package/build/Prompt/Snackbar/SnackbarContext.mjs.map +1 -0
- package/build/Prompt/Snackbar/SnackbarPortal.js +12 -0
- package/build/Prompt/Snackbar/SnackbarPortal.js.map +1 -0
- package/build/Prompt/Snackbar/SnackbarPortal.mjs +8 -0
- package/build/Prompt/Snackbar/SnackbarPortal.mjs.map +1 -0
- package/build/Prompt/Snackbar/SnackbarProvider.js +42 -0
- package/build/Prompt/Snackbar/SnackbarProvider.js.map +1 -0
- package/build/Prompt/Snackbar/SnackbarProvider.mjs +38 -0
- package/build/Prompt/Snackbar/SnackbarProvider.mjs.map +1 -0
- package/build/Prompt/Snackbar/constants.js +17 -0
- package/build/Prompt/Snackbar/constants.js.map +1 -0
- package/build/Prompt/Snackbar/constants.mjs +11 -0
- package/build/Prompt/Snackbar/constants.mjs.map +1 -0
- package/build/Prompt/Snackbar/useSnackbar.js.map +1 -0
- package/build/Prompt/Snackbar/useSnackbar.mjs.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarAutoDismiss.js +66 -0
- package/build/Prompt/Snackbar/useSnackbarAutoDismiss.js.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarAutoDismiss.mjs +62 -0
- package/build/Prompt/Snackbar/useSnackbarAutoDismiss.mjs.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarManager.js +153 -0
- package/build/Prompt/Snackbar/useSnackbarManager.js.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarManager.mjs +149 -0
- package/build/Prompt/Snackbar/useSnackbarManager.mjs.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarSwipeDismiss.js +135 -0
- package/build/Prompt/Snackbar/useSnackbarSwipeDismiss.js.map +1 -0
- package/build/Prompt/Snackbar/useSnackbarSwipeDismiss.mjs +131 -0
- package/build/Prompt/Snackbar/useSnackbarSwipeDismiss.mjs.map +1 -0
- package/build/Section/Section.js.map +1 -1
- package/build/Section/Section.mjs.map +1 -1
- package/build/_deprecated/Select/Select.js +1 -1
- package/build/_deprecated/Select/Select.js.map +1 -1
- package/build/_deprecated/Select/Select.mjs +1 -1
- package/build/_deprecated/Select/Select.mjs.map +1 -1
- package/build/common/BottomSheet/BottomSheet.js +1 -3
- package/build/common/BottomSheet/BottomSheet.js.map +1 -1
- package/build/common/BottomSheet/BottomSheet.mjs +1 -3
- package/build/common/BottomSheet/BottomSheet.mjs.map +1 -1
- package/build/i18n/en.json +1 -0
- package/build/i18n/en.json.js +1 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +1 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/index.js +3 -3
- package/build/index.mjs +3 -3
- package/build/main.css +137 -75
- package/build/styles/IconButton/IconButton.css +3 -4
- package/build/styles/Prompt/Snackbar/Snackbar.css +134 -0
- package/build/styles/main.css +137 -75
- package/build/types/Link/Link.d.ts +1 -1
- package/build/types/Loader/Loader.d.ts +1 -1
- package/build/types/Prompt/PrimitivePrompt/PrimitivePrompt.d.ts +11 -0
- package/build/types/Prompt/PrimitivePrompt/PrimitivePrompt.d.ts.map +1 -1
- package/build/types/Prompt/Snackbar/Snackbar.d.ts +62 -0
- package/build/types/Prompt/Snackbar/Snackbar.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/Snackbar.messages.d.ts +9 -0
- package/build/types/Prompt/Snackbar/Snackbar.messages.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/Snackbar.types.d.ts +12 -0
- package/build/types/Prompt/Snackbar/Snackbar.types.d.ts.map +1 -0
- package/build/types/{Snackbar → Prompt/Snackbar}/SnackbarContext.d.ts +2 -2
- package/build/types/Prompt/Snackbar/SnackbarContext.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/SnackbarPortal.d.ts +4 -0
- package/build/types/Prompt/Snackbar/SnackbarPortal.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/SnackbarProvider.d.ts +13 -0
- package/build/types/Prompt/Snackbar/SnackbarProvider.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/constants.d.ts +9 -0
- package/build/types/Prompt/Snackbar/constants.d.ts.map +1 -0
- package/build/types/{Snackbar → Prompt/Snackbar}/index.d.ts +1 -1
- package/build/types/Prompt/Snackbar/index.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/useSnackbar.d.ts +3 -0
- package/build/types/Prompt/Snackbar/useSnackbar.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/useSnackbarAutoDismiss.d.ts +13 -0
- package/build/types/Prompt/Snackbar/useSnackbarAutoDismiss.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/useSnackbarManager.d.ts +20 -0
- package/build/types/Prompt/Snackbar/useSnackbarManager.d.ts.map +1 -0
- package/build/types/Prompt/Snackbar/useSnackbarSwipeDismiss.d.ts +22 -0
- package/build/types/Prompt/Snackbar/useSnackbarSwipeDismiss.d.ts.map +1 -0
- package/build/types/Section/Section.d.ts +1 -1
- package/build/types/_deprecated/Select/Select.d.ts +1 -1
- package/build/types/common/BottomSheet/BottomSheet.d.ts +1 -3
- package/build/types/common/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/build/types/index.d.ts +5 -5
- package/build/types/index.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +2 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/IconButton/IconButton.css +3 -4
- package/src/IconButton/IconButton.less +14 -1
- package/src/Link/Link.tsx +1 -1
- package/src/ListItem/AdditionalInfo/_stories/ListItemAdditionalInfo.story.tsx +1 -1
- package/src/ListItem/AvatarView/_stories/ListItemAvatarView.story.tsx +3 -3
- package/src/ListItem/_stories/ListItem.disabled.story.tsx +1 -1
- package/src/ListItem/_stories/ListItem.scenarios.story.tsx +2 -2
- package/src/Loader/Loader.tsx +1 -1
- package/src/Prompt/PrimitivePrompt/PrimitivePrompt.test.tsx +24 -1
- package/src/Prompt/PrimitivePrompt/PrimitivePrompt.tsx +8 -2
- package/src/Prompt/Snackbar/Snackbar.css +134 -0
- package/src/Prompt/Snackbar/Snackbar.less +159 -0
- package/src/Prompt/Snackbar/Snackbar.messages.ts +9 -0
- package/src/Prompt/Snackbar/Snackbar.test.tsx +1079 -0
- package/src/Prompt/Snackbar/Snackbar.tsx +245 -0
- package/src/Prompt/Snackbar/Snackbar.types.ts +17 -0
- package/src/{Snackbar → Prompt/Snackbar}/SnackbarContext.ts +2 -2
- package/src/Prompt/Snackbar/SnackbarPortal.tsx +8 -0
- package/src/Prompt/Snackbar/SnackbarProvider.test.tsx +783 -0
- package/src/Prompt/Snackbar/SnackbarProvider.tsx +41 -0
- package/src/Prompt/Snackbar/_stories/Snackbar.accessibility.docs.mdx +58 -0
- package/src/{Snackbar → Prompt/Snackbar}/_stories/Snackbar.rtl.test.story.tsx +17 -9
- package/src/Prompt/Snackbar/_stories/Snackbar.story.tsx +416 -0
- package/src/Prompt/Snackbar/_stories/Snackbar.test.story.tsx +263 -0
- package/src/Prompt/Snackbar/constants.ts +10 -0
- package/src/{Snackbar → Prompt/Snackbar}/index.ts +1 -1
- package/src/Prompt/Snackbar/useSnackbarAutoDismiss.ts +74 -0
- package/src/Prompt/Snackbar/useSnackbarManager.ts +169 -0
- package/src/Prompt/Snackbar/useSnackbarSwipeDismiss.ts +169 -0
- package/src/Section/Section.tsx +1 -1
- package/src/_deprecated/Select/Select.tsx +1 -1
- package/src/_deprecated/Select/_stories/Select.story.tsx +1 -1
- package/src/common/BottomSheet/BottomSheet.tsx +1 -3
- package/src/i18n/en.json +1 -0
- package/src/index.ts +5 -5
- package/src/main.css +137 -75
- package/src/main.less +1 -1
- package/build/Snackbar/Snackbar.js +0 -128
- package/build/Snackbar/Snackbar.js.map +0 -1
- package/build/Snackbar/Snackbar.mjs +0 -123
- package/build/Snackbar/Snackbar.mjs.map +0 -1
- package/build/Snackbar/SnackbarContext.js.map +0 -1
- package/build/Snackbar/SnackbarContext.mjs.map +0 -1
- package/build/Snackbar/SnackbarProvider.js +0 -46
- package/build/Snackbar/SnackbarProvider.js.map +0 -1
- package/build/Snackbar/SnackbarProvider.mjs +0 -42
- package/build/Snackbar/SnackbarProvider.mjs.map +0 -1
- package/build/Snackbar/useSnackbar.js.map +0 -1
- package/build/Snackbar/useSnackbar.mjs.map +0 -1
- package/build/styles/Snackbar/Snackbar.css +0 -71
- package/build/types/Snackbar/Snackbar.d.ts +0 -16
- package/build/types/Snackbar/Snackbar.d.ts.map +0 -1
- package/build/types/Snackbar/SnackbarContext.d.ts.map +0 -1
- package/build/types/Snackbar/SnackbarProvider.d.ts +0 -6
- package/build/types/Snackbar/SnackbarProvider.d.ts.map +0 -1
- package/build/types/Snackbar/index.d.ts.map +0 -1
- package/build/types/Snackbar/useSnackbar.d.ts +0 -3
- package/build/types/Snackbar/useSnackbar.d.ts.map +0 -1
- package/src/Snackbar/Snackbar.css +0 -71
- package/src/Snackbar/Snackbar.less +0 -80
- package/src/Snackbar/Snackbar.test.tsx +0 -206
- package/src/Snackbar/Snackbar.tsx +0 -129
- package/src/Snackbar/SnackbarProvider.tsx +0 -33
- package/src/Snackbar/_stories/Snackbar.story.tsx +0 -89
- package/src/Snackbar/_stories/Snackbar.test.story.tsx +0 -139
- /package/build/{Snackbar → Prompt/Snackbar}/SnackbarContext.js +0 -0
- /package/build/{Snackbar → Prompt/Snackbar}/SnackbarContext.mjs +0 -0
- /package/build/{Snackbar → Prompt/Snackbar}/useSnackbar.js +0 -0
- /package/build/{Snackbar → Prompt/Snackbar}/useSnackbar.mjs +0 -0
- /package/src/{Snackbar → Prompt/Snackbar}/useSnackbar.ts +0 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { act, mockMatchMedia, render, screen, userEvent } from '../../test-utils';
|
|
4
|
+
import { resetLiveRegionAnnouncementQueue } from '../../common/LiveRegion/LiveRegion';
|
|
5
|
+
import { EXIT_ANIMATION as DIMMER_EXIT_DURATION } from '../../Dimmer/Dimmer';
|
|
6
|
+
import Modal from '../../Modal';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
SNACKBAR_DEFAULT_DURATION,
|
|
10
|
+
SNACKBAR_ENTER_DURATION,
|
|
11
|
+
SNACKBAR_EXIT_DURATION,
|
|
12
|
+
SNACKBAR_EXTENDED_DURATION,
|
|
13
|
+
} from './constants';
|
|
14
|
+
import type { SnackbarCreationProps } from './Snackbar.types';
|
|
15
|
+
import SnackbarProvider from './SnackbarProvider';
|
|
16
|
+
import { SnackbarConsumer } from './SnackbarContext';
|
|
17
|
+
import useSnackbar from './useSnackbar';
|
|
18
|
+
import useSnackbarManager from './useSnackbarManager';
|
|
19
|
+
|
|
20
|
+
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTimeAsync });
|
|
21
|
+
|
|
22
|
+
mockMatchMedia();
|
|
23
|
+
|
|
24
|
+
const advanceTimers = (duration: number) => {
|
|
25
|
+
act(() => jest.advanceTimersByTime(duration));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type SnackbarTrigger = { label: string; snackbar: SnackbarCreationProps };
|
|
29
|
+
|
|
30
|
+
const SnackbarTriggers = ({ triggers }: { triggers: SnackbarTrigger[] }) => {
|
|
31
|
+
const createSnackbar = useSnackbar();
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{triggers.map(({ label, snackbar }) => (
|
|
36
|
+
<button key={label} type="button" onClick={() => createSnackbar(snackbar)}>
|
|
37
|
+
{label}
|
|
38
|
+
</button>
|
|
39
|
+
))}
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const BatchedSnackbarTrigger = ({
|
|
45
|
+
clear = false,
|
|
46
|
+
onFirstDismiss,
|
|
47
|
+
onLatestDismiss,
|
|
48
|
+
}: {
|
|
49
|
+
clear?: boolean;
|
|
50
|
+
onFirstDismiss?: () => void;
|
|
51
|
+
onLatestDismiss?: () => void;
|
|
52
|
+
}) => {
|
|
53
|
+
const createSnackbar = useSnackbar();
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={() => {
|
|
59
|
+
createSnackbar({ text: 'First message', onDismiss: onFirstDismiss });
|
|
60
|
+
createSnackbar({ text: 'Latest message', onDismiss: onLatestDismiss });
|
|
61
|
+
if (clear) {
|
|
62
|
+
createSnackbar({ text: '' });
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
Show latest
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const renderProvider = (triggers: SnackbarTrigger[], timeout?: number) =>
|
|
72
|
+
render(
|
|
73
|
+
<SnackbarProvider timeout={timeout}>
|
|
74
|
+
<SnackbarTriggers triggers={triggers} />
|
|
75
|
+
</SnackbarProvider>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
jest.useFakeTimers();
|
|
80
|
+
resetLiveRegionAnnouncementQueue();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
jest.clearAllTimers();
|
|
85
|
+
jest.useRealTimers();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/* eslint-disable testing-library/no-node-access -- Portal placement, teardown and transition classes require structural DOM assertions. */
|
|
89
|
+
describe('SnackbarProvider orchestration', () => {
|
|
90
|
+
it('does not mount a portal or live region while idle', () => {
|
|
91
|
+
const { baseElement } = renderProvider([]);
|
|
92
|
+
|
|
93
|
+
expect(baseElement.querySelector('.wds-Snackbar')).toBeNull();
|
|
94
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it.each(['', ' ', undefined, null, false, true])(
|
|
98
|
+
'does not show an empty surface when text is %p',
|
|
99
|
+
async (text) => {
|
|
100
|
+
renderProvider([{ label: 'Show', snackbar: { text } }]);
|
|
101
|
+
|
|
102
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
103
|
+
|
|
104
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
105
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
106
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
it('keeps zero as valid message content', async () => {
|
|
111
|
+
renderProvider([{ label: 'Show', snackbar: { text: 0 } }]);
|
|
112
|
+
|
|
113
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
114
|
+
|
|
115
|
+
expect(screen.getByRole('status')).toHaveTextContent('0');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('creates a Snackbar programmatically in document.body', async () => {
|
|
119
|
+
const { container } = renderProvider([
|
|
120
|
+
{ label: 'Show', snackbar: { text: 'Changes saved', sentiment: 'success' } },
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
124
|
+
|
|
125
|
+
expect(screen.getByRole('status')).toHaveTextContent('Changes saved');
|
|
126
|
+
expect(container).not.toContainElement(screen.getByRole('status'));
|
|
127
|
+
expect(screen.getByText('Changes saved').closest('.wds-Snackbar')?.parentElement).toBe(
|
|
128
|
+
document.body,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('dismisses after the default four-second duration', async () => {
|
|
133
|
+
renderProvider([{ label: 'Show', snackbar: { text: 'Changes saved' } }]);
|
|
134
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
135
|
+
|
|
136
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
137
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION - 1);
|
|
138
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
139
|
+
|
|
140
|
+
advanceTimers(1);
|
|
141
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
142
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
143
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('retains an explicitly configured Provider timeout', async () => {
|
|
147
|
+
renderProvider(
|
|
148
|
+
[{ label: 'Show', snackbar: { text: 'Changes saved' } }],
|
|
149
|
+
SNACKBAR_DEFAULT_DURATION * 2,
|
|
150
|
+
);
|
|
151
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
152
|
+
|
|
153
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
154
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION * 2 - 1);
|
|
155
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
156
|
+
|
|
157
|
+
advanceTimers(1);
|
|
158
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
159
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('uses an explicit semantic duration instead of the legacy Provider timeout', async () => {
|
|
163
|
+
renderProvider(
|
|
164
|
+
[{ label: 'Show', snackbar: { text: 'Changes saved', duration: 'extended' } }],
|
|
165
|
+
1,
|
|
166
|
+
);
|
|
167
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
168
|
+
|
|
169
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
170
|
+
advanceTimers(SNACKBAR_EXTENDED_DURATION - 1);
|
|
171
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
172
|
+
advanceTimers(1);
|
|
173
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
174
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it.each(['running', 'hovered', 'focused'] as const)(
|
|
178
|
+
'applies an updated Provider timeout while the timer is %s',
|
|
179
|
+
async (interaction) => {
|
|
180
|
+
const triggers = [
|
|
181
|
+
{ label: 'Show', snackbar: { text: 'Changes saved', action: { label: 'Undo' } } },
|
|
182
|
+
];
|
|
183
|
+
const view = renderProvider(triggers, 4000);
|
|
184
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
185
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
186
|
+
advanceTimers(2000);
|
|
187
|
+
const message = screen.getByText('Changes saved');
|
|
188
|
+
const action = screen.getByRole('button', { name: 'Undo' });
|
|
189
|
+
if (interaction === 'hovered') {
|
|
190
|
+
await user.hover(message);
|
|
191
|
+
} else if (interaction === 'focused') {
|
|
192
|
+
act(() => action.focus());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
view.rerender(
|
|
196
|
+
<SnackbarProvider timeout={9000}>
|
|
197
|
+
<SnackbarTriggers triggers={triggers} />
|
|
198
|
+
</SnackbarProvider>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (interaction !== 'running') {
|
|
202
|
+
advanceTimers(10_000);
|
|
203
|
+
}
|
|
204
|
+
expect(message).toBeInTheDocument();
|
|
205
|
+
if (interaction === 'hovered') {
|
|
206
|
+
await user.unhover(message);
|
|
207
|
+
} else if (interaction === 'focused') {
|
|
208
|
+
act(() => action.blur());
|
|
209
|
+
}
|
|
210
|
+
advanceTimers(8999);
|
|
211
|
+
expect(message).toBeInTheDocument();
|
|
212
|
+
expect(message.closest('.wds-Snackbar-container')).not.toHaveClass(
|
|
213
|
+
'wds-Snackbar-container-exit-active',
|
|
214
|
+
);
|
|
215
|
+
advanceTimers(1);
|
|
216
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
217
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
it('does not reset an explicit semantic duration when the Provider timeout changes', async () => {
|
|
222
|
+
const triggers: SnackbarTrigger[] = [
|
|
223
|
+
{ label: 'Show', snackbar: { text: 'Changes saved', duration: 'default' } },
|
|
224
|
+
];
|
|
225
|
+
const view = renderProvider(triggers, 1000);
|
|
226
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
227
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
228
|
+
advanceTimers(2000);
|
|
229
|
+
|
|
230
|
+
view.rerender(
|
|
231
|
+
<SnackbarProvider timeout={9000}>
|
|
232
|
+
<SnackbarTriggers triggers={triggers} />
|
|
233
|
+
</SnackbarProvider>,
|
|
234
|
+
);
|
|
235
|
+
advanceTimers(1999);
|
|
236
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
237
|
+
advanceTimers(1);
|
|
238
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
239
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('pauses the provider timer while keyboard focus moves within the Snackbar', async () => {
|
|
243
|
+
renderProvider([
|
|
244
|
+
{ label: 'Show', snackbar: { text: 'Changes saved', action: { label: 'Undo' } } },
|
|
245
|
+
]);
|
|
246
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
247
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
248
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION / 2);
|
|
249
|
+
|
|
250
|
+
await user.tab();
|
|
251
|
+
expect(screen.getByRole('button', { name: 'Undo' })).toHaveFocus();
|
|
252
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION * 2);
|
|
253
|
+
await user.tab();
|
|
254
|
+
expect(screen.getByRole('button', { name: 'Close' })).toHaveFocus();
|
|
255
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION * 2);
|
|
256
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
257
|
+
|
|
258
|
+
await user.tab();
|
|
259
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION / 2 - 1);
|
|
260
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
261
|
+
advanceTimers(1);
|
|
262
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
263
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('keeps indefinite notifications until manually dismissed', async () => {
|
|
267
|
+
renderProvider([
|
|
268
|
+
{ label: 'Show', snackbar: { text: 'Changes saved', duration: 'indefinite' } },
|
|
269
|
+
]);
|
|
270
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
271
|
+
|
|
272
|
+
advanceTimers(60_000);
|
|
273
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
274
|
+
await user.click(screen.getByRole('button', { name: 'Close' }));
|
|
275
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
276
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('keeps a loading Snackbar until manually dismissed and cleans up its indicator', async () => {
|
|
280
|
+
renderProvider(
|
|
281
|
+
[
|
|
282
|
+
{
|
|
283
|
+
label: 'Show',
|
|
284
|
+
snackbar: { text: 'Uploading document', loading: true, duration: 'default' },
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
0,
|
|
288
|
+
);
|
|
289
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
290
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
291
|
+
advanceTimers(SNACKBAR_EXTENDED_DURATION * 2);
|
|
292
|
+
expect(screen.getByTestId('Snackbar_ProcessIndicator')).toBeInTheDocument();
|
|
293
|
+
expect(screen.getByRole('status')).toHaveTextContent('Uploading document');
|
|
294
|
+
|
|
295
|
+
await user.click(screen.getByRole('button', { name: 'Close' }));
|
|
296
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
297
|
+
|
|
298
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
299
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('replaces loading with a timed completion Snackbar after exit', async () => {
|
|
303
|
+
renderProvider([
|
|
304
|
+
{ label: 'Upload', snackbar: { text: 'Uploading document', loading: true } },
|
|
305
|
+
{ label: 'Complete', snackbar: { text: 'Document uploaded', sentiment: 'success' } },
|
|
306
|
+
]);
|
|
307
|
+
await user.click(screen.getByRole('button', { name: 'Upload' }));
|
|
308
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
309
|
+
advanceTimers(SNACKBAR_EXTENDED_DURATION * 2);
|
|
310
|
+
|
|
311
|
+
await user.click(screen.getByRole('button', { name: 'Complete' }));
|
|
312
|
+
advanceTimers(SNACKBAR_EXIT_DURATION - 1);
|
|
313
|
+
expect(screen.getByText('Uploading document')).toBeInTheDocument();
|
|
314
|
+
expect(screen.queryByText('Document uploaded')).not.toBeInTheDocument();
|
|
315
|
+
advanceTimers(1);
|
|
316
|
+
expect(screen.queryByTestId('Snackbar_ProcessIndicator')).not.toBeInTheDocument();
|
|
317
|
+
expect(screen.getByRole('status')).toHaveTextContent('Document uploaded');
|
|
318
|
+
|
|
319
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
320
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION - 1);
|
|
321
|
+
expect(screen.getByText('Document uploaded')).toBeInTheDocument();
|
|
322
|
+
advanceTimers(1);
|
|
323
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
324
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
325
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('exits the active Snackbar before showing the latest replacement', async () => {
|
|
329
|
+
renderProvider([
|
|
330
|
+
{ label: 'Show first', snackbar: { text: 'First message' } },
|
|
331
|
+
{ label: 'Show second', snackbar: { text: 'Second message' } },
|
|
332
|
+
{ label: 'Show latest', snackbar: { text: 'Latest message' } },
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
await user.click(screen.getByRole('button', { name: 'Show first' }));
|
|
336
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
337
|
+
await user.click(screen.getByRole('button', { name: 'Show second' }));
|
|
338
|
+
await user.click(screen.getByRole('button', { name: 'Show latest' }));
|
|
339
|
+
|
|
340
|
+
advanceTimers(SNACKBAR_EXIT_DURATION - 1);
|
|
341
|
+
expect(screen.getByText('First message')).toBeInTheDocument();
|
|
342
|
+
expect(screen.queryByText('Second message')).not.toBeInTheDocument();
|
|
343
|
+
expect(screen.queryByText('Latest message')).not.toBeInTheDocument();
|
|
344
|
+
|
|
345
|
+
advanceTimers(1);
|
|
346
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
347
|
+
expect(screen.queryByText('Second message')).not.toBeInTheDocument();
|
|
348
|
+
expect(screen.getByText('Latest message')).toBeInTheDocument();
|
|
349
|
+
expect(screen.getAllByRole('status')).toHaveLength(1);
|
|
350
|
+
expect(screen.getByText('Latest message').closest('.wds-Snackbar-container')).toHaveClass(
|
|
351
|
+
'wds-Snackbar-container-appear-active',
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('shows the latest Snackbar when creations are batched before the first render', async () => {
|
|
356
|
+
const onFirstDismiss = jest.fn();
|
|
357
|
+
const onLatestDismiss = jest.fn();
|
|
358
|
+
render(
|
|
359
|
+
<SnackbarProvider>
|
|
360
|
+
<BatchedSnackbarTrigger onFirstDismiss={onFirstDismiss} onLatestDismiss={onLatestDismiss} />
|
|
361
|
+
</SnackbarProvider>,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await user.click(screen.getByRole('button', { name: 'Show latest' }));
|
|
365
|
+
|
|
366
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
367
|
+
expect(screen.getByText('Latest message')).toBeInTheDocument();
|
|
368
|
+
expect(onFirstDismiss).not.toHaveBeenCalled();
|
|
369
|
+
expect(onLatestDismiss).not.toHaveBeenCalled();
|
|
370
|
+
|
|
371
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
372
|
+
await user.click(screen.getByRole('button', { name: 'Close' }));
|
|
373
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
374
|
+
|
|
375
|
+
expect(screen.queryByText('Latest message')).not.toBeInTheDocument();
|
|
376
|
+
expect(onFirstDismiss).not.toHaveBeenCalled();
|
|
377
|
+
expect(onLatestDismiss).toHaveBeenCalledTimes(1);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('replaces Snackbars even when their timestamps and the clock are unchanged', async () => {
|
|
381
|
+
renderProvider([
|
|
382
|
+
{ label: 'Show first', snackbar: { text: 'First message', timestamp: 1 } },
|
|
383
|
+
{ label: 'Show second', snackbar: { text: 'Second message', timestamp: 1 } },
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
await user.click(screen.getByRole('button', { name: 'Show first' }));
|
|
387
|
+
await user.click(screen.getByRole('button', { name: 'Show second' }));
|
|
388
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
389
|
+
|
|
390
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
391
|
+
expect(screen.getByText('Second message')).toBeInTheDocument();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('gives a replacing Snackbar a fresh timeout after the active one exits', async () => {
|
|
395
|
+
renderProvider([
|
|
396
|
+
{ label: 'Show first', snackbar: { text: 'First message' } },
|
|
397
|
+
{ label: 'Replace', snackbar: { text: 'Replacement' } },
|
|
398
|
+
]);
|
|
399
|
+
await user.click(screen.getByRole('button', { name: 'Show first' }));
|
|
400
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION - SNACKBAR_EXIT_DURATION);
|
|
401
|
+
await user.click(screen.getByRole('button', { name: 'Replace' }));
|
|
402
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
403
|
+
|
|
404
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
405
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION - 1);
|
|
406
|
+
expect(screen.getByText('Replacement')).toBeInTheDocument();
|
|
407
|
+
advanceTimers(1);
|
|
408
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
409
|
+
expect(screen.queryByText('Replacement')).not.toBeInTheDocument();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it.each(['', undefined])('dismisses instead of replacing with empty text %p', async (text) => {
|
|
413
|
+
renderProvider([
|
|
414
|
+
{ label: 'Show first', snackbar: { text: 'First message' } },
|
|
415
|
+
{ label: 'Clear', snackbar: { text } },
|
|
416
|
+
]);
|
|
417
|
+
await user.click(screen.getByRole('button', { name: 'Show first' }));
|
|
418
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
419
|
+
await user.click(screen.getByRole('button', { name: 'Clear' }));
|
|
420
|
+
advanceTimers(SNACKBAR_EXIT_DURATION - 1);
|
|
421
|
+
expect(screen.getByText('First message')).toBeInTheDocument();
|
|
422
|
+
|
|
423
|
+
advanceTimers(1);
|
|
424
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
425
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
426
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
427
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION);
|
|
428
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('clears a pending replacement when the latest request has empty text', async () => {
|
|
432
|
+
renderProvider([
|
|
433
|
+
{ label: 'Show first', snackbar: { text: 'First message' } },
|
|
434
|
+
{ label: 'Replace', snackbar: { text: 'Pending message' } },
|
|
435
|
+
{ label: 'Clear', snackbar: { text: '' } },
|
|
436
|
+
]);
|
|
437
|
+
await user.click(screen.getByRole('button', { name: 'Show first' }));
|
|
438
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
439
|
+
await user.click(screen.getByRole('button', { name: 'Replace' }));
|
|
440
|
+
await user.click(screen.getByRole('button', { name: 'Clear' }));
|
|
441
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
442
|
+
|
|
443
|
+
expect(screen.queryByText('Pending message')).not.toBeInTheDocument();
|
|
444
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
445
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('never mounts a surface when a batched creation is followed by empty text', async () => {
|
|
449
|
+
render(
|
|
450
|
+
<SnackbarProvider>
|
|
451
|
+
<BatchedSnackbarTrigger clear />
|
|
452
|
+
</SnackbarProvider>,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
await user.click(screen.getByRole('button', { name: 'Show latest' }));
|
|
456
|
+
|
|
457
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
458
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
459
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('cleans up the active Snackbar timer when the Provider unmounts', async () => {
|
|
463
|
+
const view = renderProvider([{ label: 'Show', snackbar: { text: 'Changes saved' } }]);
|
|
464
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
465
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
466
|
+
expect(jest.getTimerCount()).toBe(1);
|
|
467
|
+
|
|
468
|
+
view.unmount();
|
|
469
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('removes the portal after the close button finishes dismissing it', async () => {
|
|
473
|
+
renderProvider([{ label: 'Show', snackbar: { text: 'Changes saved' } }]);
|
|
474
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
475
|
+
await user.click(screen.getByRole('button', { name: 'Close' }));
|
|
476
|
+
|
|
477
|
+
advanceTimers(SNACKBAR_EXIT_DURATION - 1);
|
|
478
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
479
|
+
advanceTimers(1);
|
|
480
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it.each(['timeout', 'close', 'repeated Escape', 'action', 'clear', 'replacement'] as const)(
|
|
484
|
+
'calls onDismiss once after the exit finishes for %s',
|
|
485
|
+
async (dismissal) => {
|
|
486
|
+
const onDismiss = jest.fn();
|
|
487
|
+
renderProvider([
|
|
488
|
+
{
|
|
489
|
+
label: 'Show',
|
|
490
|
+
snackbar: { text: 'Changes saved', onDismiss, action: { label: 'Undo' } },
|
|
491
|
+
},
|
|
492
|
+
{ label: 'Clear', snackbar: { text: '' } },
|
|
493
|
+
{ label: 'Replace', snackbar: { text: 'Replacement', duration: 'indefinite' } },
|
|
494
|
+
]);
|
|
495
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
496
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
497
|
+
|
|
498
|
+
if (dismissal === 'timeout') {
|
|
499
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION);
|
|
500
|
+
} else if (dismissal === 'repeated Escape') {
|
|
501
|
+
await user.keyboard('{Escape}{Escape}{Escape}');
|
|
502
|
+
} else {
|
|
503
|
+
const label = { close: 'Close', action: 'Undo', clear: 'Clear', replacement: 'Replace' }[
|
|
504
|
+
dismissal
|
|
505
|
+
];
|
|
506
|
+
await user.click(screen.getByRole('button', { name: label }));
|
|
507
|
+
}
|
|
508
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
509
|
+
advanceTimers(SNACKBAR_EXIT_DURATION - 1);
|
|
510
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
511
|
+
advanceTimers(1);
|
|
512
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
513
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
514
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION * 2);
|
|
515
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
516
|
+
},
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
it('does not call onDismiss when the provider unmounts during exit', async () => {
|
|
520
|
+
const onDismiss = jest.fn();
|
|
521
|
+
const view = renderProvider([
|
|
522
|
+
{ label: 'Show', snackbar: { text: 'Changes saved', onDismiss } },
|
|
523
|
+
]);
|
|
524
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
525
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
526
|
+
await user.keyboard('{Escape}');
|
|
527
|
+
|
|
528
|
+
view.unmount();
|
|
529
|
+
advanceTimers(SNACKBAR_EXIT_DURATION + SNACKBAR_DEFAULT_DURATION);
|
|
530
|
+
|
|
531
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
532
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('starts a replacement without the dismissed Snackbar swipe transform', async () => {
|
|
536
|
+
const onDismiss = jest.fn();
|
|
537
|
+
renderProvider([
|
|
538
|
+
{ label: 'Show', snackbar: { text: 'First message', duration: 'indefinite', onDismiss } },
|
|
539
|
+
{ label: 'Replace', snackbar: { text: 'Replacement', duration: 'indefinite' } },
|
|
540
|
+
]);
|
|
541
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
542
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
543
|
+
const surface = screen.getByText('First message').closest('.wds-Snackbar-container');
|
|
544
|
+
if (!surface) {
|
|
545
|
+
throw new Error('Expected the Snackbar surface');
|
|
546
|
+
}
|
|
547
|
+
Object.defineProperty(surface, 'offsetHeight', { configurable: true, value: 100 });
|
|
548
|
+
await user.pointer([
|
|
549
|
+
{ keys: '[MouseLeft>]', target: surface, coords: { clientX: 0, clientY: 0 } },
|
|
550
|
+
{ coords: { clientX: 0, clientY: 50 } },
|
|
551
|
+
'[/MouseLeft]',
|
|
552
|
+
]);
|
|
553
|
+
await user.click(screen.getByRole('button', { name: 'Replace' }));
|
|
554
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
555
|
+
advanceTimers(SNACKBAR_EXIT_DURATION - 1);
|
|
556
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
557
|
+
advanceTimers(1);
|
|
558
|
+
|
|
559
|
+
const replacement = screen.getByText('Replacement').closest('.wds-Snackbar');
|
|
560
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
561
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
562
|
+
expect(replacement).toHaveStyle({ transform: '' });
|
|
563
|
+
expect(replacement).not.toHaveClass('wds-Snackbar--swipe-dismiss');
|
|
564
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION * 2);
|
|
565
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('reports a throwing onDismiss without losing the pending Snackbar after the actual exit', async () => {
|
|
569
|
+
const error = new Error('Dismissal callback failed');
|
|
570
|
+
const reportError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
571
|
+
const onDismiss = jest.fn(() => {
|
|
572
|
+
throw error;
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
renderProvider([
|
|
577
|
+
{ label: 'Show', snackbar: { text: 'First message', duration: 'indefinite', onDismiss } },
|
|
578
|
+
{ label: 'Replace', snackbar: { text: 'Replacement', duration: 'indefinite' } },
|
|
579
|
+
]);
|
|
580
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
581
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
582
|
+
await user.click(screen.getByRole('button', { name: 'Replace' }));
|
|
583
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
584
|
+
|
|
585
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
586
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
587
|
+
|
|
588
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
589
|
+
expect(reportError).toHaveBeenCalledTimes(1);
|
|
590
|
+
expect(reportError).toHaveBeenCalledWith('Snackbar onDismiss callback failed:', error);
|
|
591
|
+
expect(screen.queryByText('First message')).not.toBeInTheDocument();
|
|
592
|
+
expect(screen.getByText('Replacement')).toBeInTheDocument();
|
|
593
|
+
|
|
594
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION * 2);
|
|
595
|
+
expect(screen.getByText('Replacement')).toBeInTheDocument();
|
|
596
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
597
|
+
await user.click(screen.getByRole('button', { name: 'Close' }));
|
|
598
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
599
|
+
expect(screen.queryByText('Replacement')).not.toBeInTheDocument();
|
|
600
|
+
} finally {
|
|
601
|
+
reportError.mockRestore();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('lets Modal consume Escape before dismissing the Snackbar', async () => {
|
|
606
|
+
const onClose = jest.fn();
|
|
607
|
+
const onDismiss = jest.fn();
|
|
608
|
+
const example = (open: boolean) => (
|
|
609
|
+
<SnackbarProvider>
|
|
610
|
+
<SnackbarTriggers
|
|
611
|
+
triggers={[
|
|
612
|
+
{
|
|
613
|
+
label: 'Show',
|
|
614
|
+
snackbar: { text: 'Changes saved', duration: 'indefinite', onDismiss },
|
|
615
|
+
},
|
|
616
|
+
]}
|
|
617
|
+
/>
|
|
618
|
+
<Modal open={open} body="Modal content" onClose={onClose} />
|
|
619
|
+
</SnackbarProvider>
|
|
620
|
+
);
|
|
621
|
+
const view = render(example(false));
|
|
622
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
623
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
624
|
+
view.rerender(example(true));
|
|
625
|
+
|
|
626
|
+
await user.keyboard('{Escape}');
|
|
627
|
+
|
|
628
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
629
|
+
view.rerender(example(false));
|
|
630
|
+
advanceTimers(Math.max(DIMMER_EXIT_DURATION, SNACKBAR_EXIT_DURATION));
|
|
631
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
632
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
633
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
634
|
+
|
|
635
|
+
await user.keyboard('{Escape}');
|
|
636
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
637
|
+
|
|
638
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
639
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
640
|
+
expect(screen.queryByText('Changes saved')).not.toBeInTheDocument();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('does not restart the timer when its parent rerenders', async () => {
|
|
644
|
+
const triggers = [{ label: 'Show', snackbar: { text: 'Changes saved' } }];
|
|
645
|
+
const view = renderProvider(triggers);
|
|
646
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
647
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
648
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION / 2);
|
|
649
|
+
|
|
650
|
+
view.rerender(
|
|
651
|
+
<SnackbarProvider>
|
|
652
|
+
<SnackbarTriggers triggers={triggers} />
|
|
653
|
+
</SnackbarProvider>,
|
|
654
|
+
);
|
|
655
|
+
advanceTimers(SNACKBAR_DEFAULT_DURATION / 2 - 1);
|
|
656
|
+
expect(screen.getByText('Changes saved')).toBeInTheDocument();
|
|
657
|
+
advanceTimers(1);
|
|
658
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
659
|
+
|
|
660
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it.each(['mouse', 'keyboard', 'touch'] as const)(
|
|
664
|
+
'dismisses after action activation with %s even if focus stays inside',
|
|
665
|
+
async (activation) => {
|
|
666
|
+
const onClick = jest.fn();
|
|
667
|
+
renderProvider([
|
|
668
|
+
{ label: 'Show', snackbar: { text: 'Changes saved', action: { label: 'Undo', onClick } } },
|
|
669
|
+
]);
|
|
670
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
671
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
672
|
+
const action = screen.getByRole('button', { name: 'Undo' });
|
|
673
|
+
await user.tab();
|
|
674
|
+
expect(action).toHaveFocus();
|
|
675
|
+
|
|
676
|
+
if (activation === 'keyboard') {
|
|
677
|
+
await user.keyboard('{Enter}');
|
|
678
|
+
} else if (activation === 'touch') {
|
|
679
|
+
await user.pointer([{ target: action, keys: '[TouchA>]' }, { keys: '[/TouchA]' }]);
|
|
680
|
+
} else {
|
|
681
|
+
await user.click(action);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
685
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
686
|
+
expect(document.querySelector('.wds-Snackbar')).toBeNull();
|
|
687
|
+
},
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
it('preserves a follow-up Snackbar created by the action callback', async () => {
|
|
691
|
+
render(
|
|
692
|
+
<SnackbarProvider>
|
|
693
|
+
<SnackbarConsumer>
|
|
694
|
+
{({ createSnackbar }) => (
|
|
695
|
+
<button
|
|
696
|
+
type="button"
|
|
697
|
+
onClick={() =>
|
|
698
|
+
createSnackbar({
|
|
699
|
+
text: 'Transfer deleted',
|
|
700
|
+
action: {
|
|
701
|
+
label: 'Undo',
|
|
702
|
+
onClick: () =>
|
|
703
|
+
createSnackbar({ text: 'Undo completed', duration: 'indefinite' }),
|
|
704
|
+
},
|
|
705
|
+
})
|
|
706
|
+
}
|
|
707
|
+
>
|
|
708
|
+
Show
|
|
709
|
+
</button>
|
|
710
|
+
)}
|
|
711
|
+
</SnackbarConsumer>
|
|
712
|
+
</SnackbarProvider>,
|
|
713
|
+
);
|
|
714
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
715
|
+
advanceTimers(SNACKBAR_ENTER_DURATION);
|
|
716
|
+
|
|
717
|
+
await user.click(screen.getByRole('button', { name: 'Undo' }));
|
|
718
|
+
advanceTimers(SNACKBAR_EXIT_DURATION);
|
|
719
|
+
|
|
720
|
+
expect(screen.queryByText('Transfer deleted')).not.toBeInTheDocument();
|
|
721
|
+
expect(screen.getByRole('status')).toHaveTextContent('Undo completed');
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('forwards navigation actions unchanged', async () => {
|
|
725
|
+
renderProvider([
|
|
726
|
+
{
|
|
727
|
+
label: 'Show',
|
|
728
|
+
snackbar: {
|
|
729
|
+
text: 'Transfer completed',
|
|
730
|
+
action: { label: <span>View transfer</span>, href: '/transfers/123', target: '_blank' },
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
]);
|
|
734
|
+
await user.click(screen.getByRole('button', { name: 'Show' }));
|
|
735
|
+
|
|
736
|
+
const link = screen.getByRole('link', { name: 'View transfer' });
|
|
737
|
+
expect(link).toHaveAttribute('href', '/transfers/123');
|
|
738
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
/* eslint-enable testing-library/no-node-access */
|
|
743
|
+
|
|
744
|
+
describe('Snackbar manager instance guards', () => {
|
|
745
|
+
it('ignores stale exit state updates from an already replaced instance', () => {
|
|
746
|
+
const { result } = renderHook(() => useSnackbarManager());
|
|
747
|
+
act(() => result.current.createSnackbar({ text: 'First message' }));
|
|
748
|
+
const firstExited = result.current.handleExited;
|
|
749
|
+
act(() => result.current.createSnackbar({ text: 'Replacement' }));
|
|
750
|
+
act(() => firstExited());
|
|
751
|
+
const replacementKey = result.current.announcementKey;
|
|
752
|
+
|
|
753
|
+
act(() => firstExited());
|
|
754
|
+
|
|
755
|
+
expect(result.current.active?.props.text).toBe('Replacement');
|
|
756
|
+
expect(result.current.announcementKey).toBe(replacementKey);
|
|
757
|
+
expect(result.current.visible).toBe(true);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it('preserves a follow-up Snackbar created by onDismiss', () => {
|
|
761
|
+
const { result } = renderHook(() => useSnackbarManager());
|
|
762
|
+
const onDismiss = jest.fn(() => result.current.createSnackbar({ text: 'Follow-up' }));
|
|
763
|
+
act(() => result.current.createSnackbar({ text: 'First message', onDismiss }));
|
|
764
|
+
act(() => result.current.dismissSnackbar());
|
|
765
|
+
|
|
766
|
+
act(() => result.current.handleExited());
|
|
767
|
+
|
|
768
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
769
|
+
expect(result.current.active?.props.text).toBe('Follow-up');
|
|
770
|
+
expect(result.current.visible).toBe(true);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('can dismiss a batched instance before it has rendered', () => {
|
|
774
|
+
const { result } = renderHook(() => useSnackbarManager());
|
|
775
|
+
act(() => {
|
|
776
|
+
result.current.createSnackbar({ text: 'Changes saved' });
|
|
777
|
+
result.current.dismissSnackbar();
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
expect(result.current.active).toBeNull();
|
|
781
|
+
expect(result.current.visible).toBe(false);
|
|
782
|
+
});
|
|
783
|
+
});
|