From c6bfd5851dd19e89c932eba86c713a359817c9ff Mon Sep 17 00:00:00 2001 From: sunlei Date: Fri, 24 Jul 2026 16:09:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A1=A5=E5=BC=BA=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E9=A1=B5=E9=9D=A2=E6=8E=A5=E7=BA=BF=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MessageSubscriptionModal.spec.tsx | 63 ++++++++++++++- .../components/MessageSubscriptionModal.tsx | 2 +- .../qqbot/message-subscription/list.spec.tsx | 80 ++++++++++++++----- 3 files changed, 123 insertions(+), 22 deletions(-) diff --git a/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.spec.tsx b/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.spec.tsx index 74a42b4..2548883 100644 --- a/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.spec.tsx +++ b/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.spec.tsx @@ -204,7 +204,7 @@ describe('message subscription modal', () => { }); it('contains the exact source-only fields in their locked order', () => { - mountModal(); + const wrapper = mountModal(); const fields = mocks.formOptions.schema.map( (field: any) => field.fieldName, @@ -220,6 +220,9 @@ describe('message subscription modal', () => { expect(JSON.stringify(mocks.formOptions.schema)).not.toMatch( /account|selfId|template|target|group|private|publish|delivery|event|worker|queue/i, ); + expect( + wrapper.get('section').find('[data-testid="subscription-form"]').exists(), + ).toBe(true); }); it('opens before reset/set/reset-validate and clears edit state for create', async () => { @@ -317,6 +320,64 @@ describe('message subscription modal', () => { ]); }); + it('clears a selected DDNS that is absent from the loaded options', async () => { + mountModal(); + + await mocks.formOptions.handleValuesChange( + { + ddnsRecordId: '2041700000000000099', + portForwardId: '2041700000000000001', + }, + ['portForwardId'], + ); + + expect(mocks.formApi.setValues).toHaveBeenLastCalledWith({ + ddnsRecordId: undefined, + }); + }); + + it.each([ + ['loading', undefined], + ['empty', { ddnsRecords: [], portForwards: [] }], + ])( + 'clears and does not submit a stale DDNS while options are %s', + async (_, stunOptions) => { + const values = { + ddnsRecordId: '2041700000000000099', + enabled: true, + name: '帕鲁端口变更', + portForwardId: '2041700000000000001', + remark: '', + sourceKey: 'network.stun.mapping-port-changed', + }; + mocks.formApi.setValues.mockImplementation(async (patch) => { + Object.assign(values, patch); + }); + mocks.formApi.getValues.mockImplementation(async () => values); + mount(MessageSubscriptionModal, { + props: { + sources: createSources(), + stunOptions, + }, + }); + + await mocks.formOptions.handleValuesChange(values, ['portForwardId']); + await mocks.modalOptions.onConfirm(); + + expect(mocks.create).toHaveBeenCalledWith( + expect.objectContaining({ + sourceConfig: { + ddnsRecordId: undefined, + portForwardId: '2041700000000000001', + }, + }), + ); + expect(JSON.stringify(mocks.create.mock.calls[0]?.[0])).not.toContain( + '2041700000000000099', + ); + }, + ); + it('submits the exact trimmed source-only create payload', async () => { const wrapper = mountModal(); (wrapper.vm as any).openCreate(); diff --git a/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.tsx b/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.tsx index d9924fe..5b41a66 100644 --- a/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.tsx +++ b/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.tsx @@ -78,7 +78,7 @@ export default defineComponent({ (option) => option.id === currentDdnsId, ); if ( - currentDdns && + !currentDdns || currentDdns.portForwardId !== selectedPortForwardId.value ) { await formApi.setValues({ ddnsRecordId: undefined }); diff --git a/apps/web-antdv-next/src/views/qqbot/message-subscription/list.spec.tsx b/apps/web-antdv-next/src/views/qqbot/message-subscription/list.spec.tsx index 4880577..30c81d9 100644 --- a/apps/web-antdv-next/src/views/qqbot/message-subscription/list.spec.tsx +++ b/apps/web-antdv-next/src/views/qqbot/message-subscription/list.spec.tsx @@ -11,22 +11,41 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import MessageSubscriptionList from './list'; -const mocks = vi.hoisted(() => ({ - accessCodes: new Set(), - api: { - delete: vi.fn(), - getList: vi.fn(), - getOptions: vi.fn(), - getSources: vi.fn(), - toggle: vi.fn(), - }, - modalOpenCreate: vi.fn(), - modalOpenEdit: vi.fn(), - tableApi: { - reload: vi.fn(), - }, - tableOptions: undefined as any, -})); +const mocks = vi.hoisted(() => { + const state = { + accessCodes: new Set(), + api: { + delete: vi.fn(), + getList: vi.fn(), + getOptions: vi.fn(), + getSources: vi.fn(), + toggle: vi.fn(), + }, + modalOpenCreate: vi.fn(), + modalOpenEdit: vi.fn(), + registeredTableOptions: undefined as any, + registerTable: vi.fn(), + tableApi: { + reload: vi.fn(), + }, + tableOptions: undefined as any, + }; + /** Installs the list path only when the rendered KtTable emits registration. */ + state.registerTable.mockImplementation(() => { + state.registeredTableOptions = state.tableOptions; + }); + /** Reloads through the registered table options and returns the caller result. */ + state.tableApi.reload.mockImplementation(async () => { + if (!state.registeredTableOptions) { + throw new Error('[MockKtTable]: table is not registered yet.'); + } + return await state.registeredTableOptions.api.list({ + pageNo: 1, + pageSize: 10, + }); + }); + return state; +}); vi.mock('@vben/access', () => ({ useAccess: () => ({ @@ -67,8 +86,10 @@ vi.mock('antdv-next', () => ({ vi.mock('#/components/ktTable', () => ({ KtTable: defineComponent({ name: 'MockKtTable', - /** Renders the native table marker and its built-in explicit refresh control. */ - setup(_, { slots }) { + emits: ['register'], + /** Registers the table boundary before rendering its explicit refresh control. */ + setup(_, { emit, slots }) { + emit('register', {}); return () => h('section', { 'data-testid': 'subscription-table' }, [ h( @@ -88,7 +109,7 @@ vi.mock('#/components/ktTable', () => ({ }), useKtTable: vi.fn((options) => { mocks.tableOptions = options; - return [vi.fn(), mocks.tableApi]; + return [mocks.registerTable, mocks.tableApi]; }), })); @@ -155,8 +176,8 @@ describe('message subscription list', () => { 'QqBot:MessageSubscription:Toggle', 'QqBot:MessageSubscription:Update', ]); + mocks.registeredTableOptions = undefined; mocks.tableOptions = undefined; - mocks.tableApi.reload.mockResolvedValue(undefined); mocks.api.getSources.mockResolvedValue([ { description: 'STUN 端口变化', @@ -198,6 +219,7 @@ describe('message subscription list', () => { ); expect(mocks.tableOptions.immediate).toBe(false); expect(mocks.tableOptions.rowKey).toBe('id'); + expect(mocks.registerTable).toHaveBeenCalledOnce(); }); it('pins the exact filters, columns, and strict page-result proxy', async () => { @@ -235,6 +257,8 @@ describe('message subscription list', () => { await flushPromises(); expect(mocks.tableApi.reload).not.toHaveBeenCalled(); + expect(mocks.registerTable).not.toHaveBeenCalled(); + expect(mocks.api.getList).not.toHaveBeenCalled(); expect(mocks.api.getSources).not.toHaveBeenCalled(); expect(mocks.api.getOptions).not.toHaveBeenCalled(); expect(wrapper.find('[data-testid="subscription-table"]').exists()).toBe( @@ -248,6 +272,17 @@ describe('message subscription list', () => { await flushPromises(); expect(mocks.tableApi.reload).toHaveBeenCalledOnce(); + expect(mocks.api.getList).toHaveBeenCalledOnce(); + expect(mocks.api.getList).toHaveBeenCalledWith({ + pageNo: 1, + pageSize: 10, + }); + await expect( + mocks.tableApi.reload.mock.results[0]?.value, + ).resolves.toStrictEqual({ + items: [createRow()], + total: 1, + }); expect(mocks.api.getSources).toHaveBeenCalledOnce(); expect(mocks.api.getOptions).toHaveBeenCalledOnce(); @@ -255,6 +290,7 @@ describe('message subscription list', () => { await flushPromises(); expect(mocks.tableApi.reload).toHaveBeenCalledOnce(); + expect(mocks.api.getList).toHaveBeenCalledOnce(); expect(mocks.api.getSources).toHaveBeenCalledOnce(); expect(mocks.api.getOptions).toHaveBeenCalledOnce(); expect(vi.getTimerCount()).toBe(0); @@ -327,6 +363,7 @@ describe('message subscription list', () => { const wrapper = mount(MessageSubscriptionList); await flushPromises(); mocks.tableApi.reload.mockClear(); + mocks.api.getList.mockClear(); const row = createRow(); await mocks.tableOptions.buttons[0].onClick({}); @@ -338,17 +375,20 @@ describe('message subscription list', () => { expect(mocks.modalOpenCreate).toHaveBeenCalledOnce(); expect(mocks.modalOpenEdit).toHaveBeenCalledWith(row); expect(mocks.tableApi.reload).toHaveBeenCalledOnce(); + expect(mocks.api.getList).toHaveBeenCalledOnce(); }); it('explicit refresh reloads only the list and leaves metadata cached', async () => { const wrapper = mount(MessageSubscriptionList); await flushPromises(); mocks.tableApi.reload.mockClear(); + mocks.api.getList.mockClear(); await wrapper.get('[data-testid="explicit-refresh"]').trigger('click'); await flushPromises(); expect(mocks.tableApi.reload).toHaveBeenCalledOnce(); + expect(mocks.api.getList).toHaveBeenCalledOnce(); expect(mocks.api.getSources).toHaveBeenCalledOnce(); expect(mocks.api.getOptions).toHaveBeenCalledOnce(); expect(vi.getTimerCount()).toBe(0);