Repository: Tencent/tdesign-mobile-vue (ts)

Issue:
【DateTimePicker】测试覆盖率提升
> 目前正在推进 mobile-vue 的组件测试覆盖率提升。

## 任务要求
- **行覆盖率达 80%+**
- **分支覆盖率 80%+**
- **语句覆盖率 80%+**
- 有交互的组件，需要覆盖所有场景，**要求函数覆盖率 80+%**

## 单元测试指引
 [mobile-vue 的单元测试指引文档](https://github.com/Tencent/tdesign-mobile-vue/wiki/mobile-vue%EF%BC%9A%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E6%8C%87%E5%BC%95)

> **目前每个组件都拥有自己的测试用例文件，我们可以先通过`npm run test:unit-coverage`生成测试覆盖率报告，查看该组件的单元测试覆盖率情况，然后针对性的补充测试用例**

#### Tips
- Vitest 单元测试框架：[文档](https://cn.vitest.dev/guide/)
- 若开发工具为 vscode ，可以安装 Vitest 插件

## 激励

参与共建的开发者，将可以获得 [TDesign 周边](https://github.com/Tencent/tdesign/wiki/TDesign-%E5%91%A8%E8%BE%B9)（贴纸、鼠标垫、工卡套等）；

成为核心贡献者，将会拥有 TDesign 专属头像。

## IssueShoot
- 预估时长: 3
- 期望完成时间: 2023-06-02 00:00:00 +0000 UTC
- 开发难度: 中
- 参与人数: 1
- 验收标准: 实现期望改造效果，提 PR 并通过验收无误
- 备注: 最终激励以实际提交 `pull request` 并合并为准

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


Patch:
diff --git a/src/date-time-picker/date-time-picker.vue b/src/date-time-picker/date-time-picker.vue
index e8f2bb45..42959239 100644
--- a/src/date-time-picker/date-time-picker.vue
+++ b/src/date-time-picker/date-time-picker.vue
@@ -1,12 +1,11 @@
 <template>
   <t-picker
     :class="className"
-    :value="pickerValue"
+    :value="valueOfPicker"
     :title="title"
     :confirm-btn="confirmButtonText"
     :cancel-btn="cancelButtonText"
     :columns="columns"
-    @change="onChange"
     @confirm="onConfirm"
     @cancel="onCancel"
     @pick="onPick"
@@ -52,71 +51,20 @@ export default defineComponent({
     const emitEvent = useEmitEvent(props, context.emit);
     const className = computed(() => [`${name}`]);
     const { value, modelValue } = toRefs(props);
-    const [dateTimePickerValue, setDateTimePickerValue] = useVModel(
-      value,
-      modelValue,
-      props.defaultValue,
-      props.onChange,
-    );
-    const innerValue = ref(dateTimePickerValue.value);
+    const [innerValue, setDateTimePickerValue] = useVModel(value, modelValue, props.defaultValue, props.onChange);
     const title = computed(() => {
       return props.title || '选择时间';
     });
     const confirmButtonText = computed(() => props.confirmBtn || '确定');
     const cancelButtonText = computed(() => props.cancelBtn || '取消');
-
-    const start = computed(() => {
-      return props.start && dayjs(props.start).isValid() ? dayjs(props.start) : dayjs().subtract(10, 'year');
-    });
-
-    const end = computed(() => {
-      return props.end && dayjs(props.end).isValid() ? dayjs(props.end) : dayjs().add(10, 'year');
-    });
+    const normalize = (val: string | number, defaultDay: Dayjs) =>
+      val && dayjs(val).isValid() ? dayjs(val) : defaultDay;
+    const start = computed(() => normalize(props.start, dayjs().subtract(10, 'year')));
+    const end = computed(() => normalize(props.end, dayjs().add(10, 'year')));
 
     const meaningColumn = computed(() => getMeaningColumn(props.mode));
     const isTimeMode = computed(
-      () => ['hour', 'minute', 'second'].includes(props.mode) || (isArray(props.mode) && props.mode[0] == null),
-    );
-
-    // 根据mode，判断是否需要渲染'year','month','date','hour','minute','second'对应的列
-    const isPrecision = (type: string) => {
-      if (!props.mode) {
-        return false;
-      }
-
-      const arrayIndex = ['year', 'month', 'date'].includes(type) ? 0 : 1;
-
-      if (type in precisionRankRecord) {
-        return (
-          (isString(props.mode) && precisionRankRecord[props.mode] >= precisionRankRecord[type]) ||
-          (isArray(props.mode) && precisionRankRecord[props.mode[arrayIndex]] >= precisionRankRecord[type])
-        );
-      }
-
-      return true;
-    };
-
-    // 将dateTimeValue格式的值转为pickerValue，赋值给picker组件的value
-    const getPickerValueByDateTimePickerValue = (currentDate: Dayjs) => {
-      const ret: PickerValue[] = [];
-      Object.keys(precisionRankRecord).forEach((item) => {
-        if (isPrecision(item)) {
-          ret.push(`${currentDate[item]()}`);
-        }
-      });
-      return ret;
-    };
-
-    const curDate = ref(
-      (() => {
-        let currentValue = innerValue.value;
-        if (isTimeMode.value) {
-          const dateStr = dayjs(start.value).format('YYYY-MM-DD');
-          currentValue = `${dateStr} ${currentValue}`;
-        }
-
-        return currentValue && dayjs(currentValue).isValid() ? dayjs(currentValue) : start.value;
-      })(),
+      () => isArray(props.mode) && props.mode[0] == null && ['hour', 'minute', 'second'].includes(props.mode[1]),
     );
 
     const rationalize = (val: Dayjs) => {
@@ -126,12 +74,29 @@ export default defineComponent({
       return val;
     };
 
-    const pickerValue = computed(() => getPickerValueByDateTimePickerValue(curDate.value));
+    const calcDate = (currentValue: string | number) => {
+      if (isTimeMode.value) {
+        const dateStr = dayjs(start.value).format('YYYY-MM-DD');
+        currentValue = `${dateStr} ${currentValue}`;
+      }
+
+      return currentValue && dayjs(currentValue).isValid() ? rationalize(dayjs(currentValue)) : start.value;
+    };
+    const curDate = ref(calcDate(innerValue.value));
+
+    const valueOfPicker = computed(() => meaningColumn.value.map((item) => curDate.value[item]().toString()));
 
     // 每次pick后，根据start,end生成最新的columns
     const columns = computed(() => {
       const ret: PickerColumn[] = [];
-      const getDate = (date: Dayjs) => [date.year(), date.month() + 1, date.date(), date.minute(), date.second()];
+      const getDate = (date: Dayjs) => [
+        date.year(),
+        date.month() + 1,
+        date.date(),
+        date.hour(),
+        date.minute(),
+        date.second(),
+      ];
       const [curYear, curMonth, curDay, curHour, curMinute] = getDate(curDate.value);
       const [minYear, minMonth, minDay, minHour, minMinute, minSecond] = getDate(start.value);
       const [maxYear, maxMonth, maxDay, maxHour, maxMinute, maxSecond] = getDate(end.value);
@@ -168,35 +133,35 @@ export default defineComponent({
         ret.push(arr);
       };
 
-      if (isPrecision('year')) {
+      if (meaningColumn.value.includes('year')) {
         generateColumn(minYear, maxYear, 'year');
       }
 
-      if (isPrecision('month')) {
+      if (meaningColumn.value.includes('month')) {
         const lower = isInMinYear ? minMonth : 1;
         const upper = isInMaxYear ? maxMonth : 12;
         generateColumn(lower, upper, 'month');
       }
 
-      if (isPrecision('date')) {
+      if (meaningColumn.value.includes('date')) {
         const lower = isInMinMonth ? minDay : 1;
         const upper = isInMaxMonth ? maxDay : dayjs(`${curYear}-${curMonth}`).daysInMonth();
         generateColumn(lower, upper, 'date');
       }
 
-      if (isPrecision('hour')) {
+      if (meaningColumn.value.includes('hour')) {
         const lower = isInMinDay && !isTimeMode.value ? minHour : 0;
         const upper = isInMaxDay && !isTimeMode.value ? maxHour : 23;
         generateColumn(lower, upper, 'hour');
       }
 
-      if (isPrecision('minute')) {
+      if (meaningColumn.value.includes('minute')) {
         const lower = isInMinHour && !isTimeMode.value ? minMinute : 0;
         const upper = isInMaxHour && !isTimeMode.value ? maxMinute : 59;
         generateColumn(lower, upper, 'minute');
       }
 
-      if (isPrecision('second')) {
+      if (meaningColumn.value.includes('second')) {
         const lower = isInMinMinute && !isTimeMode.value ? minSecond : 0;
         const upper = isInMaxMinute && !isTimeMode.value ? maxSecond : 59;
         generateColumn(lower, upper, 'second');
@@ -204,27 +169,28 @@ export default defineComponent({
       return ret;
     });
 
-    const onConfirm = (value: Array<PickerValue>, context: { index: number[] }) => {
+    const onConfirm = () => {
       emitEvent('confirm', dayjs(curDate.value).format(props.format));
+      setDateTimePickerValue(dayjs(curDate.value).format(props.format));
     };
 
     const onCancel = (context: { e: MouseEvent }) => {
       emitEvent('cancel', { e: context.e });
     };
 
-    const onChange = (value: Array<PickerValue>, context: { columns: Array<PickerContext>; e: MouseEvent }) => {
-      emitEvent('change', dayjs(curDate.value).format(props.format));
-    };
-
     const onPick = (value: Array<PickerValue>, context: PickerContext) => {
       const { column, index } = context;
       const type = meaningColumn.value[column];
       const val = curDate.value.set(type as UnitType, parseInt(columns.value[column][index]?.value, 10));
 
       curDate.value = rationalize(val);
-      emitEvent('pick', curDate.value.format(props.format));
+      emitEvent('pick', rationalize(val).format(props.format));
     };
 
+    watch(innerValue, (val) => {
+      curDate.value = calcDate(val);
+    });
+
     return {
       className,
       confirmButtonText,
@@ -232,12 +198,11 @@ export default defineComponent({
       title,
       start,
       end,
-      pickerValue,
+      valueOfPicker,
       columns,
       onConfirm,
       onCancel,
       onPick,
-      onChange,
     };
   },
 });
diff --git a/src/date-time-picker/demos/base.vue b/src/date-time-picker/demos/base.vue
index ce6f2389..a3983938 100644
--- a/src/date-time-picker/demos/base.vue
+++ b/src/date-time-picker/demos/base.vue
@@ -16,7 +16,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, watch } from 'vue';
 
 const visible = ref(false);
 const pickerValue = ref('2021-12-23');
diff --git a/src/date-time-picker/shared.ts b/src/date-time-picker/shared.ts
index 421199af..e0d8b96f 100644
--- a/src/date-time-picker/shared.ts
+++ b/src/date-time-picker/shared.ts
@@ -1,13 +1,8 @@
-import isString from 'lodash/isString';
 import isArray from 'lodash/isArray';
 
 export const getMeaningColumn = (mode: string | string[]) => {
   const arr = ['year', 'month', 'date', 'hour', 'minute', 'second'];
 
-  if (isString(mode)) {
-    const cutIndex = arr.indexOf(mode);
-    return arr.slice(0, cutIndex + 1);
-  }
   if (isArray(mode)) {
     const ans = [];
     const [dateMode, timeMode] = mode;
@@ -25,5 +20,8 @@ export const getMeaningColumn = (mode: string | string[]) => {
     }
     return ans;
   }
-  return [];
+
+  const cutIndex = arr.indexOf(mode);
+
+  return arr.slice(0, cutIndex + 1);
 };
diff --git a/src/picker/picker-item.vue b/src/picker/picker-item.vue
index 9087b649..053babe8 100644
--- a/src/picker/picker-item.vue
+++ b/src/picker/picker-item.vue
@@ -82,8 +82,6 @@ export default defineComponent({
         el: root.value,
         defaultIndex: getIndexByValue(props.value) || 0,
         onChange: (index: number) => {
-          console.log('onchange');
-
           const curItem = props.options[index];
           const changeValue = { value: curItem.value, index };
           emitEvent('pick', changeValue);


Test patch:
diff --git a/src/date-time-picker/__test__/index.test.jsx b/src/date-time-picker/__test__/index.test.jsx
index 9f8a461c..b5b883a8 100644
--- a/src/date-time-picker/__test__/index.test.jsx
+++ b/src/date-time-picker/__test__/index.test.jsx
@@ -1,6 +1,8 @@
 import { nextTick, ref } from 'vue';
 import { mount } from '@vue/test-utils';
 import { describe, it, expect, vi } from 'vitest';
+import dayjs from 'dayjs';
+
 import DateTimePicker from '../date-time-picker.vue';
 import PickerItem from '../../picker/picker-item.vue';
 import { DEFAULT_ITEM_HEIGHT, ANIMATION_TIME_LIMIT } from '../../picker/picker.class';
@@ -106,13 +108,13 @@ describe('DateTimePicker', () => {
     });
 
     it(': mode', async () => {
-      const defaultValue = defaultDateTime;
-      const modeArray = ['date', 'second'];
+      const onPick = vi.mock();
       const wrapper = mount(DateTimePicker, {
         props: {
-          defaultValue,
+          defaultValue: defaultDateTime,
           title: 'mode 测试',
-          mode: modeArray,
+          mode: ['date', 'second'],
+          onPick
         },
       });
       // mode = ['date', 'second'], 渲染年月日时分秒，6列
@@ -120,26 +122,38 @@ describe('DateTimePicker', () => {
       expect(pickerItem.exists()).toBeTruthy();
       const $pickerItems = wrapper.findAllComponents(PickerItem);
       expect($pickerItems).toHaveLength(6);
-      await wrapper.setProps({
-        mode: false,
-      });
-      // mode = false, 不渲染列
-      expect(pickerItem.exists()).toBeFalsy();
+
+      // pick
+      const el = wrapper.findAll(`.${prefix}-picker-item`);
+      const touchItemIndex = 0;
+      const touchIndex = 2;
+      simulateMoveOption(el[touchItemIndex].element, touchIndex);
     });
 
-    it(': start && end ', async () => {
-      const defaultValue = defaultDateTime;
-      const start = '2020-6-30';
-      const end = '2025-6-30';
+    it(': time mode', async () => {
+      const wrapper = mount(DateTimePicker, {
+        props: {
+          value: '10:00:00',
+          mode: [null, 'second'],
+          format: 'HH:mm:ss',
+          start: '2023-06-13'
+        },
+      });
+      expect(wrapper.vm.valueOfPicker).toStrictEqual(['10', '0', '0'])
+    })
 
+    it(': start && end ', async () => {
+      const start = '2020-6-30 10:00:00';
+      const end = '2025-10-10 10:10:10';
       const startYear = start.split('-')[0];
       const endYear = end.split('-')[0];
 
       const wrapper = mount(DateTimePicker, {
         props: {
-          defaultValue,
+          value: start,
           start,
           end,
+          mode: 'second'
         },
       });
       const $pickerItems = wrapper.findAllComponents(PickerItem);
@@ -148,6 +162,15 @@ describe('DateTimePicker', () => {
         res[precisionRankRecord[index]] = item.findAll(`.${prefix}-picker-item__item`);
       });
       expect(res[precisionRankRecord[0]].length).toEqual(endYear - startYear + 1);
+
+      // 设置到最后一天
+      await wrapper.setProps({ value: end })
+
+      $pickerItems.forEach((item, index) => {
+        const { length } = item.findAll(`.${prefix}-picker-item__item`);
+        const counter = [6, 10, 10, 11, 11, 11]
+        expect(length).toBe(counter[index])
+      });
     });
 
     it(': renderLabel', async () => {