Skip to content

Commit

Permalink
Input Unit Tests; More AutoComplete Unit Test; fix some bugs (#2041)
Browse files Browse the repository at this point in the history
* feat(input): add some features and fix some bugs

* test: update snapshots

* feat(popup): attach support current node

* test: test input & auto-complete

* feat: default value'

* fix(auto-complete): expect empty options with no panel
  • Loading branch information
chaishi committed Jan 14, 2023
1 parent 7b76c82 commit 2ffc98a
Show file tree
Hide file tree
Showing 32 changed files with 2,004 additions and 649 deletions.
43 changes: 33 additions & 10 deletions src/auto-complete/__tests__/mount.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { mount } from '@vue/test-utils';
import { createElementById } from '../../../test/utils';

function createElementById(vue2AttachTo) {
const div = document.createElement('div');
div.id = vue2AttachTo;
document.body.appendChild(div);
}

// Vue2 触发 focus 等特殊事件,需要 attachTo。
// trigger 文档:https://v1.test-utils.vuejs.org/api/wrapper/#trigger
// attachTo 文档:https://v1.test-utils.vuejs.org/api/options.html#attachto
export function getNormalAutoCompleteMount(AutoComplete, propsData = {}, listeners) {
const options = [
'FirstKeyword',
Expand All @@ -16,14 +14,16 @@ export function getNormalAutoCompleteMount(AutoComplete, propsData = {}, listene
text: 'SecondKeyword',
},
'ThirdKeyword',
{
label: 'READONLY_KEYWORD',
},
{
text: 'DISABLED_KEYWORD',
},
];

// Vue2 触发 focus 等特殊事件,需要 attachTo。
// trigger 文档:https://v1.test-utils.vuejs.org/api/wrapper/#trigger
// attachTo 文档:https://v1.test-utils.vuejs.org/api/options.html#attachto
const id = propsData.vue2AttachTo || 'auto-complete-test';
createElementById(id);

// eslint-disable-next-line
delete propsData.vue2AttachTo;

Expand All @@ -38,4 +38,27 @@ export function getNormalAutoCompleteMount(AutoComplete, propsData = {}, listene
});
}

export function getOptionSlotAutoCompleteMount(AutoComplete, props, events) {
const options = ['First', 'Second'];
createElementById();
return mount(
{
render() {
return (
<AutoComplete
value=""
options={options}
props={props}
on={events}
scopedSlots={{
option: ({ option }) => <div class="custom-slot-option">{`${option.text} Keyword`}</div>,
}}
/>
);
},
},
{ attachTo: '#focus-dom' },
);
}

export default getNormalAutoCompleteMount;
178 changes: 157 additions & 21 deletions src/auto-complete/__tests__/vitest-auto-complete.test.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/** eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
* 该文件由脚本自动生成,如需修改请联系 PMC
* This file generated by scripts of tdesign-api. `npm run api:docs AutoComplete Vue(PC) vitest,finalProject,useDefault`
* If you need to modify this file, contact PMC first please.
*/
import { mount } from '@vue/test-utils';
import { vi } from 'vitest';
import { createElementById, simulateKeydownEvent } from '@test/utils';
import { AutoComplete } from '..';
import { getNormalAutoCompleteMount } from './mount';
import { getNormalAutoCompleteMount, getOptionSlotAutoCompleteMount } from './mount';

