fix: 补强消息订阅页面接线验证

This commit is contained in:
sunlei 2026-07-24 16:09:08 +08:00
parent 074b05107e
commit c6bfd5851d
3 changed files with 123 additions and 22 deletions

View File

@ -204,7 +204,7 @@ describe('message subscription modal', () => {
}); });
it('contains the exact source-only fields in their locked order', () => { it('contains the exact source-only fields in their locked order', () => {
mountModal(); const wrapper = mountModal();
const fields = mocks.formOptions.schema.map( const fields = mocks.formOptions.schema.map(
(field: any) => field.fieldName, (field: any) => field.fieldName,
@ -220,6 +220,9 @@ describe('message subscription modal', () => {
expect(JSON.stringify(mocks.formOptions.schema)).not.toMatch( expect(JSON.stringify(mocks.formOptions.schema)).not.toMatch(
/account|selfId|template|target|group|private|publish|delivery|event|worker|queue/i, /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 () => { 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 () => { it('submits the exact trimmed source-only create payload', async () => {
const wrapper = mountModal(); const wrapper = mountModal();
(wrapper.vm as any).openCreate(); (wrapper.vm as any).openCreate();

View File

@ -78,7 +78,7 @@ export default defineComponent({
(option) => option.id === currentDdnsId, (option) => option.id === currentDdnsId,
); );
if ( if (
currentDdns && !currentDdns ||
currentDdns.portForwardId !== selectedPortForwardId.value currentDdns.portForwardId !== selectedPortForwardId.value
) { ) {
await formApi.setValues({ ddnsRecordId: undefined }); await formApi.setValues({ ddnsRecordId: undefined });

View File

@ -11,22 +11,41 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import MessageSubscriptionList from './list'; import MessageSubscriptionList from './list';
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => {
accessCodes: new Set<string>(), const state = {
api: { accessCodes: new Set<string>(),
delete: vi.fn(), api: {
getList: vi.fn(), delete: vi.fn(),
getOptions: vi.fn(), getList: vi.fn(),
getSources: vi.fn(), getOptions: vi.fn(),
toggle: vi.fn(), getSources: vi.fn(),
}, toggle: vi.fn(),
modalOpenCreate: vi.fn(), },
modalOpenEdit: vi.fn(), modalOpenCreate: vi.fn(),
tableApi: { modalOpenEdit: vi.fn(),
reload: vi.fn(), registeredTableOptions: undefined as any,
}, registerTable: vi.fn(),
tableOptions: undefined as any, 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', () => ({ vi.mock('@vben/access', () => ({
useAccess: () => ({ useAccess: () => ({
@ -67,8 +86,10 @@ vi.mock('antdv-next', () => ({
vi.mock('#/components/ktTable', () => ({ vi.mock('#/components/ktTable', () => ({
KtTable: defineComponent({ KtTable: defineComponent({
name: 'MockKtTable', name: 'MockKtTable',
/** Renders the native table marker and its built-in explicit refresh control. */ emits: ['register'],
setup(_, { slots }) { /** Registers the table boundary before rendering its explicit refresh control. */
setup(_, { emit, slots }) {
emit('register', {});
return () => return () =>
h('section', { 'data-testid': 'subscription-table' }, [ h('section', { 'data-testid': 'subscription-table' }, [
h( h(
@ -88,7 +109,7 @@ vi.mock('#/components/ktTable', () => ({
}), }),
useKtTable: vi.fn((options) => { useKtTable: vi.fn((options) => {
mocks.tableOptions = 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:Toggle',
'QqBot:MessageSubscription:Update', 'QqBot:MessageSubscription:Update',
]); ]);
mocks.registeredTableOptions = undefined;
mocks.tableOptions = undefined; mocks.tableOptions = undefined;
mocks.tableApi.reload.mockResolvedValue(undefined);
mocks.api.getSources.mockResolvedValue([ mocks.api.getSources.mockResolvedValue([
{ {
description: 'STUN 端口变化', description: 'STUN 端口变化',
@ -198,6 +219,7 @@ describe('message subscription list', () => {
); );
expect(mocks.tableOptions.immediate).toBe(false); expect(mocks.tableOptions.immediate).toBe(false);
expect(mocks.tableOptions.rowKey).toBe('id'); expect(mocks.tableOptions.rowKey).toBe('id');
expect(mocks.registerTable).toHaveBeenCalledOnce();
}); });
it('pins the exact filters, columns, and strict page-result proxy', async () => { it('pins the exact filters, columns, and strict page-result proxy', async () => {
@ -235,6 +257,8 @@ describe('message subscription list', () => {
await flushPromises(); await flushPromises();
expect(mocks.tableApi.reload).not.toHaveBeenCalled(); 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.getSources).not.toHaveBeenCalled();
expect(mocks.api.getOptions).not.toHaveBeenCalled(); expect(mocks.api.getOptions).not.toHaveBeenCalled();
expect(wrapper.find('[data-testid="subscription-table"]').exists()).toBe( expect(wrapper.find('[data-testid="subscription-table"]').exists()).toBe(
@ -248,6 +272,17 @@ describe('message subscription list', () => {
await flushPromises(); await flushPromises();
expect(mocks.tableApi.reload).toHaveBeenCalledOnce(); 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.getSources).toHaveBeenCalledOnce();
expect(mocks.api.getOptions).toHaveBeenCalledOnce(); expect(mocks.api.getOptions).toHaveBeenCalledOnce();
@ -255,6 +290,7 @@ describe('message subscription list', () => {
await flushPromises(); await flushPromises();
expect(mocks.tableApi.reload).toHaveBeenCalledOnce(); expect(mocks.tableApi.reload).toHaveBeenCalledOnce();
expect(mocks.api.getList).toHaveBeenCalledOnce();
expect(mocks.api.getSources).toHaveBeenCalledOnce(); expect(mocks.api.getSources).toHaveBeenCalledOnce();
expect(mocks.api.getOptions).toHaveBeenCalledOnce(); expect(mocks.api.getOptions).toHaveBeenCalledOnce();
expect(vi.getTimerCount()).toBe(0); expect(vi.getTimerCount()).toBe(0);
@ -327,6 +363,7 @@ describe('message subscription list', () => {
const wrapper = mount(MessageSubscriptionList); const wrapper = mount(MessageSubscriptionList);
await flushPromises(); await flushPromises();
mocks.tableApi.reload.mockClear(); mocks.tableApi.reload.mockClear();
mocks.api.getList.mockClear();
const row = createRow(); const row = createRow();
await mocks.tableOptions.buttons[0].onClick({}); await mocks.tableOptions.buttons[0].onClick({});
@ -338,17 +375,20 @@ describe('message subscription list', () => {
expect(mocks.modalOpenCreate).toHaveBeenCalledOnce(); expect(mocks.modalOpenCreate).toHaveBeenCalledOnce();
expect(mocks.modalOpenEdit).toHaveBeenCalledWith(row); expect(mocks.modalOpenEdit).toHaveBeenCalledWith(row);
expect(mocks.tableApi.reload).toHaveBeenCalledOnce(); expect(mocks.tableApi.reload).toHaveBeenCalledOnce();
expect(mocks.api.getList).toHaveBeenCalledOnce();
}); });
it('explicit refresh reloads only the list and leaves metadata cached', async () => { it('explicit refresh reloads only the list and leaves metadata cached', async () => {
const wrapper = mount(MessageSubscriptionList); const wrapper = mount(MessageSubscriptionList);
await flushPromises(); await flushPromises();
mocks.tableApi.reload.mockClear(); mocks.tableApi.reload.mockClear();
mocks.api.getList.mockClear();
await wrapper.get('[data-testid="explicit-refresh"]').trigger('click'); await wrapper.get('[data-testid="explicit-refresh"]').trigger('click');
await flushPromises(); await flushPromises();
expect(mocks.tableApi.reload).toHaveBeenCalledOnce(); expect(mocks.tableApi.reload).toHaveBeenCalledOnce();
expect(mocks.api.getList).toHaveBeenCalledOnce();
expect(mocks.api.getSources).toHaveBeenCalledOnce(); expect(mocks.api.getSources).toHaveBeenCalledOnce();
expect(mocks.api.getOptions).toHaveBeenCalledOnce(); expect(mocks.api.getOptions).toHaveBeenCalledOnce();
expect(vi.getTimerCount()).toBe(0); expect(vi.getTimerCount()).toBe(0);