fix: 收敛消息推送弹窗保存异常
This commit is contained in:
parent
c67fc1b3de
commit
b42a393a41
@ -532,6 +532,20 @@ describe('account message-push modal', () => {
|
|||||||
expect(mocks.update).not.toHaveBeenCalled();
|
expect(mocks.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not persist or lock when normal form validation fails', async () => {
|
||||||
|
mocks.formApi.validate.mockResolvedValueOnce({ valid: false });
|
||||||
|
const wrapper = mountModal();
|
||||||
|
(wrapper.vm as any).openCreate();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
await expect(mocks.modalOptions.onConfirm()).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(mocks.create).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.update).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.modalApi.lock).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.modalApi.unlock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('closes and invalidates an open session when the implicit selfId changes', async () => {
|
it('closes and invalidates an open session when the implicit selfId changes', async () => {
|
||||||
const wrapper = mountModal();
|
const wrapper = mountModal();
|
||||||
(wrapper.vm as any).openCreate();
|
(wrapper.vm as any).openCreate();
|
||||||
@ -616,7 +630,7 @@ describe('account message-push modal', () => {
|
|||||||
expect(wrapper.emitted('saved')).toBeUndefined();
|
expect(wrapper.emitted('saved')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps a rejected session open and closes/emits exactly once on success', async () => {
|
it('contains a rejected session and closes/emits exactly once on retry success', async () => {
|
||||||
mocks.create.mockRejectedValueOnce(new Error('save failed'));
|
mocks.create.mockRejectedValueOnce(new Error('save failed'));
|
||||||
const wrapper = mountModal();
|
const wrapper = mountModal();
|
||||||
(wrapper.vm as any).openCreate();
|
(wrapper.vm as any).openCreate();
|
||||||
@ -627,17 +641,55 @@ describe('account message-push modal', () => {
|
|||||||
targets: [{ targetId: '12345', targetType: 'group' }],
|
targets: [{ targetId: '12345', targetType: 'group' }],
|
||||||
templateId: '50000000000000001',
|
templateId: '50000000000000001',
|
||||||
};
|
};
|
||||||
|
const expectedPayload = {
|
||||||
|
enabled: true,
|
||||||
|
subscriptionId: '20000000000000001',
|
||||||
|
targets: [{ targetId: '12345', targetType: 'group' as const }],
|
||||||
|
templateId: '50000000000000001',
|
||||||
|
};
|
||||||
|
|
||||||
await expect(mocks.modalOptions.onConfirm()).rejects.toThrow('save failed');
|
await expect(mocks.modalOptions.onConfirm()).resolves.toBeUndefined();
|
||||||
expect(mocks.modalApi.lock).toHaveBeenCalledOnce();
|
expect(mocks.modalApi.lock).toHaveBeenCalledOnce();
|
||||||
expect(mocks.modalApi.close).not.toHaveBeenCalled();
|
expect(mocks.modalApi.close).not.toHaveBeenCalled();
|
||||||
expect(mocks.modalApi.unlock).toHaveBeenCalledOnce();
|
expect(mocks.modalApi.unlock).toHaveBeenCalledOnce();
|
||||||
expect(wrapper.emitted('saved')).toBeUndefined();
|
expect(wrapper.emitted('saved')).toBeUndefined();
|
||||||
|
expect(mocks.create).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
'10000000000000001',
|
||||||
|
expectedPayload,
|
||||||
|
);
|
||||||
|
|
||||||
mocks.create.mockResolvedValue({});
|
mocks.create.mockResolvedValue({});
|
||||||
await mocks.modalOptions.onConfirm();
|
await mocks.modalOptions.onConfirm();
|
||||||
expect(mocks.modalApi.close).toHaveBeenCalledOnce();
|
expect(mocks.modalApi.close).toHaveBeenCalledOnce();
|
||||||
expect(mocks.modalApi.unlock).toHaveBeenCalledTimes(2);
|
expect(mocks.modalApi.unlock).toHaveBeenCalledTimes(2);
|
||||||
expect(wrapper.emitted('saved')).toHaveLength(1);
|
expect(wrapper.emitted('saved')).toHaveLength(1);
|
||||||
|
expect(mocks.create).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'10000000000000001',
|
||||||
|
expectedPayload,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains persistence failure when the real modal owner discards the callback promise', async () => {
|
||||||
|
mocks.create.mockRejectedValueOnce(new Error('discarded save failed'));
|
||||||
|
const wrapper = mountModal();
|
||||||
|
(wrapper.vm as any).openCreate();
|
||||||
|
await flushPromises();
|
||||||
|
mocks.currentValues = {
|
||||||
|
enabled: true,
|
||||||
|
subscriptionId: '20000000000000001',
|
||||||
|
targets: [{ targetId: '12345', targetType: 'group' }],
|
||||||
|
templateId: '50000000000000001',
|
||||||
|
};
|
||||||
|
|
||||||
|
void mocks.modalOptions.onConfirm();
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(mocks.modalApi.unlock).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.modalApi.close).not.toHaveBeenCalled();
|
||||||
|
expect(wrapper.emitted('saved')).toBeUndefined();
|
||||||
|
expect(mocks.create).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -107,9 +107,16 @@ export default defineComponent({
|
|||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
class: 'w-[760px]',
|
class: 'w-[760px]',
|
||||||
fullscreenButton: false,
|
fullscreenButton: false,
|
||||||
/** Validates and persists the active account-scoped session. */
|
/**
|
||||||
|
* Persists the active session while containing failures at ModalApi's
|
||||||
|
* non-awaited callback boundary.
|
||||||
|
*/
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
await submit();
|
try {
|
||||||
|
await submit();
|
||||||
|
} catch {
|
||||||
|
// The request/form layer already presents the persistence error.
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Restores only the latest session after destroy-on-close content mounts.
|
* Restores only the latest session after destroy-on-close content mounts.
|
||||||
|
|||||||
@ -422,6 +422,20 @@ describe('message subscription modal', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not persist or lock when normal form validation fails', async () => {
|
||||||
|
mocks.formApi.validate.mockResolvedValueOnce({ valid: false });
|
||||||
|
const wrapper = mountModal();
|
||||||
|
(wrapper.vm as any).openCreate();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
await expect(mocks.modalOptions.onConfirm()).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(mocks.create).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.update).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.modalApi.lock).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.modalApi.unlock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('locks, closes, emits once, and unlocks only after successful persistence', async () => {
|
it('locks, closes, emits once, and unlocks only after successful persistence', async () => {
|
||||||
const wrapper = mountModal();
|
const wrapper = mountModal();
|
||||||
(wrapper.vm as any).openCreate();
|
(wrapper.vm as any).openCreate();
|
||||||
@ -443,18 +457,29 @@ describe('message subscription modal', () => {
|
|||||||
expect(lockOrder).toBeLessThan(createOrder);
|
expect(lockOrder).toBeLessThan(createOrder);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps the modal usable and emits nothing after a rejected mutation', async () => {
|
it('contains a rejected mutation and remains usable for retry', async () => {
|
||||||
mocks.create.mockRejectedValue(new Error('save failed'));
|
mocks.create.mockRejectedValueOnce(new Error('save failed'));
|
||||||
const wrapper = mountModal();
|
const wrapper = mountModal();
|
||||||
(wrapper.vm as any).openCreate();
|
(wrapper.vm as any).openCreate();
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
const expectedPayload = {
|
||||||
|
enabled: true,
|
||||||
|
name: '帕鲁端口变更',
|
||||||
|
remark: 'managed',
|
||||||
|
sourceConfig: {
|
||||||
|
ddnsRecordId: '2041700000000000002',
|
||||||
|
portForwardId: '2041700000000000001',
|
||||||
|
},
|
||||||
|
sourceKey: 'network.stun.mapping-port-changed',
|
||||||
|
};
|
||||||
|
|
||||||
await expect(mocks.modalOptions.onConfirm()).rejects.toThrow('save failed');
|
await expect(mocks.modalOptions.onConfirm()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(mocks.modalApi.lock).toHaveBeenCalledOnce();
|
expect(mocks.modalApi.lock).toHaveBeenCalledOnce();
|
||||||
expect(mocks.modalApi.close).not.toHaveBeenCalled();
|
expect(mocks.modalApi.close).not.toHaveBeenCalled();
|
||||||
expect(wrapper.emitted('saved')).toBeUndefined();
|
expect(wrapper.emitted('saved')).toBeUndefined();
|
||||||
expect(mocks.modalApi.unlock).toHaveBeenCalledOnce();
|
expect(mocks.modalApi.unlock).toHaveBeenCalledOnce();
|
||||||
|
expect(mocks.create).toHaveBeenNthCalledWith(1, expectedPayload);
|
||||||
|
|
||||||
mocks.create.mockResolvedValue({});
|
mocks.create.mockResolvedValue({});
|
||||||
await mocks.modalOptions.onConfirm();
|
await mocks.modalOptions.onConfirm();
|
||||||
@ -462,5 +487,22 @@ describe('message subscription modal', () => {
|
|||||||
expect(mocks.modalApi.close).toHaveBeenCalledOnce();
|
expect(mocks.modalApi.close).toHaveBeenCalledOnce();
|
||||||
expect(wrapper.emitted('saved')).toHaveLength(1);
|
expect(wrapper.emitted('saved')).toHaveLength(1);
|
||||||
expect(mocks.modalApi.unlock).toHaveBeenCalledTimes(2);
|
expect(mocks.modalApi.unlock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mocks.create).toHaveBeenNthCalledWith(2, expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains persistence failure when the real modal owner discards the callback promise', async () => {
|
||||||
|
mocks.create.mockRejectedValueOnce(new Error('discarded save failed'));
|
||||||
|
const wrapper = mountModal();
|
||||||
|
(wrapper.vm as any).openCreate();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
void mocks.modalOptions.onConfirm();
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(mocks.modalApi.unlock).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.modalApi.close).not.toHaveBeenCalled();
|
||||||
|
expect(wrapper.emitted('saved')).toBeUndefined();
|
||||||
|
expect(mocks.create).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -95,9 +95,16 @@ export default defineComponent({
|
|||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
class: 'w-[680px]',
|
class: 'w-[680px]',
|
||||||
fullscreenButton: false,
|
fullscreenButton: false,
|
||||||
/** Validates and persists the current modal session. */
|
/**
|
||||||
|
* Persists the current session while containing failures at ModalApi's
|
||||||
|
* non-awaited callback boundary.
|
||||||
|
*/
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
await submit();
|
try {
|
||||||
|
await submit();
|
||||||
|
} catch {
|
||||||
|
// The request/form layer already presents the persistence error.
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Restores session values after destroy-on-close modal content has mounted.
|
* Restores session values after destroy-on-close modal content has mounted.
|
||||||
|
|||||||
@ -513,22 +513,75 @@ describe('message template modal', () => {
|
|||||||
expect(mocks.update).toHaveBeenCalledWith('10000000000000001', payload);
|
expect(mocks.update).toHaveBeenCalledWith('10000000000000001', payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stays open/unlocked after failure and closes/emits once on retry success', async () => {
|
it('does not persist or lock when normal form validation fails', async () => {
|
||||||
mocks.create.mockRejectedValueOnce(new Error('save failed'));
|
mocks.formApi.validate.mockResolvedValueOnce({ valid: false });
|
||||||
const wrapper = mountModal();
|
const wrapper = mountModal();
|
||||||
(wrapper.vm as any).openCreate();
|
(wrapper.vm as any).openCreate();
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
await expect(mocks.modalOptions.onConfirm()).rejects.toThrow('save failed');
|
await expect(mocks.modalOptions.onConfirm()).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(mocks.create).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.update).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.modalApi.lock).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.modalApi.unlock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains failure and closes/emits once on retry success', async () => {
|
||||||
|
mocks.create.mockRejectedValueOnce(new Error('save failed'));
|
||||||
|
const wrapper = mountModal();
|
||||||
|
(wrapper.vm as any).openCreate();
|
||||||
|
await flushPromises();
|
||||||
|
Object.assign(mocks.formValues, {
|
||||||
|
content: '${{endpoint}}',
|
||||||
|
enabled: true,
|
||||||
|
name: ' template ',
|
||||||
|
remark: ' note ',
|
||||||
|
sourceKey: 'source-a',
|
||||||
|
});
|
||||||
|
const expectedPayload = {
|
||||||
|
content: '${{endpoint}}',
|
||||||
|
enabled: true,
|
||||||
|
name: 'template',
|
||||||
|
remark: 'note',
|
||||||
|
sourceKey: 'source-a',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(mocks.modalOptions.onConfirm()).resolves.toBeUndefined();
|
||||||
expect(mocks.modalApi.lock).toHaveBeenCalledOnce();
|
expect(mocks.modalApi.lock).toHaveBeenCalledOnce();
|
||||||
expect(mocks.modalApi.close).not.toHaveBeenCalled();
|
expect(mocks.modalApi.close).not.toHaveBeenCalled();
|
||||||
expect(wrapper.emitted('saved')).toBeUndefined();
|
expect(wrapper.emitted('saved')).toBeUndefined();
|
||||||
expect(mocks.modalApi.unlock).toHaveBeenCalledOnce();
|
expect(mocks.modalApi.unlock).toHaveBeenCalledOnce();
|
||||||
|
expect(mocks.create).toHaveBeenNthCalledWith(1, expectedPayload);
|
||||||
|
|
||||||
mocks.create.mockResolvedValueOnce({});
|
mocks.create.mockResolvedValueOnce({});
|
||||||
await mocks.modalOptions.onConfirm();
|
await mocks.modalOptions.onConfirm();
|
||||||
expect(mocks.modalApi.close).toHaveBeenCalledOnce();
|
expect(mocks.modalApi.close).toHaveBeenCalledOnce();
|
||||||
expect(wrapper.emitted('saved')).toHaveLength(1);
|
expect(wrapper.emitted('saved')).toHaveLength(1);
|
||||||
expect(mocks.modalApi.unlock).toHaveBeenCalledTimes(2);
|
expect(mocks.modalApi.unlock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mocks.create).toHaveBeenNthCalledWith(2, expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains persistence failure when the real modal owner discards the callback promise', async () => {
|
||||||
|
mocks.create.mockRejectedValueOnce(new Error('discarded save failed'));
|
||||||
|
const wrapper = mountModal();
|
||||||
|
(wrapper.vm as any).openCreate();
|
||||||
|
await flushPromises();
|
||||||
|
Object.assign(mocks.formValues, {
|
||||||
|
content: '${{endpoint}}',
|
||||||
|
enabled: true,
|
||||||
|
name: 'template',
|
||||||
|
remark: '',
|
||||||
|
sourceKey: 'source-a',
|
||||||
|
});
|
||||||
|
|
||||||
|
void mocks.modalOptions.onConfirm();
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(mocks.modalApi.unlock).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.modalApi.close).not.toHaveBeenCalled();
|
||||||
|
expect(wrapper.emitted('saved')).toBeUndefined();
|
||||||
|
expect(mocks.create).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -103,9 +103,16 @@ export default defineComponent({
|
|||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
class: 'w-[760px]',
|
class: 'w-[760px]',
|
||||||
fullscreenButton: false,
|
fullscreenButton: false,
|
||||||
/** Validates and persists the exact five-field template payload. */
|
/**
|
||||||
|
* Persists the exact payload while containing failures at ModalApi's
|
||||||
|
* non-awaited callback boundary.
|
||||||
|
*/
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
await submit();
|
try {
|
||||||
|
await submit();
|
||||||
|
} catch {
|
||||||
|
// The request/form layer already presents the persistence error.
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Resets every mounted form/session state before loading authoritative detail.
|
* Resets every mounted form/session state before loading authoritative detail.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user