import { App, ComponentPublicInstance, createApp, defineComponent, h, nextTick, ref } from 'vue' import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' const ElTabsStub = defineComponent({ name: 'ElTabs', setup (_, { slots, attrs }) { return () => h('div', { class: 'el-tabs-stub', 'data-type': attrs.type as string || '' }, slots.default?.()) } }) const ElTabPaneStub = defineComponent({ name: 'ElTabPane', setup (_, { slots }) { return () => h('section', { class: 'el-tab-pane-stub' }, [ h('div', { class: 'el-tab-pane-label' }, slots.label?.()), h('div', { class: 'el-tab-pane-body' }, slots.default?.()) ]) } }) const ElRowStub = defineComponent({ name: 'ElRow', setup (_, { slots }) { return () => h('div', { class: 'el-row-stub' }, slots.default?.()) } }) const ElRadioStub = defineComponent({ name: 'ElRadio', props: { modelValue: { type: [String, Number, Boolean], default: undefined }, label: { type: [String, Number, Boolean], default: undefined } }, emits: ['update:modelValue'], setup (props, { slots, emit, attrs }) { return () => h('label', { class: ['el-radio-stub', attrs.class].filter(Boolean), 'data-checked': String(props.modelValue === props.label), 'data-label': String(props.label ?? ''), onClick: () => emit('update:modelValue', props.label) }, slots.default?.()) } }) const ElInputNumberStub = defineComponent({ name: 'ElInputNumber', props: { modelValue: { type: Number, default: 0 } }, emits: ['update:modelValue'], setup (props) { return () => h('span', { class: 'el-input-number-stub', 'data-value': String(props.modelValue) }) } }) const ElSelectStub = defineComponent({ name: 'ElSelect', props: { modelValue: { type: [Array, String, Number], default: undefined }, multiple: { type: Boolean, default: false }, teleported: { type: Boolean, default: true } }, setup (props, { slots }) { const value = Array.isArray(props.modelValue) ? props.modelValue.join(',') : String(props.modelValue ?? '') return () => h('div', { class: 'el-select-stub', 'data-value': value, 'data-multiple': String(props.multiple), 'data-teleported': String(props.teleported) }, slots.default?.()) } }) const ElOptionStub = defineComponent({ name: 'ElOption', props: { value: { type: [String, Number], default: undefined }, label: { type: [String, Number], default: undefined } }, setup (props, { slots }) { return () => h('div', { class: 'el-option-stub', 'data-value': String(props.value ?? ''), 'data-label': String(props.label ?? '') }, slots.default?.()) } }) const ElButtonStub = defineComponent({ name: 'ElButton', emits: ['click'], setup (_, { slots, emit }) { return () => h('button', { class: 'el-button-stub', onClick: () => emit('click') }, slots.default?.()) } }) vi.mock('element-plus', () => ({ ElTabs: ElTabsStub, ElTabPane: ElTabPaneStub, ElRow: ElRowStub, ElRadio: ElRadioStub, ElInputNumber: ElInputNumberStub, ElSelect: ElSelectStub, ElOption: ElOptionStub, ElButton: ElButtonStub })) let InfoVueCron: any interface RenderResult { app: App el: HTMLDivElement vm: ComponentPublicInstance & { cron: { value: string } parseCron: (value: string) => void minute: Record hour: Record day: Record week: Record month: Record } events: { change: ReturnType close: ReturnType } } const renderedApps: RenderResult[] = [] beforeAll(async () => { InfoVueCron = (await import('../../../components/infoVueCron/index.vue')).default }) const getCronValue = (value: any) => { if (value && typeof value === 'object' && 'value' in value) { return value.value } return value } const renderInfoVueCron = async (): Promise => { const el = document.createElement('div') document.body.appendChild(el) const events = { change: vi.fn(), close: vi.fn() } const vmRef = ref() const app = createApp({ setup () { const handleChange = (cron: any, cronString: any) => { events.change(getCronValue(cron), getCronValue(cronString)) } return () => h(InfoVueCron, { ref: vmRef, onChange: handleChange, onClose: events.close }) } }) app.config.globalProperties.$t = (key: string) => key window.$t = (key: string) => key app.mount(el) await nextTick() const result = { app, el, vm: vmRef.value, events } renderedApps.push(result) return result } const getRoot = () => document.body const click = async (selector: string) => { const target = getRoot().querySelector(selector) as HTMLElement | null expect(target).not.toBeNull() target!.click() await nextTick() } afterEach(() => { while (renderedApps.length) { const current = renderedApps.pop()! current.app.unmount() current.el.remove() } document.body.innerHTML = '' }) describe('components/infoVueCron', () => { it('按默认配置生成并展示初始 cron 表达式', async () => { const rendered = await renderInfoVueCron() expect(getCronValue(rendered.vm.cron)).toBe('0 1/5 * * * ? *') expect(getRoot().querySelector('.bottom .value')?.textContent?.trim()).toBe('0 1/5 * * * ? *') expect(getRoot().textContent).toContain('minute') expect(getRoot().textContent).toContain('hour') expect(getRoot().textContent).toContain('day') expect(getRoot().textContent).toContain('month') }) it('点击保存时抛出 change 事件,并返回当前 cron 计算结果', async () => { const rendered = await renderInfoVueCron() await click('.bottom .el-button-stub') expect(rendered.events.change).toHaveBeenCalledTimes(1) expect(rendered.events.change.mock.calls[0]?.[0]).toBe('0 1/5 * * * ? *') }) it('点击关闭时抛出 close 事件', async () => { const rendered = await renderInfoVueCron() const buttons = getRoot().querySelectorAll('.bottom .el-button-stub') expect(buttons).toHaveLength(2) ;(buttons[1] as HTMLElement).click() await nextTick() expect(rendered.events.close).toHaveBeenCalledTimes(1) }) it('parseCron 能解析步长表达式并同步到当前 cron', async () => { const rendered = await renderInfoVueCron() rendered.vm.parseCron('0 2/6 3/4 * * ? *') await nextTick() expect(getCronValue(rendered.vm.cron)).toBe('0 2/6 3/4 * * ? *') expect(getRoot().querySelector('.bottom .value')?.textContent?.trim()).toBe('0 2/6 3/4 * * ? *') }) it('parseCron 能解析天和周相关表达式', async () => { const rendered = await renderInfoVueCron() rendered.vm.parseCron('0 1/5 * ? * 2/2 *') await nextTick() expect(getCronValue(rendered.vm.cron)).toBe('0 1/5 * ? * 2/2 *') expect(getRoot().querySelector('.bottom .value')?.textContent?.trim()).toBe('0 1/5 * ? * 2/2 *') rendered.vm.parseCron('0 1/5 * L-2 * ? *') await nextTick() expect(getCronValue(rendered.vm.cron)).toBe('0 1/5 * L-2 * ? *') expect(getRoot().querySelector('.bottom .value')?.textContent?.trim()).toBe('0 1/5 * L-2 * ? *') }) it('parseCron 能解析月份范围表达式并按组件格式回显', async () => { const rendered = await renderInfoVueCron() rendered.vm.parseCron('0 1/5 * * 2-5 ? *') await nextTick() expect(getCronValue(rendered.vm.cron)).toBe('0 1/5 * * 2-5 ? *') expect(getRoot().querySelector('.bottom .value')?.textContent?.trim()).toBe('0 1/5 * * 2-5 ? *') }) })