Repository: ant-design/ant-design-mobile (ts)

Issue:
在浏览器开启翻译插件时，Button的loading属性改变会导致报错
### Version of antd-mobile

5.13.1

### Description

https://xfrhns.csb.app/
用浏览器打开上面的链接，先开启浏览器的翻译，然后点击按钮就会报错

https://codesandbox.io/s/gallant-drake-xfrhns?file=/src/App.tsx
源码地址


问题原因：
Button组件部分源码如下：
```js
{loading ? (
  <div className={`${classPrefix}-loading-wrapper`}>
    {props.loadingIcon}
    {props.loadingText}
  </div>
) : (
  props.children
)}
```

浏览器的翻译会给文本节点加上`font`标签，然后改变`loading`属性，react-dom在移除`props.children`时，找不到对应的文本节点就会报错

```js
NotFoundError
Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
```

解决方案？
可否给`props.children`加上一个`span`标签或者`div`标签？这样loading改变时，移除`span`或`div`节点就不会报错。

Pull request:
fix(Button): fix React's bug when the browser translation function is enabled
close #5442 

参考： https://github.com/facebook/react/issues/11538#issuecomment-390386520

Patch:
diff --git a/package.json b/package.json
index f01afd4a5..c00eefb2d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "antd-mobile",
-  "version": "5.18.0",
+  "version": "5.17.2",
   "scripts": {
     "start": "dumi dev",
     "build": "gulp",
diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx
index fb77dea43..693354168 100644
--- a/src/components/button/button.tsx
+++ b/src/components/button/button.tsx
@@ -118,7 +118,7 @@ export const Button = forwardRef<ButtonRef, ButtonProps>((p, ref) => {
           {props.loadingText}
         </div>
       ) : (
-        props.children
+        <span>{props.children}</span>
       )}
     </button>
   )
diff --git a/src/components/number-keyboard/number-keyboard.less b/src/components/number-keyboard/number-keyboard.less
index 7cb791d94..29091cfec 100644
--- a/src/components/number-keyboard/number-keyboard.less
+++ b/src/components/number-keyboard/number-keyboard.less
@@ -5,7 +5,7 @@
     display: flex;
     flex-wrap: wrap;
     flex: 1;
-    &&-confirmed-style .@{class-prefix-number-keyboard}-key-sign {
+    &.confirmed-style .sign-key {
       background-color: var(--adm-color-white);
     }
   }
@@ -37,7 +37,7 @@
       padding: 0 12px;
       font-size: 16px;
     }
