From b420fb1989f247b24da5e08ef4cebd59118b34db Mon Sep 17 00:00:00 2001 From: lizhensheng Date: Thu, 21 May 2026 14:38:23 +0800 Subject: [PATCH 1/4] [fix](Home): Refresh WorkflowStatCards and AIBanner data on availability zone switch Co-authored-by: Cursor --- packages/base/src/page/Home/AIBanner/index.tsx | 18 ++++++++++++++++-- .../src/page/Home/WorkflowStatCards/index.tsx | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/base/src/page/Home/AIBanner/index.tsx b/packages/base/src/page/Home/AIBanner/index.tsx index 5a11d5a6e..293020afa 100644 --- a/packages/base/src/page/Home/AIBanner/index.tsx +++ b/packages/base/src/page/Home/AIBanner/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useRequest } from 'ahooks'; import { Card, Space, Typography } from 'antd'; import { useTranslation } from 'react-i18next'; @@ -14,6 +14,8 @@ import { import { AiOutlined } from '@actiontech/icons'; import { AIBannerStyleWrapper } from './style'; import useRecentlyOpenedProjects from '../../Nav/SideMenu/useRecentlyOpenedProjects'; +import EventEmitter from '../../../utils/EventEmitter'; +import EmitterKey from '../../../data/EmitterKey'; import { ROUTE_PATHS } from '@actiontech/dms-kit'; import { AIModuleBannerCardsAiModuleTypeEnum } from '@actiontech/shared/lib/api/sqle/service/common.enum'; import { IAIModuleBannerCards } from '@actiontech/shared/lib/api/sqle/service/common.d'; @@ -45,12 +47,24 @@ const AIBanner: React.FC = () => { useState(false); const { checkPagePermission } = usePermission(); - const { data: bannerData, loading } = useRequest(() => { + const { + data: bannerData, + loading, + refresh + } = useRequest(() => { return AiHubService.GetAIHubBanner().then((res) => { return res.data?.data; }); }); + useEffect(() => { + const { unsubscribe } = EventEmitter.subscribe( + EmitterKey.DMS_Reload_Initial_Data, + refresh + ); + return unsubscribe; + }, [refresh]); + const bannerState = useMemo(() => { const modules = bannerData?.modules ?? []; const isModuleEnabled = (module?: IAIModuleBannerCards) => diff --git a/packages/base/src/page/Home/WorkflowStatCards/index.tsx b/packages/base/src/page/Home/WorkflowStatCards/index.tsx index 4d9400e57..ec47a779d 100644 --- a/packages/base/src/page/Home/WorkflowStatCards/index.tsx +++ b/packages/base/src/page/Home/WorkflowStatCards/index.tsx @@ -1,4 +1,5 @@ import { useRequest } from 'ahooks'; +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, Typography } from 'antd'; import { @@ -13,6 +14,8 @@ import { useTypedNavigate } from '@actiontech/shared'; import { ROUTE_PATHS } from '@actiontech/dms-kit'; import { RightOutlined } from '@ant-design/icons'; import { WorkflowStatCardsWrapper, WorkflowCardItemWrapper } from './style'; +import EventEmitter from '../../../utils/EventEmitter'; +import EmitterKey from '../../../data/EmitterKey'; const WORKFLOW_ACCENT: Record = { [GetGlobalWorkflowListV2FilterCardEnum.pending_for_me]: '#fa8c16', @@ -25,10 +28,22 @@ const WorkflowStatCards: React.FC = () => { const { t } = useTranslation(); const navigate = useTypedNavigate(); - const { data: workflowStats, loading } = useRequest(() => + const { + data: workflowStats, + loading, + refresh + } = useRequest(() => GlobalDashboardService.GetGlobalWorkflowStatisticsV2({}) ); + useEffect(() => { + const { unsubscribe } = EventEmitter.subscribe( + EmitterKey.DMS_Reload_Initial_Data, + refresh + ); + return unsubscribe; + }, [refresh]); + const cards = [ { key: GetGlobalWorkflowListV2FilterCardEnum.pending_for_me, From 4ed6dfab18e7b31247867bd6b0bd5ed4f85e9723 Mon Sep 17 00:00:00 2001 From: lizhensheng Date: Thu, 21 May 2026 15:24:03 +0800 Subject: [PATCH 2/4] [test](Home): Add unit tests for zone-switch refresh on WorkflowStatCards and AIBanner Co-authored-by: Cursor --- .../Home/AIBanner/__tests__/index.test.tsx | 146 ++++++++++++++++++ .../__tests__/index.test.tsx | 16 ++ 2 files changed, 162 insertions(+) create mode 100644 packages/base/src/page/Home/AIBanner/__tests__/index.test.tsx diff --git a/packages/base/src/page/Home/AIBanner/__tests__/index.test.tsx b/packages/base/src/page/Home/AIBanner/__tests__/index.test.tsx new file mode 100644 index 000000000..4b4878293 --- /dev/null +++ b/packages/base/src/page/Home/AIBanner/__tests__/index.test.tsx @@ -0,0 +1,146 @@ +import { act, cleanup, fireEvent, screen } from '@testing-library/react'; +import AIBanner from '..'; +import { baseSuperRender } from '../../../../testUtils/superRender'; +import { sqleMockApi } from '@actiontech/shared/lib/testUtil'; +import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser'; +import { mockUsePermission } from '@actiontech/shared/lib/testUtil/mockHook/mockUsePermission'; +import { mockUseRecentlyOpenedProjects } from '../../../Nav/SideMenu/testUtils/mockUseRecentlyOpenedProjects'; +import { useTypedNavigate } from '@actiontech/shared'; +import { ROUTE_PATHS } from '@actiontech/shared/lib/data/routePaths'; +import EventEmitter from '../../../../utils/EventEmitter'; +import EmitterKey from '../../../../data/EmitterKey'; + +jest.mock('@actiontech/shared', () => ({ + ...jest.requireActual('@actiontech/shared'), + useTypedNavigate: jest.fn() +})); + +describe('AIBanner', () => { + const navigateSpy = jest.fn(); + let getAIHubBannerSpy: jest.SpyInstance; + + beforeEach(() => { + jest.useFakeTimers(); + getAIHubBannerSpy = sqleMockApi.statistic.getAIHubBanner(); + (useTypedNavigate as jest.Mock).mockImplementation(() => navigateSpy); + mockUseCurrentUser(); + mockUseRecentlyOpenedProjects({ currentProjectID: '1' }); + mockUsePermission( + { checkPagePermission: jest.fn().mockReturnValue(true) }, + { useSpyOnMockHooks: true } + ); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + cleanup(); + }); + + it('should render loading state on initial render', () => { + const { baseElement } = baseSuperRender(); + expect(baseElement).toMatchSnapshot(); + expect(getAIHubBannerSpy).toHaveBeenCalledTimes(1); + }); + + it('should render AI banner after data loaded', async () => { + const { baseElement } = baseSuperRender(); + + await act(async () => jest.advanceTimersByTime(3000)); + + expect(baseElement).toMatchSnapshot(); + expect(screen.getByText('AI治理效能洞察')).toBeInTheDocument(); + expect( + screen.getByText('基于大模型实时监控规范与性能, AI驱动全链路质量闭环。') + ).toBeInTheDocument(); + expect(screen.getByText('查看完整报告')).toBeInTheDocument(); + expect(screen.getByText('风险拦截')).toBeInTheDocument(); + expect(screen.getByText('性能优化')).toBeInTheDocument(); + expect(screen.getByText('AI 性能引擎')).toBeInTheDocument(); + expect(screen.getByText('AI 智能修正')).toBeInTheDocument(); + }); + + it('should navigate to report statistics when clicking "查看完整报告"', async () => { + baseSuperRender(); + + await act(async () => jest.advanceTimersByTime(3000)); + + fireEvent.click(screen.getByText('查看完整报告')); + + expect(navigateSpy).toHaveBeenCalledWith( + ROUTE_PATHS.SQLE.REPORT_STATISTICS.index, + { queries: { tab: 'ai-governance' } } + ); + }); + + it('should navigate to SQL optimization when clicking "AI 性能引擎" with current project', async () => { + baseSuperRender(); + + await act(async () => jest.advanceTimersByTime(3000)); + + fireEvent.click(screen.getByText('AI 性能引擎')); + + expect(navigateSpy).toHaveBeenCalledWith( + ROUTE_PATHS.SQLE.SQL_OPTIMIZATION.create, + { params: { projectID: '1' } } + ); + }); + + it('should trigger smart correction handler when clicking "AI 智能修正"', async () => { + baseSuperRender(); + + await act(async () => jest.advanceTimersByTime(3000)); + + // 点击 AI 智能修正时,不会触发页面跳转(与 AI 性能引擎不同) + expect(() => fireEvent.click(screen.getByText('AI 智能修正'))).not.toThrow(); + expect(navigateSpy).not.toHaveBeenCalled(); + }); + + it('should show paid feature prompt when all modules are disabled', async () => { + getAIHubBannerSpy.mockImplementation(() => + Promise.resolve({ + data: { + data: { + modules: [ + { + ai_module_type: 'performance_engine', + is_enabled: false, + banner_cards: [{ need_display: true }] + }, + { + ai_module_type: 'smart_correction', + is_enabled: false, + banner_cards: [{ need_display: true }] + } + ] + } + } + }) + ); + + baseSuperRender(); + await act(async () => jest.advanceTimersByTime(3000)); + + fireEvent.click(screen.getByText('查看完整报告')); + + expect( + screen.getByText( + '当前功能为付费增值模块,请联系商务获取详细信息' + ) + ).toBeInTheDocument(); + }); + + it('should refresh AI banner data when DMS_Reload_Initial_Data event is emitted', async () => { + baseSuperRender(); + + await act(async () => jest.advanceTimersByTime(3000)); + expect(getAIHubBannerSpy).toHaveBeenCalledTimes(1); + + await act(async () => { + EventEmitter.emit(EmitterKey.DMS_Reload_Initial_Data); + await jest.advanceTimersByTime(3000); + }); + + expect(getAIHubBannerSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/base/src/page/Home/WorkflowStatCards/__tests__/index.test.tsx b/packages/base/src/page/Home/WorkflowStatCards/__tests__/index.test.tsx index fb02dd09a..1f031d914 100644 --- a/packages/base/src/page/Home/WorkflowStatCards/__tests__/index.test.tsx +++ b/packages/base/src/page/Home/WorkflowStatCards/__tests__/index.test.tsx @@ -5,6 +5,8 @@ import { sqleMockApi } from '@actiontech/shared/lib/testUtil'; import { useTypedNavigate } from '@actiontech/shared'; import { ROUTE_PATHS } from '@actiontech/dms-kit'; import { GetGlobalWorkflowListV2FilterCardEnum } from '@actiontech/shared/lib/api/sqle/service/GlobalDashboard/index.enum'; +import EventEmitter from '../../../../utils/EventEmitter'; +import EmitterKey from '../../../../data/EmitterKey'; jest.mock('@actiontech/shared', () => ({ ...jest.requireActual('@actiontech/shared'), @@ -152,4 +154,18 @@ describe('WorkflowStatCards', () => { } ); }); + + it('should refresh workflow statistics when DMS_Reload_Initial_Data event is emitted', async () => { + baseSuperRender(); + + await act(async () => jest.advanceTimersByTime(3000)); + expect(getGlobalWorkflowStatisticsSpy).toHaveBeenCalledTimes(1); + + await act(async () => { + EventEmitter.emit(EmitterKey.DMS_Reload_Initial_Data); + await jest.advanceTimersByTime(3000); + }); + + expect(getGlobalWorkflowStatisticsSpy).toHaveBeenCalledTimes(2); + }); }); From dc6d72d17817249a90b0b8b57a525fe79172b275 Mon Sep 17 00:00:00 2001 From: lizhensheng Date: Thu, 21 May 2026 15:45:37 +0800 Subject: [PATCH 3/4] [fix](home): code formatter Co-authored-by: Cursor --- .../src/page/Home/AIBanner/__tests__/index.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/base/src/page/Home/AIBanner/__tests__/index.test.tsx b/packages/base/src/page/Home/AIBanner/__tests__/index.test.tsx index 4b4878293..a75b9d408 100644 --- a/packages/base/src/page/Home/AIBanner/__tests__/index.test.tsx +++ b/packages/base/src/page/Home/AIBanner/__tests__/index.test.tsx @@ -6,7 +6,7 @@ import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/moc import { mockUsePermission } from '@actiontech/shared/lib/testUtil/mockHook/mockUsePermission'; import { mockUseRecentlyOpenedProjects } from '../../../Nav/SideMenu/testUtils/mockUseRecentlyOpenedProjects'; import { useTypedNavigate } from '@actiontech/shared'; -import { ROUTE_PATHS } from '@actiontech/shared/lib/data/routePaths'; +import { ROUTE_PATHS } from '@actiontech/dms-kit'; import EventEmitter from '../../../../utils/EventEmitter'; import EmitterKey from '../../../../data/EmitterKey'; @@ -81,7 +81,7 @@ describe('AIBanner', () => { fireEvent.click(screen.getByText('AI 性能引擎')); expect(navigateSpy).toHaveBeenCalledWith( - ROUTE_PATHS.SQLE.SQL_OPTIMIZATION.create, + ROUTE_PATHS.SQLE.SQL_AUDIT.create_optimization, { params: { projectID: '1' } } ); }); @@ -92,7 +92,9 @@ describe('AIBanner', () => { await act(async () => jest.advanceTimersByTime(3000)); // 点击 AI 智能修正时,不会触发页面跳转(与 AI 性能引擎不同) - expect(() => fireEvent.click(screen.getByText('AI 智能修正'))).not.toThrow(); + expect(() => + fireEvent.click(screen.getByText('AI 智能修正')) + ).not.toThrow(); expect(navigateSpy).not.toHaveBeenCalled(); }); @@ -124,9 +126,7 @@ describe('AIBanner', () => { fireEvent.click(screen.getByText('查看完整报告')); expect( - screen.getByText( - '当前功能为付费增值模块,请联系商务获取详细信息' - ) + screen.getByText('当前功能为付费增值模块,请联系商务获取详细信息') ).toBeInTheDocument(); }); From a775ef2cb279026f42f50abc6e183afb05b26f59 Mon Sep 17 00:00:00 2001 From: lizhensheng Date: Fri, 22 May 2026 11:21:51 +0800 Subject: [PATCH 4/4] [test](home): update snapshot --- .../__snapshots__/index.test.tsx.snap | 311 ++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 packages/base/src/page/Home/AIBanner/__tests__/__snapshots__/index.test.tsx.snap diff --git a/packages/base/src/page/Home/AIBanner/__tests__/__snapshots__/index.test.tsx.snap b/packages/base/src/page/Home/AIBanner/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..090ef7d1c --- /dev/null +++ b/packages/base/src/page/Home/AIBanner/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,311 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AIBanner should render AI banner after data loaded 1`] = ` + +
+
+
+
+ +
+
+
+
+ +`; + +exports[`AIBanner should render loading state on initial render 1`] = ` + +
+
+
+
+
+
+
    +
  • +
  • +
  • +
  • +
+
+
+
+
+
+
+ +`;