From adf4fbfb6bebf10b0042e02b8a15134bb7261b83 Mon Sep 17 00:00:00 2001 From: sea <45450994+warmthsea@users.noreply.github.com> Date: Fri, 28 Nov 2025 23:34:57 +0800 Subject: [PATCH] feat(components): [tree-select] add instance type and improve test cleanup logic (#22499) * feat(components): [tree-select] add instance * chore: update * chore: update test Co-authored-by: btea <2356281422@qq.com> * Update packages/components/tree-select/src/instance.ts Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> --------- Co-authored-by: btea <2356281422@qq.com> Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> --- docs/en-US/component/slider.md | 52 +++++----- docs/en-US/component/tooltip.md | 2 +- .../__tests__/tree-select.test.tsx | 98 ++++++++++++++----- packages/components/tree-select/index.ts | 2 + .../components/tree-select/src/instance.ts | 7 ++ typings/env.d.ts | 5 - 6 files changed, 107 insertions(+), 59 deletions(-) create mode 100644 packages/components/tree-select/src/instance.ts diff --git a/docs/en-US/component/slider.md b/docs/en-US/component/slider.md index 881589b9ec..ba65641fde 100644 --- a/docs/en-US/component/slider.md +++ b/docs/en-US/component/slider.md @@ -85,33 +85,33 @@ slider/show-marks ### Attributes -| Name | Description | Type | Default | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| model-value / v-model | binding value | ^[number] / ^[object]`number[]` | 0 | -| min | minimum value | ^[number] | 0 | -| max | maximum value | ^[number] | 100 | -| disabled | whether Slider is disabled | ^[boolean] | false | -| step | step size | ^[number] | 1 | -| show-input | whether to display an input box, works when `range` is false | ^[boolean] | false | -| show-input-controls | whether to display control buttons when `show-input` is true | ^[boolean] | true | -| size | size of the slider wrapper, will not work in vertical mode | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | default | -| input-size | size of the input box, when set `size`, the default is the value of `size` | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | default | -| show-stops | whether to display breakpoints | ^[boolean] | false | -| show-tooltip | whether to display tooltip value | ^[boolean] | true | -| format-tooltip | format to display tooltip value | ^[Function]`(value: number) => number \| string` | — | -| range | whether to select a range | ^[boolean] | false | -| vertical | vertical mode | ^[boolean] | false | -| height | slider height, required in vertical mode | ^[string] | — | -| aria-label ^(a11y) ^(2.7.2) | native `aria-label` attribute | ^[string] | — | -| range-start-label | when `range` is true, screen reader label for the start of the range | ^[string] | — | -| range-end-label | when `range` is true, screen reader label for the end of the range | ^[string] | — | -| format-value-text | format to display the `aria-valuenow` attribute for screen readers | ^[Function]`(value: number) => string` | — | -| tooltip-class | custom class name for the tooltip | ^[string] | — | -| placement | position of Tooltip | ^[enum]`'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end' \| 'right' \| 'right-start' \| 'right-end'` | top | -| marks | marks, type of key must be `number` and must in closed interval `[min, max]`, each mark can custom style | ^[object]`SliderMarks` | — | -| validate-event | whether to trigger form validation | ^[boolean] | true | +| Name | Description | Type | Default | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| model-value / v-model | binding value | ^[number] / ^[object]`number[]` | 0 | +| min | minimum value | ^[number] | 0 | +| max | maximum value | ^[number] | 100 | +| disabled | whether Slider is disabled | ^[boolean] | false | +| step | step size | ^[number] | 1 | +| show-input | whether to display an input box, works when `range` is false | ^[boolean] | false | +| show-input-controls | whether to display control buttons when `show-input` is true | ^[boolean] | true | +| size | size of the slider wrapper, will not work in vertical mode | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | default | +| input-size | size of the input box, when set `size`, the default is the value of `size` | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | default | +| show-stops | whether to display breakpoints | ^[boolean] | false | +| show-tooltip | whether to display tooltip value | ^[boolean] | true | +| format-tooltip | format to display tooltip value | ^[Function]`(value: number) => number \| string` | — | +| range | whether to select a range | ^[boolean] | false | +| vertical | vertical mode | ^[boolean] | false | +| height | slider height, required in vertical mode | ^[string] | — | +| aria-label ^(a11y) ^(2.7.2) | native `aria-label` attribute | ^[string] | — | +| range-start-label | when `range` is true, screen reader label for the start of the range | ^[string] | — | +| range-end-label | when `range` is true, screen reader label for the end of the range | ^[string] | — | +| format-value-text | format to display the `aria-valuenow` attribute for screen readers | ^[Function]`(value: number) => string` | — | +| tooltip-class | custom class name for the tooltip | ^[string] | — | +| placement | position of Tooltip | ^[enum]`'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end' \| 'right' \| 'right-start' \| 'right-end'` | top | +| marks | marks, type of key must be `number` and must in closed interval `[min, max]`, each mark can custom style | ^[object]`SliderMarks` | — | +| validate-event | whether to trigger form validation | ^[boolean] | true | | persistent ^(2.9.5) | when slider tooltip inactive and `persistent` is `false` , tooltip will be destroyed. `persistent` always be `false` when `show-tooltip ` is `false` | ^[boolean] | true | -| label ^(a11y) ^(deprecated) | native `aria-label` attribute | ^[string] | — | +| label ^(a11y) ^(deprecated) | native `aria-label` attribute | ^[string] | — | ### Events diff --git a/docs/en-US/component/tooltip.md b/docs/en-US/component/tooltip.md index ef8e3205b0..a0ade1aa34 100644 --- a/docs/en-US/component/tooltip.md +++ b/docs/en-US/component/tooltip.md @@ -186,7 +186,7 @@ tooltip/append-to | virtual-triggering | Indicates whether virtual triggering is enabled | ^[boolean] | — | | virtual-ref | Indicates the reference element to which the tooltip is attached | ^[HTMLElement] | — | | trigger-keys | When you click the mouse to focus on the trigger element, you can define a set of keyboard codes to control the display of tooltip through the keyboard, not valid in controlled mode | ^[Array] | ['Enter','Space'] | -| persistent | when tooltip inactive and `persistent` is `false` , tooltip will be destroyed | ^[boolean] | — | +| persistent | when tooltip inactive and `persistent` is `false` , tooltip will be destroyed | ^[boolean] | — | | aria-label ^(a11y) | same as `aria-label` | ^[string] | — | | focus-on-target ^(2.11.2) | when triggering tooltips through hover, whether to focus the trigger element, which improves accessibility | ^[boolean] | false | diff --git a/packages/components/tree-select/__tests__/tree-select.test.tsx b/packages/components/tree-select/__tests__/tree-select.test.tsx index ebbe501cee..5475e4a801 100644 --- a/packages/components/tree-select/__tests__/tree-select.test.tsx +++ b/packages/components/tree-select/__tests__/tree-select.test.tsx @@ -1,16 +1,53 @@ import { nextTick, reactive, ref } from 'vue' import { mount } from '@vue/test-utils' -import { afterEach, describe, expect, test, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { CircleClose } from '@element-plus/icons-vue' import TreeSelect from '../src/tree-select.vue' import Tree from '@element-plus/components/tree/src/tree.vue' import defineGetter from '@element-plus/test-utils/define-getter' import { EVENT_CODE } from '@element-plus/constants' +import type { TreeSelectInstance } from '../src/instance' import type { RenderFunction } from 'vue' import type { VueWrapper } from '@vue/test-utils' -import type ElSelect from '@element-plus/components/select' -import type ElTree from '@element-plus/components/tree' + +// Keep track of all mounted wrappers for cleanup +const mountedWrappers: VueWrapper[] = [] + +beforeEach(() => { + document.body.innerHTML = '' +}) + +afterEach(async () => { + mountedWrappers.forEach((wrapper) => { + if (wrapper && wrapper.exists()) { + wrapper.unmount() + } + }) + mountedWrappers.length = 0 + document.body.innerHTML = '' + vi.clearAllTimers() + + const frameIds = new Set() + const originalRequestAnimationFrame = global.requestAnimationFrame + + global.requestAnimationFrame = function (cb) { + const id = originalRequestAnimationFrame((timestamp) => { + frameIds.delete(id) + cb(timestamp) + }) + frameIds.add(id) + return id + } + + const cancelAllAnimationFrames = () => { + frameIds.forEach((id) => global.cancelAnimationFrame(id)) + frameIds.clear() + } + + cancelAllAnimationFrames() + await new Promise((resolve) => setTimeout(resolve, 0)) +}) const createComponent = ({ slots = {}, @@ -19,7 +56,7 @@ const createComponent = ({ slots?: Record props?: (typeof TreeSelect)['props'] } = {}) => { - const wrapperRef = ref>() + const wrapperRef = ref() const defaultData = ref([ { value: 1, @@ -53,9 +90,7 @@ const createComponent = ({ (bindProps.modelValue = val)} - ref={(val: InstanceType) => - (wrapperRef.value = val) - } + ref={(val: TreeSelectInstance) => (wrapperRef.value = val)} v-slots={slots} /> ) @@ -66,17 +101,22 @@ const createComponent = ({ } ) + // Add wrapper to tracking array for cleanup + mountedWrappers.push(wrapper) + return { wrapper, getWrapperRef: () => - new Promise>((resolve) => - nextTick(() => resolve(wrapperRef.value!)) + new Promise((resolve) => + nextTick(() => + resolve(wrapperRef.value! as unknown as TreeSelectInstance) + ) ), - select: wrapper.findComponent({ name: 'ElSelect' }) as VueWrapper< - InstanceType - >, + select: wrapper.findComponent({ + name: 'ElSelect', + }) as VueWrapper, tree: wrapper.findComponent({ name: 'ElTree' }) as VueWrapper< - InstanceType + TreeSelectInstance['treeRef'] >, } } @@ -160,12 +200,12 @@ describe('TreeSelect.vue', () => { await nextTick() expect(select.vm.modelValue).toBe(1) - expect(wrapperRef.getCheckedKeys()).toEqual([1]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1]) value.value = 11 await nextTick(nextTick) expect(select.vm.modelValue).toBe(11) - expect(wrapperRef.getCheckedKeys()).toEqual([11]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([11]) await tree .findAll('.el-select-dropdown__item') @@ -173,17 +213,17 @@ describe('TreeSelect.vue', () => { .trigger('click') await nextTick() expect(select.vm.modelValue).toBe(111) - expect(wrapperRef.getCheckedKeys()).toEqual([111]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([111]) await tree.find('.el-tree-node__content').trigger('click') await nextTick() expect(select.vm.modelValue).toBe(1) - expect(wrapperRef.getCheckedKeys()).toEqual([1]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1]) await tree.findAll('.el-checkbox__original')[1].trigger('click') await nextTick() expect(select.vm.modelValue).toBe(11) - expect(wrapperRef.getCheckedKeys()).toEqual([11]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([11]) }) test('disabled', async () => { @@ -235,12 +275,12 @@ describe('TreeSelect.vue', () => { await nextTick() expect(select.vm.modelValue).toEqual([1]) - expect(wrapperRef.getCheckedKeys()).toEqual([1]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1]) value.value = [11] await nextTick(nextTick) expect(select.vm.modelValue).toEqual([11]) - expect(wrapperRef.getCheckedKeys()).toEqual([11]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([11]) await tree .findAll('.el-select-dropdown__item') @@ -248,17 +288,17 @@ describe('TreeSelect.vue', () => { .trigger('click') await nextTick() expect(select.vm.modelValue).toEqual([11, 111]) - expect(wrapperRef.getCheckedKeys()).toEqual([11, 111]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([11, 111]) await tree.find('.el-tree-node__content').trigger('click') await nextTick() expect(select.vm.modelValue).toEqual([1, 11, 111]) - expect(wrapperRef.getCheckedKeys()).toEqual([1, 11, 111]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1, 11, 111]) await tree.findAll('.el-checkbox')[1].trigger('click') await nextTick() expect(select.vm.modelValue).toEqual([1, 111]) - expect(wrapperRef.getCheckedKeys()).toEqual([1, 111]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1, 111]) }) test('filter', async () => { @@ -384,14 +424,14 @@ describe('TreeSelect.vue', () => { await tree.findAll('.el-tree-node__content')[0].trigger('click') await nextTick() expect(select.vm.modelValue).toEqual([]) - expect(wrapperRef.getCheckedKeys()).toEqual([]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([]) await tree .findAll('.el-tree-node__content .el-checkbox')[0] .trigger('click') await nextTick() expect(select.vm.modelValue).toEqual([1]) - expect(wrapperRef.getCheckedKeys()).toEqual([1]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1]) }) test('check-strictly showCheckbox checkOnClickNode click node', async () => { @@ -408,14 +448,14 @@ describe('TreeSelect.vue', () => { await tree.findAll('.el-tree-node__content')[0].trigger('click') await nextTick() expect(select.vm.modelValue).toEqual([1]) - expect(wrapperRef.getCheckedKeys()).toEqual([1]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([1]) await tree .findAll('.el-tree-node__content .el-checkbox')[0] .trigger('click') await nextTick() expect(select.vm.modelValue).toEqual([]) - expect(wrapperRef.getCheckedKeys()).toEqual([]) + expect(wrapperRef.treeRef.getCheckedKeys()).toEqual([]) }) test('only show checkbox', async () => { @@ -978,6 +1018,10 @@ describe('TreeSelect.vue', () => { }, template: ``, }) + + // Add to tracking for cleanup + mountedWrappers.push(wrapper) + const select = wrapper.findComponent({ name: 'ElSelect', }) diff --git a/packages/components/tree-select/index.ts b/packages/components/tree-select/index.ts index 40f13a5d04..45465bff41 100644 --- a/packages/components/tree-select/index.ts +++ b/packages/components/tree-select/index.ts @@ -7,3 +7,5 @@ export const ElTreeSelect: SFCWithInstall = withInstall(TreeSelect) export default ElTreeSelect + +export type { TreeSelectInstance } from './src/instance' diff --git a/packages/components/tree-select/src/instance.ts b/packages/components/tree-select/src/instance.ts new file mode 100644 index 0000000000..43f921baa2 --- /dev/null +++ b/packages/components/tree-select/src/instance.ts @@ -0,0 +1,7 @@ +import type { SelectInstance } from '@element-plus/components/select' +import type { TreeInstance } from '@element-plus/components/tree' + +export type TreeSelectInstance = { + treeRef: TreeInstance + selectRef: SelectInstance +} diff --git a/typings/env.d.ts b/typings/env.d.ts index aa605e1c91..e7378407c0 100644 --- a/typings/env.d.ts +++ b/typings/env.d.ts @@ -1,4 +1,3 @@ -import type { vShow } from 'vue' import type { INSTALLED_KEY } from '@element-plus/constants' declare global { @@ -25,10 +24,6 @@ declare module 'vue' { export interface GlobalComponents { Component: (props: { is: Component | string }) => void } - - export interface ComponentCustomProperties { - vShow: typeof vShow - } } export {}