浏览代码

test(fixednav): add test (#1124)

lzzwoniu 3 年之前
父节点
当前提交
e6536c884c

+ 2 - 1
src/packages/__VUE/fixednav/__tests__/fixednav.spec.ts

@@ -51,9 +51,10 @@ describe('FixedNav', () => {
     expect(_html1.exists()).toBe(true);
     const _html = wrapper.find('.nut-fixednav__btn');
     expect(_html.html()).toContain('展开');
+    console.log(_html.html(), '00000');
     wrapper.find('.nut-fixednav__btn').trigger('click');
     await nextTick();
-    // expect(_html.html()).toContain('收起');
+    expect(wrapper.emitted('update:visible')![0]).toEqual([true]);
   });
 
   test('should be displayed after setting the position', () => {

+ 385 - 0
src/packages/__VUE/form/__test__/index.spec.ts

@@ -0,0 +1,385 @@
+import { config, DOMWrapper, mount } from '@vue/test-utils';
+import Form from '../index.vue';
+import NutCellGroup from '../../cellgroup/index.vue';
+import FormItem from '../../formitem/index.vue';
+import NutCell from '../../cell/index.vue';
+import NutIcon from '../../icon/index.vue';
+import NutButton from '../../button/index.vue';
+import NutTextarea from '../../textarea/index.vue';
+import NutSwitch from '../../switch/index.vue';
+import NutCheckbox from '../../checkbox/index.vue';
+// import NutRadio from '../../radio/index.vue';
+// import NutRadioGroup from '../../radiogroup/index.vue';
+import NutRate from '../../rate/index.vue';
+import NutInputnumber from '../../inputnumber/index.vue';
+import NutRange from '../../range/index.vue';
+import NutUploader from '../../uploader/index.vue';
+import NutAddress from '../../address/index.vue';
+import NutElevator from '../../elevator/index.vue';
+import NutProgress from '../../progress/index.vue';
+import NutPopup from '../../popup/index.vue';
+import { nextTick, toRefs, ref, reactive } from 'vue';
+
+beforeAll(() => {
+  config.global.components = {
+    NutCellGroup,
+    FormItem,
+    NutCell,
+    NutIcon,
+    NutButton,
+    NutTextarea,
+    NutSwitch,
+    NutCheckbox,
+    // NutRadio,
+    // NutRadioGroup,
+    NutRate,
+    NutInputnumber,
+    NutRange,
+    NutUploader,
+    NutAddress,
+    NutElevator,
+    NutProgress,
+    NutPopup
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
+
+describe('Form', () => {
+  test('base Form', () => {
+    const wrapper = mount(Form);
+    const rate = wrapper.find('.nut-form');
+    expect(rate.exists()).toBe(true);
+  });
+
+  test('base Form usage', async () => {
+    const wrapper = mount({
+      components: {
+        'nut-form': Form,
+        'nut-form-item': FormItem
+      },
+      template: `
+      <nut-form>
+        <nut-form-item label="姓名">
+          <input class="nut-input-text" placeholder="请输入姓名" type="text" />
+        </nut-form-item>
+        <nut-form-item label="年龄">
+          <input class="nut-input-text" placeholder="请输入年龄" type="text" />
+        </nut-form-item>
+        <nut-form-item label="联系电话">
+          <input class="nut-input-text" placeholder="请输入联系电话" type="text" />
+        </nut-form-item>
+        <nut-form-item label="地址">
+          <input class="nut-input-text" placeholder="请输入地址" type="text" />
+        </nut-form-item>
+        <nut-form-item label="备注">
+          <nut-textarea placeholder="请输入备注" type="text" />
+        </nut-form-item>
+      </nut-form>
+      `
+    });
+    await nextTick();
+    const form = wrapper.find('.nut-cell-group__warp');
+    expect(form.exists()).toBe(true);
+    const formitem = wrapper.findAll('.nut-form-item');
+    expect(formitem.length).toBe(5);
+  });
+
+  test('base Dynamic Form', async () => {
+    const wrapper = mount({
+      components: {
+        'nut-form': Form,
+        'nut-form-item': FormItem
+      },
+      template: `
+      <nut-form :model-value="dynamicForm.state" ref="dynamicRefForm">
+        <nut-form-item label="姓名" :show-error-message="false" :show-error-line="false" error-message-align="center" body-align="center" label-width="90px" prop="name" required :rules="[{ required: true, message: '请填写姓名' }]">
+          <input class="nut-input-text" v-model="dynamicForm.state.name" placeholder="请输入姓名" type="text" />
+        </nut-form-item>
+        <nut-form-item
+          :label="'联系方式' + index"
+          :prop="'tels.' + index + '.value'"
+          required
+          :rules="[{ required: true, message: '请填写联系方式' + index }]"
+          :key="item.key"
+          v-for="(item, index) in dynamicForm.state.tels"
+        >
+          <input class="nut-input-text" v-model="item.value" :placeholder="'请输入联系方式' + index" type="text" />
+        </nut-form-item>
+        <nut-cell>
+          <nut-button class="add" size="small" style="margin-right: 10px" @click="dynamicForm.methods.add">添加</nut-button>
+          <nut-button class="remove" size="small" style="margin-right: 10px" @click="dynamicForm.methods.remove">删除</nut-button>
+          <nut-button class="submit" type="primary" style="margin-right: 10px" size="small" @click="dynamicForm.methods.submit"
+            >提交
+          </nut-button>
+          <nut-button class="reset" size="small" @click="dynamicForm.methods.reset">重置提示状态</nut-button>
+        </nut-cell>
+      </nut-form>
+      `,
+      setup() {
+        const dynamicRefForm = ref<any>(null);
+        const dynamicForm = {
+          state: reactive({
+            name: '',
+            tels: new Array({
+              key: 1,
+              value: ''
+            })
+          }),
+          methods: {
+            submit() {
+              dynamicRefForm.value.validate().then(({ valid, errors }: any) => {
+                if (valid) {
+                  console.log('success', dynamicForm);
+                } else {
+                  console.log('error submit!!', errors);
+                }
+              });
+            },
+            reset() {
+              dynamicRefForm.value.reset();
+            },
+            remove() {
+              dynamicForm.state.tels.splice(dynamicForm.state.tels.length - 1, 1);
+            },
+            add() {
+              let newIndex = dynamicForm.state.tels.length;
+              dynamicForm.state.tels.push({
+                key: Date.now(),
+                value: ''
+              });
+            }
+          }
+        };
+        return {
+          dynamicForm,
+          dynamicRefForm
+        };
+      }
+    });
+    await nextTick();
+    const formitem = wrapper.findAll('.nut-form-item');
+    expect(formitem.length).toBe(2);
+    const form1 = wrapper.find('.nut-cell__title');
+    expect(form1.classes()).toContain('required');
+    expect((form1.element as HTMLElement).style.textAlign).toEqual('left');
+    expect((form1.element as HTMLElement).style.width).toEqual('90px');
+    const form2 = wrapper.find('.nut-form-item__body__slots');
+    expect((form2.element as HTMLElement).style.textAlign).toEqual('center');
+    const form4 = wrapper.find('.submit');
+    form4.trigger('click');
+    await nextTick();
+    const form3 = wrapper.find('.nut-form-item__body__tips');
+    expect(form3.exists()).toBe(true);
+    const form5 = wrapper.find('.nut-form-item.error.line::before');
+    expect(form5.exists()).toBe(false);
+    await nextTick();
+    const form6 = wrapper.find('.add');
+    form6.trigger('click');
+    setTimeout(() => {
+      expect(formitem.length).toBe(3);
+    }, 0);
+    const form7 = wrapper.find('.remove');
+    form7.trigger('click');
+    setTimeout(() => {
+      expect(formitem.length).toBe(1);
+    }, 0);
+    const form8 = wrapper.find('.reset');
+    form8.trigger('click');
+    setTimeout(() => {
+      expect(form3.exists()).toBe(false);
+    }, 0);
+  });
+
+  test('base Form verification', async () => {
+    const wrapper = mount({
+      components: {
+        'nut-form': Form,
+        'nut-form-item': FormItem
+      },
+      template: `
+      <nut-form :model-value="formData" ref="ruleForm">
+        <nut-form-item label="姓名" prop="name" required :rules="[{ required: true, message: '请填写姓名' }]">
+          <input
+            class="nut-input-text"
+            v-model="formData.name"
+            placeholder="请输入姓名,blur 事件校验"
+            type="text"
+          />
+        </nut-form-item>
+        <nut-cell>
+          <nut-button class="submit" type="primary" size="small" style="margin-right: 10px" @click="submit">提交</nut-button>
+          <nut-button class="resets" size="small" @click="reset">重置提示状态</nut-button>
+        </nut-cell>
+      </nut-form>
+      `,
+      setup() {
+        const formData = reactive({
+          name: ''
+        });
+        const ruleForm = ref<any>(null);
+        const submit = () => {
+          ruleForm.value.validate().then(({ valid, errors }: any) => {
+            if (valid) {
+              console.log('success', formData);
+            } else {
+              console.log('error submit!!!', errors);
+            }
+          });
+        };
+        const reset = () => {
+          ruleForm.value.reset();
+        };
+        return {
+          ruleForm,
+          formData,
+          submit,
+          reset
+        };
+      }
+    });
+
+    // .nut-input-text
+    const formitem: DOMWrapper<Element> = wrapper.find('.nut-input-text');
+    expect(formitem.exists()).toBe(true);
+    expect(formitem.attributes().type).toBe('text');
+    const formitem2: DOMWrapper<Element> = wrapper.find('.nut-form-item__body__tips');
+    const formitem3: DOMWrapper<Element> = wrapper.find('.submit');
+    formitem3.trigger('click');
+    await nextTick();
+    expect(formitem3.exists()).toBe(true);
+    const formitem1: DOMWrapper<Element> = wrapper.find('.resets');
+
+    formitem1.trigger('click');
+    expect(formitem2.exists()).toBe(false);
+  });
+
+  test('base Form Type', async () => {
+    const wrapper = mount({
+      components: {
+        'nut-form': Form,
+        'nut-form-item': FormItem
+      },
+      template: `
+      <nut-form>
+      <nut-form-item label="开关">
+        <nut-switch v-model="formData2.switch"></nut-switch>
+      </nut-form-item>
+      <nut-form-item label="复选框">
+        <nut-checkbox v-model="formData2.checkbox">复选框</nut-checkbox>
+      </nut-form-item>
+      <nut-form-item label="单选按钮">
+        <nut-radiogroup direction="horizontal" v-model="formData2.radio">
+          <nut-radio label="1">选项1</nut-radio>
+          <nut-radio disabled label="2">选项2</nut-radio>
+          <nut-radio label="3">选项3</nut-radio>
+        </nut-radiogroup>
+      </nut-form-item>
+      <nut-form-item label="评分">
+        <nut-rate v-model="formData2.rate" />
+      </nut-form-item>
+      <nut-form-item label="步进器">
+        <nut-inputnumber v-model="formData2.number" />
+      </nut-form-item>
+      <nut-form-item label="滑块">
+        <nut-range hidden-tag v-model="formData2.range"></nut-range>
+      </nut-form-item>
+      <nut-form-item label="文件上传">
+        <nut-uploader url="http://服务地址" v-model:file-list="formData2.defaultFileList" maximum="3" multiple>
+        </nut-uploader>
+      </nut-form-item>
+      <nut-form-item label="地址">
+        <input
+          class="nut-input-text"
+          v-model="formData2.address"
+          readonly
+          placeholder="请选择地址"
+          type="text"
+        />
+        <!-- nut-address -->
+        <nut-address
+          v-model:visible="addressModule.state.show"
+          :province="addressModule.state.province"
+          :city="addressModule.state.city"
+          :country="addressModule.state.country"
+          :town="addressModule.state.town"
+          custom-address-title="请选择所在地区"
+        ></nut-address>
+      </nut-form-item>
+    </nut-form>
+      `,
+      setup() {
+        const formData2 = reactive({
+          switch: false,
+          checkbox: false,
+          radio: 0,
+          number: 0,
+          rate: 3,
+          range: 30,
+          address: '',
+          defaultFileList: [
+            {
+              name: '文件1.png',
+              url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif',
+              status: 'success',
+              message: '上传成功',
+              type: 'image'
+            },
+            {
+              name: '文件2.png',
+              url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif',
+              status: 'uploading',
+              message: '上传中...',
+              type: 'image'
+            }
+          ]
+        });
+
+        const addressModule = reactive({
+          state: {
+            show: false,
+            province: [
+              { id: 1, name: '北京' },
+              { id: 2, name: '广西' },
+              { id: 3, name: '江西' },
+              { id: 4, name: '四川' }
+            ],
+            city: [
+              { id: 7, name: '朝阳区' },
+              { id: 8, name: '崇文区' },
+              { id: 9, name: '昌平区' },
+              { id: 6, name: '石景山区' }
+            ],
+            country: [
+              { id: 3, name: '八里庄街道' },
+              { id: 9, name: '北苑' },
+              { id: 4, name: '常营乡' }
+            ],
+            town: []
+          }
+        });
+        return {
+          formData2,
+          addressModule
+        };
+      }
+    });
+    await nextTick();
+    const formitem = wrapper.findAll('.nut-form-item');
+    expect(formitem.length).toBe(8);
+    const formitem1 = wrapper.find('.nut-switch');
+    expect(formitem1.exists()).toBe(true);
+    const formitem2 = wrapper.find('.nut-checkbox');
+    expect(formitem2.exists()).toBe(true);
+    const formitem3 = wrapper.find('.nut-rate');
+    expect(formitem3.exists()).toBe(true);
+    const formitem4 = wrapper.find('.nut-inputnumber');
+    expect(formitem4.exists()).toBe(true);
+    const formitem5 = wrapper.find('.nut-range');
+    expect(formitem5.exists()).toBe(true);
+    const formitem6 = wrapper.find('.nut-uploader');
+    expect(formitem6.exists()).toBe(true);
+  });
+});

+ 59 - 0
src/packages/__VUE/notify/__test__/function.spec.ts

@@ -0,0 +1,59 @@
+import { NotifyFunction } from './notify';
+
+function sleep(delay = 0): Promise<void> {
+  return new Promise((resolve) => {
+    setTimeout(resolve, delay);
+  });
+}
+
+describe('function notify', () => {
+  test('show text notify', async () => {
+    NotifyFunction.text('基础用法', {
+      duration: 500,
+      color: '#ad0000',
+      background: '#ffe1e1'
+    });
+    let textNotify = document.querySelector('.nut-notify') as HTMLElement;
+    expect(textNotify.innerHTML).toContain('基础用法');
+    expect(textNotify.style.color).toEqual('rgb(173, 0, 0)');
+    expect(textNotify.style.background).toEqual('rgb(255, 225, 225)');
+    await sleep(500);
+    expect(textNotify.style.display).toEqual('');
+  });
+  test('show primary notify', async () => {
+    NotifyFunction.primary('主要通知', {
+      duration: 500
+    });
+    let textNotify1 = document.querySelector('.nut-notify--primary') as HTMLElement;
+    expect(textNotify1.innerHTML).toContain('主要通知');
+    await sleep(500);
+    expect(textNotify1.style.display).toEqual('');
+  });
+  test('show success notify', async () => {
+    NotifyFunction.success('成功通知', {
+      duration: 500
+    });
+    let textNotify1 = document.querySelector('.nut-notify--success') as HTMLElement;
+    expect(textNotify1.innerHTML).toContain('成功通知');
+    await sleep(500);
+    expect(textNotify1.style.display).toEqual('');
+  });
+  test('show danger notify', async () => {
+    NotifyFunction.danger('危险通知', {
+      duration: 500
+    });
+    let textNotify1 = document.querySelector('.nut-notify--danger') as HTMLElement;
+    expect(textNotify1.innerHTML).toContain('危险通知');
+    await sleep(500);
+    expect(textNotify1.style.display).toEqual('');
+  });
+  test('show warn notify', async () => {
+    NotifyFunction.warn('警告通知', {
+      duration: 500
+    });
+    let textNotify1 = document.querySelector('.nut-notify--warning') as HTMLElement;
+    expect(textNotify1.innerHTML).toContain('警告通知');
+    await sleep(500);
+    expect(textNotify1.style.display).toEqual('');
+  });
+});

+ 118 - 0
src/packages/__VUE/notify/__test__/notify.ts

@@ -0,0 +1,118 @@
+import { createVNode, defineComponent, render, App } from 'vue';
+import Notify from '../index.vue';
+const defaultOptions = {
+  type: 'base',
+  showPopup: false,
+  msg: '',
+  color: undefined,
+  background: undefined,
+  duration: 3000,
+  className: '',
+  onClosed: null,
+  onClick: null,
+  onOpened: null,
+  textTimer: null,
+  unmount: null
+};
+
+let idsMap: string[] = [];
+let optsMap: any[] = [];
+const clearNotify = (id?: string) => {
+  if (id) {
+    const container = document.getElementById(id);
+    optsMap = optsMap.filter((item) => item.id !== id);
+    idsMap = idsMap.filter((item) => item !== id);
+    if (container) {
+      document.body.removeChild(container);
+    }
+  } else {
+    idsMap.forEach((item) => {
+      const container = document.getElementById(item);
+      if (container) {
+        document.body.removeChild(container);
+      }
+    });
+    optsMap = [];
+    idsMap = [];
+  }
+};
+
+const updateNotify = (opts: any) => {
+  const container = document.getElementById(opts.id);
+  if (container) {
+    const currentOpt = optsMap.find((item) => item.id === opts.id);
+    if (currentOpt) {
+      opts = { ...defaultOptions, ...currentOpt, ...opts };
+    } else {
+      opts = { ...defaultOptions, ...opts };
+    }
+    const instance: any = createVNode(Notify, opts);
+    render(instance, container);
+    return instance.component.ctx;
+  }
+};
+
+const mountNotify = (opts: any) => {
+  opts.unmount = clearNotify;
+  let _id;
+  if (opts.id) {
+    _id = opts.id;
+    if (idsMap.find((item) => item === opts.id)) {
+      return updateNotify(opts);
+    }
+  } else {
+    _id = new Date().getTime() + '';
+  }
+  opts = { ...defaultOptions, ...opts };
+  opts.id = _id;
+  idsMap.push(opts.id);
+  optsMap.push(opts);
+  const container = document.createElement('view');
+  container.id = opts.id;
+  const instance: any = createVNode(Notify, opts);
+  render(instance, container);
+  document.body.appendChild(container);
+  setTimeout(() => {
+    instance.showPopup = true;
+  }, 0);
+  return instance.component.ctx;
+};
+
+const errorMsg = (msg: string) => {
+  if (!msg) {
+    console.warn('[NutUI Notify]: msg不能为空');
+    return;
+  }
+};
+
+export const NotifyFunction = {
+  text(msg: string, obj = {}) {
+    errorMsg(msg);
+    return mountNotify({ ...obj, msg });
+  },
+  primary(msg: string, obj = {}) {
+    errorMsg(msg);
+    return mountNotify({ ...obj, msg, type: 'primary' });
+  },
+  success(msg: string, obj = {}) {
+    errorMsg(msg);
+    return mountNotify({ ...obj, msg, type: 'success' });
+  },
+  danger(msg: string, obj = {}) {
+    errorMsg(msg);
+    return mountNotify({ ...obj, msg, type: 'danger' });
+  },
+  warn(msg: string, obj = {}) {
+    errorMsg(msg);
+    return mountNotify({ ...obj, msg, type: 'warning' });
+  },
+  hide() {
+    clearNotify();
+  },
+  install(app: App): void {
+    app.config.globalProperties.$notify = NotifyFunction;
+  }
+};
+
+export default NotifyFunction;
+export { Notify };

+ 8 - 7
src/packages/__VUE/notify/demo.vue

@@ -10,14 +10,11 @@
       <nut-cell is-Link @click="warningNotify('警告通知')">警告通知</nut-cell>
     </nut-cell-group>
     <nut-cell-group title="自定义样式">
-      <nut-cell is-Link @click="cusBgNotify('自定义背景色和字体颜色')">
-        自定义背景色和字体颜色
-      </nut-cell>
+      <nut-cell is-Link @click="cusBgNotify('自定义背景色和字体颜色')"> 自定义背景色和字体颜色 </nut-cell>
     </nut-cell-group>
     <nut-cell-group title="自定义时长">
-      <nut-cell is-Link @click="timeNotify('自定义时长')">
-        自定义时长
-      </nut-cell>
+      <nut-cell is-Link @click="timeNotify('自定义时长')"> 自定义时长 </nut-cell>
+      <nut-cell is-Link @click="positionNotify('自定义位置')"> 自定义位置 </nut-cell>
     </nut-cell-group>
   </div>
 </template>
@@ -56,6 +53,9 @@ export default createDemo({
     const timeNotify = (msg: string) => {
       Notify.text(msg, { duration: 10000 });
     };
+    const positionNotify = (msg: string) => {
+      Notify.text(msg, { position: 'bottom' });
+    };
     return {
       baseNotify,
       primaryNotify,
@@ -63,7 +63,8 @@ export default createDemo({
       errorNotify,
       warningNotify,
       cusBgNotify,
-      timeNotify
+      timeNotify,
+      positionNotify
     };
   }
 });

+ 9 - 0
src/packages/__VUE/notify/index.scss

@@ -7,6 +7,15 @@
   transition: transform 0.3s;
   z-index: 9999;
 }
+.popup-bottom {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  overflow-y: auto;
+  transition: transform 0.3s;
+  z-index: 9999;
+}
 
 .nut-fade-enter-active {
   transition: opacity 1s;

+ 6 - 1
src/packages/__VUE/notify/index.vue

@@ -1,7 +1,7 @@
 <template>
   <Transition name="toast-fade" @after-leave="onAfterLeave">
     <view
-      :class="['popup-top', 'nut-notify', `nut-notify--${type}`, [className]]"
+      :class="[`popup-${position}`, 'nut-notify', `nut-notify--${type}`, [className]]"
       :style="{ color: color, background: background }"
       v-show="state.mounted"
       @click="clickCover"
@@ -47,6 +47,10 @@ export default create({
       type: Boolean,
       default: false
     },
+    position: {
+      type: String,
+      default: 'top'
+    },
     onClose: Function,
     onClick: Function,
     unmount: Function
@@ -59,6 +63,7 @@ export default create({
     });
     onMounted(() => {
       state.mounted = true;
+      console.log(props.className);
     });
 
     const clickCover = () => {

+ 67 - 0
src/packages/__VUE/swipe/__tests__/swipe.spec.ts

@@ -0,0 +1,67 @@
+import { config, DOMWrapper, mount } from '@vue/test-utils';
+import Swipe from '../index.vue';
+import { nextTick } from 'vue';
+import NutButton from '../../button/index.vue';
+import NutCell from '../../cell/index.vue';
+
+beforeAll(() => {
+  config.global.components = {
+    NutButton,
+    NutCell
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
+
+test('base swipe', () => {
+  const wrapper = mount(Swipe);
+  const swipe: DOMWrapper<Element> = wrapper.find('.nut-swipe');
+  expect(swipe.exists()).toBe(true);
+});
+
+test('base swipe props disabled', async () => {
+  const wrapper = mount(Swipe, {
+    props: {
+      disabled: false
+    },
+    slots: {
+      right: `<nut-button shape="square" style="height: 100%" type="danger"
+      >删除</nut-button>`
+    }
+  });
+  await nextTick();
+  const swipe1: DOMWrapper<Element> = wrapper.find('.nut-swipe__right');
+  const swipe2: DOMWrapper<Element> = wrapper.find('.nut-button');
+  expect(swipe1.exists()).toBe(true);
+  expect(swipe1.text()).toBe('删除');
+  expect(swipe2.exists()).toBe(true);
+});
+test('base swipe Slots', async () => {
+  const wrapper = mount(Swipe, {
+    slots: {
+      left: `<nut-button shape="square" style="height: 100%" type="success"
+      >选择</nut-button>`
+    }
+  });
+  await nextTick();
+  const swipe: DOMWrapper<Element> = wrapper.find('.nut-swipe__left');
+  const swipe2: DOMWrapper<Element> = wrapper.find('.nut-button');
+  expect(swipe.exists()).toBe(true);
+  expect(swipe.text()).toBe('选择');
+  expect(swipe2.exists()).toBe(true);
+});
+test('base swipe content', async () => {
+  const wrapper = mount(Swipe, {
+    slots: {
+      default: '<nut-cell round-radius="0" desc="左滑右滑都可以哦" />'
+    }
+  });
+  await nextTick();
+  const swipe2: DOMWrapper<Element> = wrapper.find('.nut-swipe__content');
+  const swipe3: DOMWrapper<Element> = wrapper.find('.nut-cell');
+  expect(swipe2.exists()).toBe(true);
+  expect(swipe2.text()).toBe('左滑右滑都可以哦');
+  expect(swipe3.exists()).toBe(true);
+});

+ 191 - 0
src/packages/__VUE/tabs/__tests__/index.spec.ts

@@ -0,0 +1,191 @@
+import { config, DOMWrapper, mount } from '@vue/test-utils';
+import Tabs from '../index.vue';
+import TabPane from './../../tabpane/index.vue';
+import { nextTick, reactive } from 'vue';
+import NutIcon from '../../icon/index.vue';
+
+beforeAll(() => {
+  config.global.components = {
+    NutIcon
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
+
+test('base Tabs', () => {
+  const wrapper = mount(Tabs);
+  const rate = wrapper.find('.nut-tabs');
+  expect(rate.exists()).toBe(true);
+});
+
+test('base tabs props', async () => {
+  const wrapper = mount(Tabs, {
+    props: {
+      'v-model': 0,
+      background: '#f5f5f5',
+      color: '#f5f5f5',
+      direction: 'horizontal',
+      type: 'smile',
+      size: 'large',
+      'title-scroll': true
+    },
+    components: {
+      'nut-tabs': Tabs,
+      'nut-tabpane': TabPane
+    }
+  });
+  await nextTick();
+  const stepItem = wrapper.find('.nut-tabs__titles');
+  expect((stepItem.element as HTMLElement).style.background).toEqual('rgb(245, 245, 245)');
+  const _stepItem = wrapper.findAll('.horizontal');
+  expect(_stepItem.length).toBe(1);
+  const _stepItem1 = wrapper.findAll('.nut-tabs__titles')[0];
+  expect(_stepItem1.classes()).toContain('smile');
+  const _stepItem2 = wrapper.findAll('.nut-tabs__titles')[0];
+  expect(_stepItem2.classes()).toContain('large');
+  const _stepItem3 = wrapper.findAll('.nut-tabs__titles')[0];
+  expect(_stepItem3.classes()).toContain('scrollable');
+});
+
+test('base other props', async () => {
+  const wrapper = mount(Tabs, {
+    props: {
+      'animated-time': 500,
+      'title-gutter': '20px'
+    },
+    components: {
+      'nut-tabs': Tabs,
+      'nut-tabpane': TabPane
+    }
+  });
+  await nextTick();
+  const stepItem = wrapper.find('.nut-tabs__content');
+  expect((stepItem.element as HTMLElement).style.transitionDuration).toEqual('500ms');
+  setTimeout(() => {
+    const stepItem1 = wrapper.find('.nut-tabs__titles-item');
+    expect((stepItem1.element as HTMLElement).style.marginLeft).toEqual('20px');
+  }, 0);
+});
+
+test('base Tabs Slots', async () => {
+  const wrapper = mount({
+    components: {
+      'nut-tabs': Tabs,
+      'nut-tabpane': TabPane
+    },
+    template: `
+    <nut-tabs v-model="state.tab7value">
+      <template v-slot:titles>
+        <div
+          class="nut-tabs__titles-item"
+          @click="state.tab7value = item.paneKey"
+          :class="{ active: state.tab7value == item.paneKey }"
+          :key="item.paneKey"
+          v-for="item in state.list6"
+        >
+          <nut-icon v-if="item.icon" :name="item.icon" />
+          <span class="nut-tabs__titles-item__text">{{ item.title }}</span>
+          <span class="nut-tabs__titles-item__line"></span>
+        </div>
+      </template>
+      <nut-tabpane v-for="item in state.list6" :pane-key="item.paneKey">
+        {{ item.title }}
+      </nut-tabpane>
+    </nut-tabs>
+    `,
+    setup() {
+      const state = reactive({
+        tab7value: 'c1',
+        list6: [
+          {
+            title: '自定义 1',
+            paneKey: 'c1',
+            icon: 'dongdong'
+          },
+          {
+            title: '自定义 2',
+            paneKey: 'c2',
+            icon: 'JD'
+          },
+          {
+            title: '自定义 3',
+            paneKey: 'c3'
+          }
+        ]
+      });
+      return { state };
+    }
+  });
+  await nextTick();
+  const tab1 = wrapper.find('.nut-tabs__titles');
+  expect(tab1.exists()).toBe(true);
+  const tab2 = wrapper.findAll('.nut-tabs__titles-item');
+  expect(tab2.length).toBe(3);
+  const tab3 = wrapper.findAll('.nut-tabs__titles-item__text');
+  expect(tab3[0].html()).toContain('自定义 1');
+  expect(tab3[1].html()).toContain('自定义 2');
+  expect(tab3[2].html()).toContain('自定义 3');
+  const tab4 = wrapper.find('.nut-tabs__content');
+  expect(tab4.exists()).toBe(true);
+});
+
+test('base Tabpane Props', async () => {
+  const wrapper = mount({
+    components: {
+      'nut-tabs': Tabs,
+      'nut-tabpane': TabPane
+    },
+    template: `
+    <nut-tabs v-model="state.tab2value">
+      <nut-tabpane title="Tab 1" pane-key="0"> </nut-tabpane>
+      <nut-tabpane title="Tab 2" pane-key="1" :disabled="true"> Tab 2 </nut-tabpane>
+      <nut-tabpane title="Tab 3" pane-key="2"> Tab 3 </nut-tabpane>
+    </nut-tabs>
+    `,
+    setup() {
+      const state = reactive({
+        tab2value: '0'
+      });
+      return { state };
+    }
+  });
+  await nextTick();
+  const tab = wrapper.findAll('.nut-tabs__titles-item');
+  expect(tab.length).toBe(3);
+  const tab1 = wrapper.findAll('.nut-tabs__titles-item')[1];
+  expect(tab1.classes()).toContain('disabled');
+  const tab2 = wrapper.findAll('.nut-tabs__titles-item')[0];
+  expect(tab2.classes()).toContain('active');
+  const tab3 = wrapper.findAll('.nut-tabs__titles-item__text');
+  expect(tab3[0].html()).toContain('Tab 1');
+});
+
+test('base click', async () => {
+  const wrapper = mount({
+    components: {
+      'nut-tabs': Tabs,
+      'nut-tabpane': TabPane
+    },
+    template: `
+    <nut-tabs v-model="state.tab1value">
+      <nut-tabpane title="Tab 1"> Tab 1 </nut-tabpane>
+      <nut-tabpane title="Tab 2"> Tab 2 </nut-tabpane>
+      <nut-tabpane title="Tab 3"> Tab 3 </nut-tabpane>
+    </nut-tabs>
+    `,
+    setup() {
+      const state = reactive({
+        tab1value: '0'
+      });
+      return { state };
+    }
+  });
+  await nextTick();
+  const tab = wrapper.find('.nut-tabs__titles-item');
+  expect(tab.classes()).toContain('active');
+  tab.trigger('click');
+  const tab1 = wrapper.find('.nut-tabs__content');
+  expect((tab1.element as HTMLElement).style.transform).toEqual('translate3d(-0%, 0, 0)');
+});

+ 146 - 0
src/packages/__VUE/uploader/__tests__/index.spec.ts

@@ -0,0 +1,146 @@
+import { config, DOMWrapper, mount } from '@vue/test-utils';
+import Uploader from '../index.vue';
+import { nextTick, ref } from 'vue';
+import NutIcon from '../../icon/index.vue';
+import NutProgress from '../../progress/index.vue';
+
+beforeAll(() => {
+  config.global.components = {
+    NutIcon,
+    NutProgress
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
+
+test('should render base uploader and type', async () => {
+  const wrapper = mount(Uploader);
+  let up_load = wrapper.find('.nut-uploader');
+  expect(up_load.exists()).toBe(true);
+  let up_load1 = wrapper.find('.nut-uploader__input');
+  expect(up_load1.attributes().type).toBe('file');
+});
+test('should render base uploader props', async () => {
+  const wrapper = mount(Uploader, {
+    props: {
+      'auto-upload': true,
+      capture: true,
+      name: 'files',
+      accept: '.jpg',
+      maximize: '1024 * 50',
+      maximum: 2
+    }
+  });
+  let toast = wrapper.find('.nut-uploader__input');
+  expect(toast.attributes().capture).toBe('camera');
+  expect(toast.attributes().name).toBe('files');
+  expect(toast.attributes().accept).toBe('.jpg');
+  expect(toast.exists()).toBe(true);
+  toast.trigger('click');
+  expect(wrapper.emitted('change'));
+  let toast1 = wrapper.find('.nut-uploader__upload');
+  expect(wrapper.emitted('oversize'));
+  expect(toast1.exists()).toBe(true);
+});
+test('should render base uploader other props', async () => {
+  const wrapper = mount(Uploader, {
+    props: {
+      'is-deletable': true,
+      'file-list': [
+        {
+          name: '文件1.png',
+          url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif',
+          status: 'success',
+          message: '上传成功',
+          type: 'image'
+        },
+        {
+          name: '文件2.png',
+          url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif',
+          status: 'error',
+          message: '上传失败',
+          type: 'image'
+        },
+        {
+          name: '文件3.png',
+          url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif',
+          status: 'uploading',
+          message: '上传中...',
+          type: 'image'
+        }
+      ],
+      headers: {},
+      multiple: true,
+      'is-preview': false,
+      'upload-icon': 'dongdong',
+      'upload-icon-size': '20px'
+    }
+  });
+  await nextTick();
+  let toast = wrapper.find('.nutui-iconfont');
+  expect(toast.exists()).toBe(true);
+  let toast4 = wrapper.find('.close');
+  expect(toast4.exists()).toBe(true);
+  toast4.trigger('click');
+  expect(wrapper.emitted('delete')).toBeTruthy();
+  let toast1 = wrapper.findAll('.nut-uploader__preview');
+  expect(toast1.length).toBe(3);
+  let toast2 = wrapper.find('.nut-uploader__preview-img__c');
+  expect(toast2.exists()).toBe(true);
+  toast2.trigger('click');
+  expect(wrapper.emitted('file-item-click')).toBeTruthy();
+  expect(toast2.attributes().src).toBe(
+    'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif'
+  );
+  let toast3 = wrapper.find('.nut-icon-dongdong');
+  expect(toast3.exists()).toBe(true);
+  expect((toast3.element as HTMLElement).style.fontSize).toEqual('20px');
+  expect((toast3.element as HTMLElement).style.width).toEqual('20px');
+  expect((toast3.element as HTMLElement).style.height).toEqual('20px');
+});
+test('should render base uploader list', async () => {
+  const wrapper = mount(Uploader, {
+    props: {
+      'upload-icon': 'dongdong',
+      'list-type': 'list',
+      'file-list': [
+        {
+          name: '文件1.png',
+          url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif',
+          status: 'success',
+          message: '上传成功',
+          type: 'image'
+        },
+        {
+          name: '文件2.png',
+          url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif',
+          status: 'error',
+          message: '上传失败',
+          type: 'image'
+        },
+        {
+          name: '文件3.png',
+          url: 'https://m.360buyimg.com/babel/jfs/t1/164410/22/25162/93384/616eac6cE6c711350/0cac53c1b82e1b05.gif',
+          status: 'uploading',
+          message: '上传中...',
+          type: 'image'
+        }
+      ]
+    }
+  });
+  await nextTick();
+  let toast3 = wrapper.find('.list');
+  expect(toast3.exists()).toBe(true);
+});
+
+test('should render base uploader props disabled', async () => {
+  const wrapper = mount(Uploader, {
+    props: {
+      disabled: true
+    }
+  });
+  let up_load1 = wrapper.find('.nut-uploader__input');
+  expect(up_load1.attributes().disabled).toBe('');
+});

+ 60 - 0
src/packages/__VUE/video/__tests__/video.spec.ts

@@ -0,0 +1,60 @@
+import { mount } from '@vue/test-utils';
+import Video from '../index.vue';
+
+describe('Video', () => {
+  test('base Video', () => {
+    const wrapper = mount(Video);
+    const rate = wrapper.find('.nut-video');
+    expect(rate.exists()).toBe(true);
+  });
+  test('should be displayed after setting the source src and type', () => {
+    const wrapper = mount(Video, {
+      props: {
+        source: {
+          src: 'xxx.mp4',
+          type: 'video/mp4'
+        },
+        options: {
+          controls: true
+        }
+      }
+    });
+    expect(wrapper.html()).toContain('src');
+    expect(wrapper.html()).toContain('type');
+    expect(wrapper.html()).toContain('controls');
+  });
+  test('should be displayed after setting the options autoplay and muted and loop', () => {
+    const wrapper = mount(Video, {
+      props: {
+        options: {
+          autoplay: true,
+          muted: true,
+          loop: true
+        }
+      }
+    });
+    expect(wrapper.html()).toContain('autoplay');
+    expect(wrapper.html()).toContain('muted');
+    expect(wrapper.html()).toContain('loop');
+  });
+  test('should be displayed after setting the options poster and playsinline', () => {
+    const wrapper = mount(Video, {
+      props: {
+        options: {
+          poster: 'xxx.png',
+          playsinline: true
+        }
+      }
+    });
+    expect(wrapper.html()).toContain('xxx.png');
+    expect(wrapper.html()).toContain('playsinline');
+  });
+  test('should be displayed after setting the click', () => {
+    const wrapper = mount(Video);
+    const _html1 = wrapper.find('.show-control');
+    expect(_html1.exists()).toBe(true);
+    wrapper.find('.control-play-btn').trigger('click');
+    const _html2 = wrapper.find('.hide-control');
+    expect(_html2.exists()).toBe(false);
+  });
+});

+ 1 - 1
src/packages/__VUE/video/index.vue

@@ -146,7 +146,7 @@ export default create({
     watch(
       props.options,
       (newValue) => {
-        state.state.isMuted = newValue.muted ? newValue.muted : false;
+        state.state.isMuted = newValue ? newValue.muted : false;
       },
       { immediate: true }
     );