Repository: ant-design/ant-design-mobile
Pull request #6770 (2024-10-29 06:03:50)

fix(Ellipsis): Use React life cycle hooks to avoid render not correct in Suspense 

github-actions[bot]: [<img width="500" alt="Prepare preview" src="https://user-images.githubusercontent.com/5378891/72351368-2c979e00-371b-11ea-9652-eb4e825d745e.gif">](https://preview-6770-ant-design-mobile.surge.sh)
<!-- AUTO_PREVIEW_HOOK -->
<!-- AUTO_PREVIEW_HOOK -->
cloudflare-workers-and-pages[bot]: ## Deploying ant-design-mobile with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>c6cfeb5</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>⚡️&nbsp; Build in progress...</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/ant-design-mobile/25f09119-2942-46c2-86e6-a37959348d81)

github-actions[bot]: **Size Change:** -414 B (-0.11%) 

**Total Size:** 370 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./lib/bundle/antd-mobile.compatible.umd.js` | 198 kB | -140 B (-0.07%) |
| `./lib/bundle/antd-mobile.umd.js` | 152 kB | -274 B (-0.18%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./lib/bundle/css-vars-patch.css` | 1.63 kB |
| `./lib/bundle/style.css` | 18.5 kB |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
github-actions[bot]: ## size-limit report 📦 
| Path                             | Size      | Loading time (3g) | Running time (snapdragon) | Total time |
| -------------------------------- | --------- | ----------------- | ------------------------- | ---------- |
| `./lib/bundle/antd-mobile.es.js` | 107.69 KB | 2.2 s (0%)        | 1.3 s (+8.08% 🔺)         | 3.4 s      |
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/ant-design/ant-design-mobile/pull/6770?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design) Report
Attention: Patch coverage is `97.87234%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 92.70%. Comparing base [(`c4e181d`)](https://app.codecov.io/gh/ant-design/ant-design-mobile/commit/c4e181d98b51c8b9ac9ca2c2841ad9ac9521f62d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design) to head [(`c6cfeb5`)](https://app.codecov.io/gh/ant-design/ant-design-mobile/commit/c6cfeb5aebf395006c447b5186883fa00101f80e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design).

| [Files with missing lines](https://app.codecov.io/gh/ant-design/ant-design-mobile/pull/6770?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design) | Patch % | Lines |
|---|---|---|
| [src/components/ellipsis/useMeasure.tsx](https://app.codecov.io/gh/ant-design/ant-design-mobile/pull/6770?src=pr&el=tree&filepath=src%2Fcomponents%2Fellipsis%2FuseMeasure.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design#diff-c3JjL2NvbXBvbmVudHMvZWxsaXBzaXMvdXNlTWVhc3VyZS50c3g=) | 97.33% | [2 Missing :warning: ](https://app.codecov.io/gh/ant-design/ant-design-mobile/pull/6770?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #6770      +/-   ##
==========================================
- Coverage   92.74%   92.70%   -0.04%     
==========================================
  Files         334      335       +1     
  Lines        7204     7192      -12     
  Branches     1763     1801      +38     
==========================================
- Hits         6681     6667      -14     
- Misses        515      517       +2     
  Partials        8        8              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ant-design/ant-design-mobile/pull/6770?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ant-design).


Patch:
diff --git a/src/components/ellipsis/demos/demo1.tsx b/src/components/ellipsis/demos/demo1.tsx
index 3a2889e1b..79db2fd2f 100644
--- a/src/components/ellipsis/demos/demo1.tsx
+++ b/src/components/ellipsis/demos/demo1.tsx
@@ -1,7 +1,7 @@
-import React from 'react'
 import { Ellipsis, Space } from 'antd-mobile'
-import { DemoBlock } from 'demos'
 import { DownOutline, UpOutline } from 'antd-mobile-icons'
+import { DemoBlock } from 'demos'
+import React from 'react'
 
 const content =
   '蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂，而且变动和并发频繁，常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件，可以通过抽象得到一些稳定且高复用性的内容。'
diff --git a/src/components/ellipsis/ellipsis.tsx b/src/components/ellipsis/ellipsis.tsx
index 08d966035..55d6fc2b3 100644
--- a/src/components/ellipsis/ellipsis.tsx
+++ b/src/components/ellipsis/ellipsis.tsx
@@ -1,14 +1,13 @@
-import React, { useMemo, useRef, useState } from 'react'
 import type { FC, ReactNode } from 'react'
-import runes from 'runes2'
-import { mergeProps } from '../../utils/with-default-props'
+import React from 'react'
 import { NativeProps, withNativeProps } from '../../utils/native-props'
 import { useResizeEffect } from '../../utils/use-resize-effect'
-import { useIsomorphicLayoutEffect } from 'ahooks'
+import { mergeProps } from '../../utils/with-default-props'
 import {
   PropagationEvent,
   withStopPropagation,
 } from '../../utils/with-stop-propagation'
+import useMeasure from './useMeasure'
 
 const classPrefix = `adm-ellipsis`
 
@@ -34,212 +33,66 @@ const defaultProps = {
   defaultExpanded: false,
 }
 
-type EllipsisedValue = {
-  leading?: string
-  tailing?: string
-}
-
 export const Ellipsis: FC<EllipsisProps> = p => {
   const props = mergeProps(defaultProps, p)
-  const rootRef = useRef<HTMLDivElement>(null)
-  const expandElRef = useRef<HTMLAnchorElement>(null)
-  const collapseElRef = useRef<HTMLAnchorElement>(null)
-
-  const [ellipsised, setEllipsised] = useState<EllipsisedValue>({})
-  const [expanded, setExpanded] = useState(props.defaultExpanded)
-  const [exceeded, setExceeded] = useState(false)
-
-  const chars = useMemo(() => runes(props.content), [props.content])
-  function getSubString(start: number, end: number) {
-    return chars.slice(start, end).join('')
-  }
-
-  function calcEllipsised() {
-    const root = rootRef.current
-    if (!root) return
-
-    const originDisplay = root.style.display
-    root.style.display = 'block'
-
-    const originStyle = window.getComputedStyle(root)
-    const container = document.createElement('div')
-
-    const styleNames: string[] = Array.prototype.slice.apply(originStyle)
-    styleNames.forEach(name => {
-      container.style.setProperty(name, originStyle.getPropertyValue(name))
-    })
-
-    root.style.display = originDisplay
-
-    container.style.height = 'auto'
-    container.style.minHeight = 'auto'
-    container.style.maxHeight = 'auto'
-    container.style.textOverflow = 'clip'
-    container.style.webkitLineClamp = 'unset'
-    container.style.display = 'block'
-
-    const lineHeight = pxToNumber(originStyle.lineHeight)
-    const maxHeight = Math.floor(
-      lineHeight * (props.rows + 0.5) +
-        pxToNumber(originStyle.paddingTop) +
-        pxToNumber(originStyle.paddingBottom)
-    )
-
-    container.innerText = props.content
-    document.body.appendChild(container)
-
-    if (container.offsetHeight <= maxHeight) {
-      setExceeded(false)
-    } else {
-      setExceeded(true)
-      const end = props.content.length
-
-      const collapseEl =
-        typeof props.collapseText === 'string'
-          ? props.collapseText
-          : collapseElRef.current?.innerHTML
-      const expandEl =
-        typeof props.expandText === 'string'
-          ? props.expandText
-          : expandElRef.current?.innerHTML
-      const actionText = expanded ? collapseEl : expandEl
-
-      function check(left: number, right: number): EllipsisedValue {
-        if (right - left <= 1) {
-          if (props.direction === 'end') {
-            return {
-              leading: getSubString(0, left) + '...',
-            }
-          } else {
-            return {
-              tailing: '...' + getSubString(right, end),
-            }
-          }
-        }
-        const middle = Math.round((left + right) / 2)
-        if (props.direction === 'end') {
-          container.innerHTML = getSubString(0, middle) + '...' + actionText
-        } else {
-          container.innerHTML = actionText + '...' + getSubString(middle, end)
-        }
-
-        if (container.offsetHeight <= maxHeight) {
-          if (props.direction === 'end') {
-            return check(middle, right)
-          } else {
-            return check(left, middle)
-          }
-        } else {
-          if (props.direction === 'end') {
-            return check(left, middle)
-          } else {
-            return check(middle, right)
-          }
-        }
-      }
-
-      function checkMiddle(
-        leftPart: [number, number],
-        rightPart: [number, number]
-      ): EllipsisedValue {
-        if (
-          leftPart[1] - leftPart[0] <= 1 &&
-          rightPart[1] - rightPart[0] <= 1
-        ) {
-          return {
-            leading: getSubString(0, leftPart[0]) + '...',
-            tailing: '...' + getSubString(rightPart[1], end),
-          }
-        }
-        const leftPartMiddle = Math.floor((leftPart[0] + leftPart[1]) / 2)
-        const rightPartMiddle = Math.ceil((rightPart[0] + rightPart[1]) / 2)
-        container.innerHTML =
-          getSubString(0, leftPartMiddle) +
-          '...' +
-          actionText +
-          '...' +
-          getSubString(rightPartMiddle, end)
-        if (container.offsetHeight <= maxHeight) {
-          return checkMiddle(
-            [leftPartMiddle, leftPart[1]],
-            [rightPart[0], rightPartMiddle]
-          )
-        } else {
-          return checkMiddle(
-            [leftPart[0], leftPartMiddle],
-            [rightPartMiddle, rightPart[1]]
-          )
-        }
-      }
-
-      const middle = Math.floor((0 + end) / 2)
-      const ellipsised =
-        props.direction === 'middle'
-          ? checkMiddle([0, middle], [middle, end])
-          : check(0, end)
-      setEllipsised(ellipsised)
-    }
-    document.body.removeChild(container)
-  }
-
-  useResizeEffect(calcEllipsised, rootRef)
-  useIsomorphicLayoutEffect(() => {
-    calcEllipsised()
-  }, [
-    props.content,
-    props.direction,
-    props.rows,
-    props.expandText,
-    props.collapseText,
-  ])
 
-  const expandActionElement =
-    !!props.expandText &&
-    withStopPropagation(
-      props.stopPropagationForActionButtons,
-      <a
-        ref={expandElRef}
-        onClick={() => {
-          setExpanded(true)
-        }}
-      >
-        {props.expandText}
-      </a>
-    )
-
-  const collapseActionElement =
-    !!props.collapseText &&
-    withStopPropagation(
-      props.stopPropagationForActionButtons,
-      <a
-        ref={collapseElRef}
-        onClick={() => {
-          setExpanded(false)
-        }}
-      >
-        {props.collapseText}
-      </a>
-    )
-
-  const renderContent = () => {
-    if (!exceeded) return props.content
-
-    if (expanded)
-      return (
-        <>
-          {props.content}
-          {collapseActionElement}
-        </>
+  const {
+    content,
+    direction,
+    rows,
+    expandText,
+    collapseText,
+    stopPropagationForActionButtons,
+    onContentClick,
+    defaultExpanded,
+  } = props
+
+  // ============================ Refs ============================
+  const rootRef = React.useRef<HTMLDivElement>(null)
+
+  // ========================== Expanded ==========================
+  const [expanded, setExpanded] = React.useState(defaultExpanded)
+
+  const expandNode = expandText
+    ? withStopPropagation(
+        stopPropagationForActionButtons,
+        <a
+          onClick={() => {
+            setExpanded(true)
+          }}
+        >
+          {expandText}
+        </a>
+      )
+    : null
+
+  const collapseNode = collapseText
+    ? withStopPropagation(
+        stopPropagationForActionButtons,
+        <a
+          onClick={() => {
+            setExpanded(false)
+          }}
+        >
+          {collapseText}
+        </a>
       )
-    return (
-      <>
-        {ellipsised.leading}
-        {expandActionElement}
-        {ellipsised.tailing}
-      </>
-    )
-  }
+    : null
+
+  // ========================== Ellipsis ==========================
+  const [measureNodes, forceResize] = useMeasure(
+    rootRef,
+    content,
+    rows,
+    direction,
+    expanded,
+    expandNode,
+    collapseNode
+  )
+
+  useResizeEffect(forceResize, rootRef)
 
+  // =========================== Render ===========================
   return withNativeProps(
     props,
     <div
@@ -247,17 +100,11 @@ export const Ellipsis: FC<EllipsisProps> = p => {
       className={classPrefix}
       onClick={e => {
         if (e.target === e.currentTarget) {
-          props.onContentClick(e)
+          onContentClick(e)
         }
       }}
     >
-      {renderContent()}
+      {measureNodes}
     </div>
   )
 }
-
-function pxToNumber(value: string | null): number {
-  if (!value) return 0
-  const match = value.match(/^\d*(\.\d*)?/)
-  return match ? Number(match[0]) : 0
-}
diff --git a/src/components/ellipsis/useMeasure.tsx b/src/components/ellipsis/useMeasure.tsx
new file mode 100644
index 000000000..a23411a73
--- /dev/null
+++ b/src/components/ellipsis/useMeasure.tsx
@@ -0,0 +1,195 @@
+import { useEvent } from 'rc-util'
+import React from 'react'
+import runes from 'runes2'
+
+const enum MEASURE_STATUS {
+  PREPARE = 1,
+  MEASURE_WALKING = 2,
+  STABLE_ELLIPSIS = 99,
+  STABLE_NO_ELLIPSIS = 100,
+}
+
+const ELLIPSIS_TEXT = '...'
+
+const measureStyle: React.CSSProperties = {
+  visibility: 'hidden',
+  whiteSpace: 'inherit',
+  lineHeight: 'inherit',
+  fontSize: 'inherit',
+}
+
+export default function useMeasure(
+  containerRef: React.RefObject<HTMLDivElement>,
+  content: string,
+  rows: number,
+  direction: 'start' | 'end' | 'middle',
+  expanded: boolean,
+  expandNode: React.ReactElement | null,
+  collapseNode: React.ReactElement | null
+) {
+  const contentChars = React.useMemo(() => runes(content), [content])
+
+  const [maxHeight, setMaxHeight] = React.useState<number>(0)
+
+  const [walkingIndexes, setWalkingIndexes] = React.useState<
+    [startOffset: number, endOffset: number]
+  >([0, 0])
+  const midIndex = Math.ceil((walkingIndexes[0] + walkingIndexes[1]) / 2)
+
+  const [status, setStatus] = React.useState<MEASURE_STATUS>(
+    MEASURE_STATUS.STABLE_NO_ELLIPSIS
+  )
+
+  // ============================ Refs ============================
+  const singleRowMeasureRef = React.useRef<HTMLDivElement>(null)
+  const fullMeasureRef = React.useRef<HTMLDivElement>(null)
+  const midMeasureRef = React.useRef<HTMLDivElement>(null)
+
+  const startMeasure = useEvent(() => {
+    setStatus(MEASURE_STATUS.PREPARE)
+    setWalkingIndexes([
+      0,
+      direction === 'middle'
+        ? Math.ceil(contentChars.length / 2)
+        : contentChars.length,
+    ])
+  })
+
+  // Initialize
+  React.useLayoutEffect(() => {
+    startMeasure()
+  }, [contentChars, rows])
+
+  // Measure element height
+  React.useLayoutEffect(() => {
+    if (status === MEASURE_STATUS.PREPARE) {
+      const fullMeasureHeight = fullMeasureRef.current?.offsetHeight || 0
+      const singleRowMeasureHeight =
+        singleRowMeasureRef.current?.offsetHeight || 0
+      const rowMeasureHeight = singleRowMeasureHeight * rows
+
+      if (fullMeasureHeight <= rowMeasureHeight) {
+        setStatus(MEASURE_STATUS.STABLE_NO_ELLIPSIS)
+      } else {
+        setMaxHeight(rowMeasureHeight)
+        setStatus(MEASURE_STATUS.MEASURE_WALKING)
+      }
+    }
+  }, [status])
+
+  // Walking measure
+  React.useLayoutEffect(() => {
+    if (status === MEASURE_STATUS.MEASURE_WALKING) {
+      const diff = walkingIndexes[1] - walkingIndexes[0]
+      const midHeight = midMeasureRef.current?.offsetHeight || 0
+
+      if (diff > 1) {
+        if (midHeight > maxHeight) {
+          setWalkingIndexes([walkingIndexes[0], midIndex])
+        } else {
+          setWalkingIndexes([midIndex, walkingIndexes[1]])
+        }
+      } else {
+        if (midHeight > maxHeight) {
+          setWalkingIndexes([walkingIndexes[0], walkingIndexes[0]])
+        } else {
+          setWalkingIndexes([walkingIndexes[1], walkingIndexes[1]])
+        }
+        setStatus(MEASURE_STATUS.STABLE_ELLIPSIS)
+      }
+    }
+  }, [status, walkingIndexes])
+
+  // =========================== Render ===========================
+  /** Render by cut index */
+  const renderContent = (index: number) => {
+    const prefixContent = contentChars.slice(0, index)
+    const suffixContent = contentChars.slice(contentChars.length - index)
+
+    return (
+      <>
+        {direction === 'start' && (
+          <>
+            {expandNode}
+            {ELLIPSIS_TEXT}
+          </>
+        )}
+        {direction !== 'start' && prefixContent.join('')}
+        {direction === 'middle' && (
+          <>
+            {ELLIPSIS_TEXT}
+            {expandNode}
+            {ELLIPSIS_TEXT}
+          </>
+        )}
+        {direction !== 'end' && suffixContent.join('')}
+        {direction === 'end' && (
+          <>
+            {ELLIPSIS_TEXT}
+            {expandNode}
+          </>
+        )}
+      </>
+    )
+  }
+
+  const finalContent = React.useMemo(() => {
+    if (expanded || status === MEASURE_STATUS.STABLE_NO_ELLIPSIS) {
+      return (
+        <React.Fragment key='display'>
+          {content}
+          {status === MEASURE_STATUS.STABLE_ELLIPSIS && collapseNode}
+        </React.Fragment>
+      )
+    }
+
+    if (status === MEASURE_STATUS.STABLE_ELLIPSIS) {
+      return renderContent(midIndex)
+    }
+
+    return null
+  }, [expanded, status, content, collapseNode, midIndex])
+
+  const allNodes = (
+    <>
+      {/******************* Measure Prepare *******************/}
+      {/* Origin full content */}
+      {status === MEASURE_STATUS.PREPARE && (
+        <div key='full' aria-hidden ref={fullMeasureRef} style={measureStyle}>
+          {content}
+          {expandNode}
+        </div>
+      )}
+
+      {/* Row measure */}
+      {status === MEASURE_STATUS.PREPARE && (
+        <div
+          key='stable'
+          aria-hidden
+          ref={singleRowMeasureRef}
+          style={measureStyle}
+        >
+          {'\u00A0'}
+        </div>
+      )}
+
+      {/******************* Measure Walking *******************/}
+      {status === MEASURE_STATUS.MEASURE_WALKING && (
+        <div
+          key='walking-mid'
+          aria-hidden
+          ref={midMeasureRef}
+          style={measureStyle}
+        >
+          {renderContent(midIndex)}
+        </div>
+      )}
+
+      {/*********************** Display ***********************/}
+      {/* Display content */}
+      {finalContent}
+    </>
+  )
+
+  return [allNodes, startMeasure] as const
+}


Test patch:
diff --git a/src/components/ellipsis/tests/__snapshots__/ellipsis.test.tsx.snap b/src/components/ellipsis/tests/__snapshots__/ellipsis.test.tsx.snap
index 1621445c1..78ded1e00 100644
--- a/src/components/ellipsis/tests/__snapshots__/ellipsis.test.tsx.snap
+++ b/src/components/ellipsis/tests/__snapshots__/ellipsis.test.tsx.snap
@@ -4,9 +4,9 @@ exports[`Ellipsis direction end 1`] = `
 <div
   class="adm-ellipsis"
   data-testid="ellipsis"
