Repository: Tencent/tdesign-mobile-vue
Pull request #321 (2022-08-29 03:30:48)

test(Message): message test
### 🤔 这个 PR 的性质是？

- [x] 测试用例

### 🔗 相关 Issue

https://github.com/Tencent/tdesign-mobile-vue/issues/271?target=loading

### 💡 需求背景和解决方案

message 增加测试用例

### 📝 更新日志

- feat(Message): 新增单元测试

- [x] 本条 PR 不需要纳入 Changelog

### ☑️ 请求合并前的自查清单

⚠️ 请自检并全部**勾选全部选项**。⚠️

- [x] 文档已补充或无须补充
- [x] 代码演示已提供或无须提供
- [x] TypeScript 定义已补充或无须补充
- [x] Changelog 已提供或无须提供


anlyyao: @zhangpaopao0609  function 覆盖率低于 80%～
zhangpaopao0609: > @zhangpaopao0609 function 覆盖率低于 80%～
已提升
github-actions[bot]: <a href="https://preview-pr321-tdesign-mobile-vue.surge.sh"><img height="96" alt="完成" src="https://user-images.githubusercontent.com/15634204/150816437-9f5bb788-cd67-4cbc-9897-b82d74e9aa65.png" /></a>
<!-- [工作流地址](https://github.com/Tencent/tdesign-mobile-vue/actions/runs/3050526929) -->
<!-- AUTO_PREVIEW_HOOK -->
LeeJim: 不要修改 site/web/test-coverage.js

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 317284e4..e78aff15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,27 @@ toc: false
 spline: explain
 ---
 
+## 🌈 0.10.6 `2022-09-20` 
+### 🚀 Features
+- `Drawer`: 新增单元测试 @anlyyao ([#311](https://github.com/Tencent/tdesign-mobile-vue/pull/311))
+- `Popup`: 新增单元测试 @anlyyao ([#310](https://github.com/Tencent/tdesign-mobile-vue/pull/310))
+- `Stepper`: 新增单元测试 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
+- `Overlay`: 新增单元测试 @HelKyle ([#336](https://github.com/Tencent/tdesign-mobile-vue/pull/336))
+- `BackTop`: 新增单元测试 @HelKyle ([#335](https://github.com/Tencent/tdesign-mobile-vue/pull/335))
+- `Toast`: 新增单元测试 @HelKyle ([#334](https://github.com/Tencent/tdesign-mobile-vue/pull/334))
+- `ActionSheet`: 新增单元测试 @HelKyle ([#319](https://github.com/Tencent/tdesign-mobile-vue/pull/319))
+- `Progress`: 新增单元测试 @HelKyle ([#337](https://github.com/Tencent/tdesign-mobile-vue/pull/337))
+- `ImageViewer`: 新增单元测试 @palmcivet ([#332](https://github.com/Tencent/tdesign-mobile-vue/pull/332))
+- `TabBar`: 新增单元测试 @HelKyle ([#314](https://github.com/Tencent/tdesign-mobile-vue/pull/314))
+- `Dialog`: 新增单元测试 @anlyyao ([#361](https://github.com/Tencent/tdesign-mobile-vue/pull/361))
+### 🐞 Bug Fixes
+- `Stepper`: 修复输入非 `number` 字符时，出现 `NaN` 问题 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
+- `Stepper`: 修复输入值能超出 `max` 的问题 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
+- `Stepper`: 修复 `value` 值小于 `min` 或超出 `max` 时，不触发 `overlimit` 的问题 @anlyyao ([#304](https://github.com/Tencent/tdesign-mobile-vue/pull/304))
+- `Collapse`: 修复 `headerRightContent` 插槽与文档描述不一致的问题 @isanxia ([#330](https://github.com/Tencent/tdesign-mobile-vue/pull/330))
+### 🚧 Others
+- `官网`: 更新浏览器兼容性说明文档 @anlyyao ([#349](https://github.com/Tencent/tdesign-mobile-vue/pull/349))
+
 ## 🌈 0.10.5 `2022-09-06` 
 ### 🚀 Features
 - `Popup`: 新增 `customStyle` 和 `overlayProps` 属性 @anlyyao ([#296](https://github.com/Tencent/tdesign-mobile-vue/pull/296))
diff --git a/package.json b/package.json
index de497356..23e00d3c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "tdesign-mobile-vue",
   "purename": "tdesign",
-  "version": "0.10.5",
+  "version": "0.10.6",
   "description": "tdesign-mobile-vue",
   "author": "TDesign Group",
   "homepage": "https://tdesign.tencent.com/mobile-vue/",
diff --git a/site/docs.config.js b/site/docs.config.js
index 6a136ffd..328c56c4 100644
--- a/site/docs.config.js
+++ b/site/docs.config.js
@@ -70,7 +70,7 @@ export default {
           path: '/mobile-vue/components/grid',
           component: () => import('@/grid/grid.md'),
         },
-      ]
+      ],
     },
     {
       title: '导航',
diff --git a/site/vite.config.js b/site/vite.config.js
index f45a56ef..7d611c45 100644
--- a/site/vite.config.js
+++ b/site/vite.config.js
@@ -79,7 +79,7 @@ export default ({ mode }) => {
         },
       }),
       vueJsx({
-        isCustomElement
+        isCustomElement,
       }),
       createTDesignPlugin(),
     ],
diff --git a/src/_common b/src/_common
index d9ad437c..51955cca 160000
--- a/src/_common
+++ b/src/_common
@@ -1,1 +1,1 @@
-Subproject commit d9ad437c0ded605ac3699f2fa19b98337a4edb4a
+Subproject commit 51955ccaf67c8498080ea9fdd628af4b0421b717
diff --git a/src/count-down/count-down.vue b/src/count-down/count-down.vue
index 1891565b..7bab8bb9 100644
--- a/src/count-down/count-down.vue
+++ b/src/count-down/count-down.vue
@@ -29,11 +29,9 @@ export default defineComponent({
     ...CountDownProps,
   },
   setup(props) {
-    const { content, size = 'small', theme = 'default', ...other } = props || {};
-    //
-    const { time, showTimes } = useCountDown(other);
+    const { time, showTimes } = useCountDown(props);
     const internalInstance = getCurrentInstance();
-    const hasChinese = /.*[\u4e00-\u9fa5]+.*$/?.test?.(other?.format);
+    const hasChinese = /.*[\u4e00-\u9fa5]+.*$/?.test?.(props?.format);
     const contentLayout = computed(() => renderTNode(internalInstance, 'content'));
     // return
     return {
diff --git a/src/shared/useCountDown/index.ts b/src/shared/useCountDown/index.ts
index 5aea3542..ef4d2472 100644
--- a/src/shared/useCountDown/index.ts
+++ b/src/shared/useCountDown/index.ts
@@ -8,7 +8,7 @@ export function useCountDown(props: TdUseCountDownProps): TdUseCountDown {
   // state
   const fps = ref();
   const count = ref(Number(time));
-  const showTimes = reactive(getShowTimes(getRemainTimes(time), format));
+  const showTimes = reactive(getShowTimes(getRemainTimes(time), format, !!millisecond));
 
   // raf
   const { pause, resume } = useRafFn(
diff --git a/src/shared/useCountDown/utils.ts b/src/shared/useCountDown/utils.ts
index 5361bf8a..a2084aef 100644
--- a/src/shared/useCountDown/utils.ts
+++ b/src/shared/useCountDown/utils.ts
@@ -30,7 +30,17 @@ export const getRemainTimes = (time?: number): TimeData => {
  * @param num
  * @returns
  */
-export const fillZero = (num: number): string | number => (num >= 10 ? num : `0${num}`);
+export const fillZero = (num: number, isMillieconds = false): string | number => {
+  if (isMillieconds) {
+    if (num >= 100) {
+      return num;
+    }
+
+    return num >= 10 ? `0${num}` : `00${num}`;
+  }
+
+  return num >= 10 ? num : `0${num}`;
+};
 
 /**
  * getMark
@@ -45,8 +55,9 @@ export const getMark = (format: string, type: string): string => format?.split?.
  * @param time
  * @returns
  */
-export const getShowTimes = (times: TimeData, format: string): TdUseCountDownShowTimes => {
+export const getShowTimes = (times: TimeData, format: string, milliseconds = false): TdUseCountDownShowTimes => {
   format = (format || 'DD:HH:mm:ss')?.toUpperCase?.();
+  milliseconds && !format.includes(':SSS') && (format = format.concat(':SSS'));
   const showTimes: TdUseCountDownShowTimes = [];
   if (format?.indexOf('DD') > -1) {
     showTimes?.push({
@@ -75,7 +86,7 @@ export const getShowTimes = (times: TimeData, format: string): TdUseCountDownSho
   if (format?.indexOf('SSS') > -1) {
     showTimes?.push({
       mark: getMark(format, 'SSS'),
-      value: fillZero(times?.milliseconds),
+      value: fillZero(times?.milliseconds, true),
     });
   }
 
diff --git a/src/tab-bar/tab-bar-item.vue b/src/tab-bar/tab-bar-item.vue
index 837bdc04..d95ab673 100644
--- a/src/tab-bar/tab-bar-item.vue
+++ b/src/tab-bar/tab-bar-item.vue
@@ -91,6 +91,7 @@ export default defineComponent({
         isSpread.value = !isSpread.value;
         if (!isToggleCurrent.value) {
           updateChild([currentName]);
+          return;
         }
       }
       updateChild(currentName);
diff --git a/src/tab-bar/tab-bar.vue b/src/tab-bar/tab-bar.vue
index e26b570c..01d900c3 100644
--- a/src/tab-bar/tab-bar.vue
+++ b/src/tab-bar/tab-bar.vue
@@ -5,7 +5,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, ref, provide, watch, Ref, computed, SetupContext } from 'vue';
+import { defineComponent, ref, provide, Ref, computed, SetupContext } from 'vue';
 import TabBarProps from './props';
 import config from '../config';
 import { useDefault } from '../shared';
diff --git a/src/upload/upload.vue b/src/upload/upload.vue
index 6d4dbf48..1210d666 100644
--- a/src/upload/upload.vue
+++ b/src/upload/upload.vue
@@ -154,9 +154,9 @@ export default defineComponent({
       uploadFiles([file]);
     };
 
-    const handleChange = (event: Event) => {
-      const input = <HTMLInputElement>inputRef.value;
-      if (props.disabled || !input.files) return;
+    const handleChange = () => {
+      const input = inputRef.value;
+      if (props.disabled || !input || !input.files) return;
       const formatFiles = formatFileToUploadFile(input.files);
       emitEvent('select-change', [...formatFiles]);
       uploadFiles(formatFiles);


Test patch:
diff --git a/site/web/test-coverage.js b/site/web/test-coverage.js
index d35a1fbb..ef34f280 100644
--- a/site/web/test-coverage.js
+++ b/site/web/test-coverage.js
@@ -1,1 +1,11 @@
-module.exports = {"button":{"statements":"98.57%","branches":"58.82%","functions":"100%","lines":"98.57%"},"checkbox":{"statements":"93.47%","branches":"75.8%","functions":"80%","lines":"93.47%"},"drawer":{"statements":"100%","branches":"93.75%","functions":"100%","lines":"100%"},"fab":{"statements":"100%","branches":"100%","functions":"100%","lines":"100%"},"overlay":{"statements":"92.55%","branches":"100%","functions":"20%","lines":"92.55%"},"popup":{"statements":"95%","branches":"64.7%","functions":"55.55%","lines":"95%"},"shared":{"statements":"70.58%","branches":"63.15%","functions":"25%","lines":"70.58%"},"switch":{"statements":"98.05%","branches":"92.85%","functions":"100%","lines":"98.05%"},"toast":{"statements":"100%","branches":"80%","functions":"100%","lines":"100%"}}
+module.exports = {
+  button: { statements: '98.57%', branches: '58.82%', functions: '100%', lines: '98.57%' },
+  checkbox: { statements: '93.47%', branches: '75.8%', functions: '80%', lines: '93.47%' },
+  drawer: { statements: '100%', branches: '93.75%', functions: '100%', lines: '100%' },
+  fab: { statements: '100%', branches: '100%', functions: '100%', lines: '100%' },
+  overlay: { statements: '92.55%', branches: '100%', functions: '20%', lines: '92.55%' },
+  popup: { statements: '95%', branches: '64.7%', functions: '55.55%', lines: '95%' },
+  shared: { statements: '70.58%', branches: '63.15%', functions: '25%', lines: '70.58%' },
+  switch: { statements: '98.05%', branches: '92.85%', functions: '100%', lines: '98.05%' },
+  toast: { statements: '100%', branches: '80%', functions: '100%', lines: '100%' },
+};
diff --git a/src/collapse/__test__/index.test.jsx b/src/collapse/__test__/index.test.jsx
new file mode 100644
index 00000000..bb792e58
--- /dev/null
+++ b/src/collapse/__test__/index.test.jsx
@@ -0,0 +1,225 @@
+import { ref, reactive } from 'vue';
+import { mount } from '@vue/test-utils';
+import { Collapse, CollapsePanel } from '../index';
+
+describe('Collapse', () => {
+  describe('collapse props', () => {
+    test('defaultExpandAll', async () => {
+      const defaultExpandAll = ref(false);
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse defaultExpandAll={defaultExpandAll}>
+              <CollapsePanel ref="1" value="1" header="标题1" content="内容1" />
+              <CollapsePanel ref="2" value="2" header="标题2" content="内容2" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel1 = wrapper.findComponent({ ref: '1' });
+      const panel2 = wrapper.findComponent({ ref: '2' });
+
+      expect(panel1.classes()).not.toContain('t-collapse-panel--active');
+      expect(panel2.classes()).not.toContain('t-collapse-panel--active');
+
+      defaultExpandAll.value = true;
+      await wrapper.vm.$nextTick();
+
+      expect(panel1.classes()).toContain('t-collapse-panel--active');
+      expect(panel2.classes()).toContain('t-collapse-panel--active');
+    });
+
+    test('default-value', async () => {
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse default-value={['1']}>
+              <CollapsePanel ref="1" value="1" header="标题1" content="内容1" />
+              <CollapsePanel ref="2" value="2" header="标题2" content="内容2" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel1 = wrapper.findComponent({ ref: '1' });
+      const panel2 = wrapper.findComponent({ ref: '2' });
+
+      expect(panel1.classes()).toContain('t-collapse-panel--active');
+      expect(panel2.classes()).not.toContain('t-collapse-panel--active');
+    });
+
+    test('value', async () => {
+      const val = ref([]);
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse v-model={val.value}>
+              <CollapsePanel ref="1" value="1" header="标题1" content="内容1" />
+              <CollapsePanel ref="2" value="2" header="标题2" content="内容2" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel1 = wrapper.findComponent({ ref: '1' });
+      const panel2 = wrapper.findComponent({ ref: '2' });
+      await panel1.find('.t-collapse-panel__header').trigger('click');
+
+      expect(val.value).toHaveLength(1);
+      expect(val.value).toContain('1');
+
+      await panel2.find('.t-collapse-panel__header').trigger('click');
+
+      expect(val.value).toHaveLength(2);
+      expect(val.value).toContain('2');
+    });
+
+    test('expandMutex', async () => {
+      const val = ref([]);
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse expandMutex v-model={val.value}>
+              <CollapsePanel ref="1" value="1" header="标题1" content="内容1" />
+              <CollapsePanel ref="2" value="2" header="标题2" content="内容2" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel1 = wrapper.findComponent({ ref: '1' });
+      const panel2 = wrapper.findComponent({ ref: '2' });
+      await panel1.find('.t-collapse-panel__header').trigger('click');
+
+      expect(val.value).toHaveLength(1);
+      expect(val.value).toContain('1');
+
+      await panel2.find('.t-collapse-panel__header').trigger('click');
+
+      expect(val.value).toHaveLength(1);
+      expect(val.value).toContain('2');
+    });
+
+    test('onChange', async () => {
+      const handleChange = vi.fn();
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse onChange={handleChange}>
+              <CollapsePanel ref="1" value="1" header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+      await panel.find('.t-collapse-panel__header').trigger('click')
+
+      expect(handleChange).toHaveBeenCalled();
+    });
+
+    test('disabled', async () => {
+      const disabled = ref(false);
+      const handleChange = vi.fn();
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse disabled={disabled.value} onChange={handleChange}>
+              <CollapsePanel ref="1" value="1" header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+      await panel.find('.t-collapse-panel__header').trigger('click');
+
+      expect(handleChange).toHaveBeenCalled();
+
+      disabled.value = true;
+      await wrapper.vm.$nextTick();
+      await panel.find('.t-collapse-panel__header').trigger('click');
+
+      expect(handleChange).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('collapse-panel props', () => {
+    test('header and content', async () => {
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse>
+              <CollapsePanel ref="1" value="1" header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+
+      expect(panel.find('.t-collapse-panel__title').text()).toBe('标题');
+      expect(panel.find('.t-collapse-panel__body').text()).toBe('内容');
+    });
+
+    test('disabled', async () => {
+      const handleChange = vi.fn();
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse onChange={handleChange}>
+              <CollapsePanel ref="1" value="1" disabled header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+
+      await panel.find('.t-collapse-panel__header').trigger('click')
+
+      expect(handleChange).not.toHaveBeenCalled();
+    });
+
+    test('headerRightContent', async () => {
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse>
+              <CollapsePanel ref="1" value="1" headerRightContent="操作" header="标题" content="内容" />
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+
+      expect(panel.find('.t-collapse-panel__header-right').text()).toBe('操作');
+    });
+
+    test('slot', async () => {
+      const wrapper = mount({
+        setup() {
+          return () => (
+            <Collapse>
+              <CollapsePanel ref="1" value="1">
+                {{
+                  header: () => <h4>标题</h4>,
+                  headerRightContent: () => <span>操作</span>,
+                  default: () => <div>内容</div>,
+                }}
+              </CollapsePanel>
+            </Collapse>
+          );
+        },
+      });
+
+      const panel = wrapper.findComponent({ ref: '1' });
+
+      expect(panel.find('.t-collapse-panel__title > h4').html()).toBe('<h4>标题</h4>');
+      expect(panel.find('.t-collapse-panel__header-extra > span').html()).toBe('<span>操作</span>');
+      expect(panel.find('.t-collapse-panel__body > div').html()).toBe('<div>内容</div>');
+    });
+  });
+});
diff --git a/src/count-down/__test__/index.test.jsx b/src/count-down/__test__/index.test.jsx
new file mode 100644
index 00000000..4be3bebc
--- /dev/null
+++ b/src/count-down/__test__/index.test.jsx
@@ -0,0 +1,103 @@
+import { nextTick } from 'vue';
+import { describe, it, expect, vi } from 'vitest';
+import { mount } from '@vue/test-utils';
+import CountDown from '../count-down.vue';
+
+const sleep = (duration) =>
+  new Promise((resolve) =>
+    setTimeout(() => {
+      resolve();
+    }, duration),
+  );
+
+describe('countdown.vue', async () => {
+  describe('props', async () => {
+    it('time', async () => {
+      const wrapper = mount(<CountDown time={2000}></CountDown>);
+      expect(wrapper.text()).toContain('00:00:02');
+      await sleep(1000);
+      expect(wrapper.text()).toContain('00:00:01');
+      await sleep(500);
+      expect(wrapper.text()).toContain('00:00:00');
+      await sleep(500);
+      await nextTick();
+      expect(wrapper.text()).toContain('default');
+    });
+
+    it('content', async () => {
+      const wrapper = mount(<CountDown time={1000} content="ok"></CountDown>);
+      await sleep(1500);
+      await nextTick();
+      expect(wrapper.text()).toContain('ok');
+
+      await wrapper.setProps({ content: 'change' });
+      expect(wrapper.text()).toContain('change');
+    });
+
+    it('format', async () => {
+      const wrapper = mount(<CountDown time={100000} format="mm:ss"></CountDown>);
+      expect(wrapper.text()).not.toContain('00:01:40');
+      expect(wrapper.text()).toContain('01:40');
+
+      const wrapper2 = mount(<CountDown time={100000} format="ss"></CountDown>);
+      expect(wrapper2.text()).not.toContain(':40');
+      expect(wrapper2.text()).toContain('40');
+
+      const wrapper3 = mount(<CountDown time={100000} format="中文"></CountDown>);
+      expect(wrapper3.find('.t-countdown---split-with-unit').exists()).toBe(false);
+
+      const wrapper4 = mount(<CountDown></CountDown>);
+      expect(wrapper4).toBeTruthy();
+    });
+
+    it('millisecond', async () => {
+      const wrapper = mount(<CountDown time={100000} millisecond={true}></CountDown>);
+      expect(wrapper.text()).toContain('00:01:40:000');
+    });
+
+    it('autoStart', async () => {
+      const wrapper = mount(<CountDown time={100000} autoStart={false}></CountDown>);
+      await sleep(1000);
+      expect(wrapper.text()).toContain('00:01:40');
+    });
+
+    it('size', async () => {
+      const wrapper = mount(<CountDown time={100000}></CountDown>);
+      expect(wrapper.find('.t-countdown--small').exists()).toBe(true);
+    });
+
+    it('theme', async () => {
+      const wrapper = mount(<CountDown time={100000}></CountDown>);
+      expect(wrapper.find('.t-countdown--default').exists()).toBe(true);
+    });
+  });
+
+  describe('events', async () => {
+    it('change', async () => {
+      const onChange = vi.fn();
+      const wrapper = mount(<CountDown time={2000} onChange={onChange}></CountDown>);
+      await sleep(1000);
+      expect(onChange).toHaveBeenCalled();
+    });
+
+    it('finish', async () => {
+      const onFinish = vi.fn();
+      const wrapper = mount(<CountDown time={1000} onFinish={onFinish}></CountDown>);
+      await sleep(1500);
+      expect(onFinish).toHaveBeenCalled();
+    });
+  });
+
+  describe('slots', async () => {
+    it('content', async () => {
+      const temp = <div>测试</div>;
+      const wrapper = mount(<CountDown time={1000}></CountDown>, {
+        slots: {
+          content: temp,
+        },
+      });
+      await sleep(1500);
+      expect(wrapper.text()).toContain('测试');
+    });
+  });
+});
diff --git a/src/dialog/__test__/index.test.jsx b/src/dialog/__test__/index.test.jsx
new file mode 100644
index 00000000..5e83cecb
--- /dev/null
+++ b/src/dialog/__test__/index.test.jsx
@@ -0,0 +1,183 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect, vi } from 'vitest';
+import { nextTick } from 'vue';
+import Dialog from '../dialog.vue';
+import Button from '../../button/button.vue';
+
+describe('dialog', () => {
+  describe('props', () => {
+    it(': visible	', async () => {
+      const visible = true;
+      const title = 'this is a title';
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          title,
+        },
+      });
+
+      expect(wrapper.find('.t-overlay').isVisible()).toBe(true)
+      expect(wrapper.find('.t-dialog__header').text()).toBe(title);
+      await wrapper.setProps({
+        visible: false,
+      })
+      expect(wrapper.find('.t-overlay').isVisible()).toBe(false)
+
+    })
+
+    it(': title', async () => {
+      const visible = true;
+      const title = 'old title';
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          title,
+        },
+      });
+      expect(wrapper.find('.t-dialog__header').text()).toBe(title);
+      const newTitle = 'new title'
+      await wrapper.setProps({
+        title: newTitle
+      })
+      expect(wrapper.find('.t-dialog__header').text()).toBe(newTitle);
+    })
+
+    it(': content', async () => {
+      const visible = true;
+      const title = 'this is title';
+      const content = 'content is string';
+
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          title,
+          content,
+        }
+      });
+
+      expect(wrapper.find('.t-dialog__body').text()).toBe(content);
+      const newContent = 'new content';
+      await wrapper.setProps({
+        content: newContent
+      })
+      expect(wrapper.find('.t-dialog__body').text()).toBe(newContent);
+    });
+
+    it(': buttonLayout', async () => {
+      const visible = true;
+      const selector = 't-dialog__vertical-footer';
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+        },
+      });
+      expect(wrapper.find('.t-dialog__footer').classes().includes(selector)).toBeFalsy();
+      await wrapper.setProps({
+        buttonLayout: 'vertical'
+      })
+      expect(wrapper.find('.t-dialog__footer').classes().includes(selector)).toBeTruthy();
+    })
+
+    it(': width', async () => {
+      const visible = true;
+      // width 为 string 类型
+      const width = '80%';
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          width,
+        },
+      });
+      expect(wrapper.find('.t-dialog').attributes('style').includes(`width: ${width}`)).toBeTruthy();
+      // width 为 number 类型
+      const newWidth = 320;
+      await wrapper.setProps({
+        width: newWidth
+      })
+      expect(wrapper.find('.t-dialog').attributes('style').includes(`width: ${newWidth}px`)).toBeTruthy();
+    })
+
+    it(': zIndex', async () => {
+      const visible = true;
+      const zIndex = 11500;
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          zIndex: zIndex,
+        },
+      });
+      // expect(wrapper).toMatchSnapshot()
+      expect(wrapper.find('.t-dialog').attributes('style').includes(`z-index: ${zIndex}`)).toBeTruthy();
+      const newZIndex = 12000;
+      await wrapper.setProps({
+        zIndex: newZIndex
+      })
+      expect(wrapper.find('.t-dialog').attributes('style').includes(`z-index: ${newZIndex}`)).toBeTruthy();
+    })
+
+    it(': closeOnOverlayClick', async () => {
+      let triggerOrigin = {};
+      const onClose = vi.fn((trigger) => {
+        triggerOrigin = trigger;
+      });
+      const onOverlayClick = vi.fn();
+      const closeOverlayClick = true;
+      const visible = true;
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          closeOverlayClick,
+          onClose,
+          onOverlayClick
+        },
+      });
+
+      const $overlay = wrapper.findComponent({ name: 't-overlay' })
+      expect($overlay.exists()).toBeTruthy();
+      $overlay.vm.$emit('click');
+      await nextTick();
+      expect(onClose).toBeCalledTimes(1)
+      expect(onOverlayClick).toBeCalledTimes(1)
+      expect(triggerOrigin).toBe('overlay')
+    })
+
+  });
+
+  describe('event', () => {
+    it(': cancel && confirm', async () => {
+      let triggerOrigin = {};
+      const visible = true;
+      const cancelBtn = 'cancel';
+      const confirmBtn = 'confirm';
+      const title = 'this is a title';
+      const onClose = vi.fn((trigger) => {
+        triggerOrigin = trigger;
+      });
+      const onCancel = vi.fn();
+      const onConfirm = vi.fn();
+      const wrapper = mount(Dialog, {
+        props: {
+          visible,
+          title,
+          cancelBtn,
+          confirmBtn,
+          onCancel,
+          onConfirm,
+          onClose
+        },
+      });
+      // expect(wrapper).toMatchSnapshot()
+      // footer
+      const $buttons = wrapper.findAllComponents(Button);
+      expect($buttons).toHaveLength(2);
+      expect($buttons.at(0).text()).toBe(cancelBtn);
+      expect($buttons.at(1).text()).toBe(confirmBtn);
+      $buttons.at(0).trigger('click');
+      expect(onClose).toBeCalledTimes(1)
+      expect(triggerOrigin).toBe('cancel')
+      expect(onCancel).toBeCalledTimes(1)
+      $buttons.at(1).trigger('click');
+      expect(onConfirm).toBeCalledTimes(1)
+    });
+  });
+})
diff --git a/src/loading/__test__/__snapshots__/index.test.jsx.snap b/src/loading/__test__/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..6bc891a2
--- /dev/null
+++ b/src/loading/__test__/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,114 @@
+// Vitest Snapshot v1
+
+exports[`Loading.vue > props > :pause 1`] = `
+<div
+  class="t-loading"
+  style=""
+>
+  
+  
+  <svg
+    class="t-loading__gradient t-icon-loading"
+    height="1em"
+    style="animation-play-state: paused; animation-duration: 800ms;"
+    version="1.1"
+    viewBox="0 0 14 14"
+    width="1em"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <foreignobject
+      height="12"
+      width="12"
+      x="1"
+      y="1"
+    >
+      <div
+        class="t-loading__gradient-conic"
+      />
+    </foreignobject>
+  </svg>
+  
+  <!--v-if-->
+  <!---->
+  
+</div>
+`;
+
+exports[`Loading.vue > props > :reverse 1`] = `
+<div
+  class="t-loading"
+  style=""
+>
+  
+  
+  <svg
+    class="t-loading__gradient t-icon-loading"
+    height="1em"
+    style="animation-direction: reverse; animation-duration: 800ms;"
+    version="1.1"
+    viewBox="0 0 14 14"
+    width="1em"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <foreignobject
+      height="12"
+      width="12"
+      x="1"
+      y="1"
+    >
+      <div
+        class="t-loading__gradient-conic"
+      />
+    </foreignobject>
+  </svg>
+  
+  <!--v-if-->
+  <!---->
+  
+</div>
+`;
+
+exports[`spinner.vue > render > :text 1`] = `
+<span
+  class="t-loading__spinner"
+>
+  
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-1"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-2"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-3"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-4"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-5"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-6"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-7"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-8"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-9"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-10"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-11"
+  />
+  <i
+    class="t-loading__spinner--line t-loading__spinner--line-12"
+  />
+  
+</span>
+`;
diff --git a/src/loading/__test__/index.test.jsx b/src/loading/__test__/index.test.jsx
new file mode 100644
index 00000000..c3550cd7
--- /dev/null
+++ b/src/loading/__test__/index.test.jsx
@@ -0,0 +1,98 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect } from 'vitest';
+import Loading from '../loading.vue';
+import Spinner from '../icon/spinner.vue';
+import { nextTick } from "vue";
+
+describe('Loading.vue', () => {
+  describe('props', () => {
+    beforeEach(() => {
+      vi.useFakeTimers()
+    })
+
+    afterEach(() => {
+      vi.restoreAllMocks()
+    })
+
+    it(':progress', () => {
+      const wrapper = mount(() => (
+          <Loading theme="bar" progress={0.5} />
+      ));
+
+      const shadow = wrapper.find('.t-loading__shadow');
+      expect(shadow.exists()).toBeTruthy();
+    });
+
+    it(':delay', async () => {
+      const wrapper = mount(() => (
+          <Loading delay={1000} />
+      ));
+      expect(wrapper.find('.t-icon-loading').exists()).not.toBeTruthy();
+      vi.runAllTimers();
+      await nextTick();
+      expect(wrapper.find('.t-icon-loading').exists()).toBeTruthy();
+    });
+
+    it(':error', () => {
+      const wrapper = mount(() => (
+          <Loading theme="error"/>
+      ));
+      const errorBlock = wrapper.find('.t-loading__text');
+      expect(errorBlock.element.innerHTML).toBe('加载失败');
+    });
+
+    it(':inheritColor', () => {
+      const wrapper = mount(Loading, {
+        props: {
+          inheritColor: true
+        },
+      });
+      expect(wrapper.vm.rootStyle).toBe('color: inherit')
+    });
+
+    it(':pause', () => {
+      const wrapper = mount(Loading, {
+        props: {
+          pause: true
+        },
+      });
+      expect(wrapper.element).toMatchSnapshot();
+      const iconWrapper = wrapper.find('.t-loading__gradient')
+      const iconWrapperStyle = iconWrapper.attributes().style;
+      expect(iconWrapperStyle).toContain('animation-play-state: paused')
+    });
+
+    it(':reverse', () => {
+      const wrapper = mount(() => (
+          <Loading reverse={true}/>
+      ));
+      expect(wrapper.element).toMatchSnapshot();
+      const iconWrapper = wrapper.find('.t-loading__gradient')
+      const iconWrapperStyle = iconWrapper.attributes().style;
+      expect(iconWrapperStyle).toContain('animation-direction: reverse')
+    });
+  })
+
+  describe('slots', () => {
+    it(':text', () => {
+      const customLoading = 'custom-loading-text'
+      const wrapper = mount(() => (
+          <Loading>{{ text: () => <p className={customLoading}>{customLoading}</p> }}</Loading>
+      ));
+      const text = wrapper.find('.t-loading__text');
+      expect(text.exists()).toBeTruthy();
+      expect(text.element.innerHTML).toBe(`<p class="${customLoading}">${customLoading}</p>`);
+    });
+  })
+})
+
+describe('spinner.vue', () => {
+  describe('render', () => {
+    it(':text', () => {
+      const wrapper = mount(() => (
+          <Spinner/>
+      ));
+      expect(wrapper.element).toMatchSnapshot();
+    });
+  })
+})
diff --git a/src/message/__test__/index.test.jsx b/src/message/__test__/index.test.jsx
new file mode 100644
index 00000000..9f8c35be
--- /dev/null
+++ b/src/message/__test__/index.test.jsx
@@ -0,0 +1,54 @@
+import { mount } from '@vue/test-utils';
+import { describe, it, expect } from 'vitest';
+import { ref, nextTick } from "vue";
+import Message from '../message.vue';
+
+describe('Message.vue', () => {
+  describe('event', () => {
+    it('open', async () => {
+      const fn = vi.fn();
+      const visible = ref(false);
+      const wrapper = mount(() => (
+        <Message v-model={visible.value} content="这是一条消息通知" onOpen={fn}/>
+      ));
+      visible.value = true;
+      await nextTick();
+      expect(fn).toBeCalledTimes(1);
+      visible.value = false;
+      await nextTick();
+      expect(fn).toBeCalledTimes(1);
+    });
+
+    it('close', () => {
+      const fn = vi.fn();
+      const wrapper = mount(() => (
+        <Message visible={true} content="这是一条带关闭的消息通知，常驻可关闭" closeBtn={true} onClose={fn}/>
+      ));
+      const closeBtn = wrapper.find('.t-icon-close');
+      closeBtn.trigger('click');
+      expect(fn).toBeCalledTimes(1);
+    });
+
+    it('after-enter-leave', async () => {
+      const fn = vi.fn();
+      const wrapper = mount(() => (
+        <Message visible={true} onOpened={fn} onClosed={fn}/>
+      ));
+      expect(fn).not.toBeCalled();
+      wrapper.findComponent(Message).vm.afterEnter();
+      expect(fn).toBeCalledTimes(1);
+      wrapper.findComponent(Message).vm.afterLeave();
+      expect(fn).toBeCalledTimes(2);
+    });
+  })
+
+  describe('slots', () => {
+    it('icon:success', () => {
+      const wrapper = mount(() => (
+        <Message visible={true} content="这是一条消息通知" theme={'success'}/>
+      ));
+      const successIcon = wrapper.find('.t-icon-check-circle-filled');
+      expect(successIcon).toBeTruthy();
+    });
+  })
+});
\ No newline at end of file
diff --git a/src/tab-bar/__test__/__snapshots__/index.test.jsx.snap b/src/tab-bar/__test__/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..63a6bfcd
--- /dev/null
+++ b/src/tab-bar/__test__/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,152 @@
+// Vitest Snapshot v1
+
+exports[`TabBar > props > snapshot 1`] = `
+<div
+  class="t-tab-bar t-tab-bar--bordered t-tab-bar--fixed"
+  role="tablist"
+>
+  
+  
+  <div
+    aria-label="TabBar"
+    aria-selected="false"
+    class="t-tab-bar-item t-no-border"
+    name="label_1"
+    role="tab"
+    text="标签一"
+  >
+    <div
+      class="t-tab-bar-item__content"
+    >
+      <div
+        class="t-tab-bar-item__icon"
+      >
+        <div
+          class="t-badge"
+        >
+          <div
+            class="t-badge__inner t-badge--has-children t-badge--medium t-badge--circle"
+            style="right: 0px; top: 5px;"
+          >
+            1
+          </div>
+          
+          <svg
+            class="t-icon t-icon-app"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M11.25 1.75a3 3 0 100 6 3 3 0 000-6zm-2 3a2 2 0 114 0 2 2 0 01-4 0zM2 3a1 1 0 011-1h3.5a1 1 0 011 1v3.5a1 1 0 01-1 1H3a1 1 0 01-1-1V3zm1 0v3.5h3.5V3H3zM2 9.5a1 1 0 011-1h3.5a1 1 0 011 1V13a1 1 0 01-1 1H3a1 1 0 01-1-1V9.5zm1 0V13h3.5V9.5H3zM8.5 9.5a1 1 0 011-1H13a1 1 0 011 1V13a1 1 0 01-1 1H9.5a1 1 0 01-1-1V9.5zm1 3.5H13V9.5H9.5V13z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
+          
+        </div>
+      </div>
+      <!--v-if-->
+    </div>
+    <transition-stub>
+      <!--v-if-->
+    </transition-stub>
+  </div>
+  <div
+    aria-label="TabBar"
+    aria-selected="false"
+    class="t-tab-bar-item t-no-border"
+    name="label_2"
+    role="tab"
+    text="标签二"
+  >
+    <div
+      class="t-tab-bar-item__content"
+    >
+      <div
+        class="t-tab-bar-item__icon"
+      >
+        <div
+          class="t-badge"
+        >
+          <div
+            class="t-badge__inner t-badge--has-children t-badge--medium t-badge--circle"
+            style="right: 0px; top: 5px;"
+          >
+            1
+          </div>
+          
+          <svg
+            class="t-icon t-icon-app"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M11.25 1.75a3 3 0 100 6 3 3 0 000-6zm-2 3a2 2 0 114 0 2 2 0 01-4 0zM2 3a1 1 0 011-1h3.5a1 1 0 011 1v3.5a1 1 0 01-1 1H3a1 1 0 01-1-1V3zm1 0v3.5h3.5V3H3zM2 9.5a1 1 0 011-1h3.5a1 1 0 011 1V13a1 1 0 01-1 1H3a1 1 0 01-1-1V9.5zm1 0V13h3.5V9.5H3zM8.5 9.5a1 1 0 011-1H13a1 1 0 011 1V13a1 1 0 01-1 1H9.5a1 1 0 01-1-1V9.5zm1 3.5H13V9.5H9.5V13z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
+          
+        </div>
+      </div>
+      <!--v-if-->
+    </div>
+    <transition-stub>
+      <!--v-if-->
+    </transition-stub>
+  </div>
+  <div
+    aria-haspopup="true"
+    aria-label="TabBar"
+    aria-selected="false"
+    class="t-tab-bar-item t-no-border"
+    name="label_3"
+    role="tab"
+    text="此处展开"
+  >
+    <div
+      class="t-tab-bar-item__content"
+    >
+      <div
+        class="t-tab-bar-item__icon"
+      >
+        <div
+          class="t-badge"
+        >
+          <div
+            class="t-badge__inner t-badge--has-children t-badge--medium t-badge--circle"
+            style="right: 0px; top: 5px;"
+          >
+            1
+          </div>
+          
+          <svg
+            class="t-icon t-icon-app"
+            fill="none"
+            height="1em"
+            viewBox="0 0 16 16"
+            width="1em"
+          >
+            <path
+              d="M11.25 1.75a3 3 0 100 6 3 3 0 000-6zm-2 3a2 2 0 114 0 2 2 0 01-4 0zM2 3a1 1 0 011-1h3.5a1 1 0 011 1v3.5a1 1 0 01-1 1H3a1 1 0 01-1-1V3zm1 0v3.5h3.5V3H3zM2 9.5a1 1 0 011-1h3.5a1 1 0 011 1V13a1 1 0 01-1 1H3a1 1 0 01-1-1V9.5zm1 0V13h3.5V9.5H3zM8.5 9.5a1 1 0 011-1H13a1 1 0 011 1V13a1 1 0 01-1 1H9.5a1 1 0 01-1-1V9.5zm1 3.5H13V9.5H9.5V13z"
+              fill="currentColor"
+              fill-opacity="0.9"
+            />
+          </svg>
+          
+        </div>
+      </div>
+      <!--v-if-->
+    </div>
+    <transition-stub>
+      <!--v-if-->
+    </transition-stub>
+  </div>
+  
+  
+</div>
+`;
diff --git a/src/tab-bar/__test__/index.test.jsx b/src/tab-bar/__test__/index.test.jsx
new file mode 100644
index 00000000..ed5468cb
--- /dev/null
+++ b/src/tab-bar/__test__/index.test.jsx
@@ -0,0 +1,149 @@
+import { describe, it, expect, vi } from 'vitest';
+import { mount } from '@vue/test-utils';
+import TabBar from '../tab-bar.vue'
+import TabBarItem from '../tab-bar-item.vue'
+import { nextTick, ref } from 'vue';
+import { AppIcon as TIconApp } from 'tdesign-icons-vue-next';
+
+const list = [
+  {
+    name: 'label_1',
+    text: '标签一',
+    icon: undefined,
+    value: '1'
+  },
+  {
+    name: 'label_2',
+    text: '标签二',
+    icon: undefined,
+    value: '2'
+  },
+  {
+    name: 'label_3',
+    text: '此处展开',
+    icon: undefined,
+    value: '3',
+    subTabBar: [
+      {
+        name: 'spread_1',
+        text: '展开项一',
+        icon: undefined,
+        value: '3-1',
+      },
+      {
+        name: 'spread_2',
+        text: '展开项二',
+        icon: undefined,
+        value: '3-2',
+      },
+      {
+        name: 'spread_3',
+        text: '展开项三',
+        icon: undefined,
+        value: '3-3',
+      },
+    ],
+  },
+];
+
+describe('TabBar', () => {
+  describe('props', () => {
+    it('bordered', async () => {
+      const wrapper = mount(TabBar, {
+        shallow: true
+      })
+      expect(wrapper.classes())
+        .toContain('t-tab-bar--bordered')
+
+      await wrapper.setProps({ 'bordered': false})
+      expect(wrapper.classes())
+        .not.toContain('t-tab-bar--bordered')
+    })
+
+    it('fixed', async () => {
+      const wrapper = mount(TabBar, {
+        shallow: true
+      })
+      expect(wrapper.classes())
+        .toContain('t-tab-bar--fixed')
+
+      await wrapper.setProps({ 'fixed': false})
+      expect(wrapper.classes())
+        .not.toContain('t-tab-bar--fixed')
+    })
+
+    it('defaultValue', async () => {
+      const wrapper = mount({
+        render: () => (
+          <TabBar defaultValue={"2"}>
+            {
+              list.map((item) => {
+                return <TabBarItem {...item}>{item.text}</TabBarItem>
+              })
+            }
+          </TabBar>
+        )
+      })
+
+      expect(wrapper.findAll('.t-tab-bar-item')).toHaveLength(list.length)
+      expect(wrapper.find('[aria-selected="true"]').attributes('text')).toEqual('标签二')
+    })
+
+    it ('snapshot', () => {
+      const icon = () => <TIconApp />
+      const wrapper = mount({
+        render: () => (
+          <TabBar>
+            {
+              list.map((item) => {
+                return <TabBarItem {...item} icon={icon} badgeProps={{count: 1}} />
+              })
+            }
+          </TabBar>
+        )
+      })
+      expect(wrapper.element).toMatchSnapshot();
+    })
+  })
+
+  describe('events', () => {
+    it ('click to change', async () => {
+      const onChange = vi.fn()
+      const value = ref('1')
+      const wrapper = mount({
+        render: () => (
+          <TabBar
+            v-model={value.value}
+            onChange={onChange}
+          >
+            {
+              list.map((item) => {
+                return <TabBarItem {...item} />
+              })
+            }
+          </TabBar>
+        )
+      })
+
+      // 从 bar1 切到 bar2
+      const bar2 = wrapper.find('[name="label_2"] > .t-tab-bar-item__content')
+      expect(onChange).toHaveBeenCalledTimes(0)
+      await bar2.trigger('click')
+      expect(onChange).toHaveBeenCalledTimes(1)
+      expect(onChange).toHaveBeenLastCalledWith('2')
+
+      // 从 bar2 切到 bar3
+      const bar3 = wrapper.find('[name="label_3"] > .t-tab-bar-item__content')
+      await bar3.trigger('click')
+      expect(onChange).toHaveBeenCalledTimes(2)
+      expect(onChange).toHaveBeenLastCalledWith(['3'])
+
+      // 点击 spread
+      const bar3_1 = wrapper.find('[name="label_3"] .t-tab-bar-item__spread-item')
+      await bar3_1.trigger('click')
+      expect(onChange).toHaveBeenCalledTimes(3)
+      expect(onChange).toHaveBeenLastCalledWith(['3', '3-1'])
+      expect(wrapper.find('[name="label_3"]').find('.t-tab-bar-item__spread-item').exists()).toBe(false)
+    })
+  })
+})