Repository: IduxFE/components
Pull request #190 (2021-02-05 04:15:45)

refactor: update utils and test
## 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
- [ ] Feature
- [ ] Component style update
- [ ] Code style update (formatting, local variables)
- [x] 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


codecov[bot]: # [Codecov](https://codecov.io/gh/IduxFE/components/pull/190?src=pr&el=h1) Report
> Merging [#190](https://codecov.io/gh/IduxFE/components/pull/190?src=pr&el=desc) (e86ac59) into [main](https://codecov.io/gh/IduxFE/components/commit/1472abdb9210dcdfe40b3e5d2eb87c4e432e8315?el=desc) (1472abd) will **increase** coverage by `3.10%`.
> The diff coverage is `95.55%`.

[![Impacted file tree graph](https://codecov.io/gh/IduxFE/components/pull/190/graphs/tree.svg?width=650&height=150&src=pr&token=PGAUXP06V3)](https://codecov.io/gh/IduxFE/components/pull/190?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main     #190      +/-   ##
==========================================
+ Coverage   92.92%   96.03%   +3.10%     
==========================================
  Files          20       82      +62     
  Lines         297     1740    +1443     
  Branches       54      297     +243     
==========================================
+ Hits          276     1671    +1395     
- Misses         10       34      +24     
- Partials       11       35      +24     
```


| [Impacted Files](https://codecov.io/gh/IduxFE/components/pull/190?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/components/input/src/useAutoRows.ts](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29tcG9uZW50cy9pbnB1dC9zcmMvdXNlQXV0b1Jvd3MudHM=) | `67.85% <0.00%> (ø)` | |
| [packages/cdk/overlay/src/useOverlay.ts](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL292ZXJsYXkvc3JjL3VzZU92ZXJsYXkudHM=) | `92.63% <92.85%> (ø)` | |
| [packages/cdk/utils/typeof.ts](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL3V0aWxzL3R5cGVvZi50cw==) | `100.00% <100.00%> (ø)` | |
| [packages/components/back-top/src/BackTop.vue](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29tcG9uZW50cy9iYWNrLXRvcC9zcmMvQmFja1RvcC52dWU=) | `100.00% <100.00%> (ø)` | |
| [packages/components/image/src/Image.vue](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29tcG9uZW50cy9pbWFnZS9zcmMvSW1hZ2UudnVl) | `96.96% <0.00%> (ø)` | |
| [...ackages/components/icon/definitions/closeCircle.ts](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29tcG9uZW50cy9pY29uL2RlZmluaXRpb25zL2Nsb3NlQ2lyY2xlLnRz) | `100.00% <0.00%> (ø)` | |
| [packages/components/button/src/Button.vue](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29tcG9uZW50cy9idXR0b24vc3JjL0J1dHRvbi52dWU=) | `100.00% <0.00%> (ø)` | |
| [packages/components/space/src/Space.vue](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29tcG9uZW50cy9zcGFjZS9zcmMvU3BhY2UudnVl) | `100.00% <0.00%> (ø)` | |
| [packages/cdk/portal/src/Portal.vue](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL3BvcnRhbC9zcmMvUG9ydGFsLnZ1ZQ==) | `100.00% <0.00%> (ø)` | |
| [packages/cdk/overlay/src/usePopperOptions.ts](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL292ZXJsYXkvc3JjL3VzZVBvcHBlck9wdGlvbnMudHM=) | `100.00% <0.00%> (ø)` | |
| ... and [58 more](https://codecov.io/gh/IduxFE/components/pull/190/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/IduxFE/components/pull/190?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/IduxFE/components/pull/190?src=pr&el=footer). Last update [1472abd...7c31494](https://codecov.io/gh/IduxFE/components/pull/190?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/packages/cdk/overlay/src/useOverlay.ts b/packages/cdk/overlay/src/useOverlay.ts
index a6341505..1e2b6fcb 100644
--- a/packages/cdk/overlay/src/useOverlay.ts
+++ b/packages/cdk/overlay/src/useOverlay.ts
@@ -11,9 +11,9 @@ import type {
   TriggerElement,
 } from './types'
 
-import { ComponentPublicInstance, computed, reactive, ref, watch } from 'vue'
+import { computed, reactive, ref, watch } from 'vue'
 import { createPopper } from '@popperjs/core'
-import { isHTMLElement, uniqueId } from '@idux/cdk/utils'
+import { isHTMLElement, off, on, uniqueId } from '@idux/cdk/utils'
 
 import { usePopperOptions } from './usePopperOptions'
 
@@ -46,10 +46,10 @@ export const useOverlay = (options: OverlayOptions): OverlayInstance => {
     if (!unrefTrigger) {
       return
     }
-    const triggerElement = isHTMLElement(unrefTrigger) ? unrefTrigger : (unrefTrigger as ComponentPublicInstance).$el
+    const triggerElement = isHTMLElement(unrefTrigger) ? unrefTrigger : unrefTrigger.$el
     popperInstance = createPopper(triggerElement, overlayElement as HTMLElement, popperOptions.value)
     popperInstance.update()
-    window.addEventListener('scroll', globalScroll)
+    on(window, 'scroll', globalScroll)
   }
 
   const _toggle = (visible: boolean) => {
@@ -57,8 +57,14 @@ export const useOverlay = (options: OverlayOptions): OverlayInstance => {
   }
 
   const _clearTimer = () => {
-    window.clearTimeout(showTimer as number)
-    window.clearTimeout(hideTimer as number)
+    if (showTimer) {
+      clearTimeout(showTimer)
+      showTimer = null
+    }
+    if (hideTimer) {
+      clearTimeout(hideTimer)
+      hideTimer = null
+    }
   }
 
   const show = (immediate?: boolean): void => {
@@ -66,7 +72,7 @@ export const useOverlay = (options: OverlayOptions): OverlayInstance => {
     if (immediate || state.showDelay === 0) {
       _toggle(true)
     } else {
-      showTimer = window.setTimeout(() => {
+      showTimer = setTimeout(() => {
         _toggle(true)
       }, state.showDelay)
     }
@@ -77,7 +83,7 @@ export const useOverlay = (options: OverlayOptions): OverlayInstance => {
     if (immediate || state.hideDelay === 0) {
       _toggle(false)
     } else {
-      hideTimer = window.setTimeout(() => {
+      hideTimer = setTimeout(() => {
         _toggle(false)
       }, state.hideDelay)
     }
@@ -89,7 +95,7 @@ export const useOverlay = (options: OverlayOptions): OverlayInstance => {
     }
     popperInstance.destroy()
     popperInstance = null
-    window.removeEventListener('scroll', globalScroll)
+    off(window, 'scroll', globalScroll)
   }
 
   const update = (options: Partial<OverlayOptions>): void => {
@@ -153,7 +159,10 @@ export const useOverlay = (options: OverlayOptions): OverlayInstance => {
 
   const onOverlayMouseEnter = () => {
     if (state.allowEnter && state.trigger !== 'click') {
-      clearTimeout(hideTimer as number)
+      if (hideTimer) {
+        clearTimeout(hideTimer)
+        hideTimer = null
+      }
     }
   }
 
diff --git a/packages/cdk/utils/dom.ts b/packages/cdk/utils/dom.ts
index 8e48d618..27be7dc6 100644
--- a/packages/cdk/utils/dom.ts
+++ b/packages/cdk/utils/dom.ts
@@ -1,28 +1,38 @@
-/* istanbul ignore file */
-
-const trim = (s: string) => (s || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
+/* eslint-disable @typescript-eslint/no-explicit-any */
 
-export const raf = window.requestAnimationFrame || (cb => window.setTimeout(cb, 1000 / 60))
+type ElType = HTMLElement | Document | Window
 
+export function on<K extends keyof HTMLElementEventMap>(
+  el: ElType,
+  type: K,
+  listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
+  options?: boolean | AddEventListenerOptions,
+): void
 export function on(
-  el: HTMLElement | Document | Window,
+  el: ElType,
   type: string,
   listener: EventListenerOrEventListenerObject,
-  options?: boolean | EventListenerOptions | undefined,
+  options?: boolean | AddEventListenerOptions,
 ): void {
   if (el && type && listener) {
-    el.addEventListener(type, listener, options ?? true)
+    el.addEventListener(type, listener, options)
   }
 }
 
+export function off<K extends keyof HTMLElementEventMap>(
+  el: ElType,
+  type: K,
+  listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
+  options?: boolean | EventListenerOptions,
+): void
 export function off(
-  el: HTMLElement | Document | Window,
+  el: ElType,
   type: string,
   listener: EventListenerOrEventListenerObject,
-  options?: boolean | EventListenerOptions | undefined,
+  options?: boolean | EventListenerOptions,
 ): void {
   if (el && type && listener) {
-    el.removeEventListener(type, listener, options ?? true)
+    el.removeEventListener(type, listener, options)
   }
 }
 
@@ -30,73 +40,30 @@ export function hasClass(el: HTMLElement, cls: string): boolean {
   if (!el || !cls) {
     return false
   }
-  if (cls.indexOf(' ') !== -1) {
-    throw new Error('className should not contain space.')
-  }
-  if (el.classList) {
-    return el.classList.contains(cls)
-  } else {
-    return ` ${el.className} `.indexOf(` ${cls} `) > -1
-  }
+  return cls
+    .split(' ')
+    .filter(item => item)
+    .every(item => el.classList.contains(item))
 }
 
 export function addClass(el: HTMLElement, cls: string): void {
-  if (!el) {
+  if (!el || !cls) {
     return
   }
-  let curClass = el.className
-  const classes = (cls || '').split(' ')
-
-  for (let i = 0, j = classes.length; i < j; i++) {
-    const clsName = classes[i]
-    if (!clsName) {
-      continue
-    }
-
-    if (el.classList) {
-      el.classList.add(clsName)
-    } else if (!hasClass(el, clsName)) {
-      curClass += ' ' + clsName
-    }
-  }
-  if (!el.classList) {
-    el.className = curClass
-  }
+  cls
+    .split(' ')
+    .filter(item => item)
+    .forEach(item => el.classList.add(item))
 }
 
 export function removeClass(el: HTMLElement, cls: string): void {
   if (!el || !cls) {
     return
   }
-  const classes = cls.split(' ')
-  let curClass = ` ${el.className} `
-
-  for (let i = 0, j = classes.length; i < j; i++) {
-    const clsName = classes[i]
-    if (!clsName) {
-      continue
-    }
-
-    if (el.classList) {
-      el.classList.remove(clsName)
-    } else if (hasClass(el, clsName)) {
-      curClass = curClass.replace(` ${clsName} `, ' ')
-    }
-  }
-  if (!el.classList) {
-    el.className = trim(curClass)
-  }
+  cls
+    .split(' ')
+    .filter(item => item)
+    .forEach(item => el.classList.remove(item))
 }
 
-/* eslint-disable @typescript-eslint/no-explicit-any */
-/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
-export function isHTMLElement(el: any): boolean {
-  const div = document.createElement('div')
-
-  try {
-    div.appendChild(el.cloneNode(true))
-    return el.nodeType == 1 ? true : false
-  } catch {
-    return false
-  }
-}
+export const rAF = requestAnimationFrame || (cb => setTimeout(cb, 1000 / 60))
diff --git a/packages/cdk/utils/typeof.ts b/packages/cdk/utils/typeof.ts
index 0ed3a468..541831d2 100644
--- a/packages/cdk/utils/typeof.ts
+++ b/packages/cdk/utils/typeof.ts
@@ -76,4 +76,4 @@ export function hasOwnProperty(val: object, key: string | symbol): key is keyof
   return Object.prototype.hasOwnProperty.call(val, key)
 }
 
-export const isHTMLElement = (val: unknown): boolean => toRawType(val).startsWith('HTML')
+export const isHTMLElement = (val: unknown): val is HTMLElement => toRawType(val).startsWith('HTML')
diff --git a/packages/components/back-top/src/BackTop.vue b/packages/components/back-top/src/BackTop.vue
index 8d46e3e4..30c58ad2 100644
--- a/packages/components/back-top/src/BackTop.vue
+++ b/packages/components/back-top/src/BackTop.vue
@@ -12,33 +12,74 @@
 import type { SetupContext } from 'vue'
 import type { BackTopProps } from './types'
 
-import { defineComponent, ref, onUnmounted, onMounted, nextTick } from 'vue'
+import { defineComponent, nextTick, onUnmounted, onMounted, ref } from 'vue'
+import throttle from 'lodash/throttle'
 import { IxIcon } from '@idux/components/icon'
-import { PropTypes, withUndefined, isString, isHTMLElement, on, off, raf } from '@idux/cdk/utils'
+import { PropTypes, withUndefined, isString, isHTMLElement, on, off, rAF } from '@idux/cdk/utils'
 import { easeInOutQuad } from '@idux/components/core/utils'
 import { useGlobalConfig } from '@idux/components/core/config'
-import throttle from 'lodash/throttle'
 import { Logger } from '@idux/components/core/logger'
 
-const getTarget = (target: HTMLElement | string | undefined) => {
-  const defaultTarget = window
+export default defineComponent({
+  name: 'IxBackTop',
+  components: { IxIcon },
+  props: {
+    target: withUndefined(PropTypes.oneOfType([PropTypes.string, HTMLElement])),
+    duration: PropTypes.number,
+    visibilityHeight: PropTypes.number,
+  },
+  emits: ['click'],
+  setup(props: BackTopProps, { emit }: SetupContext) {
+    const backTopConfig = useGlobalConfig('backTop')
+    const eventType = 'scroll'
+    const visible = ref(false)
+    const container = ref<Window | HTMLElement>((null as unknown) as HTMLElement)
 
-  if (isString(target)) {
-    const contaniner = document.querySelector<HTMLElement>(target)
+    const handleScroll = () => {
+      visible.value = getCurrentScrollTop(container.value) >= (props.visibilityHeight ?? backTopConfig.visibilityHeight)
+    }
+    const handleClick = (event: MouseEvent) => {
+      scrollToTop(0, props.duration ?? backTopConfig.duration, container.value)
+      emit('click', event)
+    }
+
+    const throttledScrollHandler = throttle(handleScroll, 300)
+
+    onMounted(() => {
+      nextTick(() => {
+        container.value = getTarget(props.target)
+        on(container.value, eventType, throttledScrollHandler)
+        handleScroll()
+      })
+    })
+
+    onUnmounted(() => {
+      off(container.value, eventType, throttledScrollHandler)
+    })
 
-    if (contaniner) {
-      return contaniner
+    return {
+      visible,
+      handleClick,
     }
+  },
+})
 
-    Logger.warn(`target does not exist: ${target}, default value are already used: window.`)
-    return defaultTarget
+const getTarget = (target: HTMLElement | string | undefined) => {
+  if (isHTMLElement(target)) {
+    return target
   }
 
-  if (isHTMLElement(target)) {
-    return target as HTMLElement
+  if (isString(target)) {
+    const container = document.querySelector<HTMLElement>(target)
+
+    if (container) {
+      return container
+    } else {
+      Logger.warn(`target does not exist: ${target}, default value are already used: window.`)
+    }
   }
 
-  return defaultTarget
+  return window
 }
 
 const getCurrentScrollTop = (currTarget: Window | HTMLElement) => {
@@ -49,7 +90,7 @@ const getCurrentScrollTop = (currTarget: Window | HTMLElement) => {
   return (currTarget as HTMLElement).scrollTop
 }
 
-const scrollTo = (y: number, duration: number, container: HTMLElement | Window) => {
+const scrollToTop = (y: number, duration: number, container: HTMLElement | Window) => {
   const scrollTop = getCurrentScrollTop(container)
   const startTime = Date.now()
   const change = y - scrollTop
@@ -65,52 +106,10 @@ const scrollTo = (y: number, duration: number, container: HTMLElement | Window)
     }
 
     if (time < duration) {
-      raf(frameFunc)
+      rAF(frameFunc)
     }
   }
 
-  raf(frameFunc)
+  rAF(frameFunc)
 }
-
-export default defineComponent({
-  name: 'IxBackTop',
-  components: { IxIcon },
-  props: {
-    target: withUndefined(PropTypes.oneOfType([PropTypes.string, HTMLElement])),
-    duration: PropTypes.number,
-    visibilityHeight: PropTypes.number,
-  },
-  emits: ['click'],
-  setup(props: BackTopProps, { emit }: SetupContext) {
-    const backTopConfig = useGlobalConfig('backTop')
-    const eventType = 'scroll'
-    const visible = ref(false)
-    const container = ref<Window | HTMLElement>((null as unknown) as HTMLElement)
-
-    const handleScroll = () => {
-      visible.value = getCurrentScrollTop(container.value) >= (props.visibilityHeight ?? backTopConfig.visibilityHeight)
-    }
-    const handleClick = (event: MouseEvent) => {
-      scrollTo(0, props.duration ?? backTopConfig.duration, container.value)
-      emit('click', event)
-    }
-
-    const throttledScrollHandler = throttle(handleScroll, 300)
-
-    onMounted(async () => {
-      await nextTick()
-      container.value = getTarget(props.target)
-      on(container.value, eventType, throttledScrollHandler)
-      handleScroll()
-    })
-    onUnmounted(() => {
-      off(container.value, eventType, throttledScrollHandler)
-    })
-
-    return {
-      visible,
-      handleClick,
-    }
-  },
-})
 </script>
diff --git a/packages/components/core/logger/index.ts b/packages/components/core/logger/index.ts
index 72f519df..d095dee0 100644
--- a/packages/components/core/logger/index.ts
+++ b/packages/components/core/logger/index.ts
@@ -1,5 +1,4 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
-/* istanbul ignore file */
 import { IDUX_COMPONENTS_PREFIX } from '../constant'
 import { isDevMode } from '../utils'
 
diff --git a/packages/components/core/utils/easingFunctions.ts b/packages/components/core/utils/easingFunctions.ts
index fd05aaa0..63997882 100644
--- a/packages/components/core/utils/easingFunctions.ts
+++ b/packages/components/core/utils/easingFunctions.ts
@@ -1,4 +1,3 @@
-/* istanbul ignore file */
 /**
  * origin: https://github.com/bameyrick/js-easing-functions/blob/master/src/index.ts
  *
diff --git a/packages/components/core/utils/installComponent.ts b/packages/components/core/utils/installComponent.ts
index de1e2a47..8fcc9b91 100644
--- a/packages/components/core/utils/installComponent.ts
+++ b/packages/components/core/utils/installComponent.ts
@@ -1,4 +1,3 @@
-/* istanbul ignore file */
 import type { App, DefineComponent } from 'vue'
 
 export const installComponent = (component: DefineComponent) => {
diff --git a/packages/components/i18n/useI18n.ts b/packages/components/i18n/useI18n.ts
index 079375f6..6acb80fb 100644
--- a/packages/components/i18n/useI18n.ts
+++ b/packages/components/i18n/useI18n.ts
@@ -39,9 +39,7 @@ export function getLocale(): ComputedRef<Locale>
 export function getLocale<T extends LocaleKey>(key?: T): ComputedRef<Locale | Locale[T]> {
   return computed(() => {
     let currLocale = localeMap[currentType.value]
-    /* istanbul ignore next */
     if (!currLocale) {
-      /* istanbul ignore next */
       currLocale = defaultLocale
     }
     return key ? currLocale[key] : currLocale
diff --git a/packages/components/input/src/useAutoRows.ts b/packages/components/input/src/useAutoRows.ts
index 2002e3c6..d9fb4fa8 100644
--- a/packages/components/input/src/useAutoRows.ts
+++ b/packages/components/input/src/useAutoRows.ts
@@ -6,7 +6,7 @@ import type { TextareaAutoRows } from './types'
 
 import { nextTick, onMounted, onUnmounted, watch, watchEffect } from 'vue'
 import { isFirefox } from '@idux/cdk/platform'
-import { isObject, isNumber, on, off } from '@idux/cdk/utils'
+import { isObject, isNumber, on, off, rAF } from '@idux/cdk/utils'
 
 const isAutoRowsObject = (value: unknown): value is TextareaAutoRows => {
   return isObject(value) && isNumber(value.minRows) && isNumber(value.maxRows)
@@ -81,13 +81,7 @@ export function useAutoRows(
     textarea.classList.remove(measuringClass)
     textarea.placeholder = placeholderText
 
-    nextTick(() => {
-      if (typeof requestAnimationFrame !== 'undefined') {
-        requestAnimationFrame(() => scrollToCaretPosition(textarea))
-      } else {
-        setTimeout(() => scrollToCaretPosition(textarea))
-      }
-    })
+    nextTick(() => rAF(() => scrollToCaretPosition(textarea)))
 
     previousValue = value
     previousMinRows = minRows
diff --git a/typings/global.d.ts b/typings/global.d.ts
index 1531059b..04e6bb4b 100644
--- a/typings/global.d.ts
+++ b/typings/global.d.ts
@@ -5,3 +5,6 @@ declare type OptionalKeys<T> = { [K in keyof T]-?: IsNullable<T[K], K> }[keyof T
 declare type RequiredKeys<T> = keyof Omit<T, OptionalKeys<T>>
 
 declare type Nullable<T> = T | null
+
+declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number
+declare function clearTimeout(handle?: number): void


Test patch:
diff --git a/packages/cdk/clipboard/__tests__/useClipboard.spec.ts b/packages/cdk/clipboard/__tests__/useClipboard.spec.ts
index 8c02185f..64d8ffac 100644
--- a/packages/cdk/clipboard/__tests__/useClipboard.spec.ts
+++ b/packages/cdk/clipboard/__tests__/useClipboard.spec.ts
@@ -49,12 +49,11 @@ describe('useClipboard.ts', () => {
 
   test('unmounted work', async () => {
     const wrapper = mount(getComp())
-
-    window.clearTimeout = jest.fn().mockReturnValue(true)
+    const clearTimeout = jest.spyOn(window, 'clearTimeout')
     document.execCommand = jest.fn().mockReturnValue(false)
     wrapper.vm.copy(copyText, 3)
     wrapper.unmount()
 
-    expect(window.clearTimeout).toHaveBeenCalledTimes(1)
+    expect(clearTimeout).toHaveBeenCalledTimes(1)
   })
 })
diff --git a/packages/cdk/overlay/__tests__/overlay.spec.ts b/packages/cdk/overlay/__tests__/overlay.spec.ts
index ef9d0f2b..a04ccd29 100644
--- a/packages/cdk/overlay/__tests__/overlay.spec.ts
+++ b/packages/cdk/overlay/__tests__/overlay.spec.ts
@@ -1,8 +1,8 @@
 import type { OverlayOptions, OverlayTrigger } from '@idux/cdk/overlay'
 
-import { defineComponent, onMounted, onUpdated, PropType, unref } from 'vue'
+import { onMounted, onUpdated, PropType, unref } from 'vue'
 import { mount } from '@vue/test-utils'
-import { IxButton } from '@idux/components'
+import { IxButton } from '@idux/components/button'
 import { useOverlay } from '../src/useOverlay'
 
 const defaultOverlayOptions: OverlayOptions = {
@@ -33,7 +33,7 @@ describe('useOverlay.ts', () => {
   })
 
   test('visible work', async () => {
-    const TestComponent = defineComponent({
+    const TestComponent = {
       setup() {
         const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, visibility, show, hide } = useOverlay(
           options,
@@ -52,7 +52,7 @@ describe('useOverlay.ts', () => {
       <button id="immediate" @click="handleClick">Immediate Toggle</button>
       <div id="overlay" ref="overlayRef" @mouseenter="overlayEvents.onMouseEnter" @mouseleave="overlayEvents.onMouseLeave">Overlay</div>
       `,
-    })
+    }
     const wrapper = mount(TestComponent)
     expect(wrapper.get('#overlay').attributes('style')).toContain('display: none;')
 
