Repository: Tencent/tdesign-mobile-vue
Pull request #376 (2022-09-29 01:46:27)

test(list): list组件 单元测试
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？

- [ ] 日常 bug 修复
- [ ] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] CI/CD 改进
- [ ] 重构
- [ ] 代码风格优化
- [x] 测试用例
- [ ] 分支合并
- [ ] 其他

### 🔗 相关 Issue

<!--
1. 描述相关需求的来源，如相关的 issue 讨论链接。
-->

### 💡 需求背景和解决方案

<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->
List 组件单元测试补充
### 📝 更新日志

<!--
从用户角度描述具体变化，以及可能的 breaking change 和其他风险。
-->

- fix(list): `List` 组件单元测试补充
- fix(list): 修复`load-more`可选参数问题
- [ ] 本条 PR 不需要纳入 Changelog

### ☑️ 请求合并前的自查清单

⚠️ 请自检并全部**勾选全部选项**。⚠️

- [x] 文档已补充或无须补充
- [x] 代码演示已提供或无须提供
- [x] TypeScript 定义已补充或无须补充
- [x] 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/3148169113)
<!-- AUTO_PREVIEW_HOOK -->

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/list/list.vue b/src/list/list.vue
index b3aa4690..e96c8994 100644
--- a/src/list/list.vue
+++ b/src/list/list.vue
@@ -4,7 +4,7 @@
     <slot />
     <div @click.stop="onLoadMore">
       <t-loading
-        v-if="typeof asyncLoading === 'string' && ['loading', 'loadingMore'].includes(asyncLoading)"
+        v-if="typeof asyncLoading === 'string' && ['loading', 'load-more'].includes(asyncLoading)"
         :loading="asyncLoading === 'loading'"
         :text="typeof asyncLoading === 'string' ? LOADING_TEXT_MAP[asyncLoading] : ''"
         :class="`${name}__loading`"
