From c2272b12d84880b7d86ca29075508e3a58865847 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Thu, 5 Nov 2026 13:53:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=E4=B8=8E=E4=BB=A3=E7=A0=81=E5=9D=97=E5=88=A0=E9=99=A4=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E6=94=B9=E7=94=A8=20Popconfirm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sidebar/code-block/CodeBlockList.vue | 75 ++++++++----- .../sidebar/data-source/DataSourceList.vue | 32 ++++-- .../data-source/DataSourceListPanel.vue | 11 +- packages/editor/src/theme/tree.scss | 12 +++ .../sidebar/code-block/CodeBlockList.spec.ts | 68 ++++++++++-- .../data-source/DataSourceList.spec.ts | 64 ++++++++++- .../data-source/DataSourceListPanel.spec.ts | 32 +++++- .../layouts/sidebar/delete-popconfirm.spec.ts | 101 ++++++++++++++++++ 8 files changed, 341 insertions(+), 54 deletions(-) create mode 100644 packages/editor/tests/unit/layouts/sidebar/delete-popconfirm.spec.ts diff --git a/packages/editor/src/layouts/sidebar/code-block/CodeBlockList.vue b/packages/editor/src/layouts/sidebar/code-block/CodeBlockList.vue index 57724c66..5feb2ef3 100644 --- a/packages/editor/src/layouts/sidebar/code-block/CodeBlockList.vue +++ b/packages/editor/src/layouts/sidebar/code-block/CodeBlockList.vue @@ -20,12 +20,29 @@ @@ -37,9 +54,8 @@ import { Close, Edit, View } from '@element-plus/icons-vue'; import type { Id, MNode } from '@tmagic/core'; import { DepTargetType } from '@tmagic/core'; -import { tMagicMessage, tMagicMessageBox, TMagicTooltip } from '@tmagic/design'; +import { TMagicButton, tMagicMessage, TMagicPopconfirm, TMagicTooltip } from '@tmagic/design'; -import Icon from '@editor/components/Icon.vue'; import Tree from '@editor/components/Tree.vue'; import { useFilter } from '@editor/hooks/use-filter'; import { useNodeStatus } from '@editor/hooks/use-node-status'; @@ -142,29 +158,38 @@ const editCode = (id: string) => { emit('edit', id); }; -const deleteCode = async (id: string, { historySource }: { historySource?: HistoryOpSource } = {}) => { +/** 返回不能删除的原因,可删除时返回 undefined */ +const getDeleteErrorType = (id: string): CodeDeleteErrorType | undefined => { const currentCode = codeList.value.find((codeItem) => codeItem.id === id); - const existBinds = Boolean(currentCode?.items?.length); - const undeleteableList = codeBlockService.getUndeletableList() || []; - if (!existBinds && !undeleteableList.includes(id)) { - await tMagicMessageBox.confirm('确定删除该代码块吗?', '提示', { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning', - }); + if (currentCode?.items?.length) { + return CodeDeleteErrorType.BIND; + } - // 无绑定关系,且不在不可删除列表中 - emit('remove', id, { historySource }); - } else { + return (codeBlockService.getUndeletableList() || []).includes(id) ? CodeDeleteErrorType.UNDELETEABLE : undefined; +}; + +/** 删除确认由 TMagicPopconfirm 完成,这里只做校验与删除 */ +const deleteCode = (id: string, { historySource }: { historySource?: HistoryOpSource } = {}) => { + const errorType = getDeleteErrorType(id); + + if (errorType) { if (typeof props.customError === 'function') { - props.customError(id, existBinds ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE); + props.customError(id, errorType); + } else if (errorType === CodeDeleteErrorType.BIND) { + tMagicMessage.error('代码块存在绑定关系,不可删除'); } else { - if (existBinds) { - tMagicMessage.error('代码块存在绑定关系,不可删除'); - } else { - tMagicMessage.error('代码块不可删除'); - } + tMagicMessage.error('代码块不可删除'); } + return; + } + + emit('remove', id, { historySource }); +}; + +/** 不可删除的代码块点击后直接说明原因,无需等待确认 */ +const deleteClickHandler = (id: string) => { + if (getDeleteErrorType(id)) { + deleteCode(id, { historySource: 'tree' }); } }; diff --git a/packages/editor/src/layouts/sidebar/data-source/DataSourceList.vue b/packages/editor/src/layouts/sidebar/data-source/DataSourceList.vue index 1c09da0c..b4324001 100644 --- a/packages/editor/src/layouts/sidebar/data-source/DataSourceList.vue +++ b/packages/editor/src/layouts/sidebar/data-source/DataSourceList.vue @@ -19,12 +19,29 @@ @@ -35,9 +52,8 @@ import { computed } from 'vue'; import { Close, Edit, View } from '@element-plus/icons-vue'; import { DepData, DepTargetType, Id, MNode } from '@tmagic/core'; -import { TMagicTooltip } from '@tmagic/design'; +import { TMagicButton, TMagicPopconfirm, TMagicTooltip } from '@tmagic/design'; -import Icon from '@editor/components/Icon.vue'; import Tree from '@editor/components/Tree.vue'; import { useFilter } from '@editor/hooks/use-filter'; import { useNodeStatus } from '@editor/hooks/use-node-status'; @@ -156,7 +172,7 @@ const editHandler = (id: string) => { emit('edit', id); }; -const removeHandler = async (id: string) => { +const removeHandler = (id: string) => { emit('remove', id); }; diff --git a/packages/editor/src/layouts/sidebar/data-source/DataSourceListPanel.vue b/packages/editor/src/layouts/sidebar/data-source/DataSourceListPanel.vue index 1b2115b8..529fd803 100644 --- a/packages/editor/src/layouts/sidebar/data-source/DataSourceListPanel.vue +++ b/packages/editor/src/layouts/sidebar/data-source/DataSourceListPanel.vue @@ -50,7 +50,7 @@ import { computed, inject, ref, useTemplateRef, watch } from 'vue'; import { mergeWith } from 'lodash-es'; -import { tMagicMessageBox, TMagicScrollbar } from '@tmagic/design'; +import { TMagicScrollbar } from '@tmagic/design'; import ContentMenu from '@editor/components/ContentMenu.vue'; import SearchInput from '@editor/components/SearchInput.vue'; @@ -133,13 +133,8 @@ const addHandler = (type: string) => { editDialog.value.show(); }; -const removeHandler = async (id: string) => { - await tMagicMessageBox.confirm('确定删除?', '提示', { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning', - }); - +/** 删除确认由 DataSourceList 中的 TMagicPopconfirm 完成,这里只负责执行删除 */ +const removeHandler = (id: string) => { dataSourceService.remove(id, { historySource: 'tree-contextmenu' }); }; diff --git a/packages/editor/src/theme/tree.scss b/packages/editor/src/theme/tree.scss index d57e7d63..1c1d522e 100644 --- a/packages/editor/src/theme/tree.scss +++ b/packages/editor/src/theme/tree.scss @@ -76,9 +76,21 @@ display: flex; align-items: center; + // Element Plus 的 link 按钮左右 padding 是 2px,TDesign 的 text 变体默认 + // 为 calc(var(--td-comp-paddingLR-l) - 1px)(15px),会把工具栏图标按钮撑宽。 + .t-button--variant-text { + padding-left: 2px; + padding-right: 2px; + } + .tmagic-design-icon { margin-right: 10px; } + + .delete-icon-wrapper { + display: inline-flex; + align-items: center; + } } } } diff --git a/packages/editor/tests/unit/layouts/sidebar/code-block/CodeBlockList.spec.ts b/packages/editor/tests/unit/layouts/sidebar/code-block/CodeBlockList.spec.ts index 24ff68c6..599bf21c 100644 --- a/packages/editor/tests/unit/layouts/sidebar/code-block/CodeBlockList.spec.ts +++ b/packages/editor/tests/unit/layouts/sidebar/code-block/CodeBlockList.spec.ts @@ -50,6 +50,21 @@ vi.mock('@tmagic/design', () => ({ return () => h('div', { class: 'fake-tooltip' }, slots.default?.()); }, }), + TMagicPopconfirm: defineComponent({ + name: 'TMagicPopconfirm', + props: ['title', 'placement', 'width'], + emits: ['confirm', 'cancel'], + setup(_p, { slots }) { + return () => h('div', { class: 'fake-popconfirm' }, slots.reference?.()); + }, + }), + TMagicButton: defineComponent({ + name: 'TMagicButton', + props: ['icon', 'link'], + setup(_p, { slots, attrs }) { + return () => h('button', { ...attrs, class: 'edit-icon' }, slots.default?.()); + }, + }), })); vi.mock('@editor/components/Icon.vue', () => ({ @@ -90,6 +105,7 @@ beforeEach(() => { codeBlockService.getCodeDsl.mockReturnValue({ c1: { name: 'C1' } }); codeBlockService.getEditStatus.mockReturnValue(true); codeBlockService.getUndeletableList.mockReturnValue([]); + depService.getTarget.mockReturnValue(null); editorService.get.mockImplementation((k: string) => { if (k === 'root') return { items: [{ id: 'p1', name: 'page1' }] }; if (k === 'stage') return { select: vi.fn() }; @@ -109,12 +125,20 @@ describe('CodeBlockList', () => { expect(wrapper.emitted('edit')?.[0]?.[0]).toBe('c1'); }); - test('删除按钮 弹窗确认后 emit remove', async () => { + test('删除图标由 Popconfirm 包裹,确认后 emit remove', async () => { const wrapper = mount(CodeBlockList); + const popconfirm = wrapper.findComponent({ name: 'TMagicPopconfirm' }); + expect(popconfirm.exists()).toBe(true); + expect(popconfirm.props('title')).toBe('确定删除该代码块吗?'); + expect(popconfirm.props('placement')).toBe('right'); + await wrapper.findAll('.edit-icon')[1].trigger('click'); - await new Promise((r) => setTimeout(r, 0)); - expect(messageBoxConfirm).toHaveBeenCalled(); + expect(wrapper.emitted('remove')).toBeFalsy(); + expect(messageBoxConfirm).not.toHaveBeenCalled(); + + popconfirm.vm.$emit('confirm'); expect(wrapper.emitted('remove')?.[0]?.[0]).toBe('c1'); + expect(wrapper.emitted('remove')?.[0]?.[1]).toEqual({ historySource: 'tree' }); }); test('点击 Tree 节点选中组件', async () => { @@ -136,21 +160,53 @@ describe('CodeBlockList', () => { expect(wrapper.emitted('node-contextmenu')).toBeTruthy(); }); - test('删除按钮: 在不可删除列表 提示错误', async () => { + test('删除按钮: 在不可删除列表 点击时提示错误', async () => { codeBlockService.getUndeletableList.mockReturnValue(['c1']); const wrapper = mount(CodeBlockList); await wrapper.findAll('.edit-icon')[1].trigger('click'); - await new Promise((r) => setTimeout(r, 0)); expect(wrapper.emitted('remove')).toBeFalsy(); expect(messageError).toHaveBeenCalledWith('代码块不可删除'); }); + test('Popconfirm 确认时仍校验不可删除列表', () => { + codeBlockService.getUndeletableList.mockReturnValue(['c1']); + const wrapper = mount(CodeBlockList); + wrapper.findComponent({ name: 'TMagicPopconfirm' }).vm.$emit('confirm'); + expect(wrapper.emitted('remove')).toBeFalsy(); + expect(messageError).toHaveBeenCalledWith('代码块不可删除'); + }); + + test('删除按钮: 存在绑定关系 提示错误', async () => { + depService.getTarget.mockReturnValue({ + id: 'c1', + deps: { n1: { name: 'N1', keys: ['k'], data: { pageId: 'p1' } } }, + } as any); + const wrapper = mount(CodeBlockList); + await wrapper.findAll('.edit-icon')[1].trigger('click'); + expect(wrapper.emitted('remove')).toBeFalsy(); + expect(messageError).toHaveBeenCalledWith('代码块存在绑定关系,不可删除'); + }); + test('customError 函数被调用', async () => { codeBlockService.getUndeletableList.mockReturnValue(['c1']); const customError = vi.fn(); const wrapper = mount(CodeBlockList, { props: { customError } as any }); await wrapper.findAll('.edit-icon')[1].trigger('click'); - await new Promise((r) => setTimeout(r, 0)); expect(customError).toHaveBeenCalledWith('c1', 'undeleteable'); }); + + test('customError 在 Popconfirm 确认时被调用', () => { + codeBlockService.getUndeletableList.mockReturnValue(['c1']); + const customError = vi.fn(); + const wrapper = mount(CodeBlockList, { props: { customError } as any }); + wrapper.findComponent({ name: 'TMagicPopconfirm' }).vm.$emit('confirm'); + expect(customError).toHaveBeenCalledWith('c1', 'undeleteable'); + }); + + test('editable=false 时不显示删除图标和 Popconfirm', () => { + codeBlockService.getEditStatus.mockReturnValue(false); + const wrapper = mount(CodeBlockList); + expect(wrapper.findAll('.edit-icon').length).toBe(1); + expect(wrapper.findComponent({ name: 'TMagicPopconfirm' }).exists()).toBe(false); + }); }); diff --git a/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceList.spec.ts b/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceList.spec.ts index b725ce42..a294c2b0 100644 --- a/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceList.spec.ts +++ b/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceList.spec.ts @@ -29,7 +29,9 @@ vi.mock('@editor/hooks/use-node-status', () => ({ })); vi.mock('@editor/hooks/use-filter', () => ({ - useFilter: () => ({ filterTextChangeHandler: vi.fn() }), + useFilter: (list: any, _nodeStatusMap: any, filterNode: any) => ({ + filterTextChangeHandler: (value: string) => list.value.forEach((item: any) => filterNode(value, item)), + }), })); vi.mock('@editor/components/Tree.vue', () => ({ @@ -74,10 +76,26 @@ vi.mock('@tmagic/design', () => ({ return () => h('div', { class: 'fake-tooltip' }, slots.default?.()); }, }), + TMagicPopconfirm: defineComponent({ + name: 'TMagicPopconfirm', + props: ['title', 'placement', 'width'], + emits: ['confirm', 'cancel'], + setup(_p, { slots }) { + return () => h('div', { class: 'fake-popconfirm' }, slots.reference?.()); + }, + }), + TMagicButton: defineComponent({ + name: 'TMagicButton', + props: ['icon', 'link'], + setup(_p, { slots, attrs }) { + return () => h('button', { ...attrs, class: 'edit-icon' }, slots.default?.()); + }, + }), })); beforeEach(() => { vi.clearAllMocks(); + depService.getTargets.mockReturnValue({}); dataSourceService.get.mockImplementation((k: string) => { if (k === 'editable') return true; if (k === 'dataSources') return [{ id: 'd1', title: 'D1', methods: [] }]; @@ -123,14 +141,21 @@ describe('DataSourceList', () => { expect(wrapper.emitted('edit')?.[0]?.[0]).toBe('d1'); }); - test('点击删除图标 emit remove (editable=true)', async () => { + test('删除图标由 Popconfirm 包裹,确认后才 emit remove', async () => { const wrapper = mount(DataSourceList); - const icons = wrapper.findAll('.edit-icon'); - await icons[1].trigger('click'); + const popconfirm = wrapper.findComponent({ name: 'TMagicPopconfirm' }); + expect(popconfirm.exists()).toBe(true); + expect(popconfirm.props('title')).toBe('确定删除?'); + expect(popconfirm.props('placement')).toBe('right'); + + await wrapper.findAll('.edit-icon')[1].trigger('click'); + expect(wrapper.emitted('remove')).toBeFalsy(); + + popconfirm.vm.$emit('confirm'); expect(wrapper.emitted('remove')?.[0]?.[0]).toBe('d1'); }); - test('editable=false 时不显示删除图标', () => { + test('editable=false 时不显示删除图标和 Popconfirm', () => { dataSourceService.get.mockImplementation((k: string) => { if (k === 'editable') return false; if (k === 'dataSources') return [{ id: 'd1', title: 'D1' }]; @@ -138,6 +163,7 @@ describe('DataSourceList', () => { }); const wrapper = mount(DataSourceList); expect(wrapper.findAll('.edit-icon').length).toBe(1); + expect(wrapper.findComponent({ name: 'TMagicPopconfirm' }).exists()).toBe(false); }); test('list 计算: dataSources 为空时为空数组', () => { @@ -149,4 +175,32 @@ describe('DataSourceList', () => { const wrapper = mount(DataSourceList); expect(wrapper.findComponent({ name: 'TreeStub' }).props('data').length).toBe(0); }); + + test('list 计算: 合并依赖节点并按页面分类', () => { + depService.getTargets.mockReturnValue({ + d1: { deps: { n1: { name: 'N1', keys: ['k1'], data: { pageId: 'p1' } } } }, + } as any); + editorService.get.mockImplementation((k: string) => { + if (k === 'root') + return { + items: [ + { id: 'p1', name: 'page1' }, + { id: 'p2', name: 'page2' }, + ], + }; + return null; + }); + + const wrapper = mount(DataSourceList); + const list = wrapper.findComponent({ name: 'TreeStub' }).props('data') as any[]; + expect(list[0].items).toHaveLength(1); + expect(list[0].items[0].items.length).toBeGreaterThan(0); + }); + + test('filter 空关键字与名称匹配', () => { + const wrapper = mount(DataSourceList); + (wrapper.vm as any).filter(''); + (wrapper.vm as any).filter('D1'); + expect(wrapper.findComponent({ name: 'TreeStub' }).props('data')).toHaveLength(1); + }); }); diff --git a/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceListPanel.spec.ts b/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceListPanel.spec.ts index 6b0d2244..dec7dc59 100644 --- a/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceListPanel.spec.ts +++ b/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceListPanel.spec.ts @@ -187,8 +187,8 @@ describe('DataSourceListPanel', () => { await wrapper.find('.edit-btn').trigger('click'); expect(editHandler).toHaveBeenCalledWith('d1'); await wrapper.find('.remove-btn').trigger('click'); - expect(messageBoxConfirm).toHaveBeenCalled(); - await new Promise((r) => setTimeout(r, 0)); + // 确认已下沉到 DataSourceList 的 Popconfirm,面板不再弹出 MessageBox + expect(messageBoxConfirm).not.toHaveBeenCalled(); expect(dataSourceService.remove).toHaveBeenCalledWith('d1', { historySource: 'tree-contextmenu' }); await wrapper.find('.ctx-btn').trigger('click'); expect(nodeContentMenuHandler).toHaveBeenCalled(); @@ -217,4 +217,32 @@ describe('DataSourceListPanel', () => { expect(events).toContain('edit-data-source'); expect(events).toContain('remove-data-source'); }); + + test('eventBus remove-data-source 执行删除', () => { + mountIt(); + const handler = eventBusOn.mock.calls.find((c: any[]) => c[0] === 'remove-data-source')?.[1]; + handler?.('d2'); + expect(dataSourceService.remove).toHaveBeenCalledWith('d2', { historySource: 'tree-contextmenu' }); + }); + + test('eventBus 编辑事件重置定位信息并打开编辑', () => { + mountIt(); + const handlerOf = (name: string) => eventBusOn.mock.calls.find((c: any[]) => c[0] === name)?.[1]; + + handlerOf('edit-data-source')?.('d1'); + handlerOf('edit-data-source-method')?.('d2', 'm1'); + handlerOf('edit-data-source-field')?.('d3', ['f1']); + + expect(editHandler).toHaveBeenCalledWith('d1'); + expect(editHandler).toHaveBeenCalledWith('d2'); + expect(editHandler).toHaveBeenCalledWith('d3'); + }); + + test('addHandler 合并表单配置中的数组值', async () => { + editDialog.value = { show: editDialogShow }; + dataSourceService.getFormValue.mockReturnValueOnce({ id: 'fv', type: 'http', fields: [{ name: 'a' }] }); + const wrapper = mountIt(); + await wrapper.find('.add-btn').trigger('click'); + expect(dataSourceValues.value.fields).toEqual([{ name: 'a' }]); + }); }); diff --git a/packages/editor/tests/unit/layouts/sidebar/delete-popconfirm.spec.ts b/packages/editor/tests/unit/layouts/sidebar/delete-popconfirm.spec.ts new file mode 100644 index 00000000..f215ac66 --- /dev/null +++ b/packages/editor/tests/unit/layouts/sidebar/delete-popconfirm.spec.ts @@ -0,0 +1,101 @@ +/* + * Tencent is pleased to support the open source community by making TMagicEditor available. + * + * Copyright (C) 2025 Tencent. + */ +import { describe, expect, test, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; + +import { setDesignConfig } from '@tmagic/design'; + +import CodeBlockList from '@editor/layouts/sidebar/code-block/CodeBlockList.vue'; +import DataSourceList from '@editor/layouts/sidebar/data-source/DataSourceList.vue'; + +import elementPlusAdapter from '../../../../../element-plus-adapter/src/index'; + +setDesignConfig(elementPlusAdapter); + +const editorService = { + get: vi.fn((key: string) => (key === 'root' ? { items: [{ id: 'p1', name: 'page1' }] } : null)), + select: vi.fn(), +}; +const depService = { + getTargets: vi.fn(() => ({})), + getTarget: vi.fn(() => null), +}; +const dataSourceService = { + get: vi.fn((key: string) => { + if (key === 'editable') return true; + if (key === 'dataSources') return [{ id: 'd1', title: 'D1' }]; + return null; + }), +}; +const codeBlockService = { + getCodeDsl: vi.fn(() => ({ c1: { name: 'C1' } })), + getCodeContentById: vi.fn(), + getEditStatus: vi.fn(() => true), + getUndeletableList: vi.fn(() => []), +}; + +vi.mock('@editor/hooks/use-services', () => ({ + useServices: () => ({ editorService, depService, dataSourceService, codeBlockService }), +})); + +/** 点击 Popconfirm 的确认按钮(Element Plus 默认文案为 Yes) */ +const clickConfirm = async () => { + await vi.waitFor(() => { + expect(document.body.textContent).toContain('Yes'); + }); + const confirmBtn = Array.from(document.querySelectorAll('button')).find( + (btn) => btn.textContent?.trim().toLowerCase() === 'yes', + ); + confirmBtn?.click(); +}; + +/** 浮层需挂载到 body,且 placement 真正作用到 popper 上 */ +const expectRightPlacement = async (wrapper: { element: Element }) => { + await vi.waitFor(() => { + expect(document.querySelector('.el-popper')?.getAttribute('data-popper-placement')).toBe('right'); + }); + const popper = document.querySelector('.el-popper'); + expect(popper).toBeTruthy(); + expect(wrapper.element.contains(popper!)).toBe(false); +}; + +describe('侧栏删除 Popconfirm(真实 element-plus)', () => { + test('数据源:点击删除图标弹出 Popconfirm,确认后 emit remove', async () => { + const wrapper = mount(DataSourceList, { attachTo: document.body }); + + await wrapper.findAll('.edit-icon')[1].trigger('click'); + await vi.waitFor(() => { + expect(document.body.textContent).toContain('确定删除?'); + }); + await expectRightPlacement(wrapper); + expect(wrapper.emitted('remove')).toBeFalsy(); + + await clickConfirm(); + await vi.waitFor(() => { + expect(wrapper.emitted('remove')?.[0]?.[0]).toBe('d1'); + }); + + wrapper.unmount(); + }); + + test('代码块:点击删除图标弹出 Popconfirm,确认后 emit remove', async () => { + const wrapper = mount(CodeBlockList, { attachTo: document.body }); + + await wrapper.findAll('.edit-icon')[1].trigger('click'); + await vi.waitFor(() => { + expect(document.body.textContent).toContain('确定删除该代码块吗?'); + }); + await expectRightPlacement(wrapper); + expect(wrapper.emitted('remove')).toBeFalsy(); + + await clickConfirm(); + await vi.waitFor(() => { + expect(wrapper.emitted('remove')?.[0]?.[0]).toBe('c1'); + }); + + wrapper.unmount(); + }); +});