@@ -72,7 +72,7 @@ describe('useOverlay.ts', () => {
   })
 
   test('component trigger work', async () => {
-    const TestComponent = defineComponent({
+    const TestComponent = {
       components: { IxButton },
       setup() {
         const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents } = useOverlay({
@@ -89,7 +89,7 @@ describe('useOverlay.ts', () => {
       <ix-button id="trigger" ref="triggerRef" @click="triggerEvents.onClick">Trigger</ix-button>
       <div id="overlay" ref="overlayRef" @mouseenter="overlayEvents.onMouseEnter" @mouseleave="overlayEvents.onMouseLeave">Overlay</div>
       `,
-    })
+    }
     const wrapper = mount(TestComponent)
     await wrapper.get('#trigger').trigger('click')
     expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;')
@@ -99,7 +99,7 @@ describe('useOverlay.ts', () => {
   })
 
   test('destroy work', async () => {
-    const TestComponent = defineComponent({
+    const TestComponent = {
       setup() {
         const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, destroy } = useOverlay({
           ...options,
@@ -111,7 +111,6 @@ describe('useOverlay.ts', () => {
 
         const handleClick = () => {
           destroy()
-          console.log('destroy')
         }
 
         return { overlayRef, triggerRef, triggerEvents, overlayEvents, handleClick }
@@ -121,16 +120,16 @@ describe('useOverlay.ts', () => {
       <button id="destroy" @click="handleClick">Destroy</button>
       <div id="overlay" ref="overlayRef" @mouseenter="overlayEvents.onMouseEnter" @mouseleave="overlayEvents.onMouseLeave">Overlay</div>
       `,
-    })
+    }
     const wrapper = mount(TestComponent)
 
-    const log = jest.spyOn(console, 'log')
     await wrapper.get('#destroy').trigger('click')
-    expect(log).toBeCalled()
+
+    // TODO expect
   })
 
   test('update work', async () => {
-    const TestComponent = defineComponent({
+    const TestComponent = {
       components: { IxButton },
       setup() {
         const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, update } = useOverlay(options)
@@ -148,7 +147,7 @@ describe('useOverlay.ts', () => {
       <button id="update" @click="handleClick">Update</button>
       <div id="overlay" ref="overlayRef" @mouseenter="overlayEvents.onMouseEnter" @mouseleave="overlayEvents.onMouseLeave">Overlay</div>
       `,
-    })
+    }
     const wrapper = mount(TestComponent)
 
     await wrapper.get('#trigger').trigger('click')
@@ -165,7 +164,7 @@ describe('useOverlay.ts', () => {
   })
 
   test('trigger work', async () => {
-    const TestComponent = defineComponent({
+    const TestComponent = {
       components: { IxButton },
       props: {
         trigger: {
@@ -173,7 +172,7 @@ describe('useOverlay.ts', () => {
           default: 'click',
         },
       },
-      setup(props) {
+      setup(props: { trigger: OverlayTrigger }) {
         const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, update } = useOverlay({
           ...options,
           showDelay: 0,
@@ -193,7 +192,7 @@ describe('useOverlay.ts', () => {
       <button id="trigger" ref="triggerRef" @focus='triggerEvents.onFocus' @blur='triggerEvents.onBlur' @mouseenter='triggerEvents.onMouseEnter' @mouseleave='triggerEvents.onMouseLeave' @click="triggerEvents.onClick">Trigger</button>
       <div id="overlay" ref="overlayRef" @mouseenter="overlayEvents.onMouseEnter" @mouseleave="overlayEvents.onMouseLeave">Overlay</div>
       `,
-    })
+    }
 
     const wrapper = mount(TestComponent)
     await wrapper.get('#trigger').trigger('click')
@@ -219,7 +218,7 @@ describe('useOverlay.ts', () => {
   })
 
   test('hover overlay work', async () => {
-    const TestComponent = defineComponent({
+    const TestComponent = {
       components: { IxButton },
       setup() {
         const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents } = useOverlay({
@@ -239,7 +238,7 @@ describe('useOverlay.ts', () => {
       <button id="trigger" ref="triggerRef" @mouseenter='triggerEvents.onMouseEnter' @mouseleave='triggerEvents.onMouseLeave'>Trigger</button>
       <div id="overlay" ref="overlayRef" @mouseenter="overlayEvents.onMouseEnter" @mouseleave="overlayEvents.onMouseLeave">Overlay</div>
       `,
-    })
+    }
     const wrapper = mount(TestComponent)
     await wrapper.get('#overlay').trigger('mouseenter')
     expect(wrapper.get('#overlay').attributes('style')).toContain('display: block;')
@@ -249,7 +248,7 @@ describe('useOverlay.ts', () => {
   })
 
   test('arrow work', async () => {
-    const TestComponent = defineComponent({
+    const TestComponent = {
       components: { IxButton },
       setup() {
         const { initialize, overlayRef, triggerRef, triggerEvents, overlayEvents, arrowRef } = useOverlay({
@@ -268,7 +267,7 @@ describe('useOverlay.ts', () => {
         <div ref="arrowRef" id='arrow'></div>
       </div>
       `,
-    })
+    }
     const wrapper = mount(TestComponent)
     await wrapper.get('#trigger').trigger('click')
     expect(wrapper.get('#arrow')).toBeDefined()
diff --git a/packages/components/back-top/__test__/backTop.spec.ts b/packages/components/back-top/__test__/backTop.spec.ts
index d833a06d..9cfcb798 100644
--- a/packages/components/back-top/__test__/backTop.spec.ts
+++ b/packages/components/back-top/__test__/backTop.spec.ts
@@ -1,7 +1,7 @@
 import { mount } from '@vue/test-utils'
+import IxBackTop from '../src/BackTop.vue'
+import { renderWork, waitRAF, isShow, wait, scrollTarget } from '@tests'
 import { nextTick } from 'vue'
-import BackTop from '../src/BackTop.vue'
-import { renderWork, waitRAF, isShow, wait } from '@tests'
 
 const mockFn = jest.fn()
 const warn = jest.spyOn(console, 'warn').mockImplementation()
@@ -9,9 +9,7 @@ const warn = jest.spyOn(console, 'warn').mockImplementation()
 const backTopMount = (template: string, options = {}) =>
   mount(
     {
-      components: {
-        BackTop,
-      },
+      components: { IxBackTop },
       template,
       ...options,
     },
@@ -19,14 +17,14 @@ const backTopMount = (template: string, options = {}) =>
   )
 
 describe('BackTop.vue', () => {
-  renderWork(BackTop)
+  renderWork(IxBackTop)
 
   test('scroll work', async () => {
     const wrapper = backTopMount(
       `
       <div class="ix-back-top-test" style="height: 300px; overflow: auto;">
         <div style="height: 1000px;">
-          <back-top :duration="100" :visibilityHeight="200" target=".ix-back-top-test"></back-top>
+          <ix-back-top :duration="100" :visibilityHeight="200" target=".ix-back-top-test" />
         </div>
       </div>
     `,
@@ -39,13 +37,13 @@ describe('BackTop.vue', () => {
 
     expect(isShow(wrapper.find('.ix-back-top'))).toBe(false)
 
-    wrapper.element.scrollTop = 600
-    await wrapper.trigger('scroll')
-    await wait(1000)
+    await scrollTarget(600, wrapper.element)
+
     expect(isShow(wrapper.find('.ix-back-top'))).toBe(true)
 
     await wrapper.find('.ix-back-top').trigger('click')
-    await wait(1000)
+    await wait(200)
+
     expect(wrapper.element.scrollTop).toBe(0)
 
     wrapper.unmount()
@@ -56,7 +54,7 @@ describe('BackTop.vue', () => {
     const wrapper = backTopMount(
       `
       <div style="height: 1000px;">
-        <back-top :target="target"></back-top>
+        <ix-back-top :target="target" />
       </div>
     `,
       {
@@ -70,39 +68,32 @@ describe('BackTop.vue', () => {
 
     expect(isShow(wrapper.find('.ix-back-top'))).toBe(false)
 
-    document.documentElement.scrollTop = 600
-    window.dispatchEvent(new Event('scroll'))
-
-    await wait(1000)
+    await scrollTarget(600, document.documentElement)
 
     expect(isShow(wrapper.find('.ix-back-top'))).toBe(true)
   })
 
   test('props work: target does not exist', async () => {
-    mount(BackTop, {
-      props: {
-        target: '#ix-back-top-test',
-      },
-    })
+    mount(IxBackTop, { props: { target: '#ix-back-top-test' } })
 
     await nextTick()
+
     expect(warn).toBeCalled()
   })
 
   test('props work: target is the default value', async () => {
     window.scrollTo = mockFn
+
     const wrapper = backTopMount(`
       <div style="height: 1000px; overflow: auto;">
-        <back-top></back-top>
+        <ix-back-top />
       </div>
     `)
 
-    document.documentElement.scrollTop = 600
-    window.dispatchEvent(new Event('scroll'))
-    await wait(1000)
+    await scrollTarget(600, window)
     await wrapper.find('.ix-back-top').trigger('click')
-
     await waitRAF()
+
     expect(mockFn).toBeCalled()
   })
 })
diff --git a/tests/utils.ts b/tests/utils.ts
index b028659b..5c54ea17 100644
--- a/tests/utils.ts
+++ b/tests/utils.ts
@@ -11,17 +11,31 @@ export const waitRAF = (): Promise<unknown> => {
   return new Promise(resolve => requestAnimationFrame(resolve))
 }
 
-export const resize = (width = window.innerWidth, height = window.innerHeight): Promise<unknown> => {
-  ;(window as any).innerWidth = width
-  ;(window as any).innerHeight = height
-  window.dispatchEvent(new Event('resize'))
-  return wait(100)
+export const resizeTarget = (
+  width?: number,
+  height?: number,
+  target: Window | Element = window,
+  timeout = 200,
+): Promise<unknown> => {
+  if (target === window) {
+    ;(target as any).innerWidth = width || target.innerWidth
+    ;(target as any).innerHeight = height || target.innerHeight
+  } else {
+    ;(target as any).clientWidth = width || (target as any).clientWidth
+    ;(target as any).clientHeight = height || (target as any).clientHeight
+  }
+  target.dispatchEvent(new Event('resize'))
+  return wait(timeout)
 }
 
-export const scroll = (y: number): Promise<unknown> => {
-  ;(window as any).pageYOffset = y
-  window.dispatchEvent(new Event('scroll'))
-  return wait(100)
+export const scrollTarget = (y: number, target: Window | Element = window, timeout = 200): Promise<unknown> => {
+  if (target === window) {
+    ;(target as any).pageYOffset = y
+  } else {
+    ;(target as any).scrollTop = y
+  }
+  target.dispatchEvent(new Event('scroll'))
+  return wait(timeout)
 }
 
 export const renderWork = (component: any, options = {}): void => {