diff --git a/web/src/pages/studio/Producer.tsx b/web/src/pages/studio/Producer.tsx index 241adffc..f88b8197 100644 --- a/web/src/pages/studio/Producer.tsx +++ b/web/src/pages/studio/Producer.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import { useState, useRef } from 'react'; +import { useEffect, useState } from 'react'; import { Button, Form, Input, Select, Table, Card, App } from 'antd'; import { MagnifyingGlass } from '@phosphor-icons/react'; import { useLang } from '../../i18n/LangContext'; @@ -32,21 +32,30 @@ const ProducerPage = () => { const [loading, setLoading] = useState(false); const { t } = useLang(); const { message } = App.useApp(); + const fetchTopicFailedMessage = t('producer.fetchTopicFailed'); + + useEffect(() => { + let cancelled = false; - // Load topic list on mount (once) - const initialized = useRef(null); - if (initialized.current == null) { - initialized.current = true; const loadTopics = async () => { try { const topics = await fetchTopicList(); - setTopicList(topics); + if (!cancelled) { + setTopicList(topics); + } } catch { - message.error(t('producer.fetchTopicFailed')); + if (!cancelled) { + message.error(fetchTopicFailedMessage); + } } }; - loadTopics(); - } + + void loadTopics(); + + return () => { + cancelled = true; + }; + }, [fetchTopicFailedMessage, message]); const onFinish = async (values: { selectedTopic: string; producerGroup: string }) => { setLoading(true); diff --git a/web/src/pages/studio/__tests__/Producer.test.tsx b/web/src/pages/studio/__tests__/Producer.test.tsx new file mode 100644 index 00000000..b4dbb091 --- /dev/null +++ b/web/src/pages/studio/__tests__/Producer.test.tsx @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { App } from 'antd'; +import { LangProvider } from '../../../i18n/LangContext'; +import ProducerPage from '../Producer'; +import { fetchTopicList } from '../../../api/producer'; + +vi.mock('../../../api/producer', () => ({ + fetchTopicList: vi.fn(), + queryProducerConnection: vi.fn(), +})); + +beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); +}); + +const renderWithProviders = (ui: React.ReactElement) => { + return render( + + {ui} + , + ); +}; + +describe('ProducerPage', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(fetchTopicList).mockResolvedValue(['order-events', 'payment-events']); + }); + + it('loads topic options after mount', async () => { + renderWithProviders(); + + await waitFor(() => { + expect(fetchTopicList).toHaveBeenCalledTimes(1); + }); + }); + + it('renders topic options loaded from the API', async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await waitFor(() => { + expect(fetchTopicList).toHaveBeenCalledTimes(1); + }); + + await user.click(screen.getByRole('combobox')); + await screen.findByRole('option', { name: 'order-events' }); + expect(await screen.findByRole('option', { name: 'payment-events' })).toBeInTheDocument(); + }); +});