This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new a704b59ae fix(topic): lock message sending against repeat 
confirmations (#6448)
a704b59ae is described below

commit a704b59ae3ce1ce7e8a71adc90f63b228a8ab8e0
Author: messere <[email protected]>
AuthorDate: Thu Oct 15 18:12:00 2026 +0800

    fix(topic): lock message sending against repeat confirmations (#6448)
---
 .../pages/instance/__tests__/TopicPage.test.tsx    | 50 ++++++++++++++++++++++
 web/src/pages/instance/topic.tsx                   |  8 +++-
 2 files changed, 57 insertions(+), 1 deletion(-)

diff --git a/web/src/pages/instance/__tests__/TopicPage.test.tsx 
b/web/src/pages/instance/__tests__/TopicPage.test.tsx
index f566d9a13..1178debe0 100644
--- a/web/src/pages/instance/__tests__/TopicPage.test.tsx
+++ b/web/src/pages/instance/__tests__/TopicPage.test.tsx
@@ -440,6 +440,56 @@ describe('TopicPage', () => {
     });
   });
 
+  it('sends each test message once per confirmation while the first request is 
pending', async () => {
+    topicServiceMocks.sendTopicMessage.mockImplementation(() => new Promise(() 
=> {}));
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    instanceServiceMocks.listInstances.mockResolvedValue([selectedInstance]);
+    mockTopicsList([buildTopics(1)[0]]);
+    renderWithProviders();
+
+    await user.click(await screen.findByRole('button', { name: /发送/ }));
+    const dialog = await getSendDialog();
+    fireEvent.change(within(dialog).getByLabelText('消息体 Body'), {
+      target: { value: 'double-send-payload' },
+    });
+    const send = within(dialog).getByRole('button', { name: /发\s*送/ });
+
+    fireEvent.click(send);
+    // The OK button must lock with the first click: `sending` alone flips 
only after
+    // validateFields resolves and React re-renders, so a repeat confirmation 
in that
+    // window re-enters handleSend and sends the same message twice.
+    expect(send).toHaveClass('ant-btn-loading');
+    fireEvent.click(send);
+
+    await waitFor(() => 
expect(topicServiceMocks.sendTopicMessage).toHaveBeenCalledTimes(1));
+    expect(topicServiceMocks.sendTopicMessage).toHaveBeenCalledWith(
+      expect.objectContaining({ body: 'double-send-payload' }),
+    );
+    expect(send).toHaveClass('ant-btn-loading');
+  });
+
+  it('keeps the send dialog usable for consecutive sends after one completes', 
async () => {
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    instanceServiceMocks.listInstances.mockResolvedValue([selectedInstance]);
+    mockTopicsList([buildTopics(1)[0]]);
+    renderWithProviders();
+
+    await user.click(await screen.findByRole('button', { name: /发送/ }));
+    const dialog = await getSendDialog();
+    fireEvent.change(within(dialog).getByLabelText('消息体 Body'), {
+      target: { value: 'consecutive-payload' },
+    });
+    const send = within(dialog).getByRole('button', { name: /发\s*送/ });
+
+    await user.click(send);
+    await waitFor(() => 
expect(topicServiceMocks.sendTopicMessage).toHaveBeenCalledTimes(1));
+    await waitFor(() => expect(send).not.toHaveClass('ant-btn-loading'));
+
+    await user.click(send);
+    await waitFor(() => 
expect(topicServiceMocks.sendTopicMessage).toHaveBeenCalledTimes(2));
+    expect(within(dialog).getByText(/发送消息到 topic-01/)).toBeInTheDocument();
+  });
+
   it('opens a clean create dialog after a cancelled edit', async () => {
     const user = userEvent.setup({ pointerEventsCheck: 0 });
     renderWithProviders();
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index e2382ad54..ee33974ae 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -361,6 +361,7 @@ const TopicPageContent = ({
   const consumersRequestIdRef = useRef(0);
   const syncRequestIdRef = useRef(0);
   const createInFlightRef = useRef(false);
+  const sendInFlightRef = useRef(false);
 
   useEffect(
     () => () => {
@@ -1154,14 +1155,18 @@ const TopicPageContent = ({
 
   // ─── Send message modal submit ────────────────────────────────
   const handleSend = async () => {
+    if (sendInFlightRef.current) return;
+    sendInFlightRef.current = true;
+    setSending(true);
     let values: SendMessageFormValues;
     try {
       values = await sendForm.validateFields();
     } catch {
       // validation error, keep the modal open
+      sendInFlightRef.current = false;
+      setSending(false);
       return;
     }
-    setSending(true);
     try {
       const payloadPreview = analyzeMessagePayloadPreview({
         topic: values.topic,
@@ -1191,6 +1196,7 @@ const TopicPageContent = ({
     } catch (error) {
       message.error(error instanceof Error ? error.message : '消息发送失败,请稍后重试');
     } finally {
+      sendInFlightRef.current = false;
       setSending(false);
     }
   };

Reply via email to