Skip to content

Commit b74915d

Browse files
authored
fix: Nest loadData crash (#216)
* test: test driven * fix: nest load should not crash
1 parent 3042a55 commit b74915d

File tree

4 files changed

+189
-134
lines changed

4 files changed

+189
-134
lines changed

examples/dynamic-options.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class Demo extends React.Component {
4040
{
4141
label: `${targetOption.label}动态加载1`,
4242
value: 'dynamic1',
43+
isLeaf: false,
4344
},
4445
{
4546
label: `${targetOption.label}动态加载2`,
@@ -50,7 +51,7 @@ class Demo extends React.Component {
5051
// eslint-disable-next-line react/no-access-state-in-setstate
5152
options: [...this.state.options],
5253
});
53-
}, 1000);
54+
}, 500);
5455
};
5556

5657
render() {

src/OptionList/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const RefOptionList = React.forwardRef<RefOptionListProps, OptionListProps>((pro
5353
const { options: optionList } = restoreCompatibleValue(entity as any, fieldNames);
5454
const rawOptionList = optionList.map(opt => opt.node);
5555

56-
setLoadingKeys(keys => [...keys, optionList[optionList.length - 1].value]);
56+
setLoadingKeys(keys => [...keys, entity.key]);
5757

5858
loadData(rawOptionList);
5959
}
@@ -64,7 +64,7 @@ const RefOptionList = React.forwardRef<RefOptionListProps, OptionListProps>((pro
6464
if (loadingKeys.length) {
6565
loadingKeys.forEach(loadingKey => {
6666
const option = flattenOptions.find(opt => opt.value === loadingKey);
67-
if (option.data.children || option.data.isLeaf === true) {
67+
if (!option || option.data.children || option.data.isLeaf === true) {
6868
setLoadingKeys(keys => keys.filter(key => key !== loadingKey));
6969
}
7070
});

tests/index.spec.tsx

Lines changed: 0 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint-disable react/jsx-no-bind */
22

33
import React from 'react';
4-
import { act } from 'react-dom/test-utils';
54
import { resetWarned } from 'rc-util/lib/warning';
65
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
76
import { mount } from './enzyme';
@@ -511,136 +510,6 @@ describe('Cascader.Basic', () => {
511510
expect(wrapper.isOpen()).toBeFalsy();
512511
});
513512

514-
describe('loadData', () => {
515-
it('basic load', () => {
516-
const loadData = jest.fn();
517-
const wrapper = mount(
518-
<Cascader
519-
loadingIcon={<div className="loading-icon" />}
520-
options={[
521-
{
522-
label: 'Bamboo',
523-
value: 'bamboo',
524-
isLeaf: false,
525-
},
526-
]}
527-
loadData={loadData}
528-
open
529-
/>,
530-
);
531-
532-
wrapper.find('.rc-cascader-menu-item-content').first().simulate('click');
533-
expect(wrapper.exists('.loading-icon')).toBeTruthy();
534-
expect(loadData).toHaveBeenCalledWith([
535-
expect.objectContaining({
536-
value: 'bamboo',
537-
}),
538-
]);
539-
540-
expect(wrapper.exists('.rc-cascader-menu-item-loading')).toBeTruthy();
541-
expect(wrapper.exists('.rc-cascader-menu-item-loading-icon')).toBeTruthy();
542-
543-
// Fill data
544-
wrapper.setProps({
545-
options: [
546-
{
547-
label: 'Bamboo',
548-
value: 'bamboo',
549-
isLeaf: false,
550-
children: [],
551-
},
552-
],
553-
});
554-
wrapper.update();
555-
expect(wrapper.exists('.loading-icon')).toBeFalsy();
556-
});
557-
558-
it('not load leaf', () => {
559-
const loadData = jest.fn();
560-
const onValueChange = jest.fn();
561-
const wrapper = mount(
562-
<Cascader
563-
open
564-
loadData={loadData}
565-
onChange={onValueChange}
566-
options={[
567-
{
568-
label: 'Light',
569-
value: 'light',
570-
},
571-
]}
572-
/>,
573-
);
574-
575-
wrapper.clickOption(0, 0);
576-
expect(onValueChange).toHaveBeenCalled();
577-
expect(loadData).not.toHaveBeenCalled();
578-
});
579-
580-
// https://github.com/ant-design/ant-design/issues/9084
581-
it('should trigger loadData when expandTrigger is hover', () => {
582-
const options = [
583-
{
584-
value: 'zhejiang',
585-
label: 'Zhejiang',
586-
isLeaf: false,
587-
},
588-
{
589-
value: 'jiangsu',
590-
label: 'Jiangsu',
591-
isLeaf: false,
592-
},
593-
];
594-
const loadData = jest.fn();
595-
const wrapper = mount(
596-
<Cascader options={options} loadData={loadData} changeOnSelect expandTrigger="hover">
597-
<input readOnly />
598-
</Cascader>,
599-
);
600-
wrapper.find('input').simulate('click');
601-
const menus = wrapper.find('.rc-cascader-menu');
602-
const menu1Items = menus.at(0).find('.rc-cascader-menu-item');
603-
menu1Items.at(0).simulate('mouseEnter');
604-
jest.runAllTimers();
605-
expect(loadData).toHaveBeenCalled();
606-
});
607-
608-
it('change isLeaf back to true should not loop loading', async () => {
609-
const Demo = () => {
610-
const [options, setOptions] = React.useState([
611-
{ value: 'zhejiang', label: 'Zhejiang', isLeaf: false },
612-
]);
613-
614-
const loadData = () => {
615-
Promise.resolve().then(() => {
616-
act(() => {
617-
setOptions([
618-
{
619-
value: 'zhejiang',
620-
label: 'Zhejiang',
621-
isLeaf: true,
622-
},
623-
]);
624-
});
625-
});
626-
};
627-
628-
return <Cascader options={options} loadData={loadData} open />;
629-
};
630-
631-
const wrapper = mount(<Demo />);
632-
wrapper.find('.rc-cascader-menu-item-content').first().simulate('click');
633-
expect(wrapper.exists('.rc-cascader-menu-item-loading')).toBeTruthy();
634-
635-
for (let i = 0; i < 3; i += 1) {
636-
await Promise.resolve();
637-
}
638-
wrapper.update();
639-
640-
expect(wrapper.exists('.rc-cascader-menu-item-loading')).toBeFalsy();
641-
});
642-
});
643-
644513
// https://github.com/ant-design/ant-design/issues/9793
645514
it('should not trigger onBlur and onFocus when select item', () => {
646515
// This function is handled by `rc-select` instead

tests/loadData.spec.tsx

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/* eslint-disable react/jsx-no-bind */
2+
3+
import React from 'react';
4+
import { act } from 'react-dom/test-utils';
5+
import { mount } from './enzyme';
6+
import Cascader from '../src';
7+
8+
describe('Cascader.LoadData', () => {
9+
beforeEach(() => {
10+
jest.useFakeTimers();
11+
});
12+
13+
afterEach(() => {
14+
jest.useRealTimers();
15+
});
16+
17+
it('basic load', () => {
18+
const loadData = jest.fn();
19+
const wrapper = mount(
20+
<Cascader
21+
loadingIcon={<div className="loading-icon" />}
22+
options={[
23+
{
24+
label: 'Bamboo',
25+
value: 'bamboo',
26+
isLeaf: false,
27+
},
28+
]}
29+
loadData={loadData}
30+
open
31+
/>,
32+
);
33+
34+
wrapper.find('.rc-cascader-menu-item-content').first().simulate('click');
35+
expect(wrapper.exists('.loading-icon')).toBeTruthy();
36+
expect(loadData).toHaveBeenCalledWith([
37+
expect.objectContaining({
38+
value: 'bamboo',
39+
}),
40+
]);
41+
42+
expect(wrapper.exists('.rc-cascader-menu-item-loading')).toBeTruthy();
43+
expect(wrapper.exists('.rc-cascader-menu-item-loading-icon')).toBeTruthy();
44+
45+
// Fill data
46+
wrapper.setProps({
47+
options: [
48+
{
49+
label: 'Bamboo',
50+
value: 'bamboo',
51+
isLeaf: false,
52+
children: [],
53+
},
54+
],
55+
});
56+
wrapper.update();
57+
expect(wrapper.exists('.loading-icon')).toBeFalsy();
58+
});
59+
60+
it('not load leaf', () => {
61+
const loadData = jest.fn();
62+
const onValueChange = jest.fn();
63+
const wrapper = mount(
64+
<Cascader
65+
open
66+
loadData={loadData}
67+
onChange={onValueChange}
68+
options={[
69+
{
70+
label: 'Light',
71+
value: 'light',
72+
},
73+
]}
74+
/>,
75+
);
76+
77+
wrapper.clickOption(0, 0);
78+
expect(onValueChange).toHaveBeenCalled();
79+
expect(loadData).not.toHaveBeenCalled();
80+
});
81+
82+
// https://github.com/ant-design/ant-design/issues/9084
83+
it('should trigger loadData when expandTrigger is hover', () => {
84+
const options = [
85+
{
86+
value: 'zhejiang',
87+
label: 'Zhejiang',
88+
isLeaf: false,
89+
},
90+
{
91+
value: 'jiangsu',
92+
label: 'Jiangsu',
93+
isLeaf: false,
94+
},
95+
];
96+
const loadData = jest.fn();
97+
const wrapper = mount(
98+
<Cascader options={options} loadData={loadData} changeOnSelect expandTrigger="hover">
99+
<input readOnly />
100+
</Cascader>,
101+
);
102+
wrapper.find('input').simulate('click');
103+
const menus = wrapper.find('.rc-cascader-menu');
104+
const menu1Items = menus.at(0).find('.rc-cascader-menu-item');
105+
menu1Items.at(0).simulate('mouseEnter');
106+
jest.runAllTimers();
107+
expect(loadData).toHaveBeenCalled();
108+
});
109+
110+
it('change isLeaf back to true should not loop loading', async () => {
111+
const Demo = () => {
112+
const [options, setOptions] = React.useState([
113+
{ value: 'zhejiang', label: 'Zhejiang', isLeaf: false },
114+
]);
115+
116+
const loadData = () => {
117+
Promise.resolve().then(() => {
118+
act(() => {
119+
setOptions([
120+
{
121+
value: 'zhejiang',
122+
label: 'Zhejiang',
123+
isLeaf: true,
124+
},
125+
]);
126+
});
127+
});
128+
};
129+
130+
return <Cascader options={options} loadData={loadData} open />;
131+
};
132+
133+
const wrapper = mount(<Demo />);
134+
wrapper.find('.rc-cascader-menu-item-content').first().simulate('click');
135+
expect(wrapper.exists('.rc-cascader-menu-item-loading')).toBeTruthy();
136+
137+
for (let i = 0; i < 3; i += 1) {
138+
await Promise.resolve();
139+
}
140+
wrapper.update();
141+
142+
expect(wrapper.exists('.rc-cascader-menu-item-loading')).toBeFalsy();
143+
});
144+
145+
it('nest load should not crash', async () => {
146+
const Demo = () => {
147+
const [options, setOptions] = React.useState([{ label: 'top', value: 'top', isLeaf: false }]);
148+
149+
const loadData = selectedOptions => {
150+
Promise.resolve().then(() => {
151+
act(() => {
152+
selectedOptions[selectedOptions.length - 1].children = [
153+
{
154+
label: 'child',
155+
value: 'child',
156+
isLeaf: false,
157+
},
158+
];
159+
setOptions(list => [...list]);
160+
});
161+
});
162+
};
163+
164+
return <Cascader options={options} loadData={loadData} open />;
165+
};
166+
167+
const wrapper = mount(<Demo />);
168+
169+
// First column click
170+
wrapper.find('.rc-cascader-menu-item-content').last().simulate('click');
171+
for (let i = 0; i < 3; i += 1) {
172+
await Promise.resolve();
173+
}
174+
wrapper.update();
175+
176+
// Second column click
177+
wrapper.find('.rc-cascader-menu-item-content').last().simulate('click');
178+
for (let i = 0; i < 3; i += 1) {
179+
await Promise.resolve();
180+
}
181+
wrapper.update();
182+
183+
expect(wrapper.find('ul.rc-cascader-menu')).toHaveLength(3);
184+
});
185+
});

0 commit comments

Comments
 (0)