Repository: IduxFE/components (ts)

Issue:
[comp: Rate] add Rate component
- [x] I have searched the [issues](https://github.com/IduxFE/components/issues) of this repository and believe that this is not a duplicate.

### What problem does this feature solve?
<!-- 这个功能解决了什么问题？ -->
<!-- 请尽可能详尽地说明这个需求的用例和场景。最重要的是：解释清楚是怎样的用户体验需求催生了这个功能上的需求。 -->
- 支持鼠标悬浮和点击图标时，图标高亮，可以自定义高亮和未选状态的颜色
- 支持图标数目
- 支持半分（选）
- 支持只读状态
- 图标支持自定义字符或icon，一般来说是是使用某个文字或者某个iconfont
- 可捕获图标被hover时的事件，实现譬如鼠标悬浮到不同分数时，某个位置显示一些状态文字
- 可捕获分数change时的事件

### What does the proposed API look like?
<!-- 你期望的 API 是怎样的？-->
<!-- 描述一下你期望这个新功能的 API 是如何使用的，并提供一些代码示例。请用 Markdown 格式化你的代码片段。 -->

**props**

| 参数             | 说明       | 默认值   | 类型      | 备注           |
|----------------|----------|-------|---------|--------------|
| value          | 高亮的数目    | 0     | number  | -            |
| strokeColor | 高亮颜色     | 待定    | string  | -             |
| backgroundColor| 未被选择时的颜色 | 待定    | string  |   -           |
| count          | 图标数目     | 5     | number  |    -          |
| icon           | 自定义图标    | ⭐     | string  | props 或 slot |
| allowHalf      | 支持半分（选）  | false | boolean |       -       |
| disabled | 只读模式     | false | boolean |      -        |
| tooltips| 每一个元素的提示信息     | [] | string[]|      -        |
| allowClear | 是否可以取消选择     | false | boolean|      -        |

**event**
| 参数     | 说明        | 类型                      |
|--------|-----------|-------------------------|
| hover  | 鼠标经过图标时触发 | Function(value: number) |
| change | 分数发生改变时触发 | Function(value: number) |


Pull request:
feat(comp:rate): add cpmponent rate
fix #34

## PR Checklist

Please check if your PR fulfills the following requirements:

- [x] The commit message follows [our guidelines](https://github.com/IduxFE/components/blob/main/docs/contributing.zh.md#commit)
- [x] Tests for the changes have been added/updated or not needed
- [x] Docs and demo have been added/updated or not needed

## PR Type

What kind of change does this PR introduce?

- [ ] Bugfix
- [x] Feature
- [ ] Component style update
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] Application (the showcase website) / infrastructure changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

## What is the new behavior?

## Other information


Patch:
diff --git a/packages/components/components.less b/packages/components/components.less
index af85a40a..df3b059a 100644
--- a/packages/components/components.less
+++ b/packages/components/components.less
@@ -8,3 +8,4 @@
 @import './empty/style/index.less';
 @import './result/style/index.less';
 @import "./typography/style/index.less";
+@import './rate/style/index.less';
diff --git a/packages/components/core/config/defaultConfig.ts b/packages/components/core/config/defaultConfig.ts
index d8d18b59..cf52f62e 100644
--- a/packages/components/core/config/defaultConfig.ts
+++ b/packages/components/core/config/defaultConfig.ts
@@ -8,6 +8,7 @@ import type {
   SpinConfig,
   SpaceConfig,
   ResultConfig,
+  RateConfig,
 } from './types'
 
 import { shallowReactive } from 'vue'
@@ -43,6 +44,13 @@ const space = shallowReactive<SpaceConfig>({ size: 'small' })
 
 const result = shallowReactive<ResultConfig>({ status: 'info' })
 
+const rate = shallowReactive<RateConfig>({
+  count: 5,
+  icon: 'star',
+  allowHalf: false,
+  allowClear: false,
+})
+
 export const defaultConfig: GlobalConfig = {
   button,
   icon,
@@ -52,4 +60,5 @@ export const defaultConfig: GlobalConfig = {
   spin,
   space,
   result,
+  rate,
 }
diff --git a/packages/components/core/config/types.ts b/packages/components/core/config/types.ts
index 71e877ac..d137a226 100644
--- a/packages/components/core/config/types.ts
+++ b/packages/components/core/config/types.ts
@@ -9,6 +9,7 @@ export interface GlobalConfig {
   spin: SpinConfig
   space: SpaceConfig
   result: ResultConfig
+  rate: RateConfig
 }
 
 export type ButtonMode = 'primary' | 'default' | 'dashed' | 'text' | 'link'
@@ -61,3 +62,10 @@ export type ResultStatus = 'success' | 'error' | 'info' | 'warning'
 export interface ResultConfig {
   status: ResultStatus
 }
+
+export interface RateConfig {
+  count: number
+  icon: string
+  allowHalf: boolean
+  allowClear: boolean
+}
diff --git a/packages/components/icon/src/icons.ts b/packages/components/icon/src/icons.ts
index e81b23a4..362d9778 100644
--- a/packages/components/icon/src/icons.ts
+++ b/packages/components/icon/src/icons.ts
@@ -14,6 +14,7 @@ import { CheckCircle } from '../definitions/checkCircle'
 import { CloseCircle } from '../definitions/closeCircle'
 import { InfoCircle } from '../definitions/infoCircle'
 import { ExclamationCircle } from '../definitions/exclamationCircle'
+import { Star } from '../definitions/star'
 
 export const innerStaticIcons: IconDefinition[] = [
   Up,
@@ -31,4 +32,5 @@ export const innerStaticIcons: IconDefinition[] = [
   CloseCircle,
   InfoCircle,
   ExclamationCircle,
+  Star,
 ]
diff --git a/packages/components/index.ts b/packages/components/index.ts
index 2bbdaebe..8352b13a 100644
--- a/packages/components/index.ts
+++ b/packages/components/index.ts
@@ -9,8 +9,21 @@ import { IxSpace } from './space'
 import { IxEmpty } from './empty'
 import { IxResult } from './result'
 import { IxTypography } from './typography'
+import { IxRate } from './rate'
 
-const components = [IxButton, IxButtonGroup, IxIcon, IxBadge, IxDivider, IxImage, IxSpin, IxSpace, IxEmpty, IxResult]
+const components = [
+  IxButton,
+  IxButtonGroup,
+  IxIcon,
+  IxBadge,
+  IxDivider,
+  IxImage,
+  IxSpin,
+  IxSpace,
+  IxEmpty,
+  IxResult,
+  IxRate,
+]
 
 const directives: Record<string, Directive> = {
   typography: IxTypography,
@@ -45,3 +58,4 @@ export { IxSpace }
 export { IxEmpty }
 export { IxResult }
 export { IxTypography }
+export { IxRate }
diff --git a/packages/components/rate/demo/allowClear.md b/packages/components/rate/demo/allowClear.md
new file mode 100644
index 00000000..560e228b
--- /dev/null
+++ b/packages/components/rate/demo/allowClear.md
@@ -0,0 +1,31 @@
+---
+order: 4
+title:
+  zh: 允许取消
+  en: allow clear
+---
+
+## zh
+
+允许点击已经选择的图标后清除所有选择项
+
+## demo
+
+```html
+<template>
+  <ix-rate v-model:value="value" allowClear />
+</template>
+<script>
+import { defineComponent, ref } from 'vue'
+export default defineComponent({
+ setup(){
+    const value = ref(3);
+
+    return {
+      value,
+    }
+  }
+})
+
+</script>
+```
diff --git a/packages/components/rate/demo/basic.md b/packages/components/rate/demo/basic.md
new file mode 100644
index 00000000..fc23ea87
--- /dev/null
+++ b/packages/components/rate/demo/basic.md
@@ -0,0 +1,31 @@
+---
+order: 0
+title:
+  zh: 基本用法
+  en: Basic usage
+---
+
+## zh
+
+简单展示，图标默认是 `star`
+
+## demo
+
+```html
+<template>
+  <ix-rate v-model:value="value" />
+</template>
+<script>
+import { defineComponent, ref } from 'vue'
+export default defineComponent({
+ setup(){
+    const value = ref(3);
+
+    return {
+      value,
+    }
+  }
+})
+
+</script>
+```
diff --git a/packages/components/rate/demo/disabled.md b/packages/components/rate/demo/disabled.md
new file mode 100644
index 00000000..7b0e3615
--- /dev/null
+++ b/packages/components/rate/demo/disabled.md
@@ -0,0 +1,31 @@
+---
+order: 3
+title:
+  zh: 只读模式
+  en: disbaled
+---
+
+## zh
+
+此时图标被禁用，无法选择
+
+## demo
+
+```html
+<template>
+  <ix-rate v-model:value="value" disabled  />
+</template>
+<script>
+import { defineComponent, ref } from 'vue'
+export default defineComponent({
+ setup(){
+    const value = ref(3);
+
+    return {
+      value,
+    }
+  }
+})
+
+</script>
+```
diff --git a/packages/components/rate/demo/half.md b/packages/components/rate/demo/half.md
new file mode 100644
index 00000000..fbe83792
--- /dev/null
+++ b/packages/components/rate/demo/half.md
@@ -0,0 +1,31 @@
+---
+order: 1
+title:
+  zh: 支持半选
+  en: support half selection
+---
+
+## zh
+
+可以选择或展示半选（分）
+
+## demo
+
+```html
+<template>
+  <ix-rate v-model:value="value" allowHalf  />
+</template>
+<script>
+import { defineComponent, ref } from 'vue'
+export default defineComponent({
+ setup(){
+    const value = ref(3.5);
+
+    return {
+      value,
+    }
+  }
+})
+
+</script>
+```
diff --git a/packages/components/rate/demo/iconAndTooltips.md b/packages/components/rate/demo/iconAndTooltips.md
new file mode 100644
index 00000000..ac37311a
--- /dev/null
+++ b/packages/components/rate/demo/iconAndTooltips.md
@@ -0,0 +1,36 @@
+---
+order: 2
+title:
+  zh: 自定义icon和tooltips
+  en: customize icon and tooltips
+---
+
+## zh
+
+- 可以自定义icon，icon名称见 icon 组件
+- 自定义每一个元素悬浮展示的信息
+
+## demo
+
+```html
+<template>
+  <ix-rate v-model:value="value" :icon="icon" :tooltips="tooltip"  />
+</template>
+<script>
+import { defineComponent, ref } from 'vue'
+export default defineComponent({
+ setup(){
+    const value = ref(3);
+    const icon = 'smile'
+    const tooltip = ['很差', '差', '一般', '不错', '很好'];
+
+    return {
+      value,
+      icon,
+      tooltip
+    }
+  }
+})
+
+</script>
+```
diff --git a/packages/components/rate/docs/index.zh.md b/packages/components/rate/docs/index.zh.md
new file mode 100644
index 00000000..d92aafe5
--- /dev/null
+++ b/packages/components/rate/docs/index.zh.md
@@ -0,0 +1,33 @@
+---
+category: components
+type: 数据录入
+title: Rate
+subtitle: 评分
+cover: 评分组件
+---
+
+
+
+## 何时使用
+
+- 对某个内容进行评分或展示评分
+
+## API
+
+### `ix-rate`
+
+| 名称 | 说明 | 类型  | 默认值 | 全局配置 | 备注 |
+| --- | --- | --- | --- | --- | --- |
+| `v-model:value` | 高亮的数目（分数） | `string \| number` | 0 | - | - |
+| `count` | 图标数目 | `string \| number` | 5 | ✅ | - |
+| `icon` | 自定义图标名称 | `string` | `star` | ✅ | - |
+| `allowHalf` | 支持半分（选） | `boolean` | false | ✅ | - |
+| `disabled` | 只读模式 | `boolean` | false | - |-|
+| `tooltips` | 每一个元素的悬浮提示信息 | `Array` | [] | - |-|
+| `allowClear` | 二次点击元素后清除选择 | `boolean` | false | ✅ |-|
+
+### Emits
+
+| 名称 | 说明 | 参数类型 | 备注 |
+| --- | --- | --- | --- |
+| `update:value` | 点击评分元素时触发 | `number` | - |
diff --git a/packages/components/rate/index.ts b/packages/components/rate/index.ts
new file mode 100644
index 00000000..dffb6032
--- /dev/null
+++ b/packages/components/rate/index.ts
@@ -0,0 +1,8 @@
+import { installComponent } from '@idux/components/core/utils'
+import IxRate from './src/Rate.vue'
+
+IxRate.install = installComponent(IxRate)
+
+export { IxRate }
+
+export type { RateComponent } from './src/types'
diff --git a/packages/components/rate/src/Rate.vue b/packages/components/rate/src/Rate.vue
new file mode 100644
index 00000000..005b76a4
--- /dev/null
+++ b/packages/components/rate/src/Rate.vue
@@ -0,0 +1,158 @@
+<template>
+  <div class="ix-rate-wrap">
+    <div
+      v-for="(item, index) in rateCount"
+      :key="index"
+      :ref="
+        el => {
+          if (el) starRefs[index] = el
+        }
+      "
+      class="ix-rate-item"
+      :title="getTooltip(index)"
+      :style="{ cursor: disabled ? 'auto' : 'pointer' }"
+      @mousemove="handleMouseEnter(item, $event)"
+      @mouseleave="handleMouseLeave(item)"
+      @click="handleClick(item, $event)"
+    >
+      <div class="ix-rate-full">
+        <ix-icon :name="rateIcon" class="ix-rate-iconfont-main" :class="getIconClass(item)" />
+      </div>
+
+      <div v-if="showDecimalIcon(item)" class="ix-rate-half">
+        <ix-icon :name="rateIcon" class="ix-rate-iconfont-main ix-rate-half-icon" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { isArray, PropTypes, toNumber, withUndefined } from '@idux/cdk/utils'
+import { computed, defineComponent, onBeforeUpdate, ref, watchEffect } from 'vue'
+import { IxIcon } from '@idux/components/icon'
+import { useGlobalConfig } from '@idux/components/core/config'
+
+const HALF = 2
+
+export default defineComponent({
+  name: 'IxRate',
+  components: { IxIcon },
+  props: {
+    value: PropTypes.oneOfType([Number, String]).def(0),
+    count: withUndefined(PropTypes.oneOfType([Number, String])),
+    icon: PropTypes.string,
+    allowHalf: PropTypes.bool,
+    disabled: PropTypes.bool,
+    tooltips: PropTypes.array,
+    allowClear: PropTypes.bool,
+  },
+  emits: ['update:value'],
+  setup(props, { emit }) {
+    const score = ref(props.value)
+    const touchHalf = ref(false)
+    const starRefs = ref([])
+
+    const rateGlobalConfig = useGlobalConfig('rate')
+    const rateCount = computed(() => props.count ?? rateGlobalConfig.count)
+    const rateIcon = computed(() => props.icon ?? rateGlobalConfig.icon)
+    const allowHalf = computed(() => props.allowHalf ?? rateGlobalConfig.allowHalf)
+    const allowClear = computed(() => props.allowClear ?? rateGlobalConfig.allowClear)
+
+    onBeforeUpdate(() => {
+      starRefs.value = []
+    })
+
+    watchEffect(() => {
+      const value = toNumber(props.value)
+      score.value = value
+      touchHalf.value = value !== Math.floor(value)
+    })
+
+    function getIconClass(item: number) {
+      return item <= score.value ? 'ix-rate-highlight' : 'ix-rate-normal'
+    }
+
+    function showDecimalIcon(item: number) {
+      return allowHalf.value && item - 0.5 <= score.value && item > score.value
+    }
+
+    function getTooltip(index: number) {
+      if (!props.tooltips || !isArray(props.tooltips)) {
+        return
+      }
+
+      return props.tooltips[index] || ''
+    }
+
+    function handleMouseEnter(item: number, e: MouseEvent) {
+      if (props.disabled) {
+        return
+      }
+
+      if (allowHalf.value) {
+        updateTouchHalf(item - 1, e)
+        score.value = touchHalf.value ? item - 0.5 : item
+      } else {
+        score.value = item
+      }
+    }
+
+    function updateTouchHalf(index: number, e: MouseEvent) {
+      const target = starRefs.value[index] as HTMLElement
+
+      if (e.offsetX <= target.clientWidth / HALF) {
+        touchHalf.value = true
+        return
+      }
+
+      touchHalf.value = false
+    }
+
+    function handleMouseLeave() {
+      if (props.disabled) {
+        return
+      }
+
+      const value = toNumber(props.value)
+
+      if (allowHalf.value) {
+        touchHalf.value = value !== Math.floor(value)
+      }
+      score.value = value
+    }
+
+    function handleClick() {
+      if (props.disabled) {
+        return
+      }
+
+      let clearValue = false
+      let emitValue = 0
+      const beforeValue = toNumber(props.value)
+      const currentScore = toNumber(score.value)
+
+      if (allowClear.value) {
+        clearValue = beforeValue === currentScore
+      }
+
+      emitValue = clearValue ? 0 : currentScore
+
+      if (emitValue !== beforeValue) {
+        emit('update:value', emitValue)
+      }
+    }
+
+    return {
+      rateCount,
+      rateIcon,
+      getTooltip,
+      showDecimalIcon,
+      handleMouseEnter,
+      handleMouseLeave,
+      handleClick,
+      getIconClass,
+      starRefs,
+    }
+  },
+})
+</script>
diff --git a/packages/components/rate/src/types.ts b/packages/components/rate/src/types.ts
new file mode 100644
index 00000000..40bf3ef3
--- /dev/null
+++ b/packages/components/rate/src/types.ts
@@ -0,0 +1,15 @@
+import type { DefineComponent } from 'vue'
+
+interface RateOriginalProps {
+  value: number
+  count?: number
+  icon?: string
+  allowHalf?: boolean
+  disabled?: boolean
+  tooltips?: string[]
+  allowClear?: boolean
+}
+
+export type RateProps = Readonly<RateOriginalProps>
+
+export type RateComponent = InstanceType<DefineComponent<RateProps>>
diff --git a/packages/components/rate/style/index.less b/packages/components/rate/style/index.less
new file mode 100644
index 00000000..98d7d689
--- /dev/null
+++ b/packages/components/rate/style/index.less
@@ -0,0 +1,33 @@
+@import '../../style/default.less';
+
+@rate-prefix: ~'@{idux-prefix}-rate';
+
+.@{rate-prefix} {
+  &-item {
+    display: inline-block;
+    position: relative;
+  }
+  &-iconfont-main {
+    position: relative;
+    font-size: @rate-icon-size;
+    margin-right: @rate-icon-margin;
+    vertical-align: baseline;
+  }
+  &-half {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 50%;
+    height: 100%;
+    overflow: hidden;
+  }
+  &-highlight {
+    color: @rate-stroke-color;
+  }
+  &-normal {
+    color: @rate-background-color;
+  }
+  &-half-icon {
+    color: @rate-stroke-color;
+  }
+}
diff --git a/packages/components/style/default.less b/packages/components/style/default.less
index d349ed8b..159118d8 100644
--- a/packages/components/style/default.less
+++ b/packages/components/style/default.less
@@ -160,3 +160,8 @@
 @typography-ul-list-style: circle;
 @typography-sub-ul-list-style: disc;
 @typography-ol-list-style: decimal;
+/* rate -------------------------- */
+@rate-icon-size: 24px;
+@rate-icon-margin: 8px;
+@rate-stroke-color: #f7ba2a;
+@rate-background-color: #c6d1de;


Test patch:
diff --git a/packages/components/rate/__tests__/__snapshots__/rate.spec.ts.snap b/packages/components/rate/__tests__/__snapshots__/rate.spec.ts.snap
new file mode 100644
index 00000000..ddcebf69
--- /dev/null
+++ b/packages/components/rate/__tests__/__snapshots__/rate.spec.ts.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Rate.vue component render 1`] = `"<div class=\\"ix-rate-wrap\\"><div class=\\"ix-rate-item\\" style=\\"cursor: pointer;\\"><div class=\\"ix-rate-full\\"><i class=\\"ix-icon ix-icon-star ix-rate-iconfont-main ix-rate-highlight\\" role=\\"img\\" arialabel=\\"star\\"></i></div><!--v-if--></div><div class=\\"ix-rate-item\\" style=\\"cursor: pointer;\\"><div class=\\"ix-rate-full\\"><i class=\\"ix-icon ix-icon-star ix-rate-iconfont-main ix-rate-highlight\\" role=\\"img\\" arialabel=\\"star\\"></i></div><!--v-if--></div><div class=\\"ix-rate-item\\" style=\\"cursor: pointer;\\"><div class=\\"ix-rate-full\\"><i class=\\"ix-icon ix-icon-star ix-rate-iconfont-main ix-rate-highlight\\" role=\\"img\\" arialabel=\\"star\\"></i></div><!--v-if--></div><div class=\\"ix-rate-item\\" style=\\"cursor: pointer;\\"><div class=\\"ix-rate-full\\"><i class=\\"ix-icon ix-icon-star ix-rate-iconfont-main ix-rate-normal\\" role=\\"img\\" arialabel=\\"star\\"></i></div><!--v-if--></div><div class=\\"ix-rate-item\\" style=\\"cursor: pointer;\\"><div class=\\"ix-rate-full\\"><i class=\\"ix-icon ix-icon-star ix-rate-iconfont-main ix-rate-normal\\" role=\\"img\\" arialabel=\\"star\\"></i></div><!--v-if--></div></div>"`;
diff --git a/packages/components/rate/__tests__/rate.spec.ts b/packages/components/rate/__tests__/rate.spec.ts
new file mode 100644
index 00000000..3da30345
--- /dev/null
+++ b/packages/components/rate/__tests__/rate.spec.ts
@@ -0,0 +1,153 @@
+import { mount, MountingOptions, VueWrapper } from '@vue/test-utils'
+import { DefineComponent } from 'vue'
+
+import Rate from '../src/Rate.vue'
+
+import { RateProps } from '../src/types'
+
+describe('Rate.vue', () => {
+  let RateMount: (options?: MountingOptions<Partial<RateProps>>) => VueWrapper<InstanceType<DefineComponent<RateProps>>>
+  let findNormalIcon: (wrapper: VueWrapper<InstanceType<DefineComponent<RateProps>>>) => number
+
+  beforeEach(() => {
+    RateMount = (options = {}) => {
+      return mount<RateProps>(Rate, {
+        ...options,
+      })
+    }
+
+    findNormalIcon = (wrapper: VueWrapper<InstanceType<DefineComponent<RateProps>>>): number => {
+      const res = wrapper.findAll('.ix-rate-iconfont-main').filter(item => {
+        return item.classes().includes('ix-rate-normal')
+      })
+
+      return res.length
+    }
+  })
+
+  test('component render', () => {
+    const wrapper = RateMount({
+      props: {
+        value: 3,
+      },
+    })
+
+    expect(wrapper.html()).toMatchSnapshot()
+  })
+
+  test('init hightlight as value', () => {
+    const wrapper = RateMount({
+      props: {
+        value: 3,
+      },
+    })
+
+    const normalIcon = findNormalIcon(wrapper)
+
+    expect(normalIcon).toBe(2)
+  })
+
+  test('test count', () => {
+    const wrapper = RateMount({
+      props: {
+        value: 3,
+        count: 10,
+      },
+    })
+
+    expect(wrapper.findAll('.ix-rate-full').length).toBe(10)
+  })
+
+  test('test tooltip', () => {
+    const wrapper = RateMount({
+      props: {
+        value: 3,
+        tooltips: ['1', '2', '3', '4', '5'],
+      },
+    })
+
+    const titleList = wrapper.findAll('.ix-rate-item').filter(item => {
+      return item.attributes()['title']
+    })
+
+    expect(titleList.length).toBe(5)
+  })
+
+  test('test click', async () => {
+    const wrapper = RateMount({})
+
+    await wrapper.setProps({ value: 3 })
+
+    const thirdStar = wrapper.findAll('.ix-rate-item')[2].element as HTMLElement
+    thirdStar.click()
+
+    const normalIcon = findNormalIcon(wrapper)
+
+    expect(normalIcon).toBe(2)
+  })
+
+  test('touch half and allow half', async () => {
+    const wrapper = mount({
+      template: `<Rate v-model:value="value" allow-half ref="testRateRef" />`,
+      data() {
+        return {
+          value: 4,
+        }
+      },
+      components: { Rate },
+    })
+
+    const vm = wrapper.vm
+    const testHalfItem = wrapper.findAll('.ix-rate-item')[1]
+
+    await testHalfItem.trigger('mousemove', { offsetX: 0 })
+    await testHalfItem.trigger('click')
+    await testHalfItem.trigger('mouseleave')
+
+    expect(vm.value).toEqual(1.5)
+  })
+
+  test('disable', async () => {
+    const wrapper = mount({
+      template: `<Rate v-model:value="value" disabled ref="testRateRef" />`,
+      data() {
+        return {
+          value: 4,
+        }
+      },
+      components: { Rate },
+    })
+
+    const vm = wrapper.vm
+    const testItem = wrapper.findAll('.ix-rate-item')[1]
+
+    await testItem.trigger('mousemove')
+    await testItem.trigger('click')
+    await testItem.trigger('mouseleave')
+
+    expect(vm.value).toEqual(4)
+  })
+
+  test('allow clear', async () => {
+    const wrapper = mount({
+      template: `<Rate v-model:value="value" allow-clear ref="testRateRef" />`,
+      data() {
+        return {
+          value: 2,
+        }
+      },
+      components: { Rate },
+    })
+
+    await wrapper.setProps({ value: 3 })
+
+    const vm = wrapper.vm
+    const testItem = wrapper.findAll('.ix-rate-item')[2]
+
+    await testItem.trigger('mousemove', { offsetX: 26 })
+    await testItem.trigger('click')
+    await testItem.trigger('mouseleave')
+
+    expect(vm.value).toEqual(0)
+  })
+})