-    &&-with-title {
+    &.with-title {
       justify-content: space-between;
       .@{class-prefix-number-keyboard}-header-close-button {
         padding-right: 0;
@@ -93,28 +93,28 @@
       box-sizing: content-box;
     }
 
-    &&-mid {
+    &.mid-key {
       flex-basis: 66.67%;
     }
 
-    &&-sign:active::before {
+    &.sign-key:active::before {
       opacity: 0.1;
     }
 
-    &&-extra {
+    &.extra-key {
       border-left: solid 1px var(--adm-color-border);
       border-right: none;
     }
 
-    &&-extra,
-    &&-number {
+    &.extra-key,
+    &.number-key {
       background-color: var(--adm-color-background);
       &:active::before {
         opacity: 0.1;
       }
     }
 
-    &&-ok {
+    &.ok-key {
       background-color: var(--adm-color-primary);
       color: var(--adm-color-white);
       height: 144px;
@@ -122,4 +122,8 @@
       border: none;
     }
   }
+
+  &-bs-key {
+    height: 20px;
+  }
 }
diff --git a/src/components/number-keyboard/number-keyboard.tsx b/src/components/number-keyboard/number-keyboard.tsx
index e76a6da7b..dfe187db1 100644
--- a/src/components/number-keyboard/number-keyboard.tsx
+++ b/src/components/number-keyboard/number-keyboard.tsx
@@ -121,7 +121,7 @@ export const NumberKeyboard: React.FC<NumberKeyboardProps> = p => {
     return (
       <div
         className={classNames(`${classPrefix}-header`, {
-          [`${classPrefix}-header-with-title`]: !!title,
+          'with-title': !!title,
         })}
       >
         {title && <div className={`${classPrefix}-title`}>{title}</div>}
@@ -145,9 +145,9 @@ export const NumberKeyboard: React.FC<NumberKeyboardProps> = p => {
   const renderKey = (key: string, index: number) => {
     const isNumberKey = /^\d$/.test(key)
     const className = classNames(`${classPrefix}-key`, {
-      [`${classPrefix}-key-number`]: isNumberKey,
-      [`${classPrefix}-key-sign`]: !isNumberKey && key,
-      [`${classPrefix}-key-mid`]: index === 9 && !!confirmText,
+      'number-key': isNumberKey,
+      'sign-key': !isNumberKey && key,
+      'mid-key': index === 9 && !!confirmText,
     })
 
     const ariaProps = key
@@ -204,7 +204,7 @@ export const NumberKeyboard: React.FC<NumberKeyboardProps> = p => {
           <div className={`${classPrefix}-wrapper`}>
             <div
               className={classNames(`${classPrefix}-main`, {
-                [`${classPrefix}-main-confirmed-style`]: !!confirmText,
+                'confirmed-style': !!confirmText,
               })}
             >
               {keys.map(renderKey)}
@@ -212,7 +212,7 @@ export const NumberKeyboard: React.FC<NumberKeyboardProps> = p => {
             {!!confirmText && (
               <div className={`${classPrefix}-confirm`}>
                 <div
-                  className={`${classPrefix}-key ${classPrefix}-key-extra ${classPrefix}-key-bs`}
+                  className={`${classPrefix}-key extra-key bs-key`}
                   onTouchStart={() => {
                     onBackspacePressStart()
                   }}
@@ -226,7 +226,7 @@ export const NumberKeyboard: React.FC<NumberKeyboardProps> = p => {
                   <TextDeletionOutline />
                 </div>
                 <div
-                  className={`${classPrefix}-key ${classPrefix}-key-extra ${classPrefix}-key-ok`}
+                  className={`${classPrefix}-key extra-key ok-key`}
                   onTouchEnd={e => onKeyPress(e, 'OK')}
                   role='button'
                 >
diff --git a/src/components/switch/demos/demo2.tsx b/src/components/switch/demos/demo2.tsx
index e21a3d740..bf8fba1ac 100644
--- a/src/components/switch/demos/demo2.tsx
+++ b/src/components/switch/demos/demo2.tsx
@@ -1,9 +1,15 @@
-import React, { useState } from 'react'
+import React from 'react'
 import { Space, Switch } from 'antd-mobile'
 import { DemoBlock } from 'demos'
 
 export default () => {
-  const [checked, setChecked] = useState(false)
+  const beforeChange = (): Promise<void> => {
+    return new Promise(resolve => {
+      setTimeout(() => {
+        resolve()
+      }, 1000)
+    })
+  }
 
   return (
     <>
@@ -20,23 +26,9 @@ export default () => {
 
       <DemoBlock title='异步'>
         <Space wrap>
-          <Switch
-            checked={checked}
-            onChange={async val => {
-              await mockRequest()
-              setChecked(val)
-            }}
-          />
+          <Switch defaultChecked beforeChange={() => beforeChange()} />
         </Space>
       </DemoBlock>
     </>
   )
 }
-
-const mockRequest = (): Promise<void> => {
-  return new Promise(resolve => {
-    setTimeout(() => {
-      resolve()
-    }, 1000)
-  })
-}
diff --git a/src/components/switch/index.en.md b/src/components/switch/index.en.md
index 988d4fa7a..2d877b4b3 100644
--- a/src/components/switch/index.en.md
+++ b/src/components/switch/index.en.md
@@ -17,16 +17,16 @@ switch selector.
 
 ### Props
 
-| Name           | Description                                                                                                           | Type                                      | Default |
-| -------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ------- |
-| beforeChange   | Execute before change                                                                                                 | `(val: boolean) => Promise<void>`         | -       |
-| checked        | Specify whether it is currently opened                                                                                | `boolean`                                 | `false` |
-| checkedText    | Selected text                                                                                                         | `ReactNode`                               | -       |
-| defaultChecked | Whether to open initially                                                                                             | `boolean`                                 | `false` |
-| disabled       | Disabled status                                                                                                       | `boolean`                                 | `false` |
-| loading        | Loading status                                                                                                        | `boolean`                                 | `false` |
-| onChange       | The callback function when changing, when the Promise is returned, the loading status will be displayed automatically | `(val: boolean) => void \| Promise<void>` | -       |
-| uncheckedText  | Non-selected text                                                                                                     | `ReactNode`                               | -       |
+| Name           | Description                                 | Type                              | Default |
+| -------------- | ------------------------------------------- | --------------------------------- | ------- |
+| beforeChange   | Execute before change                       | `(val: boolean) => Promise<void>` | -       |
+| checked        | Specify whether it is currently opened      | `boolean`                         | `false` |
+| checkedText    | Selected text                               | `ReactNode`                       | -       |
+| defaultChecked | Whether to open initially                   | `boolean`                         | `false` |
+| disabled       | Disabled status                             | `boolean`                         | `false` |
+| loading        | Loading status                              | `boolean`                         | `false` |
+| onChange       | Callback function when the value is changed | `(val: boolean) => void`          | -       |
+| uncheckedText  | Non-selected text                           | `ReactNode`                       | -       |
 
 ### CSS Variables
 
@@ -36,21 +36,3 @@ switch selector.
 | --checked-color | Filled color | `var(--adm-color-primary)` |
 | --height        | Height       | `31px`                     |
 | --width         | Width        | `51px`                     |
-
-## FAQ
-
-### How to handle exceptions in async `onChange`?
-
-The `onChange` event supports returning a Promise, when the Promise starts, the Switch will automatically enter the loading state, and when the Promise completes or fails, the Switch will automatically exit the loading state. In general, this satisfies the needs of most projects.
-
-But when Promise fails, Switch will not eat the error, but will re-throw the error object. This is the expected behavior. If you want to intercept some errors yourself and avoid them being thrown, you can use `try/ catch` wraps the processing logic in `onChange`, for example:
-
-```tsx
-async function onChange(val: boolean) {
-  try {
-    await doSomething();
-  } catch (e) {
-    // handle or ignore error
-  }
-}
-```
diff --git a/src/components/switch/index.zh.md b/src/components/switch/index.zh.md
index 212ccd33b..fd1053355 100644
--- a/src/components/switch/index.zh.md
+++ b/src/components/switch/index.zh.md
@@ -17,16 +17,16 @@
 
 ### 属性
 
-| 参数                   | 说明                                                    | 类型                                      | 默认值  |
-| ---------------------- | ------------------------------------------------------- | ----------------------------------------- | ------- |
-| beforeChange（已弃用） | 变化前执行（已弃用，推荐使用 `onChange` 属性）          | `(val: boolean) => Promise<void>`         | -       |
-| checked                | 指定当前是否打开                                        | `boolean`                                 | `false` |
-| checkedText            | 选中时的内容                                            | `ReactNode`                               | -       |
-| defaultChecked         | 初始是否打开                                            | `boolean`                                 | `false` |
-| disabled               | 禁用状态                                                | `boolean`                                 | `false` |
-| loading                | 加载状态                                                | `boolean`                                 | `false` |
-| onChange               | 变化时的回调函数，当返回 Promise 时，会自动显示加载状态 | `(val: boolean) => void \| Promise<void>` | -       |
-| uncheckedText          | 非选中时的内容                                          | `ReactNode`                               | -       |
+| 参数           | 说明             | 类型                              | 默认值  |
+| -------------- | ---------------- | --------------------------------- | ------- |
+| beforeChange   | 变化前执行       | `(val: boolean) => Promise<void>` | -       |
+| checked        | 指定当前是否打开 | `boolean`                         | `false` |
+| checkedText    | 选中时的内容     | `ReactNode`                       | -       |
+| defaultChecked | 初始是否打开     | `boolean`                         | `false` |
+| disabled       | 禁用状态         | `boolean`                         | `false` |
+| loading        | 加载状态         | `boolean`                         | `false` |
+| onChange       | 变化时回调函数   | `(val: boolean) => void`          | -       |
+| uncheckedText  | 非选中时的内容   | `ReactNode`                       | -       |
 
 ### CSS 变量
 
@@ -36,21 +36,3 @@
 | --checked-color | 填充颜色 | `var(--adm-color-primary)` |
 | --height        | 高度     | `31px`                     |
 | --width         | 宽度     | `51px`                     |
-
-## FAQ
-
-### 如何处理异步 `onChange` 中的异常？
-
-`onChange` 事件支持返回一个 Promise，当 Promise 开始时，Switch 会自动进入加载状态，而当 Promise 完成或失败时，Switch 会自动退出加载状态。一般情况下，这满足大多数项目的需求。
-
-但是当 Promise 失败时，Switch 并不会吃掉报错，而是会把这个错误对象重新抛出来，这是预期行为，如果你想自己拦截掉一些错误，避免他们被抛出，可以使用 `try/catch` 包裹 `onChange` 中的处理逻辑，例如：
-
-```tsx
-async function onChange(val: boolean) {
-  try {
-    await doSomething();
-  } catch (e) {
-    // handle or ignore error
-  }
-}
-```
diff --git a/src/components/switch/switch.tsx b/src/components/switch/switch.tsx
index f4e8698db..4c020a089 100644
--- a/src/components/switch/switch.tsx
+++ b/src/components/switch/switch.tsx
@@ -5,7 +5,6 @@ import { usePropsValue } from '../../utils/use-props-value'
 import { mergeProps } from '../../utils/with-default-props'
 import { SpinIcon } from './spin-icon'
 import { useConfig } from '../config-provider'
-import { isPromise } from '../../utils/validate'
 
 const classPrefix = `adm-switch`
 
@@ -14,9 +13,8 @@ export type SwitchProps = {
   disabled?: boolean
   checked?: boolean
   defaultChecked?: boolean
-  /** @deprecated use `onChange` instead */
   beforeChange?: (val: boolean) => Promise<void>
-  onChange?: (checked: boolean) => void | Promise<void>
+  onChange?: (checked: boolean) => void
   checkedText?: ReactNode
   uncheckedText?: ReactNode
 } & NativeProps<'--checked-color' | '--width' | '--height' | '--border-width'>
@@ -46,22 +44,14 @@ export const Switch: FC<SwitchProps> = p => {
       setChanging(true)
       try {
         await props.beforeChange(nextChecked)
+        setChecked(nextChecked)
         setChanging(false)
       } catch (e) {
         setChanging(false)
         throw e
       }
-    }
-    const result = setChecked(nextChecked)
-    if (isPromise(result)) {
-      setChanging(true)
-      try {
-        await result
-        setChanging(false)
-      } catch (e) {
-        setChanging(false)
-        throw e
-      }
+    } else {
+      setChecked(nextChecked)
     }
   }
 
diff --git a/src/utils/use-props-value.ts b/src/utils/use-props-value.ts
index 211ff9cf7..4bf7aee11 100644
--- a/src/utils/use-props-value.ts
+++ b/src/utils/use-props-value.ts
@@ -27,7 +27,7 @@ export function usePropsValue<T>(options: Options<T>) {
       if (!forceTrigger && nextValue === stateRef.current) return
       stateRef.current = nextValue
       update()
-      return onChange?.(nextValue)
+      onChange?.(nextValue)
     }
   )
   return [stateRef.current, setState] as const


Test patch:
diff --git a/src/components/action-sheet/tests/__snapshots__/action-sheet.test.tsx.snap b/src/components/action-sheet/tests/__snapshots__/action-sheet.test.tsx.snap
index 90824cf3e..df2386512 100644
--- a/src/components/action-sheet/tests/__snapshots__/action-sheet.test.tsx.snap
+++ b/src/components/action-sheet/tests/__snapshots__/action-sheet.test.tsx.snap
@@ -9,7 +9,9 @@ exports[`ActionSheet basic usage 1`] = `
       class="adm-button adm-button-default adm-button-shape-default"
       type="button"
     >
-      button
+      <span>
+        button
+      </span>
     </button>
   </div>
   <div
diff --git a/src/components/button/tests/button.test.tsx b/src/components/button/tests/button.test.tsx
index b3525e510..b847a61b7 100644
--- a/src/components/button/tests/button.test.tsx
+++ b/src/components/button/tests/button.test.tsx
@@ -27,10 +27,18 @@ describe('Button', () => {
         <Button color='warning'>Warning</Button>
       </>
     )
-    expect(getByText('Primary')).toHaveClass(`${classPrefix}-primary`)
-    expect(getByText('Success')).toHaveClass(`${classPrefix}-success`)
-    expect(getByText('Danger')).toHaveClass(`${classPrefix}-danger`)
-    expect(getByText('Warning')).toHaveClass(`${classPrefix}-warning`)
+    expect(getByText('Primary').closest('button')).toHaveClass(
+      `${classPrefix}-primary`
+    )
+    expect(getByText('Success').closest('button')).toHaveClass(
+      `${classPrefix}-success`
+    )
+    expect(getByText('Danger').closest('button')).toHaveClass(
+      `${classPrefix}-danger`
+    )
+    expect(getByText('Warning').closest('button')).toHaveClass(
+      `${classPrefix}-warning`
+    )
   })
 
   test('renders with fill', () => {
@@ -41,9 +49,15 @@ describe('Button', () => {
         <Button fill='none'>none</Button>
       </>
     )
-    expect(getByText('outline')).toHaveClass(`${classPrefix}-fill-outline`)
-    expect(getByText('none')).toHaveClass(`${classPrefix}-fill-none`)
-    expect(getByText('solid')).not.toHaveClass(`${classPrefix}-fill-solid`)
+    expect(getByText('outline').closest('button')).toHaveClass(
+      `${classPrefix}-fill-outline`
+    )
+    expect(getByText('none').closest('button')).toHaveClass(
+      `${classPrefix}-fill-none`
+    )
+    expect(getByText('solid').closest('button')).not.toHaveClass(
+      `${classPrefix}-fill-solid`
+    )
   })
 
   test('renders with size', () => {
@@ -63,10 +77,16 @@ describe('Button', () => {
         </Button>
       </>
     )
-    expect(getByText('Mini')).toHaveClass(`${classPrefix}-mini`)
-    expect(getByText('Small')).toHaveClass(`${classPrefix}-small`)
-    expect(getByText('Middle')).toHaveClass(`${classPrefix}`)
-    expect(getByText('Large')).toHaveClass(`${classPrefix}-large`)
+    expect(getByText('Mini').closest('button')).toHaveClass(
+      `${classPrefix}-mini`
+    )
+    expect(getByText('Small').closest('button')).toHaveClass(
+      `${classPrefix}-small`
+    )
+    expect(getByText('Middle').closest('button')).toHaveClass(`${classPrefix}`)
+    expect(getByText('Large').closest('button')).toHaveClass(
+      `${classPrefix}-large`
+    )
   })
 
   test('renders with block', () => {
@@ -75,7 +95,9 @@ describe('Button', () => {
         Block
       </Button>
     )
-    expect(getByText('Block')).toHaveClass(`${classPrefix}-block`)
+    expect(getByText('Block').closest('button')).toHaveClass(
+      `${classPrefix}-block`
+    )
   })
 
   test('renders with disabled', () => {
diff --git a/src/components/image-viewer/tests/__snapshots__/image-viewer.test.tsx.snap b/src/components/image-viewer/tests/__snapshots__/image-viewer.test.tsx.snap
index f865f50cb..8f0e00dc3 100644
--- a/src/components/image-viewer/tests/__snapshots__/image-viewer.test.tsx.snap
+++ b/src/components/image-viewer/tests/__snapshots__/image-viewer.test.tsx.snap
@@ -6,7 +6,9 @@ exports[`ImageViewer.Multi calling ref.current.swipeTo before initialization 1`]
     class="adm-button adm-button-default adm-button-shape-default"
     type="button"
   >
-    Show
+    <span>
+      Show
+    </span>
   </button>
 </div>
 `;
@@ -17,7 +19,9 @@ exports[`ImageViewer.Multi calling ref.current.swipeTo before initialization 2`]
     class="adm-button adm-button-default adm-button-shape-default"
     type="button"
   >
-    Show
+    <span>
+      Show
+    </span>
   </button>
   <div
     class="adm-mask"
@@ -234,7 +238,9 @@ exports[`ImageViewer.Multi rendering with footer 1`] = `
           class="adm-button adm-button-default adm-button-shape-default"
           type="button"
         >
-          查看原图
+          <span>
+            查看原图
+          </span>
         </button>
         <div
           class="adm-safe-area adm-safe-area-position-bottom"
diff --git a/src/components/number-keyboard/tests/number-keyboard.test.tsx b/src/components/number-keyboard/tests/number-keyboard.test.tsx
deleted file mode 100644
index d07c0d844..000000000
--- a/src/components/number-keyboard/tests/number-keyboard.test.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import React, { useState } from 'react'
-import { fireEvent, render, testA11y, screen, waitFor } from 'testing'
-import NumberKeyboard from '..'
-
-const classPrefix = 'adm-number-keyboard'
-
-function mockClick(el: HTMLElement) {
-  fireEvent.touchStart(el, { touches: [{}] })
-  fireEvent.touchEnd(el, { touches: [{}] })
-}
-
-describe('NumberKeyboard', () => {
-  test('a11y', async () => {
-    await testA11y(<NumberKeyboard visible />)
-  })
-
-  test('renders with title', () => {
-    render(<NumberKeyboard visible title='title' />)
-    const title = screen.getByText('title')
-    expect(title).toBeInTheDocument()
-    expect(title).toHaveClass(`${classPrefix}-title`)
-    expect(document.querySelector(`.${classPrefix}-header`)).toHaveClass(
-      `${classPrefix}-header-with-title`
-    )
-  })
-
-  test('onClose should be called by close button', () => {
-    const onClose = jest.fn()
-    render(<NumberKeyboard visible onClose={onClose} />)
-    fireEvent.click(screen.getByTitle('CLOSE'))
-    expect(onClose).toBeCalledTimes(1)
-  })
-
-  test('onInput should be work', () => {
-    const onInput = jest.fn()
-    render(<NumberKeyboard visible onInput={onInput} />)
-    const keys = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
-    keys.forEach(key => {
-      const el = screen.getByTitle(key)
-      mockClick(el)
-    })
-    expect(onInput).toBeCalledTimes(10)
-  })
-
-  test('renders with customKey', () => {
-    const onDelete = jest.fn()
-    const onInput = jest.fn()
-    render(
-      <NumberKeyboard
-        customKey='-'
-        visible
-        onInput={onInput}
-        onDelete={onDelete}
-      />
-    )
-
-    mockClick(screen.getByTitle('BACKSPACE'))
-    expect(onDelete).toBeCalled()
-
-    mockClick(screen.getByText('-'))
-    expect(onInput).toBeCalledWith('-')
-  })
-
-  test('renders with confirmText', async () => {
-    const onConfirm = jest.fn()
-    const onClose = jest.fn()
-    const onDelete = jest.fn()
-    render(
-      <NumberKeyboard
-        visible
-        confirmText='confirm'
-        onConfirm={onConfirm}
-        onClose={onClose}
-        onDelete={onDelete}
-      />
-    )
-    const confirm = screen.getByText('confirm')
-    const del = screen.getByTitle('BACKSPACE')
-    expect(confirm).toBeInTheDocument()
-    expect(confirm).toHaveClass(
-      `${classPrefix}-key-extra ${classPrefix}-key-ok`
-    )
-    expect(del).toHaveClass(`${classPrefix}-key-extra ${classPrefix}-key-bs`)
-
-    mockClick(confirm)
-    expect(onConfirm).toBeCalled()
-    expect(onClose).toBeCalled()
-    mockClick(del)
-    expect(onDelete).toBeCalled()
-  })
-
-  test('renders witout close button', () => {
-    render(<NumberKeyboard visible showCloseButton={false} />)
-    expect(
-      document.querySelector(`.${classPrefix}-header`)
-    ).not.toBeInTheDocument()
-  })
-
-  test('renders with random order', async () => {
-    const App = () => {
-      const [visible, setVisible] = useState(false)
-      return (
-        <>
-          <NumberKeyboard
-            visible={visible}
-            randomOrder
-            onClose={() => {
-              setVisible(false)
-            }}
-          />
-          <button onClick={() => setVisible(true)}>open</button>
-        </>
-      )
-    }
-    render(<App />)
-    fireEvent.click(screen.getByText('open'))
-    const popup = document.querySelector(`.${classPrefix}-popup`)
-    const main = document.querySelector(`.${classPrefix}-main`)
-    expect(main).not.toHaveTextContent('1234567890')
-    fireEvent.click(screen.getByTitle('CLOSE'))
-    await waitFor(() => expect(popup).not.toBeVisible())
-  })
-
-  test('onDelete will be called when backspace press long time', () => {
-    jest.useFakeTimers()
-    const onDelete = jest.fn()
-    render(<NumberKeyboard visible onDelete={onDelete} />)
-    const del = screen.getByTitle('BACKSPACE')
-    fireEvent.touchStart(del, { touches: [{}] })
-    jest.runOnlyPendingTimers()
-    expect(onDelete).toBeCalledTimes(1)
-    jest.runOnlyPendingTimers()
-    expect(onDelete).toBeCalledTimes(2)
-    jest.useRealTimers()
-  })
-})
diff --git a/src/components/number-keyboard/tests/numberic-keyboard.test.tsx b/src/components/number-keyboard/tests/numberic-keyboard.test.tsx
new file mode 100644
index 000000000..1ecf36bbf
--- /dev/null
+++ b/src/components/number-keyboard/tests/numberic-keyboard.test.tsx
@@ -0,0 +1,76 @@
+import * as React from 'react'
+import { fireEvent, render, sleep, testA11y, waitFor } from 'testing'
+import NumberKeyboard from '..'
+
+const classPrefix = 'adm-number-keyboard'
+
+describe('NumberKeyboard', () => {
+  test('a11y', async () => {
+    await testA11y(<NumberKeyboard visible />)
+  })
+
+  test('renders with title & close button', async () => {
+    let inputValue = ''
+    const onClose = jest.fn()
+    const onInput = jest.fn(value => {
+      inputValue = value
+    })
+
+    const { getByTitle, getByText } = render(
+      <NumberKeyboard
+        visible
+        showCloseButton
+        onClose={onClose}
+        onInput={onInput}
+        title='title'
+      />
+    )
+
+    expect(getByText('title')).toHaveClass(`${classPrefix}-title`)
+
+    // 点击关闭箭头
+    fireEvent.click(getByTitle('CLOSE'))
+    await waitFor(() => {
+      expect(onClose).toBeCalledTimes(1)
+    })
+
+    // touch every key button
+    document.querySelectorAll(`.${classPrefix}-key`).forEach(element => {
+      fireEvent.touchEnd(element)
+    })
+
+    await sleep(100)
+    expect(onInput).toBeCalledTimes(10)
+    expect(inputValue).toEqual('0')
+  })
+
+  test('renders with customKey', async () => {
+    let inputValue = ''
+    const onDelete = jest.fn()
+    const onInput = jest.fn(value => {
+      inputValue = value
+    })
+
+    const { getByText, getByTitle } = render(
+      <NumberKeyboard
+        customKey='-'
+        visible
+        onInput={onInput}
+        onDelete={onDelete}
+      />
+    )
+
+    // 点击删除
+    fireEvent.touchEnd(getByTitle('BACKSPACE'))
+    await waitFor(() => {
+      expect(onDelete).toBeCalledTimes(1)
+    })
+
+    // 点击自定义按钮
+    fireEvent.touchEnd(getByText('-'))
+    await waitFor(() => {
+      expect(onInput).toBeCalledTimes(1)
+      expect(inputValue).toEqual('-')
+    })
+  })
+})
diff --git a/src/components/switch/tests/switch.test.tsx b/src/components/switch/tests/switch.test.tsx
index 23d29c3be..96b9bb6c1 100644
--- a/src/components/switch/tests/switch.test.tsx
+++ b/src/components/switch/tests/switch.test.tsx
@@ -1,13 +1,5 @@
 import React, { useState } from 'react'
-import {
-  fireEvent,
-  render,
-  testA11y,
-  waitFor,
-  screen,
-  sleep,
-  act,
-} from 'testing'
+import { fireEvent, render, testA11y, waitFor, screen } from 'testing'
 import Switch from '..'
 
 const classPrefix = `adm-switch`
@@ -77,30 +69,4 @@ describe('Switch', () => {
     })
     jest.useRealTimers()
   })
-
-  test('`onChange` returns a Promise', async () => {
-    jest.useFakeTimers()
-    const App = () => {
-      const [checked, setChecked] = useState(false)
-      return (
-        <Switch
-          checked={checked}
-          onChange={async val => {
-            await sleep(1000)
-            setChecked(val)
-          }}
-        />
-      )
-    }
-
-    render(<App />)
-    const switchEl = screen.getByRole('switch')
-    fireEvent.click(switchEl)
-    expect(switchEl).toHaveClass(`${classPrefix}-disabled`)
-    await act(async () => {
-      jest.runAllTimers()
-    })
-    expect(switchEl).toHaveClass(`${classPrefix}-checked`)
-    jest.useRealTimers()
-  })
 })