diff --git a/src/_common b/src/_common index a78db5d83..01f83a36f 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit a78db5d8382c3c96cacedc2a9cb669584d0d3d9d +Subproject commit 01f83a36fe9879ee2e7a99d9a56b790e217042f3 diff --git a/src/common.ts b/src/common.ts index 5b27058c1..1e35eb7ba 100644 --- a/src/common.ts +++ b/src/common.ts @@ -45,7 +45,7 @@ export type OptionData = { } & PlainObject; export type TreeOptionData = { - children?: Array>; + children?: Array> | boolean; /** option label content */ label?: string | TNode; /** option search text */ diff --git a/src/tree-select/useTreeSelect.ts b/src/tree-select/useTreeSelect.ts index b542586b4..8a521e32c 100644 --- a/src/tree-select/useTreeSelect.ts +++ b/src/tree-select/useTreeSelect.ts @@ -177,7 +177,10 @@ export default function useTreeSelect(props: TdTreeSelectProps, context: SetupCo return; } const onlyLeafNode = Boolean( - !props.multiple && props.treeProps?.valueMode === 'onlyLeaf' && ctx.node?.data?.children?.length, + !props.multiple + && props.treeProps?.valueMode === 'onlyLeaf' + && Array.isArray(ctx.node?.data?.children) + && ctx.node?.data?.children?.length, ); let current: TreeSelectValue = value; const nodeValue = Array.isArray(value) ? value[0] : value; diff --git a/src/tree-select/utils.ts b/src/tree-select/utils.ts index 7235c98e6..566127a6f 100644 --- a/src/tree-select/utils.ts +++ b/src/tree-select/utils.ts @@ -18,7 +18,7 @@ export function getNodeDataByValue( // results.push(item); results.set(values[index], item); } - if (item.children?.length) { + if (Array.isArray(item.children) && item.children?.length) { getTreeNodeData(values, item.children, keys, results); } if (results.size >= values.length) { diff --git a/src/tree/__tests__/activable.test.jsx b/src/tree/__tests__/activable.test.jsx index 68e9e8cc2..075ca275e 100644 --- a/src/tree/__tests__/activable.test.jsx +++ b/src/tree/__tests__/activable.test.jsx @@ -166,6 +166,55 @@ describe('Tree:activable', () => { expect(wrapper.find('[data-value="t1.1"]').classes('t-is-active')).toBe(false); expect(wrapper.find('[data-value="t1.2"]').classes('t-is-active')).toBe(false); }); + + it('actived 受控处理可赋值空值', async () => { + const data = [ + { + value: 't1', + children: [ + { + value: 't1.1', + }, + { + value: 't1.2', + }, + ], + }, + ]; + const wrapper = mount({ + data() { + return { + actived: ['t1'], + }; + }, + methods: { + onActive(vals) { + const actived = vals.filter((val) => val !== 't1'); + this.actived = actived; + }, + }, + render() { + return ( + + ); + }, + }); + await delay(1); + expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(true); + await wrapper.find('[data-value="t1"] .t-tree__label').trigger('click'); + expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false); + await delay(1); + await wrapper.find('[data-value="t1"] .t-tree__label').trigger('click'); + expect(wrapper.find('[data-value="t1"]').classes('t-is-active')).toBe(false); + }); }); describe('props.activeMultiple', () => { diff --git a/src/tree/__tests__/adapt.js b/src/tree/__tests__/adapt.js new file mode 100644 index 000000000..455962b2d --- /dev/null +++ b/src/tree/__tests__/adapt.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { defineComponent } from '@vue/composition-api'; diff --git a/src/tree/__tests__/api.test.jsx b/src/tree/__tests__/api.test.jsx index b8666da45..400a2dedf 100644 --- a/src/tree/__tests__/api.test.jsx +++ b/src/tree/__tests__/api.test.jsx @@ -24,16 +24,14 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); expect(wrapper.find('[data-value="t1"]').exists()).toBe(true); expect(wrapper.find('[data-value="t1.1"]').exists()).toBe(true); expect(wrapper.find('[data-value="t2"]').exists()).toBe(true); - wrapper.vm.$refs.tree.remove('t2'); await delay(10); - expect(wrapper.find('[data-value="t2"]').exists()).toBe(false); }); }); @@ -57,7 +55,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -86,7 +84,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -109,7 +107,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -146,7 +144,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -186,7 +184,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -224,7 +222,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -264,7 +262,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -308,7 +306,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -354,7 +352,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -392,7 +390,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -430,7 +428,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -472,7 +470,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -518,7 +516,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -556,7 +554,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -594,7 +592,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -636,7 +634,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -668,7 +666,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -690,7 +688,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -722,7 +720,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -762,7 +760,7 @@ describe('Tree:api', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); diff --git a/src/tree/__tests__/checkable.test.jsx b/src/tree/__tests__/checkable.test.jsx index 86c0f5247..0a4a15c84 100644 --- a/src/tree/__tests__/checkable.test.jsx +++ b/src/tree/__tests__/checkable.test.jsx @@ -18,7 +18,7 @@ describe('Tree:checkable', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); expect(wrapper.find('[data-value="t1"] input[type=checkbox]').exists()).toBe(false); @@ -37,7 +37,7 @@ describe('Tree:checkable', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); expect(wrapper.find('[data-value="t1"] input[type=checkbox]').exists()).toBe(true); @@ -67,7 +67,7 @@ describe('Tree:checkable', () => { }; }, render() { - return ; + return ; }, }); expect(wrapper.find('[data-value="t1"] .t-checkbox').classes('t-is-checked')).toBe(false); @@ -99,7 +99,7 @@ describe('Tree:checkable', () => { }; }, render() { - return ; + return ; }, }); expect(wrapper.find('[data-value="t1"] .t-checkbox').classes('t-is-checked')).toBe(false); @@ -129,7 +129,7 @@ describe('Tree:checkable', () => { }; }, render() { - return ; + return ; }, }); wrapper.setData({ @@ -166,7 +166,7 @@ describe('Tree:checkable', () => { }); }, render() { - return ; + return ; }, }); @@ -195,15 +195,18 @@ describe('Tree:checkable', () => { ], }, ]; + let changeParams = null; + const onChange = (checked, context) => { + changeParams = [checked, context]; + }; const wrapper = mount({ render() { - return ; + return ( + + ); }, }); - await wrapper.find('[data-value="t1"] input[type="checkbox"]').setChecked(); - const treeWrapper = wrapper.findComponent(Tree); - const changeParams = treeWrapper.emitted().change[0]; expect(changeParams[0]).toEqual(['t1.1.1']); expect(changeParams[1].node.value).toEqual('t1'); }); @@ -224,14 +227,16 @@ describe('Tree:checkable', () => { ], }, ]; + let changeParams = null; + const onChange = (checked, context) => { + changeParams = [checked, context]; + }; const wrapper = mount({ render() { - return ; + return ; }, }); - const treeWrapper = wrapper.findComponent(Tree); await wrapper.find('[data-value="t1"] input[type="checkbox"]').setChecked(); - const changeParams = treeWrapper.emitted().change[0]; expect(changeParams[0]).toEqual(['t1', 't1.1', 't1.1.1']); expect(changeParams[1].node.value).toEqual('t1'); }); @@ -252,14 +257,25 @@ describe('Tree:checkable', () => { ], }, ]; + let changeParams = null; + const onChange = (checked, context) => { + changeParams = [checked, context]; + }; const wrapper = mount({ render() { - return ; + return ( + + ); }, }); - const treeWrapper = wrapper.findComponent(Tree); await wrapper.find('[data-value="t1"] input[type="checkbox"]').setChecked(); - const changeParams = treeWrapper.emitted().change[0]; expect(changeParams[0]).toEqual(['t1']); expect(changeParams[1].node.value).toEqual('t1'); }); diff --git a/src/tree/__tests__/debug.md b/src/tree/__tests__/debug.md index cebbe0395..35a3b0eaa 100644 --- a/src/tree/__tests__/debug.md +++ b/src/tree/__tests__/debug.md @@ -17,6 +17,35 @@ tree 针对性测试命令: npx vitest ./src/tree/__tests__/ ``` +## 分支维护 + +- 原项目 clone 到本地,并 fork 项目用于分支开发。 +- 配置分支推送到个人仓库,merge request 从个人仓库分支发起,个人仓库 develop 分支保持与原仓库一致。 +- 创建分支时,从原仓库分支创建,确保原仓库分支 upstream 为原仓库 develop 分支。 + +以用户名为 author 为例,流程指令一览: + +```bash +# 进入项目,添加个人远程分支 +git remote add {author} git@github.com:{author}/tdesign-vue-next.git +# 切换到原仓库 develop 分支 +git checkout develop +# 更新原仓库代码 +git pull +# 更新 submodule 仓库代码 +gi submodule update +# 本地建立调试分支 +git checkout -b fix/tree/debug +# 分支推送到个人仓库进行维护 +git push {author} +# 该分支实际第一次推送代码时,配置 upstream +git push --set-upstream {author} fix/tree/debug +``` + +分支维护完毕后,在 github 选择该分支发起 merge request。 + +该流程可避免 MR 时混入大量重复的提交日志。 + ## 调试界面 单独组件调试地址示例 diff --git a/src/tree/__tests__/event.test.jsx b/src/tree/__tests__/event.test.jsx index 4f8c11102..a185918d7 100644 --- a/src/tree/__tests__/event.test.jsx +++ b/src/tree/__tests__/event.test.jsx @@ -1,20 +1,37 @@ -import Vue from 'vue'; +/* eslint-disable vue/order-in-components */ import { mount } from '@vue/test-utils'; import Tree from '@/src/tree/index.ts'; +import { defineComponent } from './adapt'; +import { delay, step } from './kit'; + +// 2023.09.27 测试逻辑变更 +// 直接操作数据的动作,不再触发 onChange, onActive, onExpand 事件 +// 仅用户操作视图时的点击等动作,会触发 onChange, onActive, onExpand 事件 describe('Tree:props:events', () => { vi.useRealTimers(); describe('event:active', () => { - it('onActive 回调可触发', () => new Promise((resolve) => { + it('onActive 回调可触发', async () => { const data = [{ value: 't1' }, { value: 't2' }]; - mount( - Vue.component('test', { + const step1 = step(); + let rsActived = []; + let rsContext = null; + const wrapper = mount( + // eslint-disable-next-line vue/one-component-per-file + defineComponent({ components: { Tree, }, - template: [''].join( - ' ', - ), + // 使用 template 写法是为了 vue2, vue3 统一测试用例 + template: [ + '', + ].join(' '), data() { return { items: data, @@ -22,29 +39,43 @@ describe('Tree:props:events', () => { }, methods: { onActive(actived, context) { - expect(actived.length).toBe(1); - expect(actived[0]).toBe('t2'); - expect(context.node.value).toBe('t2'); - resolve(); + rsActived = actived; + rsContext = context; + step1.ready(); }, }, - mounted() { - this.$refs.tree.setItem('t2', { - actived: true, - }); - }, }), ); - }, 10)); - it('active 事件可触发', () => new Promise((resolve) => { + await delay(1); + wrapper.find('[data-value="t2"] .t-tree__label').trigger('click'); + await step1; + + expect(rsActived.length).toBe(1); + expect(rsActived[0]).toBe('t2'); + expect(rsContext.node.value).toBe('t2'); + }, 300); + + it('active 事件可触发', async () => { const data = [{ value: 't1' }, { value: 't2' }]; - mount( - Vue.component('test', { + const step1 = step(); + let rsActived = []; + let rsContext = null; + const wrapper = mount( + // eslint-disable-next-line vue/one-component-per-file + defineComponent({ components: { Tree, }, - template: [''].join(' '), + template: [ + '', + ].join(' '), data() { return { items: data, @@ -52,24 +83,26 @@ describe('Tree:props:events', () => { }, methods: { onActive(actived, context) { - expect(actived.length).toBe(1); - expect(actived[0]).toBe('t2'); - expect(context.node.value).toBe('t2'); - resolve(); + rsActived = actived; + rsContext = context; + step1.ready(); }, }, - mounted() { - this.$refs.tree.setItem('t2', { - actived: true, - }); - }, }), ); - }, 10)); + + await delay(1); + wrapper.find('[data-value="t2"] .t-tree__label').trigger('click'); + await step1; + + expect(rsActived.length).toBe(1); + expect(rsActived[0]).toBe('t2'); + expect(rsContext.node.value).toBe('t2'); + }, 300); }); describe('event:expand', () => { - it('onExpand 回调可触发', () => new Promise((resolve) => { + it('onExpand 回调可触发', async () => { const data = [ { value: 't1', @@ -89,12 +122,23 @@ describe('Tree:props:events', () => { }, ]; - mount( - Vue.component('test', { + const step1 = step(); + let rsExpanded = []; + let rsContext = null; + const wrapper = mount( + // eslint-disable-next-line vue/one-component-per-file + defineComponent({ components: { Tree, }, - template: [''].join(' '), + template: [ + '', + ].join(' '), data() { return { items: data, @@ -102,22 +146,24 @@ describe('Tree:props:events', () => { }, methods: { onExpand(expanded, context) { - expect(expanded.length).toBe(1); - expect(expanded[0]).toBe('t2'); - expect(context.node.value).toBe('t2'); - resolve(); + rsExpanded = expanded; + rsContext = context; + step1.ready(); }, }, - mounted() { - this.$refs.tree.setItem('t2', { - expanded: true, - }); - }, }), ); - }, 10)); - it('expand 事件可触发', () => new Promise((resolve) => { + await delay(1); + wrapper.find('[data-value="t2"] .t-tree__icon').trigger('click'); + await step1; + + expect(rsExpanded.length).toBe(1); + expect(rsExpanded[0]).toBe('t2'); + expect(rsContext.node.value).toBe('t2'); + }, 300); + + it('expand 事件可触发', async () => { const data = [ { value: 't1', @@ -137,12 +183,23 @@ describe('Tree:props:events', () => { }, ]; - mount( - Vue.component('test', { + const step1 = step(); + let rsExpanded = []; + let rsContext = null; + const wrapper = mount( + // eslint-disable-next-line vue/one-component-per-file + defineComponent({ components: { Tree, }, - template: [''].join(' '), + template: [ + '', + ].join(' '), data() { return { items: data, @@ -150,24 +207,26 @@ describe('Tree:props:events', () => { }, methods: { onExpand(expanded, context) { - expect(expanded.length).toBe(1); - expect(expanded[0]).toBe('t2'); - expect(context.node.value).toBe('t2'); - resolve(); + rsExpanded = expanded; + rsContext = context; + step1.ready(); }, }, - mounted() { - this.$refs.tree.setItem('t2', { - expanded: true, - }); - }, }), ); - }, 10)); + + await delay(1); + wrapper.find('[data-value="t2"] .t-tree__icon').trigger('click'); + await step1; + + expect(rsExpanded.length).toBe(1); + expect(rsExpanded[0]).toBe('t2'); + expect(rsContext.node.value).toBe('t2'); + }, 300); }); - describe('event:change', () => { - it('onChange 回调可触发', () => new Promise((resolve) => { + describe('event:change', async () => { + it('onChange 回调可触发', async () => { const data = [ { value: 't1', @@ -186,27 +245,114 @@ describe('Tree:props:events', () => { ], }, ]; - const onChange = (checked, context) => { - expect(checked.length).toBe(1); - expect(checked[0]).toBe('t2.1'); - expect(context.node.value).toBe('t2'); - resolve(); - }; - mount({ - mounted() { - this.$refs.tree.setItem('t2', { - checked: true, - }); + + const step1 = step(); + let rsValue = []; + let rsContext = null; + const wrapper = mount( + // eslint-disable-next-line vue/one-component-per-file + defineComponent({ + components: { + Tree, + }, + template: [ + '', + ].join(' '), + data() { + return { + items: data, + }; + }, + methods: { + onChange(value, context) { + rsValue = value; + rsContext = context; + step1.ready(); + }, + }, + }), + ); + + await delay(1); + wrapper.find('[data-value="t2"] input[type="checkbox"]').setChecked(); + await step1; + + expect(rsValue.length).toBe(1); + expect(rsValue[0]).toBe('t2.1'); + expect(rsContext.node.value).toBe('t2'); + }, 300); + + it('change 事件可触发', async () => { + const data = [ + { + value: 't1', + children: [ + { + value: 't1.1', + }, + ], }, - render() { - return ; + { + value: 't2', + children: [ + { + value: 't2.1', + }, + ], }, - }); - }, 10)); + ]; + + const step1 = step(); + let rsValue = []; + let rsContext = null; + const wrapper = mount( + // eslint-disable-next-line vue/one-component-per-file + defineComponent({ + components: { + Tree, + }, + template: [ + '', + ].join(' '), + data() { + return { + items: data, + }; + }, + methods: { + onChange(value, context) { + rsValue = value; + rsContext = context; + step1.ready(); + }, + }, + }), + ); + + await delay(1); + wrapper.find('[data-value="t2"] input[type="checkbox"]').setChecked(); + await step1; + + expect(rsValue.length).toBe(1); + expect(rsValue[0]).toBe('t2.1'); + expect(rsContext.node.value).toBe('t2'); + }, 300); }); describe('event:load', () => { - it('onLoad 回调可触发', () => new Promise((itResolve) => { + it('onLoad 回调可触发', async () => { const data = [ { label: '1', @@ -221,11 +367,6 @@ describe('Tree:props:events', () => { loadedValues.push(context.node.value); }; - setTimeout(() => { - expect(loadedValues[0]).toBe('t1'); - itResolve(); - }, 10); - const loadData = (node) => new Promise((resolve) => { setTimeout(() => { let nodes = []; @@ -244,9 +385,22 @@ describe('Tree:props:events', () => { mount({ render() { - return ; + return ( + + ); }, }); - }, 20)); + + await delay(10); + expect(loadedValues[0]).toBe('t1'); + }, 300); }); }); diff --git a/src/tree/__tests__/expand.test.jsx b/src/tree/__tests__/expand.test.jsx index e49c0a073..c235c2e90 100644 --- a/src/tree/__tests__/expand.test.jsx +++ b/src/tree/__tests__/expand.test.jsx @@ -18,7 +18,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); expect(wrapper.find('[data-value="t1.1"]').exists()).toBe(true); @@ -37,7 +37,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); expect(wrapper.find('[data-value="t1.1"]').exists()).toBe(false); @@ -66,7 +66,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -97,7 +97,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -196,7 +196,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); @@ -245,7 +245,7 @@ describe('Tree:expand', () => { const wrapper = mount({ render() { - return ; + return ; }, }); @@ -315,7 +315,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); wrapper.find('[data-value="t1"] .t-tree__icon').trigger('click'); @@ -360,7 +360,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); wrapper.find('[data-value="t1"]').trigger('click'); @@ -381,7 +381,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); wrapper.find('[data-value="t1"]').trigger('click'); @@ -409,7 +409,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -441,7 +441,7 @@ describe('Tree:expand', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; diff --git a/src/tree/__tests__/filter.test.jsx b/src/tree/__tests__/filter.test.jsx index bca37279f..9969d0d56 100644 --- a/src/tree/__tests__/filter.test.jsx +++ b/src/tree/__tests__/filter.test.jsx @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import Tree from '@/src/tree/index.ts'; +import { defineComponent } from './adapt'; import { delay } from './kit'; describe('Tree:filter', () => { @@ -20,25 +21,32 @@ describe('Tree:filter', () => { }, ]; - const wrapper = mount({ - data() { - return { - filter: null, - }; - }, - created() { - this.filter = (node) => node.value.indexOf('2') >= 0; - }, - render() { - return ( - -
- 暂无数据 -
-
- ); - }, - }); + const wrapper = mount( + defineComponent({ + components: { + Tree, + }, + data() { + return { + items: data, + filter: null, + }; + }, + created() { + this.filter = (node) => node.value.indexOf('2') >= 0; + }, + // 使用 template 写法是为了 vue2, vue3 统一测试用例 + template: [ + '
', + ].join(' '), + }), + ); await delay(10); diff --git a/src/tree/__tests__/index.test.jsx b/src/tree/__tests__/index.test.jsx index 261b6703f..1c187597b 100644 --- a/src/tree/__tests__/index.test.jsx +++ b/src/tree/__tests__/index.test.jsx @@ -1,49 +1,58 @@ +/* eslint-disable vue/one-component-per-file */ import { mount } from '@vue/test-utils'; import Tree from '@/src/tree/index.ts'; +import { defineComponent } from './adapt'; import { delay } from './kit'; describe('Tree:init', () => { vi.useRealTimers(); describe(':props.data', () => { it('传递空数据时,展示兜底界面', async () => { - const wrapper = mount({ - render() { - return ( - -
- 暂无数据 -
-
- ); - }, - }); + const wrapper = mount( + defineComponent({ + components: { + Tree, + }, + // 使用 template 写法是为了 vue2, vue3 统一测试用例 + template: [ + '
', + ].join(' '), + }), + ); await delay(1); expect(wrapper.find('.tree-empty').exists()).toBe(true); }); it('空数据初始化后,允许插入根节点', () => new Promise((resolve) => { - const wrapper = mount({ - mounted() { - const { tree } = this.$refs; - tree.appendTo('', { - value: 'insert1', - }); - setTimeout(() => { - expect(wrapper.find('.tree-empty').exists()).toBe(false); - expect(wrapper.find('[data-value="insert1"]').exists()).toBe(true); - resolve(); - }); - }, - render() { - return ( - -
- 暂无数据 -
-
- ); - }, - }); + const wrapper = mount( + defineComponent({ + components: { + Tree, + }, + mounted() { + const { tree } = this.$refs; + tree.appendTo('', { + value: 'insert1', + }); + setTimeout(() => { + expect(wrapper.find('.tree-empty').exists()).toBe(false); + expect(wrapper.find('[data-value="insert1"]').exists()).toBe(true); + resolve(); + }); + }, + template: [ + '
', + ].join(' '), + }), + ); })); it('可以传递一个树结构的数据来完成初始化', () => { @@ -60,17 +69,25 @@ describe('Tree:init', () => { value: 't2', }, ]; - const wrapper = mount({ - render() { - return ( - -
- 暂无数据 -
-
- ); - }, - }); + const wrapper = mount( + defineComponent({ + components: { + Tree, + }, + data() { + return { + items: data, + }; + }, + template: [ + '
', + ].join(' '), + }), + ); expect(wrapper.find('.tree-empty').exists()).toBe(false); expect(wrapper.find('[data-value="t1"]').exists()).toBe(true); expect(wrapper.find('[data-value="t1.1"]').exists()).toBe(false); diff --git a/src/tree/__tests__/keys.test.jsx b/src/tree/__tests__/keys.test.jsx index 65b7f1e01..a83e2d7d9 100644 --- a/src/tree/__tests__/keys.test.jsx +++ b/src/tree/__tests__/keys.test.jsx @@ -33,7 +33,7 @@ describe('Tree:keys', () => { return {}; }, render() { - return ; + return ; }, }); diff --git a/src/tree/__tests__/kit.js b/src/tree/__tests__/kit.js index 4296f8461..7fdace561 100644 --- a/src/tree/__tests__/kit.js +++ b/src/tree/__tests__/kit.js @@ -5,3 +5,14 @@ export function delay(time) { setTimeout(resolve, time); }); } + +export function step() { + let fn = null; + const pm = new Promise((resolve) => { + fn = resolve; + }); + pm.ready = () => { + if (fn) fn(); + }; + return pm; +} diff --git a/src/tree/__tests__/lazy.test.jsx b/src/tree/__tests__/lazy.test.jsx index bd6ff1d39..e7c5ffc1b 100644 --- a/src/tree/__tests__/lazy.test.jsx +++ b/src/tree/__tests__/lazy.test.jsx @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; import Tree from '@/src/tree/index.ts'; -import { delay } from './kit'; +import { delay, step } from './kit'; describe('Tree:lazy-load', () => { vi.useRealTimers(); @@ -62,7 +62,7 @@ describe('Tree:lazy-load', () => { }); }); await pm; - + await delay(1); expect(wrapper.find('[data-value="t1.1"]').exists()).toBe(true); expect(wrapper.find('[data-value="t1.1.1"]').exists()).toBe(true); expect(wrapper.find('[data-value="t1.1.1.1"]').exists()).toBe(false); @@ -130,7 +130,7 @@ describe('Tree:lazy-load', () => { }); await pm; - + await delay(1); expect(wrapper.find('[data-value="t1"] .t-checkbox').classes('t-is-indeterminate')).toBe(true); expect(wrapper.find('[data-value="t1"] .t-checkbox').classes('t-is-checked')).toBe(false); expect(wrapper.find('[data-value="t1.1"] .t-checkbox').classes('t-is-indeterminate')).toBe(true); @@ -200,7 +200,7 @@ describe('Tree:lazy-load', () => { }); await pm; - + await delay(1); expect(wrapper.find('[data-value="t1"] .t-checkbox').classes('t-is-indeterminate')).toBe(false); expect(wrapper.find('[data-value="t1"] .t-checkbox').classes('t-is-checked')).toBe(false); expect(wrapper.find('[data-value="t1.1"] .t-checkbox').classes('t-is-indeterminate')).toBe(false); @@ -218,6 +218,9 @@ describe('Tree:lazy-load', () => { }, ]; + const step1 = step(); + const step2 = step(); + let loadIndex = 0; const wrapper = mount({ data() { @@ -245,7 +248,12 @@ describe('Tree:lazy-load', () => { }, onLoad() { loadIndex += 1; - this.$emit('load', loadIndex); + if (loadIndex >= 1) { + step1.ready(); + } + if (loadIndex >= 2) { + step2.ready(); + } }, }, render() { @@ -264,15 +272,6 @@ describe('Tree:lazy-load', () => { await delay(10); - const step1 = new Promise((resolve) => { - wrapper.vm.$off('load'); - wrapper.vm.$on('load', (index) => { - if (index >= 1) { - resolve(); - } - }); - }); - expect(wrapper.find('[data-value="t1.1"]').exists()).toBe(false); wrapper.find('[data-value="t1"] .t-tree__icon').trigger('click'); @@ -281,15 +280,6 @@ describe('Tree:lazy-load', () => { // 留给 dom 渲染时间 await delay(10); - const step2 = new Promise((resolve) => { - wrapper.vm.$off('load'); - wrapper.vm.$on('load', (index) => { - if (index >= 2) { - resolve(); - } - }); - }); - expect(wrapper.find('[data-value="t1.1"]').exists()).toBe(true); expect(wrapper.find('[data-value="t1.1.1"]').exists()).toBe(false); wrapper.find('[data-value="t1.1"] .t-tree__icon').trigger('click'); @@ -311,6 +301,8 @@ describe('Tree:lazy-load', () => { }, ]; + const step1 = step(); + let loadIndex = 0; const wrapper = mount({ data() { @@ -334,7 +326,9 @@ describe('Tree:lazy-load', () => { }, onLoad() { loadIndex += 1; - this.$emit('load', loadIndex); + if (loadIndex >= 1) { + step1.ready(); + } }, }, mounted() { @@ -361,15 +355,6 @@ describe('Tree:lazy-load', () => { await delay(10); - const step1 = new Promise((resolve) => { - wrapper.vm.$off('load'); - wrapper.vm.$on('load', (index) => { - if (index >= 1) { - resolve(); - } - }); - }); - expect(wrapper.find('[data-value="t1"] .t-checkbox').classes('t-is-checked')).toBe(true); wrapper.find('[data-value="t1"] .t-tree__icon').trigger('click'); @@ -388,6 +373,8 @@ describe('Tree:lazy-load', () => { }, ]; + const step1 = step(); + let loadIndex = 0; const wrapper = mount({ data() { @@ -411,7 +398,9 @@ describe('Tree:lazy-load', () => { }, onLoad() { loadIndex += 1; - this.$emit('load', loadIndex); + if (loadIndex >= 1) { + step1.ready(); + } }, }, mounted() { @@ -439,15 +428,6 @@ describe('Tree:lazy-load', () => { await delay(10); - const step1 = new Promise((resolve) => { - wrapper.vm.$off('load'); - wrapper.vm.$on('load', (index) => { - if (index >= 1) { - resolve(); - } - }); - }); - expect(wrapper.find('[data-value="t1"] .t-checkbox').classes('t-is-checked')).toBe(true); wrapper.find('[data-value="t1"] .t-tree__icon').trigger('click'); diff --git a/src/tree/__tests__/tree-node-model.test.jsx b/src/tree/__tests__/tree-node-model.test.jsx index a77380510..9dec0ef93 100644 --- a/src/tree/__tests__/tree-node-model.test.jsx +++ b/src/tree/__tests__/tree-node-model.test.jsx @@ -18,7 +18,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -46,7 +46,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -73,7 +73,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -100,7 +100,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -127,7 +127,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -153,7 +153,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -192,7 +192,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -225,7 +225,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -278,7 +278,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -310,7 +310,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -344,7 +344,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -375,7 +375,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -408,7 +408,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -445,7 +445,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -476,7 +476,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const { tree } = wrapper.vm.$refs; @@ -501,7 +501,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); expect(wrapper.find('[data-value="t1.1"]').exists()).toBe(true); @@ -524,7 +524,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); expect(wrapper.find('[data-value="t1.1"]').exists()).toBe(true); @@ -547,7 +547,7 @@ describe('Tree:treeNodeModel', () => { ]; const wrapper = mount({ render() { - return ; + return ; }, }); const node = wrapper.vm.$refs.tree.getItem('t1'); @@ -580,7 +580,7 @@ describe('Tree:treeNodeModel', () => { }, }, render() { - return ; + return ; }, }); const el = wrapper.find('[data-value="t1.1"]'); diff --git a/src/tree/_example/activable.vue b/src/tree/_example/activable.vue index 1ca747582..073da371e 100644 --- a/src/tree/_example/activable.vue +++ b/src/tree/_example/activable.vue @@ -1,29 +1,28 @@ diff --git a/src/tree/_example/base.vue b/src/tree/_example/base.vue index d04681e65..56b337f6a 100644 --- a/src/tree/_example/base.vue +++ b/src/tree/_example/base.vue @@ -1,8 +1,6 @@ diff --git a/src/tree/_example/checkable.vue b/src/tree/_example/checkable.vue index 04bf7c201..4bd31897b 100644 --- a/src/tree/_example/checkable.vue +++ b/src/tree/_example/checkable.vue @@ -1,37 +1,35 @@ diff --git a/src/tree/_example/controlled.vue b/src/tree/_example/controlled.vue index 13d038e3a..3383f49a9 100644 --- a/src/tree/_example/controlled.vue +++ b/src/tree/_example/controlled.vue @@ -1,42 +1,55 @@ @@ -44,10 +57,13 @@ export default { data() { return { + syncProps: false, + checkable: true, + activable: false, valueMode: 'onlyLeaf', - checked: ['1.1.1.1', '1.1.1.2'], - expanded: ['1', '1.1', '1.1.1', '2'], - actived: ['2'], + checked: ['1.2.1', '1.2.2'], + expanded: ['1', '1.1'], + actived: [], items: [ { value: '1', @@ -60,30 +76,10 @@ export default { { value: '1.1.1', label: '1.1.1', - children: [ - { - value: '1.1.1.1', - label: '1.1.1.1', - }, - { - value: '1.1.1.2', - label: '1.1.1.2', - }, - ], }, { value: '1.1.2', label: '1.1.2', - children: [ - { - value: '1.1.2.1', - label: '1.1.2.1', - }, - { - value: '1.1.2.2', - label: '1.1.2.2', - }, - ], }, ], }, @@ -94,30 +90,10 @@ export default { { value: '1.2.1', label: '1.2.1', - children: [ - { - value: '1.2.1.1', - label: '1.2.1.1', - }, - { - value: '1.2.1.2', - label: '1.2.1.2', - }, - ], }, { value: '1.2.2', label: '1.2.2', - children: [ - { - value: '1.2.2.1', - label: '1.2.2.1', - }, - { - value: '1.2.2.2', - label: '1.2.2.2', - }, - ], }, ], }, @@ -125,18 +101,34 @@ export default { }, { value: '2', - label: '2 这个节点不允许展开, 不允许激活', + label: '2', checkable: false, children: [ { value: '2.1', label: '2.1 这个节点不允许选中', - checkable: false, }, { value: '2.2', - label: '2.2', + label: '2.2 这个节点不允许激活', + checkable: false, + }, + { + value: '2.3', + label: '2.3 这个节点不允许展开', checkable: false, + children: [ + { + value: '2.3.1', + label: '2.3.1', + checkable: false, + }, + { + value: '2.3.2', + label: '2.3.2', + checkable: false, + }, + ], }, ], }, @@ -167,37 +159,50 @@ export default { }, }, methods: { + selectNode() { + this.checked = ['1.1']; + }, + activeNode() { + this.actived = ['2']; + }, + expandNode() { + this.expanded = ['1', '1.2']; + }, onClick(context) { console.info('onClick:', context); const { node } = context; - console.info(node.value, 'checked:', node.checked); - console.info(node.value, 'expanded:', node.expanded); - console.info(node.value, 'actived:', node.actived); + console.info(node.value, 'checked:', node.checked, 'expanded:', node.expanded, 'actived:', node.actived); }, onChange(vals, context) { console.info('onChange:', vals, context); const checked = vals.filter((val) => val !== '2.1'); console.info('节点 2.1 不允许选中'); - this.checked = checked; + if (this.syncProps) { + this.checked = checked; + } const { node } = context; console.info(node.value, 'checked:', node.checked); }, - onExpand(vals, context) { - console.info('onExpand:', vals, context); - const expanded = vals.filter((val) => val !== '2'); - console.info('节点 2 不允许展开'); - this.expanded = expanded; - const { node } = context; - console.info(node.value, 'expanded:', node.expanded); - }, onActive(vals, context) { console.info('onActive:', vals, context); - const actived = vals.filter((val) => val !== '2'); - console.info('节点 2 不允许激活'); - this.actived = actived; + const actived = vals.filter((val) => val !== '2.2'); + console.info('节点 2.2 不允许激活', actived); + if (this.syncProps) { + this.actived = actived; + } const { node } = context; console.info(node.value, 'actived:', node.actived); }, + onExpand(vals, context) { + console.info('onExpand:', vals, context); + const expanded = vals.filter((val) => val !== '2.3'); + console.info('节点 2.3 不允许展开', expanded); + if (this.syncProps) { + this.expanded = expanded; + } + const { node } = context; + console.info(node.value, 'expanded:', node.expanded); + }, }, }; diff --git a/src/tree/_example/debug-data.vue b/src/tree/_example/debug-data.vue index f3d5fece7..1aa0109ab 100644 --- a/src/tree/_example/debug-data.vue +++ b/src/tree/_example/debug-data.vue @@ -1,55 +1,56 @@ - - diff --git a/src/tree/_example/debug-state.vue b/src/tree/_example/debug-state.vue deleted file mode 100644 index ae976a88d..000000000 --- a/src/tree/_example/debug-state.vue +++ /dev/null @@ -1,121 +0,0 @@ - - - diff --git a/src/tree/_example/debug-vscroll.vue b/src/tree/_example/debug-vscroll.vue index fb3106da3..0ae19ae95 100644 --- a/src/tree/_example/debug-vscroll.vue +++ b/src/tree/_example/debug-vscroll.vue @@ -1,41 +1,56 @@