Repository: Tencent/tdesign-mobile-vue
Pull request #401 (2022-10-10 16:41:01)

test(Image): improve unit test coverage
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？
- [x] 测试用例

### 📝 更新日志

<!--
从用户角度描述具体变化，以及可能的 breaking change 和其他风险。
-->

- test(Image): 提升组件单元测试覆盖率

- [x] 本条 PR 不需要纳入 Changelog



github-actions[bot]: [<img height="96" alt="失败" src="https://user-images.githubusercontent.com/5378891/75333447-1e63a280-58c1-11ea-975d-235367fd1522.png">](https://github.com/Tencent/tdesign-mobile-vue/actions/runs/3220983783)
<!-- AUTO_PREVIEW_HOOK -->
github-actions[bot]: **Size Change:** 0 B 

**Total Size:** 258 kB



<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/tdesign.css` | 24.4 kB |
| `./dist/tdesign.js` | 131 kB |
| `./dist/tdesign.min.css` | 22.6 kB |
| `./dist/tdesign.min.js` | 80.3 kB |

</details>


github-actions[bot]: **Size Change:** -14 B (0%) 

**Total Size:** 258 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.js` | 131 kB | -9 B (0%) |
| `./dist/tdesign.min.js` | 80.4 kB | -5 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/tdesign.css` | 24.4 kB |
| `./dist/tdesign.min.css` | 22.6 kB |

</details>



Patch:
diff --git a/src/image/image.vue b/src/image/image.vue
index d2dd9de9..9e80d238 100644
--- a/src/image/image.vue
+++ b/src/image/image.vue
@@ -9,8 +9,8 @@
       :style="imageStyles"
       :src="realSrc"
       :alt="alt"
-      @Load="handleImgLoadCompleted"
-      @Error="handleImgLoadError"
+      @load="handleImgLoadCompleted"
+      @error="handleImgLoadError"
     />
   </div>
 </template>
diff --git a/src/input/input.vue b/src/input/input.vue
index 3a92a9e8..704ba26e 100644
--- a/src/input/input.vue
+++ b/src/input/input.vue
@@ -137,7 +137,8 @@ export default defineComponent({
     };
 
     const blur = () => {
-      inputRef.value?.blur();
+      focused.value = false;
+      // inputRef.value?.blur();
     };
 
     extendAPI({ focus, blur });
diff --git a/src/overlay/overlay.md b/src/overlay/overlay.md
index f624bb55..e72390b7 100644
--- a/src/overlay/overlay.md
+++ b/src/overlay/overlay.md
@@ -10,6 +10,7 @@ preventScrollThrough | Boolean | true | 防止滚动穿透 | N
 transparent | Boolean | false | 遮罩层是否透明 | N
 visible | Boolean | true | 必需。是否展示 | Y
 zIndex | Number | 1000 | 遮罩的层级 | N
+customStyle | String | '' | 弹出层的自定义样式 | N
 onClick | Function |  | TS 类型：`(context: { e: MouseEvent }) => void`<br/>遮罩层的点击事件 | N
 
 ### Overlay Events
diff --git a/src/overlay/overlay.vue b/src/overlay/overlay.vue
index 41d8e478..f75c51ff 100644
--- a/src/overlay/overlay.vue
+++ b/src/overlay/overlay.vue
@@ -1,6 +1,6 @@
 <template>
   <Transition :name="name">
-    <div v-show="visible" :class="classes" :style="customStyle" @click="handleClick" @touchmove="handleTouchMove">
+    <div v-show="visible" :class="classes" :style="rootStyles" @click="handleClick" @touchmove="handleTouchMove">
       <slot />
     </div>
   </Transition>
@@ -10,7 +10,6 @@
 import { computed, defineComponent } from 'vue';
 import config from '../config';
 import props from './props';
-import { TdOverlayProps } from './type';
 
 const { prefix } = config;
 const name = `${prefix}-overlay`;
@@ -18,16 +17,20 @@ const name = `${prefix}-overlay`;
 export default defineComponent({
   name,
   props,
-  setup(props: TdOverlayProps) {
+  setup(props) {
     const classes = computed(() => ({
       [`${name}`]: true,
       [`${name}--active`]: props.visible,
       [`${name}--transparent`]: props.transparent,
     }));
-    const customStyle = computed(() => ({
-      'z-index': props.zIndex,
-      'transition-duration': `${props.duration}ms`,
-    }));
+
+    const rootStyles = computed(() =>
+      props.customStyle || props.zIndex || props.duration
+        ? (props.customStyle && `${props.customStyle};`) +
+          (props.zIndex && `z-index:${props.zIndex};`) +
+          (props.duration && `transition-duration:${props.duration}ms;`)
+        : undefined,
+    );
 
     const handleTouchMove = (e: TouchEvent) => {
       if (props.preventScrollThrough) {
@@ -42,7 +45,7 @@ export default defineComponent({
     return {
       name,
       classes,
-      customStyle,
+      rootStyles,
       handleClick,
       handleTouchMove,
     };
diff --git a/src/overlay/props.ts b/src/overlay/props.ts
index b125421e..826be00f 100644
--- a/src/overlay/props.ts
+++ b/src/overlay/props.ts
@@ -31,6 +31,11 @@ export default {
     type: Number,
     default: 1000,
   },
+  /** 遮罩的自定义样式 */
+  customStyle: {
+    type: String,
+    default: '',
+  },
   /** 遮罩层的点击事件 */
   onClick: Function as PropType<TdOverlayProps['onClick']>,
 };
diff --git a/src/overlay/type.ts b/src/overlay/type.ts
index a066a121..7e5eb5f4 100644
--- a/src/overlay/type.ts
+++ b/src/overlay/type.ts
@@ -30,6 +30,11 @@ export interface TdOverlayProps {
    * @default 1000
    */
   zIndex?: number;
+  /**
+   * 遮罩的自定义样式
+   * @default ''
+   */
+  customStyle?: string;
   /**
    * 遮罩层的点击事件
    */
diff --git a/src/popup/demos/placement-top.vue b/src/popup/demos/placement-top.vue
index c1000859..c48575d1 100644
--- a/src/popup/demos/placement-top.vue
+++ b/src/popup/demos/placement-top.vue
@@ -3,7 +3,7 @@
     <div class="button-group">
       <t-button block variant="outline" size="large" @click="top = true">顶部弹出</t-button>
     </div>
-    <t-popup v-model="top" placement="top">
+    <t-popup v-model="top" placement="top" :overlay-props="overlayProps">
       <div style="height: 200px; background: #fff"></div>
     </t-popup>
   </div>
@@ -12,6 +12,9 @@
 <script lang="ts" setup>
 import { ref } from 'vue';
 
+const overlayProps = {
+  customStyle: `background-color: rgba(0,0,0,.3)`, // 自定义遮罩层样式
+};
 const top = ref(false);
 </script>
 
diff --git a/src/textarea/textarea.vue b/src/textarea/textarea.vue
index 227574e9..45d533bd 100644
--- a/src/textarea/textarea.vue
+++ b/src/textarea/textarea.vue
@@ -41,7 +41,7 @@ export default defineComponent({
     TNode,
   },
   props: TextareaProps,
-  emits: ['update:value', 'update:modelValue', 'click-icon', 'focus', 'blur', 'clear', 'change'],
+  emits: ['update:value', 'update:modelValue', 'focus', 'blur', 'change'],
   setup(props, context: SetupContext) {
     const emitEvent = useEmitEvent(props, context.emit);
     const textareaRef = ref<null | HTMLElement>(null);
@@ -108,10 +108,6 @@ export default defineComponent({
       textareaValueChangeHandle(e as InputEvent);
     };
 
-    const handleClear = (e: MouseEvent) => {
-      setInnerValue('');
-      emitEvent('clear', { e });
-    };
     const handleFocus = (e: FocusEvent) => {
       emitEvent('focus', innerValue.value, { e });
     };
@@ -135,7 +131,6 @@ export default defineComponent({
       textareaStyle,
       textareaClassNames,
       textareaLength,
-      handleClear,
       handleFocus,
       handleBlur,
       handleInput,


Test patch:
diff --git a/src/image/__test__/__snapshots__/index.test.jsx.snap b/src/image/__test__/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..91f2e618
--- /dev/null
+++ b/src/image/__test__/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,15 @@
+// Vitest Snapshot v1
+
+exports[`Image > props > : lazy 1`] = `
+<div
+  class="t-image t-image--round"
+>
+  <!--v-if-->
+  <img
+    alt=""
+    class="t-image__img"
+    src="https://tdesign.gtimg.com/site/upload1.png"
+    style="object-fit: fill; object-position: center;"
+  />
+</div>
+`;
diff --git a/src/image/__test__/index.test.jsx b/src/image/__test__/index.test.jsx
index 47907ee7..8089e300 100644
--- a/src/image/__test__/index.test.jsx
+++ b/src/image/__test__/index.test.jsx
@@ -1,86 +1,144 @@
 import { mount } from '@vue/test-utils';
-import { describe, it, expect } from 'vitest';
-import Image from '../image.vue';
+import { vi, describe, it, expect } from 'vitest';
 import { LoadingIcon } from 'tdesign-icons-vue-next';
 import { nextTick } from 'vue';
-import { vi } from 'vitest';
+import Image from '../image.vue';
+import { MockIntersectionObserver } from './utils';
 
+const prefix = 't'
+const name = `${prefix}-image`;
 const IMAGE = 'https://tdesign.gtimg.com/site/upload1.png';
 const FAIL_IMAGE = 'https://123.jpg';
 const fitList = ['fill', 'contain', 'cover', 'none', 'scale-down'];
 const shapeList = ['circle', 'round', 'square'];
 const positionList = ['top', 'bottom', 'center', 'left', 'right'];
 
-describe('Image.vue', () => {
-  it('create', async () => {
-    const wrapper = mount(() => <Image src={IMAGE} />);
-    const img = wrapper.find('.t-image__img');
-    expect(wrapper.classes()).toContain('t-image');
-    expect(img.exists()).toBeTruthy();
-    expect(img.attributes('src')).toBe(IMAGE);
+describe('Image', () => {
+  beforeAll(() => {
+    window.IntersectionObserver = MockIntersectionObserver;
   });
 
-  it('alt render', async () => {
-    const wrapper = mount(() => <Image src={IMAGE} alt="图片" />);
-    const img = wrapper.find('.t-image__img');
-    expect(wrapper.classes()).toContain('t-image');
-    expect(img.attributes('alt')).toBe('图片');
-  });
+  describe('props', () => {
+    it(': lazy', async () => {
+      const wrapper = mount(() => <Image src={IMAGE} lazy />);
+      await nextTick();
+      const $image = wrapper.find(`.${name}__img`);
+      expect(wrapper.find(`.${name}__status`).exists()).toBeTruthy();
+      // 触发 IntersectionObserver , 但图片加载完成不会触发 load 回调,
+      $image.trigger('resize');
+      await nextTick();
+      expect($image.attributes('src')).toBe(IMAGE);
+      // 手动触发 图片加载完成的回调函数
+      await $image.trigger('load');
+      expect(wrapper.element).toMatchSnapshot();
+      expect(wrapper.find(`.${name}__status`).exists()).toBeFalsy();
+    });
 
-  it('fit render', async () => {
-    fitList.forEach((fit) => {
-      const wrapper = mount(() => <Image src={IMAGE} fit={fit} />);
-      const img = wrapper.find('.t-image__img');
-      expect(img.attributes('style')).toContain(`object-fit: ${fit}`);
+    it(': alt', async () => {
+      const wrapper = mount(() => <Image src={IMAGE} alt="图片" />);
+      const $image = wrapper.find(`.${name}__img`);
+      expect(wrapper.classes()).toContain(`${name}`);
+      expect($image.attributes('alt')).toBe('图片');
     });
-  });
 
-  it('shape render', async () => {
-    shapeList.forEach((shape) => {
-      const wrapper = mount(() => <Image src={IMAGE} shape={shape} />);
-      expect(wrapper.classes()).toContain(`t-image--${shape}`);
+    it(': fit', async () => {
+      const wrapper = mount(Image, {
+        props: {
+          fit: '',
+          src: IMAGE,
+        }
+      })
+      const $image = wrapper.find(`.${name}__img`);
+      // fit = ''
+      fitList.forEach(item => {
+        expect($image.attributes('style').includes(`object-fit: ${item}`)).toBeFalsy();
+      })
+
+      const fit = 'cover';
+      await wrapper.setProps({
+        fit,
+      });
+      // fit = 'square'
+      expect($image.attributes('style')).toContain(`object-fit: ${fit}`);
     });
-  });
 
-  it('position render', async () => {
-    positionList.forEach((position) => {
-      const wrapper = mount(() => <Image src={IMAGE} position={position} />);
-      const img = wrapper.find('.t-image__img');
-      expect(img.attributes('style')).toContain(`object-position: ${position}`);
+    it(': shape', async () => {
+      const wrapper = mount(Image, {
+        props: {
+          shape: '',
+          src: IMAGE,
+        }
+      })
+      const $image = wrapper.findComponent(Image);
+
+      // shape = ''
+      shapeList.forEach(item => {
+        expect($image.classes().includes(`${name}--${item}`)).toBeFalsy();
+      })
+
+      const shape = 'square';
+      await wrapper.setProps({
+        shape,
+      });
+      // shape = 'square'
+      expect($image.classes()).toContain(`${name}--${shape}`);
     });
-  });
 
-  it('loading render', async () => {
-    const slots = {
-      loading: () => <LoadingIcon />,
-    };
-    const wrapper = mount(() => <Image src={IMAGE} v-slots={slots} />);
-    const status = wrapper.find('.t-image__status');
-    expect(status.exists()).toBeTruthy();
-    expect(wrapper.findComponent(LoadingIcon).exists()).toBeTruthy();
-  });
+    it(': position', async () => {
+      positionList.forEach((position) => {
+        const wrapper = mount(() => <Image src={IMAGE} position={position} />);
+        const $image = wrapper.find('.t-image__img');
+        expect($image.attributes('style')).toContain(`object-position: ${position}`);
+      });
+    });
 
-  it('error render', async () => {
-    const onError = vi.fn();
-    const slots = {
-      error: () => '加载失败',
-    };
-    const wrapper = mount(() => <Image src={FAIL_IMAGE} v-slots={slots} onError={onError} />);
-    const img = wrapper.find('.t-image__img');
-    const status = wrapper.find('.t-image__status');
-    await nextTick();
-    await img.trigger('error');
-    expect(status.exists()).toBeTruthy();
-    // expect(status.text()).toBe('加载失败');
-    // expect(onError).toBeCalled();
-  });
+    it(': loading', async () => {
+      const slots = {
+        loading: () => <LoadingIcon />,
+      };
+      const wrapper = mount(() => <Image src={IMAGE} v-slots={slots} />);
+      const $status = wrapper.find(`.${name}__status`);
+      expect($status.exists()).toBeTruthy();
+      expect(wrapper.findComponent(LoadingIcon).exists()).toBeTruthy();
+    });
 
-  it('load render', async () => {
-    const onLoad = vi.fn();
-    const wrapper = mount(() => <Image src={IMAGE} onLoad={onLoad} />);
-    const img = wrapper.find('.t-image__img');
-    await nextTick();
-    img.trigger('load');
-    // expect(onLoad).toBeCalled();
+    it(': src', async () => {
+      const onError = vi.fn();
+      const wrapper = mount(() => <Image src='' onError={onError} />);
+      await nextTick();
+      const $image = wrapper.find(`.${name}__img`);
+      // 手动触发 图片加载失败的回调函数
+      await $image.trigger('error');
+      expect(wrapper.find(`.${name}__status`).exists()).toBeTruthy();
+      // src = ''，不会触发 error
+      expect(onError).toBeCalledTimes(0);
+    });
+
+    it(': onError', async () => {
+      const onError = vi.fn();
+      const slots = {
+        error: () => '加载失败',
+      };
+      const wrapper = mount(() => <Image src={FAIL_IMAGE} v-slots={slots} onError={onError} />);
+      await nextTick();
+      const $image = wrapper.find(`.${name}__img`);
+      // 手动触发 图片加载失败的回调函数
+      await $image.trigger('error');
+      const status = wrapper.find(`.${name}__status`);
+      expect(status.exists()).toBeTruthy();
+      expect(status.text()).toBe('加载失败');
+      expect(onError).toBeCalledTimes(1);
+    });
+
+    it(': onLoad', async () => {
+      const onLoad = vi.fn();
+      const wrapper = mount(() => <Image src={IMAGE} onLoad={onLoad} />);
+      await nextTick();
+      const $image = wrapper.find(`.${name}__img`);
+      expect($image.attributes('src')).toBe(IMAGE);
+      // 手动触发 图片加载完成的回调函数
+      await $image.trigger('load');
+      expect(onLoad).toBeCalledTimes(1);
+    });
   });
 });
diff --git a/src/image/__test__/utils.ts b/src/image/__test__/utils.ts
new file mode 100644
index 00000000..c28acdf8
--- /dev/null
+++ b/src/image/__test__/utils.ts
@@ -0,0 +1,34 @@
+interface IntersectionObserverCallback {
+  (entries: IntersectionObserverEntry[], observer: IntersectionObserver): void;
+}
+
+export class MockIntersectionObserver {
+  _callback: Function;
+
+  _element!: HTMLElement;
+
+  constructor(callback: Function) {
+    this._callback = callback;
+  }
+
+  observe(element: HTMLElement) {
+    this._element = element;
+    this._element.addEventListener('resize', this.trigger);
+  }
+
+  unobserve() {
+    this._element.removeEventListener('resize', this.trigger);
+  }
+
+  disconnect() {
+    this._element.removeEventListener('resize', this.trigger);
+  }
+
+  trigger = (event: UIEvent) => {
+    this._callback([
+      {
+        isIntersecting: true,
+      },
+    ]);
+  };
+}
diff --git a/src/input/__test__/__snapshots__/index.test.jsx.snap b/src/input/__test__/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..d7670e07
--- /dev/null
+++ b/src/input/__test__/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,37 @@
+// Vitest Snapshot v1
+
+exports[`Input.vue > event > : focus 1`] = `
+<div
+  class="t-cell t-cell--middle t-cell--bordered t-input t-input--size-small"
+>
+  <div
+    class="t-cell__left-icon"
+  >
+    <!--v-if-->
+    <!--v-if-->
+  </div>
+  <!--v-if-->
+  <div
+    class="t-cell__note"
+  >
+    
+    <div
+      class="t-input__wrap"
+    >
+      <input
+        autocomplete="Off"
+        class="t-input__control"
+        maxlength="-1"
+        name=""
+        placeholder="placeholder content"
+        type="text"
+      />
+      <!--v-if-->
+      <!--v-if-->
+    </div>
+    <!--v-if-->
+    
+  </div>
+  <!--v-if-->
+</div>
+`;
diff --git a/src/input/__test__/index.test.jsx b/src/input/__test__/index.test.jsx
index 9acd170c..6f89b027 100644
--- a/src/input/__test__/index.test.jsx
+++ b/src/input/__test__/index.test.jsx
@@ -1,153 +1,195 @@
-import { nextTick, ref } from 'vue';
+import { nextTick, ref, onMounted } from 'vue';
 import { mount } from '@vue/test-utils';
 import { describe, it, expect, vi } from 'vitest';
 import Input from '../input.vue';
+
 import { CloseCircleFilledIcon, InfoCircleFilledIcon, AppIcon } from 'tdesign-icons-vue-next';
 
+const prefix = 't'
+const name = `${prefix}-input`;
 const alignList = ['center', 'right'];
 
+const simulateEvent = (target, text, event) => {
+  target.value = text;
+  target.dispatchEvent(new Event(event));
+};
+
 describe('Input.vue', async () => {
-  it('create', async () => {
-    const value = ref('123');
-    const wrapper = mount(<Input v-model={value.value} />);
-    expect(wrapper.classes()).toContain('t-input');
-    expect(wrapper.classes()).toContain('t-cell');
-    const input = wrapper.find('.t-input__wrap input');
-    expect(input.exists()).toBeTruthy();
-    expect(input.element.value).toBe('123');
-  });
+  describe('props', async () => {
+    it(': value', async () => {
+      const value = ref('123');
+      const wrapper = mount(<Input v-model={value.value} />);
+      expect(wrapper.classes()).toContain('t-input');
+      expect(wrapper.classes()).toContain('t-cell');
+      const input = wrapper.find('.t-input__wrap input');
+      expect(input.exists()).toBeTruthy();
+      expect(input.element.value).toBe('123');
+    });
 
-  it('placeholder render', async () => {
-    const wrapper = mount(<Input placeholder="请输入" />);
-    const input = wrapper.find('.t-input__wrap input');
-    expect(input.attributes('placeholder')).toBe('请输入');
-  });
+    it(': maxcharacter', async () => {
+      const value = ref('');
+      const maxcharacter = 2;
+      const onChange = vi.fn()
+      const wrapper = mount(<Input v-model={value.value} maxcharacter={maxcharacter} onChange={onChange}/>);
+      const el = wrapper.find('input').element;
+      await simulateEvent(el, '一个汉字等于两个字符，超出会被剪切', 'input');
+      expect(onChange).toBeCalledTimes(1);
+      expect(onChange).toHaveBeenCalledWith('一');
+    });
 
-  it('label render', async () => {
-    const wrapper = mount(<Input label="标题" />);
-    const label = wrapper.find('.t-cell__title .t-input--label');
-    expect(label.exists()).toBeTruthy();
-    expect(label.text()).toBe('标题');
-  });
+    it(': placeholder', async () => {
+      const wrapper = mount(<Input placeholder="请输入" />);
+      const input = wrapper.find('.t-input__wrap input');
+      expect(input.attributes('placeholder')).toBe('请输入');
+    });
 
-  it('required render', async () => {
-    const wrapper = mount(<Input label="标题" required />);
-    const required = wrapper.find('.t-cell__title .t-cell--required');
-    expect(required.exists()).toBeTruthy();
-    expect(required.text()).toContain('*');
-  });
+    it(': label', async () => {
+      const wrapper = mount(<Input label="标题" />);
+      const label = wrapper.find('.t-cell__title .t-input--label');
+      expect(label.exists()).toBeTruthy();
+      expect(label.text()).toBe('标题');
+    });
 
-  it('errorMessage render', async () => {
-    const wrapper = mount(<Input label="标题" errorMessage="错误信息" />);
-    const errorMessage = wrapper.find('.t-input__error-msg');
-    expect(errorMessage.exists()).toBeTruthy();
-    expect(errorMessage.text()).toBe('错误信息');
-  });
+    it(': required', async () => {
+      const wrapper = mount(<Input label="标题" required />);
+      const required = wrapper.find('.t-cell__title .t-cell--required');
+      expect(required.exists()).toBeTruthy();
+      expect(required.text()).toContain('*');
+    });
 
-  it('align render', async () => {
-    alignList.forEach((a) => {
-      const wrapper = mount(<Input label="标题" align={a} />);
-      const control = wrapper.find(`.t-input__control--${a}`);
-      expect(control.exists()).toBeTruthy();
+    it(': errorMessage', async () => {
+      const wrapper = mount(<Input label="标题" errorMessage="错误信息" />);
+      const errorMessage = wrapper.find('.t-input__error-msg');
+      expect(errorMessage.exists()).toBeTruthy();
+      expect(errorMessage.text()).toBe('错误信息');
     });
-  });
 
-  it('clearable render', async () => {
-    const value = ref('123');
-    const handleClear = vi.fn();
-    const wrapper = mount(<Input label="标题" v-model={value.value} clearable onClear={handleClear} />);
-    const closeIcon = wrapper.findComponent(CloseCircleFilledIcon);
-    expect(closeIcon.exists()).toBeTruthy();
-    await closeIcon.trigger('click');
-    expect(value.value).toBe('');
-    expect(handleClear).toBeCalled();
-  });
+    it(': align', async () => {
+      alignList.forEach((a) => {
+        const wrapper = mount(<Input label="标题" align={a} />);
+        const control = wrapper.find(`.t-input__control--${a}`);
+        expect(control.exists()).toBeTruthy();
+      });
+    });
 
-  it('disabled render', async () => {
-    const value = ref('123');
-    const wrapper = mount(<Input label="标题" v-model={value.value} disabled />);
-    const label = wrapper.find('.t-input--label');
-    expect(label.classes()).toContain('t-is-disabled');
-    const input = wrapper.find('.t-input__wrap input');
-    expect(input.element.hasAttribute('disabled')).toBeTruthy();
-  });
+    it(': clearable', async () => {
+      const value = ref('123');
+      const handleClear = vi.fn();
+      const wrapper = mount(<Input label="标题" v-model={value.value} clearable onClear={handleClear} />);
+      const closeIcon = wrapper.findComponent(CloseCircleFilledIcon);
+      expect(closeIcon.exists()).toBeTruthy();
+      await closeIcon.trigger('click');
+      expect(value.value).toBe('');
+      expect(handleClear).toBeCalled();
+    });
 
-  it('readonly render', async () => {
-    const value = ref('123');
-    const wrapper = mount(<Input label="标题" v-model={value.value} readonly />);
-    const input = wrapper.find('.t-input__wrap input');
-    expect(input.element.hasAttribute('readonly')).toBeTruthy();
-  });
+    it(': disabled', async () => {
+      const value = ref('123');
+      const wrapper = mount(<Input label="标题" v-model={value.value} disabled />);
+      const label = wrapper.find('.t-input--label');
+      expect(label.classes()).toContain('t-is-disabled');
+      const input = wrapper.find('.t-input__wrap input');
+      expect(input.element.hasAttribute('disabled')).toBeTruthy();
+    });
 
-  it('maxlength render', async () => {
-    const value = ref('');
-    const wrapper = mount(<Input label="标题" v-model={value.value} maxlength={10} />);
-    const input = wrapper.find('.t-input__wrap input');
-    expect(input.element.getAttribute('maxlength')).toBe('10');
-  });
+    it(': readonly', async () => {
+      const value = ref('123');
+      const wrapper = mount(<Input label="标题" v-model={value.value} readonly />);
+      const input = wrapper.find('.t-input__wrap input');
+      expect(input.element.hasAttribute('readonly')).toBeTruthy();
+    });
 
-  it('autocomplete render', async () => {
-    const wrapper = mount(<Input label="标题" autocomplete />);
-    const input = wrapper.find('.t-input__wrap input');
-    expect(input.element.getAttribute('autocomplete')).toBe('On');
-  });
+    it(': maxlength', async () => {
+      const value = ref('');
+      const wrapper = mount(<Input label="标题" v-model={value.value} maxlength={10} />);
+      const input = wrapper.find('.t-input__wrap input');
+      expect(input.element.getAttribute('maxlength')).toBe('10');
+    });
 
-  it('size render', async () => {
-    const wrapper = mount(() => <Input label="标题" size="small" />);
-    expect(wrapper.classes()).toContain('t-input--size-small');
-  });
+    it(': autocomplete', async () => {
+      const wrapper = mount(<Input label="标题" autocomplete />);
+      const input = wrapper.find('.t-input__wrap input');
+      expect(input.element.getAttribute('autocomplete')).toBe('On');
+    });
 
-  it('icon slot render', async () => {
-    const slots = {
-      prefixIcon: () => <InfoCircleFilledIcon />,
-      suffixIcon: () => <AppIcon />,
-    };
-    const wrapper = mount(<Input label="标题" v-slots={slots} />);
-    const leftIcon = wrapper.find('.t-cell__left-icon svg');
-    const rightIcon = wrapper.find('.t-cell__right-icon svg');
-    expect(wrapper.findComponent(InfoCircleFilledIcon).exists()).toBeTruthy();
-    expect(wrapper.findComponent(AppIcon).exists()).toBeTruthy();
-    expect(leftIcon.exists()).toBeTruthy();
-    expect(rightIcon.exists()).toBeTruthy();
-  });
+    it(': size', async () => {
+      const wrapper = mount(() => <Input label="标题" size="small" />);
+      expect(wrapper.classes()).toContain('t-input--size-small');
+    });
 
-  it('type render', async () => {
-    const wrapper = mount(<Input label="标题" type="number" />);
-    const input = wrapper.find('.t-input__wrap input');
-    expect(input.element.getAttribute('type')).toBe('number');
-  });
+    it(': type', async () => {
+      const wrapper = mount(<Input label="标题" type="number" />);
+      const input = wrapper.find('.t-input__wrap input');
+      expect(input.element.getAttribute('type')).toBe('number');
+    });
 
-  it('blur trigger', async () => {
-    const onBlur = vi.fn();
-    const wrapper = mount(<Input label="标题" onBlur={onBlur} />);
-    const input = wrapper.find('.t-input__wrap input');
-    await input.trigger('blur');
-    expect(onBlur).toBeCalled();
-  });
+    it(': onBlur', async () => {
+      const onBlur = vi.fn();
+      const wrapper = mount(<Input label="标题" onBlur={onBlur} />);
+      await nextTick();
+      const input = wrapper.find('.t-input__wrap input');
+      await input.trigger('blur');
+      expect(onBlur).toBeCalled();
+    });
+
+    it(': onFocus', async () => {
+      const onFocus = vi.fn();
+      const wrapper = mount(<Input label="标题" onFocus={onFocus} />);
+      const input = wrapper.find('.t-input__wrap input');
+      await input.trigger('focus');
+      expect(onFocus).toBeCalled();
+    });
 
-  it('focus trigger', async () => {
-    const onFocus = vi.fn();
-    const wrapper = mount(<Input label="标题" onFocus={onFocus} />);
-    const input = wrapper.find('.t-input__wrap input');
-    await input.trigger('focus');
-    expect(onFocus).toBeCalled();
+    it(': onChange', async () => {
+      const value = ref('');
+      const onChange = vi.fn();
+      const wrapper = mount(<Input label="标题" v-model={value.value} onChange={onChange} />);
+      const el = wrapper.find('.t-input__wrap input').element;
+      await simulateEvent(el, '文本', 'input');
+      expect(onChange).toBeCalledTimes(1);
+      expect(onChange).toHaveBeenCalledWith('文本');
+    });
+  });
+  describe('event', async () => {
+    it(': focus && blur', async () => {
+      const inputRef = ref(null);
+      const placeholder = 'placeholder content';
+      const onBlur = vi.fn();
+      const onFocus = vi.fn();
+      const wrapper = mount({
+        render() {
+          return <Input ref={inputRef} placeholder={placeholder} onBlur={onBlur} onFocus={onFocus} />
+        },
+      });
+      const input = wrapper.find(`.${name}__wrap input`);
+
+      inputRef.value.focus(); // 调用 focus 方法, 实现聚焦
+      await nextTick();
+
+      await input.trigger('blur'); // 模拟触发 blur
+      expect(onBlur).toBeCalledTimes(1);
+
+      await input.trigger('focus'); // 模拟触发 focus
+      expect(onFocus).toBeCalledTimes(1);
+
+      inputRef.value.blur(); // 调用 blur 方法，实现失焦
+      await nextTick();
+    });
   });
 
-  it('change trigger', async () => {
-    const data = ref('');
-    const value = ref('');
-    const handleChange = (val) => {
-      value.value = val;
-    };
-    const wrapper = mount(<Input label="标题" v-model={data.value} onChange={handleChange} />);
-    const el = wrapper.find('.t-input__wrap input').element;
-    await nextTick();
-    const simulateEvent = (text, event) => {
-      el.value = text;
-      el.dispatchEvent(new Event(event));
-    };
-    simulateEvent('2', 'input');
-    await nextTick();
-    expect(value.value).toBe('2');
+  describe('slots', async () => {
+    it(': icon', async () => {
+      const slots = {
+        prefixIcon: () => <InfoCircleFilledIcon />,
+        suffixIcon: () => <AppIcon />,
+      };
+      const wrapper = mount(<Input label="标题" v-slots={slots} />);
+      const leftIcon = wrapper.find('.t-cell__left-icon svg');
+      const rightIcon = wrapper.find('.t-cell__right-icon svg');
+      expect(wrapper.findComponent(InfoCircleFilledIcon).exists()).toBeTruthy();
+      expect(wrapper.findComponent(AppIcon).exists()).toBeTruthy();
+      expect(leftIcon.exists()).toBeTruthy();
+      expect(rightIcon.exists()).toBeTruthy();
+    });
   });
 });
diff --git a/src/popup/__test__/__snapshots__/demo.test.jsx.snap b/src/popup/__test__/__snapshots__/demo.test.jsx.snap
index cb423126..1663c6ad 100644
--- a/src/popup/__test__/__snapshots__/demo.test.jsx.snap
+++ b/src/popup/__test__/__snapshots__/demo.test.jsx.snap
@@ -136,7 +136,7 @@ exports[`Popup > Popup mobileVue demo works fine 1`] = `
           >
             <div
               class="t-overlay"
-              style="z-index: 1000; transition-duration: 300ms; display: none;"
+              style="background-color: rgba(0, 0, 0, 0.3); z-index: 1000; transition-duration: 300ms; display: none;"
             >
               
               
@@ -814,7 +814,7 @@ exports[`Popup > Popup placementTopVue demo works fine 1`] = `
       >
         <div
           class="t-overlay"
-          style="z-index: 1000; transition-duration: 300ms; display: none;"
+          style="background-color: rgba(0, 0, 0, 0.3); z-index: 1000; transition-duration: 300ms; display: none;"
         >
           
           
diff --git a/src/textarea/__test__/index.test.jsx b/src/textarea/__test__/index.test.jsx
index 1d2425c1..590b7785 100644
--- a/src/textarea/__test__/index.test.jsx
+++ b/src/textarea/__test__/index.test.jsx
@@ -3,31 +3,56 @@ import { mount } from '@vue/test-utils';
 import { describe, it, expect, vi } from 'vitest';
 import Textarea from '../textarea.vue';
 
+const simulateEvent = (target, text, event) => {
+  target.value = text;
+  target.dispatchEvent(new Event(event));
+};
+
 describe('Textarea.vue', () => {
-  it('create', async () => {
-    const value = ref('123');
-    const wrapper = mount(() => <Textarea v-model={value.value} />);
+  it(': value', async () => {
+    const value = ref('文本');
+    const wrapper = mount(() => <Textarea value={value.value} />);
     expect(wrapper.classes()).toContain('t-textarea');
     const textarea = wrapper.find('textarea');
     expect(textarea.exists()).toBeTruthy();
-    expect(textarea.element.value).toBe('123');
+    expect(textarea.element.value).toBe('文本');
+  });
+
+  it(': maxcharacter ', async () => {
+    const value = ref('');
+    const maxcharacter = 2;
+    const onChange = vi.fn()
+    const wrapper = mount(<Textarea label="标题" v-model={value.value} maxcharacter={maxcharacter} onChange={onChange} />);
+    const el = wrapper.find('textarea').element;
+    await simulateEvent(el, '一个汉字等于两个字符，超出会被剪切', 'input');
+    expect(onChange).toBeCalledTimes(1);
+    expect(onChange).toHaveBeenCalledWith('一');
   });
 
-  it('placeholder render', async () => {
+  it(': autosize ', async () => {
+    const value = ref('');
+    const onChange = vi.fn()
+    const wrapper = mount(<Textarea label="标题" v-model={value.value} autosize onChange={onChange} />);
+    const el = wrapper.find('textarea').element;
+    await simulateEvent(el, '这里是一段很长很长很长的长文本，支持自动换行', 'input');
+    expect(onChange).toBeCalledTimes(1);
+  });
+
+  it(': placeholder', async () => {
     const wrapper = mount(<Textarea placeholder="请输入" />);
     const input = wrapper.find('textarea');
     expect(input.attributes('placeholder')).toBe('请输入');
   });
 
-  it('label render', async () => {
+  it(': label', async () => {
     const wrapper = mount(<Textarea label="标题" />);
     const label = wrapper.find('.t-textarea__name');
     expect(label.exists()).toBeTruthy();
     expect(label.text()).toBe('标题');
   });
 
-  it('disabled render', async () => {
-    const value = ref('123');
+  it(': disabled', async () => {
+    const value = ref('文本');
     const wrapper = mount(<Textarea label="标题" v-model={value.value} disabled />);
     const label = wrapper.find('.t-textarea__wrapper');
     expect(label.classes()).toContain('t-textarea-is-disabled');
@@ -35,22 +60,23 @@ describe('Textarea.vue', () => {
     expect(textarea.element.hasAttribute('disabled')).toBeTruthy();
   });
 
-  it('maxlength render', async () => {
+  it(': maxlength', async () => {
     const value = ref('');
     const wrapper = mount(<Textarea label="标题" v-model={value.value} maxlength={10} />);
     const textarea = wrapper.find('textarea');
     expect(textarea.element.getAttribute('maxlength')).toBe('10');
   });
 
-  it('blur trigger', async () => {
+  it(': onBlur', async () => {
     const onBlur = vi.fn();
-    const wrapper = mount(<Textarea label="标题" onBlur={onBlur} />);
+    const onFocus = vi.fn();
+    const wrapper = mount(<Textarea label="标题" autofocus onBlur={onBlur} onFocus={onFocus}/>);
     const textarea = wrapper.find('textarea');
     await textarea.trigger('blur');
     expect(onBlur).toBeCalled();
   });
 
-  it('focus trigger', async () => {
+  it(': onFocus', async () => {
     const onFocus = vi.fn();
     const wrapper = mount(<Textarea label="标题" onFocus={onFocus} />);
     const textarea = wrapper.find('textarea');
@@ -58,7 +84,7 @@ describe('Textarea.vue', () => {
     expect(onFocus).toBeCalled();
   });
 
-  it('change trigger', async () => {
+  it(': onChange ', async () => {
     const data = ref('');
     const value = ref('');
     const handleChange = (val) => {
@@ -66,13 +92,7 @@ describe('Textarea.vue', () => {
     };
     const wrapper = mount(<Textarea label="标题" v-model={data.value} onChange={handleChange} />);
     const el = wrapper.find('textarea').element;
-    await nextTick();
-    const simulateEvent = (text, event) => {
-      el.value = text;
-      el.dispatchEvent(new Event(event));
-    };
-    simulateEvent('2', 'input');
-    await nextTick();
-    expect(value.value).toBe('2');
+    await simulateEvent(el, '文本', 'input');
+    expect(value.value).toBe('文本');
   });
 });