describe('AutoComplete Component', () => {
it('props.autofocus is equal to false', () => {
Expand Down Expand Up @@ -48,10 +49,10 @@ describe('AutoComplete Component', () => {
expect(wrapper.find('.t-input__suffix-clear').exists()).toBeTruthy();
wrapper.find('.t-input__suffix-clear').trigger('click');
await wrapper.vm.$nextTick();
expect(onClearFn1).toHaveBeenCalled();
expect(onClearFn1).toHaveBeenCalled(1);
expect(onClearFn1.mock.calls[0][0].e.stopPropagation).toBeTruthy();
expect(onClearFn1.mock.calls[0][0].e.type).toBe('click');
expect(onChangeFn1).toHaveBeenCalled();
expect(onChangeFn1).toHaveBeenCalled(1);
expect(onChangeFn1.mock.calls[0][0]).toBe('');
expect(onChangeFn1.mock.calls[0][1].e.stopPropagation).toBeTruthy();
expect(onChangeFn1.mock.calls[0][1].e.type).toBe('click');
Expand All @@ -69,7 +70,7 @@ describe('AutoComplete Component', () => {
it('slots.default works fine', () => {
const wrapper = mount({
render() {
return <AutoComplete scopedSlots={{ default: () => <span class="custom-node">TNode</span> }}></AutoComplete>;
return <AutoComplete scopedSlots={{ default: (h) => <span class="custom-node">TNode</span> }}></AutoComplete>;
},
});
expect(wrapper.find('.custom-node').exists()).toBeTruthy();
Expand Down Expand Up @@ -142,17 +143,50 @@ describe('AutoComplete Component', () => {
expect(customNodeDom).toBeDefined();
// remove node in document to avoid influencing following test cases
customNodeDom?.remove();
document.querySelectorAll('.t-popup').forEach((node) => node.remove());
document.querySelectorAll('.t-select-option').forEach((node) => node.remove());
});
it('props.options: 3 options should exist', async () => {
it('props.options: 5 options should exist', async () => {
const wrapper = getNormalAutoCompleteMount(AutoComplete);
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
const tSelectOptionDom = document.querySelectorAll('.t-select-option');
expect(tSelectOptionDom.length).toBe(3);
expect(tSelectOptionDom.length).toBe(5);
// remove nodes from document to avoid influencing following test cases
tSelectOptionDom.forEach((node) => node.remove());
document.querySelectorAll('.t-popup').forEach((node) => node.remove());
});
it('props.options: expect empty options with no panel', async () => {
// Vue2 need attachTo to trigger `focus` event. https://v1.test-utils.vuejs.org/api/wrapper/#trigger
createElementById();
const wrapper = mount(
{
render() {
return <AutoComplete popupProps={{ overlayClassName: 'empty-options-class-name' }}></AutoComplete>;
},
},
{ attachTo: '#focus-dom' },
);
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
const emptyOptionsClassNameTAutocompletePanelDom = document.querySelectorAll(
'.empty-options-class-name .t-autocomplete__panel',
);
expect(emptyOptionsClassNameTAutocompletePanelDom.length).toBe(0);
// remove nodes from document to avoid influencing following test cases
emptyOptionsClassNameTAutocompletePanelDom.forEach((node) => node.remove());
});
it('props.options: define one option', async () => {
const wrapper = getOptionSlotAutoCompleteMount(AutoComplete, {
popupProps: { overlayClassName: 'option-slot-class-name' },
});
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
const optionSlotClassNameCustomSlotOptionDom = document.querySelector(
'.option-slot-class-name .custom-slot-option',
);
expect(optionSlotClassNameCustomSlotOptionDom.textContent).toBe('First Keyword');
// remove nodes from document to avoid influencing following test cases
optionSlotClassNameCustomSlotOptionDom.remove();
document.querySelectorAll('.t-select-option').forEach((node) => node.remove());
});

it('props.panelBottomContent works fine', async () => {
Expand All @@ -178,7 +212,7 @@ describe('AutoComplete Component', () => {
render() {
return (
<AutoComplete
scopedSlots={{ panelBottomContent: () => <span class="custom-node">TNode</span> }}
scopedSlots={{ panelBottomContent: (h) => <span class="custom-node">TNode</span> }}
></AutoComplete>
);
},
Expand Down Expand Up @@ -238,7 +272,7 @@ describe('AutoComplete Component', () => {
const wrapper = mount({
render() {
return (
<AutoComplete scopedSlots={{ panelTopContent: () => <span class="custom-node">TNode</span> }}></AutoComplete>
<AutoComplete scopedSlots={{ panelTopContent: (h) => <span class="custom-node">TNode</span> }}></AutoComplete>
);
},
});
Expand Down Expand Up @@ -278,13 +312,34 @@ describe('AutoComplete Component', () => {
it('props.placeholder is equal to \'type keyword to search\'', () => {
const wrapper = mount({
render() {
return <AutoComplete placeholder={'type keyword to search'}></AutoComplete>;
return <AutoComplete placeholder="type keyword to search"></AutoComplete>;
},
});
const domWrapper = wrapper.find('input');
expect(domWrapper.attributes('placeholder')).toBe('type keyword to search');
});

it('props.popupProps works fine', async () => {
const wrapper = getNormalAutoCompleteMount(AutoComplete, { popupProps: { overlayClassName: 'custom-class-name' } });
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
const customClassNameDom = document.querySelector('.custom-class-name');
expect(customClassNameDom).toBeDefined();
// remove node in document to avoid influencing following test cases
customClassNameDom?.remove();
});
it('props.popupProps works fine', async () => {
const wrapper = getNormalAutoCompleteMount(AutoComplete, {
popupProps: { overlayInnerClassName: 'custom-class-name' },
});
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
const customClassNameDom = document.querySelector('.custom-class-name');
expect(customClassNameDom).toBeDefined();
// remove node in document to avoid influencing following test cases
customClassNameDom?.remove();
});

it('props.readonly works fine', () => {
// readonly default value is
const wrapper1 = getNormalAutoCompleteMount(AutoComplete).find('.t-input');
Expand All @@ -310,17 +365,23 @@ describe('AutoComplete Component', () => {
});
});

['default', 'success', 'warning', 'error'].forEach((item) => {
const statusClassNameList = [{ 't-is-default': false }, 't-is-success', 't-is-warning', 't-is-error'];
['default', 'success', 'warning', 'error'].forEach((item, index) => {
it(`props.status is equal to ${item}`, () => {
const wrapper = getNormalAutoCompleteMount(AutoComplete, { status: item }).find('.t-input');
expect(wrapper.classes(`t-is-${item}`)).toBeTruthy();
if (typeof statusClassNameList[index] === 'string') {
expect(wrapper.classes(statusClassNameList[index])).toBeTruthy();
} else if (typeof statusClassNameList[index] === 'object') {
const classNameKey = Object.keys(statusClassNameList[index])[0];
expect(wrapper.classes(classNameKey)).toBeFalsy();
}
});
});

it('props.tips is equal this is a tip', () => {
const wrapper = mount({
render() {
return <AutoComplete tips={'this is a tip'}></AutoComplete>;
return <AutoComplete tips="this is a tip"></AutoComplete>;
},
});
expect(wrapper.find('.t-input__tips').exists()).toBeTruthy();
Expand All @@ -339,7 +400,7 @@ describe('AutoComplete Component', () => {
const wrapper = mount({
render() {
return (
<AutoComplete scopedSlots={{ triggerElement: () => <span class="custom-node">TNode</span> }}></AutoComplete>
<AutoComplete scopedSlots={{ triggerElement: (h) => <span class="custom-node">TNode</span> }}></AutoComplete>
);
},
});
Expand All @@ -361,7 +422,7 @@ describe('AutoComplete Component', () => {
it('props.value is equal to \'DefaultKeyword\'', () => {
const wrapper = mount({
render() {
return <AutoComplete value={'DefaultKeyword'}></AutoComplete>;
return <AutoComplete value="DefaultKeyword"></AutoComplete>;
},
});
const domWrapper = wrapper.find('input');
Expand All @@ -374,22 +435,48 @@ describe('AutoComplete Component', () => {
const wrapper = getNormalAutoCompleteMount(AutoComplete, {}, { focus: onFocusFn, blur: onBlurFn1 });
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
expect(onFocusFn).toHaveBeenCalled();
expect(onFocusFn).toHaveBeenCalled(1);
expect(onFocusFn.mock.calls[0][0].e.type).toBe('focus');
wrapper.find('input').trigger('blur');
await wrapper.vm.$nextTick();
expect(onBlurFn1).toHaveBeenCalled();
expect(onBlurFn1).toHaveBeenCalled(1);
expect(onBlurFn1.mock.calls[0][0].e.type).toBe('blur');
});

it('events.compositionend works fine', async () => {
const onCompositionendFn = vi.fn();
const wrapper = mount({
render() {
return <AutoComplete on={{ compositionend: onCompositionendFn }}></AutoComplete>;
},
});
wrapper.find('input').trigger('compositionend');
await wrapper.vm.$nextTick();
expect(onCompositionendFn).toHaveBeenCalled(1);
expect(onCompositionendFn.mock.calls[0][0].e.type).toBe('compositionend');
});

it('events.compositionstart works fine', async () => {
const onCompositionstartFn = vi.fn();
const wrapper = mount({
render() {
return <AutoComplete on={{ compositionstart: onCompositionstartFn }}></AutoComplete>;
},
});
wrapper.find('input').trigger('compositionstart');
await wrapper.vm.$nextTick();
expect(onCompositionstartFn).toHaveBeenCalled(1);
expect(onCompositionstartFn.mock.calls[0][0].e.type).toBe('compositionstart');
});

it('events.enter works fine', async () => {
const onEnterFn1 = vi.fn();
const wrapper = getNormalAutoCompleteMount(AutoComplete, {}, { enter: onEnterFn1 });
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
wrapper.find('input').trigger('keydown.enter');
await wrapper.vm.$nextTick();
expect(onEnterFn1).toHaveBeenCalled();
expect(onEnterFn1).toHaveBeenCalled(1);
expect(onEnterFn1.mock.calls[0][0].e.type).toBe('keydown');
expect(/Enter/i.test(onEnterFn1.mock.calls[0][0].e.key)).toBeTruthy();
});
Expand All @@ -400,7 +487,56 @@ describe('AutoComplete Component', () => {
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
expect(wrapper.find('.t-is-focused').exists()).toBeTruthy();
expect(onFocusFn).toHaveBeenCalled();
expect(onFocusFn).toHaveBeenCalled(1);
expect(onFocusFn.mock.calls[0][0].e.type).toBe('focus');
});

it('events.select works fine', async () => {
const onSelectFn1 = vi.fn();
const wrapper = getNormalAutoCompleteMount(
AutoComplete,
{ popupProps: { overlayClassName: 'select-event-class-name' } },
{ select: onSelectFn1 },
);
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
document.querySelector('.select-event-class-name .t-select-option').click();
await wrapper.vm.$nextTick();
document.querySelectorAll('.t-select-option').forEach((node) => node.remove());
expect(onSelectFn1).toHaveBeenCalled(1);
expect(onSelectFn1.mock.calls[0][0]).toBe('FirstKeyword');
expect(onSelectFn1.mock.calls[0][1].e.type).toBe('click');
});
it('events.select: keyboard operations: ArrowDown & ArrowUp & Enter', async () => {
const onSelectFn6 = vi.fn();
const wrapper = getNormalAutoCompleteMount(AutoComplete, {}, { select: onSelectFn6 });
wrapper.find('input').trigger('focus');
await wrapper.vm.$nextTick();
simulateKeydownEvent(document, 'ArrowDown');
await wrapper.vm.$nextTick();
const domWrapper1 = document.querySelector('.t-select-option:first-child');
expect(domWrapper1.classList.contains('t-select-option--hover')).toBeTruthy();
simulateKeydownEvent(document, 'ArrowDown');
await wrapper.vm.$nextTick();
const domWrapper2 = document.querySelector('.t-select-option:nth-child(2)');
expect(domWrapper2.classList.contains('t-select-option--hover')).toBeTruthy();
simulateKeydownEvent(document, 'ArrowUp');
await wrapper.vm.$nextTick();
const domWrapper3 = document.querySelector('.t-select-option:first-child');
expect(domWrapper3.classList.contains('t-select-option--hover')).toBeTruthy();
simulateKeydownEvent(document, 'ArrowUp');
await wrapper.vm.$nextTick();
const domWrapper4 = document.querySelector('.t-select-option:nth-child(5)');
expect(domWrapper4.classList.contains('t-select-option--hover')).toBeTruthy();
simulateKeydownEvent(document, 'ArrowDown');
await wrapper.vm.$nextTick();
const domWrapper5 = document.querySelector('.t-select-option:first-child');
expect(domWrapper5.classList.contains('t-select-option--hover')).toBeTruthy();
simulateKeydownEvent(document, 'Enter');
await wrapper.vm.$nextTick();
document.querySelectorAll('.t-select-option').forEach((node) => node.remove());
expect(onSelectFn6).toHaveBeenCalled(1);
expect(onSelectFn6.mock.calls[0][0]).toBe('FirstKeyword');
expect(onSelectFn6.mock.calls[0][1].e.type).toBe('keydown');
});
});
4 changes: 2 additions & 2 deletions src/auto-complete/auto-complete.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ triggerElement | String / Slot / Function | - | Typescript:`string \| TNode`
value | String | - | `v-model` is supported | N
defaultValue | String | - | uncontrolled property | N
onBlur | Function | | Typescript:`(context: { e: FocusEvent; value: string }) => void`<br/> | N
onChange | Function | | Typescript:`(value: string, context?: { e?: InputEvent \| MouseEvent \| KeyboardEvent }) => void`<br/> | N
onChange | Function | | Typescript:`(value: string, context?: { e?: InputEvent \| MouseEvent \| CompositionEvent \| KeyboardEvent }) => void`<br/> | N
onClear | Function | | Typescript:`(context: { e: MouseEvent }) => void`<br/> | N
onCompositionend | Function | | Typescript:`(context: { e: CompositionEvent; value: string }) => void`<br/>trigger on compositionend | N
onCompositionstart | Function | | Typescript:`(context: { e: CompositionEvent; value: string }) => void`<br/>trigger on compositionstart | N
Expand All @@ -40,7 +40,7 @@ onSelect | Function | | Typescript:`(value: string, context: { e: MouseEvent
name | params | description
-- | -- | --
blur | `(context: { e: FocusEvent; value: string })` | \-
change | `(value: string, context?: { e?: InputEvent \| MouseEvent \| KeyboardEvent })` | \-
change | `(value: string, context?: { e?: InputEvent \| MouseEvent \| CompositionEvent \| KeyboardEvent })` | \-
clear | `(context: { e: MouseEvent })` | \-
compositionend | `(context: { e: CompositionEvent; value: string })` | trigger on compositionend
compositionstart | `(context: { e: CompositionEvent; value: string })` | trigger on compositionstart
Expand Down
Loading

0 comments on commit 2ffc98a

Please sign in to comment.