Skip to content

Commit 52adda7

Browse files
fix: Auto-dismiss success snackbars for signal workflow buttons (#1231)
## Summary - Success notifications from signal buttons in markdown query results now auto-dismiss after 5 seconds (`DURATION.medium`) instead of persisting indefinitely - Error notifications remain persistent and require manual dismissal - Added tests for signal button snackbar behavior ## What changed When clicking signal buttons in markdown query results, success snackbars previously required manual dismissal via the "OK" button. This caused snackbars to stack up when triggering multiple signals in succession, and they persisted even when navigating away from the page. Now, success notifications auto-dismiss after 5 seconds using baseui's `DURATION.medium` constant, passed as the second argument to `enqueue()`. Error notifications are unaffected and still require manual dismissal to ensure users notice failures. ## Test plan - [x] Added unit tests for `SignalButton` component covering: - Button rendering and disabled state - Success snackbar fires with `DURATION.medium` (5000ms) auto-dismiss - Error snackbar fires without a duration (uses provider default: infinite) - [x] Verified existing markdown tests still pass - [x] Linting passes with no warnings or errors Closes #1219 Signed-off-by: Asish Kumar <officialasishkumar@gmail.com> Co-authored-by: Adhitya Mamallan <adhitya.mamallan@uber.com>
1 parent 400ff1b commit 52adda7

3 files changed

Lines changed: 125 additions & 4 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { HttpResponse } from 'msw';
2+
3+
import { render, screen, userEvent, waitFor } from '@/test-utils/rtl';
4+
5+
import SignalButton from '../signal-button';
6+
import { SIGNAL_SUCCESS_NOTIFICATION_DURATION_MS } from '../signal-button.constants';
7+
8+
const mockEnqueue = jest.fn();
9+
jest.mock('baseui/snackbar', () => ({
10+
...jest.requireActual('baseui/snackbar'),
11+
useSnackbar: () => ({
12+
enqueue: mockEnqueue,
13+
dequeue: jest.fn(),
14+
}),
15+
}));
16+
17+
const SIGNAL_ENDPOINT =
18+
'/api/domains/:domain/:cluster/workflows/:workflowId/:runId/signal';
19+
20+
const defaultProps = {
21+
signalName: 'test-signal',
22+
label: 'Send Signal',
23+
domain: 'test-domain',
24+
cluster: 'test-cluster',
25+
workflowId: 'test-workflow-id',
26+
runId: 'test-run-id',
27+
};
28+
29+
describe(SignalButton.name, () => {
30+
beforeEach(() => {
31+
jest.clearAllMocks();
32+
});
33+
34+
it('renders the button with the provided label', () => {
35+
setup({});
36+
37+
expect(
38+
screen.getByRole('button', { name: 'Send Signal' })
39+
).toBeInTheDocument();
40+
});
41+
42+
it('disables the button when required props are missing', () => {
43+
setup({ propsOverrides: { domain: undefined } });
44+
45+
expect(screen.getByRole('button', { name: 'Send Signal' })).toHaveAttribute(
46+
'disabled'
47+
);
48+
});
49+
50+
it('shows a success snackbar with auto-dismiss duration after signaling', async () => {
51+
const { user } = setup({});
52+
53+
const button = screen.getByRole('button', { name: 'Send Signal' });
54+
await user.click(button);
55+
56+
await waitFor(() => {
57+
expect(mockEnqueue).toHaveBeenCalledWith(
58+
expect.objectContaining({
59+
message: 'Successfully sent signal "test-signal"',
60+
actionMessage: 'OK',
61+
}),
62+
SIGNAL_SUCCESS_NOTIFICATION_DURATION_MS
63+
);
64+
});
65+
});
66+
67+
it('shows an error snackbar without auto-dismiss duration on failure', async () => {
68+
const { user } = setup({ error: true });
69+
70+
const button = screen.getByRole('button', { name: 'Send Signal' });
71+
await user.click(button);
72+
73+
await waitFor(() => {
74+
expect(mockEnqueue).toHaveBeenCalledWith(
75+
expect.objectContaining({
76+
actionMessage: 'Dismiss',
77+
})
78+
);
79+
});
80+
81+
// Error snackbar should not pass a duration (uses provider default: infinite)
82+
expect(mockEnqueue).toHaveBeenCalledTimes(1);
83+
expect(mockEnqueue.mock.calls[0]).toHaveLength(1);
84+
});
85+
});
86+
87+
function setup({
88+
error,
89+
propsOverrides,
90+
}: {
91+
error?: boolean;
92+
propsOverrides?: Partial<typeof defaultProps>;
93+
}) {
94+
const user = userEvent.setup();
95+
96+
render(<SignalButton {...defaultProps} {...propsOverrides} />, {
97+
endpointsMocks: [
98+
{
99+
path: SIGNAL_ENDPOINT,
100+
httpMethod: 'POST',
101+
mockOnce: false,
102+
httpResolver: async () => {
103+
if (error) {
104+
return HttpResponse.json(
105+
{ message: 'Failed to signal workflow' },
106+
{ status: 500 }
107+
);
108+
}
109+
return HttpResponse.json({});
110+
},
111+
},
112+
],
113+
});
114+
115+
return { user };
116+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const SIGNAL_SUCCESS_NOTIFICATION_DURATION_MS = 5000;

src/components/markdown/markdoc-components/signal-button/signal-button.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useSnackbar } from 'baseui/snackbar';
66
import losslessJsonStringify from '@/utils/lossless-json-stringify';
77
import request from '@/utils/request';
88

9+
import { SIGNAL_SUCCESS_NOTIFICATION_DURATION_MS } from './signal-button.constants';
910
import { overrides } from './signal-button.styles';
1011
import { type SignalButtonProps } from './signal-button.types';
1112

@@ -45,10 +46,13 @@ export default function SignalButton({
4546
return response.json();
4647
},
4748
onSuccess: () => {
48-
enqueue({
49-
message: `Successfully sent signal "${signalName}"`,
50-
actionMessage: 'OK',
51-
});
49+
enqueue(
50+
{
51+
message: `Successfully sent signal "${signalName}"`,
52+
actionMessage: 'OK',
53+
},
54+
SIGNAL_SUCCESS_NOTIFICATION_DURATION_MS
55+
);
5256
},
5357
onError: (error: Error) => {
5458
enqueue({

0 commit comments

Comments
 (0)