-  style=""
 >
-  蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅...
+  蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量
+  ...
 </div>
 `;
 
@@ -14,10 +14,11 @@ exports[`Ellipsis direction middle 1`] = `
 <div
   class="adm-ellipsis"
   data-testid="ellipsis"
-  style=""
 >
-  蚂蚁的企业级产品是一个庞...
-  ...些稳定且高复用性的内容。
+  蚂蚁的企业级产品是一个庞
+  ...
+  ...
+  些稳定且高复用性的内容。
 </div>
 `;
 
@@ -25,9 +26,9 @@ exports[`Ellipsis direction start 1`] = `
 <div
   class="adm-ellipsis"
   data-testid="ellipsis"
-  style=""
 >
-  ...以及组件，可以通过抽象得到一些稳定且高复用性的内容。
+  ...
+  面以及组件，可以通过抽象得到一些稳定且高复用性的内容。
 </div>
 `;
 
@@ -35,7 +36,6 @@ exports[`Ellipsis expand and collapse 1`] = `
 <div
   class="adm-ellipsis"
   data-testid="ellipsis"
-  style=""
 >
   蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂，而且变动和并发频繁，常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件，可以通过抽象得到一些稳定且高复用性的内容。
   <a>
@@ -48,9 +48,9 @@ exports[`Ellipsis expand and collapse 2`] = `
 <div
   class="adm-ellipsis"
   data-testid="ellipsis"
-  style=""
 >
-  蚂蚁的企业级产品是一个庞大且复杂的体系。...
+  蚂蚁的企业级产品是一个庞大且复杂的体系。这
+  ...
   <a>
     expand
   </a>
@@ -61,8 +61,8 @@ exports[`Ellipsis multi line 1`] = `
 <div
   class="adm-ellipsis"
   data-testid="ellipsis"