@@ -26,7 +26,7 @@ const { prefix } = config;
 const name = `${prefix}-list`;
 const LOADING_TEXT_MAP = {
   loading: '加载中...',
-  'loading-more': '点击加载更多',
+  'load-more': '点击加载更多',
 };
 
 export default defineComponent({
@@ -49,7 +49,7 @@ export default defineComponent({
     const footerContent = computed(() => renderTNode(internalInstance, 'footer'));
 
     const onLoadMore = (e: MouseEvent) => {
-      if (props.asyncLoading === 'loading-more') {
+      if (props.asyncLoading === 'load-more') {
         emitEvent('load-more');
       }
     };
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/search/demos/mobile.vue b/src/search/demos/mobile.vue
index cbe53dcf..5d7fe73f 100644
--- a/src/search/demos/mobile.vue
+++ b/src/search/demos/mobile.vue
@@ -9,10 +9,10 @@
       <t-search placeholder="搜索预设文案" @change="onChange"></t-search>
     </tdesign-demo-block>
     <tdesign-demo-block summary="点击状态">
-      <t-search placeholder="搜索预设文案" :autofocus="true" @change="onChange"></t-search>
+      <t-search placeholder="搜索预设文案" :focus="true" @change="onChange"></t-search>
     </tdesign-demo-block>
     <tdesign-demo-block summary="输入状态">
-      <t-search v-model="value2" placeholder="搜索预设文案" :autofocus="true" @change="onChange"></t-search>
+      <t-search v-model="value2" placeholder="搜索预设文案" :focus="true" @change="onChange"></t-search>
     </tdesign-demo-block>
   </div>
 </template>
diff --git a/src/search/search.vue b/src/search/search.vue
index ac9db97d..a088fa9e 100644
--- a/src/search/search.vue
+++ b/src/search/search.vue
@@ -1,6 +1,6 @@
 <template>
   <div :class="classes">
-    <div :class="`${name}__form`">
+    <div :class="`${name}__form`" :style="shapeStyle">
       <div :class="`${name}__box`">
         <div :class="`${name}__icon-search`">
           <slot name="leftIcon">
@@ -14,13 +14,15 @@
           :class="`${name}__input`"
           :autofocus="focus"
           :placeholder="placeholder"
-          :clearable="clearable"
           @blur="onBlur"
           @focus="onFocus"
           @change="onChange"
         />
+        <div :class="`${name}__icon-close`">
+          <t-icon-clear @click="onClear" />
+        </div>
       </div>
-      <label v-show="state.labelActive" :class="`${name}__label`" @click="onClick">
+      <label v-show="state.labelActive" :class="`${name}__label`" :style="shapeStyle" @click="onClick">
         <div :class="`${name}__label-icon-search`">
           <slot name="leftIcon">
             <t-icon-search />
@@ -44,7 +46,7 @@
 </template>
 
 <script lang="ts">
-import { SearchIcon as TIconSearch } from 'tdesign-icons-vue-next';
+import { SearchIcon as TIconSearch, CloseCircleFilledIcon as TIconClear } from 'tdesign-icons-vue-next';
 import { ref, reactive, computed, defineComponent, toRefs } from 'vue';
 import config from '../config';
 import TButton from '../button';
@@ -62,7 +64,7 @@ type InputChangeContext = { e?: MouseEvent | InputEvent | undefined } | undefine
 
 export default defineComponent({
   name,
-  components: { TIconSearch, TButton, TInput },
+  components: { TIconSearch, TIconClear, TButton, TInput },
   props: searchProps,
   setup(props, { emit }) {
     const classes = computed(() => ({
@@ -70,6 +72,9 @@ export default defineComponent({
       [`${prefix}-is-focused`]: !state.labelActive,
     }));
     const [value] = useDefault(props, emit, 'value', 'change');
+    const shapeStyle = computed(() => ({
+      borderRadius: props.shape === 'square' ? undefined : '50px',
+    }));
     const searchInput = ref();
 
     const state = reactive({
@@ -96,6 +101,11 @@ export default defineComponent({
       props.onFocus?.(value as string, { e: context.e });
     };
 
+    const onClear = (e: MouseEvent) => {
+      searchInput.value.innerValue = '';
+      props.onClear?.({ e });
+    };
+
     const onCancel = (e: MouseEvent) => {
       state.labelActive = !state.labelActive;
       props.onActionClick?.({ e });
@@ -111,10 +121,12 @@ export default defineComponent({
       ...toRefs(props),
       name: ref(name),
       classes,
+      shapeStyle,
       onClick,
       onCancel,
       onBlur,
       onFocus,
+      onClear,
       onChange,
       state,
       value,
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/list/__test__/index.test.tsx b/src/list/__test__/index.test.tsx
new file mode 100644
index 00000000..92f641c6
--- /dev/null
+++ b/src/list/__test__/index.test.tsx
@@ -0,0 +1,115 @@
+import { ref, nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import { describe, it, expect, vi } from 'vitest';
+import List from '../list.vue';
+import TLoading from '@/loading/loading.vue';
+import TCell from '@/cell/index';
+
+const LOADING_TEXT_MAP = {
+  loading: '加载中...',
+  'load-more': '点击加载更多',
+};
+describe('list', () => {
+  describe('props', () => {
+    it(': asyncLoading', () => {
+      const wrapper = mount(List, {
+        props: {
+          // 加载中状态
+          asyncLoading: 'loading',
+        },
+      });
+      expect(wrapper.findComponent(TLoading).props().text).toEqual(LOADING_TEXT_MAP.loading);
+    });
+
+    it(': footer', () => {
+      const FOOTER_CONTENT = 'this is a footer content';
+      const wrapper = mount(List, {
+        props: {
+          footer: FOOTER_CONTENT,
+        },
+      });
+      expect(wrapper.text()).toEqual(FOOTER_CONTENT);
+    });
+
+    it(': header', () => {
+      const HEADER_CONTENT = 'this is a header content';
+      const wrapper = mount(List, {
+        props: {
+          header: HEADER_CONTENT,
+        },
+      });
+      expect(wrapper.text()).toEqual(HEADER_CONTENT);
+    });
+  });
+
+  describe('slots', () => {
+    it(': default', () => {
+      const LIST_COUNT = 10;
+      const MOCK_LIST = Array(LIST_COUNT).fill(1);
+      const wrapper = mount(List, {
+        slots: {
+          default: MOCK_LIST.map((item, index) => (
+            <TCell align="middle">
+              <span class="cell">{index}</span>
+            </TCell>
+          )),
+        },
+      });
+      expect(wrapper.findAllComponents(TCell).length).toEqual(LIST_COUNT);
+    });
+  });
+
+  describe('event', () => {
+    it(': load-more', () => {
+      const LIST_COUNT = 10;
+      const MOCK_LIST = Array(LIST_COUNT).fill(1);
+      const wrapper = mount(List, {
+        props: {
+          asyncLoading: 'load-more', // 点击加载更多, 用于 onLoadMore 点击事件, 当前演示页暂无演示代码
+          onLoadMore: () => {
+            MOCK_LIST.push(...Array(LIST_COUNT).fill(1));
+          },
+        },
+        slots: {
+          default: MOCK_LIST.map((item, index) => (
+            <TCell align="middle">
+              <span class="cell">{index}</span>
+            </TCell>
+          )),
+        },
+      });
+      expect(wrapper.findAllComponents(TCell).length).toEqual(LIST_COUNT);
+    });
+    it(': scroll', async (ctx) => {
+      beforeEach(() => {
+        vi.useFakeTimers();
+      });
+      afterEach(() => {
+        vi.restoreAllMocks();
+      });
+
+      const LIST_COUNT = 10;
+      const MOCK_LIST = ref(Array(LIST_COUNT).fill(1));
+      const wrapper = mount(List, {
+        props: {
+          asyncLoading: 'loading', // 点击加载更多, 用于 onLoadMore 点击事件, 当前演示页暂无演示代码
+          onScroll: () => {
+            MOCK_LIST.value.push(...Array(LIST_COUNT).fill(1));
+          },
+        },
+        slots: {
+          default: MOCK_LIST.value.map((item, index) => (
+            <TCell align="middle">
+              <span class="cell">{index}</span>
+            </TCell>
+          )),
+        },
+      });
+
+      wrapper.vm.$emit('scroll');
+      nextTick(() => {
+        expect(MOCK_LIST.value.length).toEqual(LIST_COUNT * 2);
+      });
+    });
+  });
+});
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/search/__test__/__snapshots__/demo.test.jsx.snap b/src/search/__test__/__snapshots__/demo.test.jsx.snap
index dc4c959a..70cd9cb8 100644
--- a/src/search/__test__/__snapshots__/demo.test.jsx.snap
+++ b/src/search/__test__/__snapshots__/demo.test.jsx.snap
@@ -63,6 +63,23 @@ exports[`Search > Search baseVue demo works fine 1`] = `
           </div>
           <!--v-if-->
         </div>
+        <div
+          class="t-search__icon-close"
+        >
+          <svg
+            class="t-icon t-icon-close-circle-filled"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M15 8A7 7 0 101 8a7 7 0 0014 0zM5.67 4.95L8 7.29l2.33-2.34.7.7L8.7 8l2.34 2.35-.71.7L8 8.71l-2.33 2.34-.7-.7L7.3 8 4.96 5.65l.71-.7z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
+        </div>
       </div>
       <label
         class="t-search__label"
@@ -164,6 +181,23 @@ exports[`Search > Search colorVue demo works fine 1`] = `
           </div>
           <!--v-if-->
         </div>
+        <div
+          class="t-search__icon-close"
+        >
+          <svg
+            class="t-icon t-icon-close-circle-filled"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M15 8A7 7 0 101 8a7 7 0 0014 0zM5.67 4.95L8 7.29l2.33-2.34.7.7L8.7 8l2.34 2.35-.71.7L8 8.71l-2.33 2.34-.7-.7L7.3 8 4.96 5.65l.71-.7z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
+        </div>
       </div>
       <label
         class="t-search__label"
@@ -290,6 +324,23 @@ exports[`Search > Search mobileVue demo works fine 1`] = `
             </div>
             <!--v-if-->
           </div>
+          <div
+            class="t-search__icon-close"
+          >
+            <svg
+              class="t-icon t-icon-close-circle-filled"
+              fill="none"
+              height="1em"
+              viewBox="0 0 16 16"
+              width="1em"
+            >
+              <path
+                d="M15 8A7 7 0 101 8a7 7 0 0014 0zM5.67 4.95L8 7.29l2.33-2.34.7.7L8.7 8l2.34 2.35-.71.7L8 8.71l-2.33 2.34-.7-.7L7.3 8 4.96 5.65l.71-.7z"
+                fill="currentColor"
+                fill-opacity="0.9"
+              />
+            </svg>
+          </div>
         </div>
         <label
           class="t-search__label"
@@ -401,6 +452,23 @@ exports[`Search > Search mobileVue demo works fine 1`] = `
             </div>
             <!--v-if-->
           </div>
+          <div
+            class="t-search__icon-close"
+          >
+            <svg
+              class="t-icon t-icon-close-circle-filled"
+              fill="none"
+              height="1em"
+              viewBox="0 0 16 16"
+              width="1em"
+            >
+              <path
+                d="M15 8A7 7 0 101 8a7 7 0 0014 0zM5.67 4.95L8 7.29l2.33-2.34.7.7L8.7 8l2.34 2.35-.71.7L8 8.71l-2.33 2.34-.7-.7L7.3 8 4.96 5.65l.71-.7z"
+                fill="currentColor"
+                fill-opacity="0.9"
+              />
+            </svg>
+          </div>
         </div>
         <label
           class="t-search__label"
@@ -448,7 +516,6 @@ exports[`Search > Search mobileVue demo works fine 1`] = `
     </p>
     
     <div
-      autofocus="true"
       class="t-search"
     >
       <div
@@ -509,6 +576,23 @@ exports[`Search > Search mobileVue demo works fine 1`] = `
             </div>
             <!--v-if-->
           </div>
+          <div
+            class="t-search__icon-close"
+          >
+            <svg
+              class="t-icon t-icon-close-circle-filled"
+              fill="none"
+              height="1em"
+              viewBox="0 0 16 16"
+              width="1em"
+            >
+              <path
+                d="M15 8A7 7 0 101 8a7 7 0 0014 0zM5.67 4.95L8 7.29l2.33-2.34.7.7L8.7 8l2.34 2.35-.71.7L8 8.71l-2.33 2.34-.7-.7L7.3 8 4.96 5.65l.71-.7z"
+                fill="currentColor"
+                fill-opacity="0.9"
+              />
+            </svg>
+          </div>
         </div>
         <label
           class="t-search__label"
@@ -556,7 +640,6 @@ exports[`Search > Search mobileVue demo works fine 1`] = `
     </p>
     
     <div
-      autofocus="true"
       class="t-search t-is-focused"
     >
       <div
@@ -609,23 +692,7 @@ exports[`Search > Search mobileVue demo works fine 1`] = `
                   placeholder="搜索预设文案"
                   type="search"
                 />
-                <div
-                  class="t-input__wrap--icon"
-                >
-                  <svg
-                    class="t-icon t-icon-close-circle-filled"
-                    fill="none"
-                    height="1em"
-                    viewBox="0 0 16 16"
-                    width="1em"
-                  >
-                    <path
-                      d="M15 8A7 7 0 101 8a7 7 0 0014 0zM5.67 4.95L8 7.29l2.33-2.34.7.7L8.7 8l2.34 2.35-.71.7L8 8.71l-2.33 2.34-.7-.7L7.3 8 4.96 5.65l.71-.7z"
-                      fill="currentColor"
-                      fill-opacity="0.9"
-                    />
-                  </svg>
-                </div>
+                <!--v-if-->
                 <!--v-if-->
               </div>
               <!--v-if-->
@@ -633,6 +700,23 @@ exports[`Search > Search mobileVue demo works fine 1`] = `
             </div>
             <!--v-if-->
           </div>
+          <div
+            class="t-search__icon-close"
+          >
+            <svg
+              class="t-icon t-icon-close-circle-filled"
+              fill="none"
+              height="1em"
+              viewBox="0 0 16 16"
+              width="1em"
+            >
+              <path
+                d="M15 8A7 7 0 101 8a7 7 0 0014 0zM5.67 4.95L8 7.29l2.33-2.34.7.7L8.7 8l2.34 2.35-.71.7L8 8.71l-2.33 2.34-.7-.7L7.3 8 4.96 5.65l.71-.7z"
+                fill="currentColor"
+                fill-opacity="0.9"
+              />
+            </svg>
+          </div>
         </div>
         <label
           class="t-search__label"
diff --git a/src/search/__test__/index.test.jsx b/src/search/__test__/index.test.jsx
new file mode 100644
index 00000000..1a595a41
--- /dev/null
+++ b/src/search/__test__/index.test.jsx
@@ -0,0 +1,176 @@
+import { ref } from 'vue';
+import { mount } from '@vue/test-utils';
+import { describe, it, expect, vi } from 'vitest';
+import Search from '../search.vue';
+import Input from '../../input/index';
+import Button from '../../button/index';
+import { CloseCircleFilledIcon as TIconClear } from 'tdesign-icons-vue-next';
+
+const prefix = 't'
+const name = `${prefix}-search`;
+
+describe('search', () => {
+  describe('props', () => {
+    it(': placeholder', async () => {
+      const placeholder = 'placeholder content';
+      const wrapper = mount(Search, {
+        props: {
+          placeholder,
+        }
+      })
+      const $input = wrapper.findComponent(Input).find(`input`);
+      expect($input.attributes('placeholder')).toEqual(placeholder);
+    });
+
+    it(': action', async () => {
+      const action = '提交';
+      const wrapper = mount(Search, {
+        props: {
+          action,
+        }
+      })
+      const $actionBtn = wrapper.findComponent(Button);
+      const $input = wrapper.findComponent(Input).find(`input`);
+
+      // focus = false, value = '', defaultValue = '' (默认，action 内容隐藏)
+      expect($actionBtn.attributes('style').includes(`display: none;`)).toBeTruthy();
+
+      // 模拟触发 input 的 focus , action 内容显示
+      await $input.trigger('focus');
+      expect($actionBtn.attributes('style').includes(`display: none;`)).toBeFalsy();
+      expect($actionBtn.text()).toEqual(action);
+    });
+
+    it(': defaultValue', async () => {
+      const placeholder = 'placeholder content';
+      const wrapper = mount(Search, {
+        props: {
+          placeholder,
+        }
+      })
+      const $search = wrapper.find(`.${name}`);
+      const $iconSearch = wrapper.find(`.${name}__label`);
+      await $iconSearch.trigger('click'); // 触发 click, 组件变成聚焦态
+      expect($search.classes().includes(`${prefix}-is-focused`)).toBeTruthy();
+    });
+
+    it(': shape', async () => {
+      const placeholder = 'placeholder content';
+      const wrapper = mount(Search, {
+        props: {
+          placeholder,
+          shape: ''
+        }
+      })
+      const $form = wrapper.find(`.${name}__form`);
+      const $label = wrapper.find(`.${name}__label`)
+      // shape = '', 则取 'round'
+      expect($form.attributes('style').includes(`border-radius: 50px;`)).toBeTruthy();
+      expect($label.attributes('style').includes(`border-radius: 50px;`)).toBeTruthy();
+
+      await wrapper.setProps({
+        shape: 'square'
+      });
+      // shape = 'square'
+      expect($form.attributes('style')).toEqual('');
+      expect($label.attributes('style')).toEqual('');
+    });
+
+    it(': onActionClick', async () => {
+      const value = ref('测试');
+      const action = '提交';
+      const onActionClick = vi.fn();
+      const wrapper = mount(Search, {
+        props: {
+          value: value.value,
+          action,
+          onActionClick
+        }
+      })
+      const $actionBtn = wrapper.findComponent(Button).find(`button`);
+      await $actionBtn.trigger('click')
+      expect(onActionClick).toBeCalled();
+    });
+
+    it(': onFocus', async () => {
+      const value = '聚焦测试';
+      let focusValue = '';
+      const onFocus = vi.fn((e) => {
+        focusValue = e;
+      });
+      const wrapper = mount(Search, {
+        props: {
+          value,
+          onFocus,
+        }
+      })
+      const $search = wrapper.find(`.${name}`);
+      const $input = wrapper.findComponent(Input).find(`input`);
+
+      await $input.trigger('focus');
+      expect(onFocus).toBeCalled();
+      expect(focusValue).toEqual(value);
+      expect($search.classes().includes(`${prefix}-is-focused`)).toBeTruthy();
+    });
+
+    it(': onClear && onChange', async () => {
+      const value = ref('清除测试');
+      const onClear = vi.fn();
+      const onChange = vi.fn((e) => {
+        value.value = e
+      });
+      const wrapper = mount(Search, {
+        props: {
+          value: value.value,
+          onClear,
+          onChange
+        }
+      })
+      const $search = wrapper.find(`.${name}`);
+      const closeIcon = wrapper.findComponent(TIconClear);
+      // clearable = true, 清除图标存在
+      expect(closeIcon.exists()).toBeTruthy();
+
+      await closeIcon.trigger('click'); // 清除, 同时也会触发 change
+      expect(onClear).toBeCalled();
+      expect(onChange).toBeCalled();
+      expect(value.value).toEqual('');
+    });
+
+    it(': onBlur', async () => {
+      const onFocus = vi.fn();
+      const onBlur = vi.fn();
+      const wrapper = mount(Search, {
+        props: {
+          onFocus,
+          onBlur,
+        }
+      })
+      const $search = wrapper.find(`.${name}`);
+      const $input = wrapper.findComponent(Input).find(`input`);
+      await $input.trigger('focus'); // 聚焦
+      expect(onFocus).toBeCalled();
+      await $input.trigger('blur'); // 失焦
+      expect(onBlur).toBeCalled();
+      expect($search.classes().includes(`${prefix}-is-focused`)).toBeFalsy();
+    });
+  });
+
+  describe('slots', () => {
+    it(': action', async () => {
+      const action = "插槽"
+      const wrapper = mount(Search, {
+        slots: {
+          action,
+        },
+      });
+      // TODO: 插槽实现的 dom 结构不正确，后期另提 pr 修复
+      const $search = wrapper.find(`.${name}`)
+      expect($search.text()).toEqual(action);
+    });
+  });
+
+  // describe('event', () => {
+  //   it(': event name', () => {});
+  // });
+})
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('文本');
   });
 });