-  style=""
 >
-  蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂，而且变动和并发频繁，常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的...
+  蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂，而且变动和并发频繁，常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件，
+  ...
 </div>
 `;
diff --git a/src/components/ellipsis/tests/ellipsis.test.tsx b/src/components/ellipsis/tests/ellipsis.test.tsx
index fdb89ed8b..8cb1738ab 100644
--- a/src/components/ellipsis/tests/ellipsis.test.tsx
+++ b/src/components/ellipsis/tests/ellipsis.test.tsx
@@ -1,5 +1,6 @@
+import { spyElementPrototypes } from 'rc-util/lib/test/domHook'
 import React from 'react'
-import { render, testA11y, fireEvent } from 'testing'
+import { fireEvent, render, testA11y } from 'testing'
 import Ellipsis from '..'
 
 const classPrefix = `adm-ellipsis`
@@ -9,36 +10,19 @@ const content =
 const lineHeight = 19.5
 
 describe('Ellipsis', () => {
-  const originGetComputedStyle = window.getComputedStyle
-
   beforeAll(() => {
-    window.getComputedStyle = el => {
-      const style = originGetComputedStyle(el)
-      style.lineHeight = `${lineHeight}px`
-      return style
-    }
-  })
-
-  beforeEach(() => {
-    Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
-      get() {
-        if (this.innerHTML.includes('...')) {
-          const row = Math.ceil(
-            // the width of '...' is equal to a Chinese char
-            (this.innerHTML.replace(/\.\.\./g, '中').length / content.length) *
-              4
-          )
-          return lineHeight * row
-        }
-        return lineHeight * 4
+    spyElementPrototypes(HTMLElement, {
+      offsetHeight: {
+        get() {
+          const that = this as HTMLElement
+          const charLen = (that.textContent || '').length || 1
+          const rows = Math.ceil(charLen / 30)
+          return rows * lineHeight
+        },
       },
     })
   })
 
-  afterAll(() => {
-    window.getComputedStyle = originGetComputedStyle
-  })
-
   test('a11y', async () => {
     await testA11y(<Ellipsis content={content} />)
   })
@@ -119,9 +103,6 @@ describe('Ellipsis', () => {
   })
 
   test('content not exceeded', () => {
-    Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
-      value: lineHeight,
-    })
     const { getByTestId } = render(
       <Ellipsis content='abc' data-testid='ellipsis' />
     )
@@ -151,22 +132,4 @@ describe('Ellipsis', () => {
     fireEvent.click(getByText('expand'))
     expect(getByText('collapse')).toBeInTheDocument()
   })
-
-  test('the `whiteSpace` style of the calc container should be the same as Ellipsis', () => {
-    let container: Element | null = null
-    Object.defineProperty(HTMLElement.prototype, 'removeChild', {
-      value: (child: Element) => {
-        container = child
-      },
-    })
-    render(
-      <Ellipsis
-        style={{
-          whiteSpace: 'pre-wrap',
-        }}
-        content={content}
-      />
-    )
-    expect(container).toHaveStyle('white-space: pre-wrap')
-  })
 })