From b0079b6e6086ac5a0db94242392cf87f897cbe05 Mon Sep 17 00:00:00 2001 From: chaishi <974383157@qq.com> Date: Sun, 15 Jan 2023 18:38:28 +0800 Subject: [PATCH 1/3] test(skeleton): unit tests --- src/skeleton/Skeleton.tsx | 4 +- .../vitest-skeleton.test.jsx.snap | 21 ++++ src/skeleton/__tests__/mount.jsx | 14 +++ src/skeleton/__tests__/skeleton.test.tsx | 10 -- .../__tests__/vitest-skeleton.test.jsx | 96 +++++++++++++++++++ src/skeleton/skeleton.en-US.md | 3 +- src/skeleton/skeleton.md | 4 +- src/skeleton/type.ts | 10 +- 8 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 src/skeleton/__tests__/__snapshots__/vitest-skeleton.test.jsx.snap create mode 100644 src/skeleton/__tests__/mount.jsx delete mode 100644 src/skeleton/__tests__/skeleton.test.tsx create mode 100644 src/skeleton/__tests__/vitest-skeleton.test.jsx diff --git a/src/skeleton/Skeleton.tsx b/src/skeleton/Skeleton.tsx index 9890018af1..bf7f33a5ff 100644 --- a/src/skeleton/Skeleton.tsx +++ b/src/skeleton/Skeleton.tsx @@ -9,7 +9,7 @@ import { pxCompat } from '../_util/helper'; import parseTNode from '../_util/parseTNode'; import { skeletonDefaultProps } from './defaultProps'; -export type SkeletonProps = TdSkeletonProps & StyledProps & { children: React.ReactNode }; +export type SkeletonProps = TdSkeletonProps & StyledProps; const ThemeMap: Record = { text: [1], @@ -101,7 +101,7 @@ const Skeleton = (props: SkeletonProps) => { }, [delay, loading]); if (!ctrlLoading) { - return <>{children}; + return <>{children || props.content}; } const childrenContent = []; diff --git a/src/skeleton/__tests__/__snapshots__/vitest-skeleton.test.jsx.snap b/src/skeleton/__tests__/__snapshots__/vitest-skeleton.test.jsx.snap new file mode 100644 index 0000000000..c46c4ee795 --- /dev/null +++ b/src/skeleton/__tests__/__snapshots__/vitest-skeleton.test.jsx.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1 + +exports[`Skeleton Component > props.children works fine 1`] = ` +
+ + TNode + +
+`; + +exports[`Skeleton Component > props.content works fine 1`] = ` +
+ + TNode + +
+`; diff --git a/src/skeleton/__tests__/mount.jsx b/src/skeleton/__tests__/mount.jsx new file mode 100644 index 0000000000..c82d80b2e7 --- /dev/null +++ b/src/skeleton/__tests__/mount.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render } from '@test/utils'; + +export function getSkeletonDefaultMount(Skeleton, props, events) { + return render( + +
+

+ 骨架屏组件,是指当网络较慢时,在页面真实数据加载之前,给用户展示出页面的大致结构。 +

+
+
+ ); +} diff --git a/src/skeleton/__tests__/skeleton.test.tsx b/src/skeleton/__tests__/skeleton.test.tsx deleted file mode 100644 index c3af1b648b..0000000000 --- a/src/skeleton/__tests__/skeleton.test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -// import { render } from '@test/utils'; -// import React from 'react'; -// import Skeleton from '../index'; - -// TODO -describe('Skeleton 组件测试', () => { - test('dom', () => { - expect(true).toBe(true); - }); -}); diff --git a/src/skeleton/__tests__/vitest-skeleton.test.jsx b/src/skeleton/__tests__/vitest-skeleton.test.jsx new file mode 100644 index 0000000000..31d2a11208 --- /dev/null +++ b/src/skeleton/__tests__/vitest-skeleton.test.jsx @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * 该文件由脚本自动生成,如需修改请联系 PMC + * This file generated by scripts of tdesign-api. `npm run api:docs Skeleton React(PC) vitest,finalProject` + * If you need to modify this file, contact PMC first please. + */ +import React from 'react'; +import { fireEvent, vi, render, mockDelay } from '@test/utils'; +import { Skeleton } from '..'; +import { getSkeletonDefaultMount } from './mount'; + +describe('Skeleton Component', () => { + ['gradient', 'flashed', 'none'].forEach((item) => { + it(`props.animation is equal to ${item}`, () => { + const wrapper = getSkeletonDefaultMount(Skeleton, { animation: item }); + const container = wrapper.container.querySelector('.t-skeleton__col'); + expect(container).toHaveClass(`t-skeleton--animation-${item}`); + }); + }); + + it('props.children works fine', () => { + const { container } = render( + + TNode + , + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + it('props.content works fine', () => { + const { container } = render( + TNode} loading={false}>, + ); + expect(container.querySelector('.custom-node')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + it('props.delay: show loading delay 300ms', async () => { + const { container } = getSkeletonDefaultMount(Skeleton, { delay: 1000, loading: true }); + expect(container.querySelector('.t-skeleton__row')).toBeFalsy(); + await mockDelay(1000); + expect(container.querySelector('.t-skeleton__row')).toBeTruthy(); + }); + + it('props.loading is equal true', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { loading: true }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(1); + }); + + it('props.loading is equal false', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { loading: false }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(0); + }); + + it('props.theme is equal text', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { theme: 'text' }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton__col').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton--type-text').length).toBe(1); + }); + + it('props.theme is equal avatar', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { theme: 'avatar' }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton__col').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton--type-circle').length).toBe(1); + }); + + it('props.theme is equal paragraph', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { theme: 'paragraph' }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(3); + expect(container.querySelectorAll('.t-skeleton__col').length).toBe(3); + expect(container.querySelectorAll('.t-skeleton--type-text').length).toBe(3); + }); + + it('props.theme is equal avatar-text', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { theme: 'avatar-text' }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton__col').length).toBe(2); + expect(container.querySelectorAll('.t-skeleton--type-text').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton--type-circle').length).toBe(1); + }); + + it('props.theme is equal tab', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { theme: 'tab' }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(2); + expect(container.querySelectorAll('.t-skeleton__col').length).toBe(2); + expect(container.querySelectorAll('.t-skeleton--type-text').length).toBe(2); + }); + + it('props.theme is equal article', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { theme: 'article' }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(6); + }); +}); diff --git a/src/skeleton/skeleton.en-US.md b/src/skeleton/skeleton.en-US.md index 15354c585f..4be30e9114 100644 --- a/src/skeleton/skeleton.en-US.md +++ b/src/skeleton/skeleton.en-US.md @@ -1,7 +1,6 @@ :: BASE_DOC :: ## API - ### Skeleton Props name | type | default | description | required @@ -9,6 +8,8 @@ name | type | default | description | required className | String | - | 类名 | N style | Object | - | 样式,Typescript:`React.CSSProperties` | N animation | String | none | options:gradient/flashed/none | N +children | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +content | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N delay | Number | 0 | \- | N loading | Boolean | true | \- | N rowCol | Array | - | Typescript:`SkeletonRowCol` `type SkeletonRowCol = Array>` `interface SkeletonRowColObj { width?: string; height?: string; size?: string; marginRight?: string; marginLeft?: string; margin?: string; content?: string \| TNode; type?: 'rect' \| 'circle' \| 'text' }`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/src/skeleton/type.ts) | N diff --git a/src/skeleton/skeleton.md b/src/skeleton/skeleton.md index ccf047b825..5b994b16c0 100644 --- a/src/skeleton/skeleton.md +++ b/src/skeleton/skeleton.md @@ -8,7 +8,9 @@ className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N animation | String | none | 动画效果,有「渐变加载动画」和「闪烁加载动画」两种。值为 'none' 则表示没有动画。可选项:gradient/flashed/none | N -delay | Number | 0 | 【开发中】延迟显示加载效果的时间,用于防止请求速度过快引起的加载闪烁,单位:毫秒 | N +children | TNode | - | 加载完成的内容,同 content。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +content | TNode | - | 加载完成的内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts) | N +delay | Number | 0 | 延迟显示加载效果的时间,用于防止请求速度过快引起的加载闪烁,单位:毫秒 | N loading | Boolean | true | 是否为加载状态,如果是则显示骨架图,如果不是则显示加载完成的内容 | N rowCol | Array | - | 高级设置,用于自定义行列数量、宽度高度、间距等。【示例一】,`[1, 1, 2]` 表示输出三行骨架图,第一行一列,第二行一列,第三行两列。【示例二】,`[1, 1, { width: '100px' }]` 表示自定义第三行的宽度为 `100px`。【示例三】,`[1, 2, [{ width, height }, { width, height, marginLeft }]]` 表示第三行有两列,且自定义宽度、高度、尺寸(圆形或方形使用)、间距、内容等。TS 类型:`SkeletonRowCol` `type SkeletonRowCol = Array>` `interface SkeletonRowColObj { width?: string; height?: string; size?: string; marginRight?: string; marginLeft?: string; margin?: string; content?: string \| TNode; type?: 'rect' \| 'circle' \| 'text' }`。[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/src/skeleton/type.ts) | N theme | String | text | 快捷定义骨架图风格,有基础、头像组合等,具体参看代码示例。可选项:text/avatar/paragraph/avatar-text/tab/article | N diff --git a/src/skeleton/type.ts b/src/skeleton/type.ts index d95435a7e3..afd5fe0af7 100644 --- a/src/skeleton/type.ts +++ b/src/skeleton/type.ts @@ -13,7 +13,15 @@ export interface TdSkeletonProps { */ animation?: 'gradient' | 'flashed' | 'none'; /** - * 【开发中】延迟显示加载效果的时间,用于防止请求速度过快引起的加载闪烁,单位:毫秒 + * 加载完成的内容,同 content + */ + children?: TNode; + /** + * 加载完成的内容 + */ + content?: TNode; + /** + * 延迟显示加载效果的时间,用于防止请求速度过快引起的加载闪烁,单位:毫秒 * @default 0 */ delay?: number; From 9077590677ebc403b86a277e087e53ae51071df3 Mon Sep 17 00:00:00 2001 From: chaishi <974383157@qq.com> Date: Sun, 15 Jan 2023 18:57:09 +0800 Subject: [PATCH 2/3] test(skeleton): add rowCol unit tests --- .../__tests__/vitest-skeleton.test.jsx | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/skeleton/__tests__/vitest-skeleton.test.jsx b/src/skeleton/__tests__/vitest-skeleton.test.jsx index 31d2a11208..1cc39c038d 100644 --- a/src/skeleton/__tests__/vitest-skeleton.test.jsx +++ b/src/skeleton/__tests__/vitest-skeleton.test.jsx @@ -53,6 +53,61 @@ describe('Skeleton Component', () => { expect(container.querySelectorAll('.t-skeleton__row').length).toBe(0); }); + it('props.rowCol is equal [1,1,2]', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { rowCol: [1, 1, 2] }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(3); + expect(container.querySelectorAll('.t-skeleton__row:first-child .t-skeleton__col').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton__row:nth-child(2) .t-skeleton__col').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton__row:nth-child(3) .t-skeleton__col').length).toBe(2); + }); + + it('props.rowCol is equal [1, 1, { width: 100px }]', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { rowCol: [1, 1, { width: '100px' }] }); + expect(container.querySelectorAll('.t-skeleton__row').length).toBe(3); + expect(container.querySelectorAll('.t-skeleton__row:first-child .t-skeleton__col').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton__row:nth-child(3) .t-skeleton__col').length).toBe(1); + }); + + it('props.rowCol is equal [1, 2, [{ width, height }, { width, height, marginLeft }]]', () => { + const { container } = getSkeletonDefaultMount(Skeleton, { + rowCol: [ + 1, + 2, + [ + { width, height }, + { width, height, marginLeft }, + ], + ], + }); + expect(container.querySelectorAll('.t-skeleton__row:first-child .t-skeleton__col').length).toBe(1); + expect(container.querySelectorAll('.t-skeleton__row:nth-child(2) .t-skeleton__col').length).toBe(2); + expect(container.querySelectorAll('.t-skeleton__row:nth-child(3) .t-skeleton__col').length).toBe(2); + }); + + it(`props.rowCol is equal to [1, 1, { width: '100px' }]`, () => { + const { container } = getSkeletonDefaultMount(Skeleton, { rowCol: [1, 1, { width: '100px' }] }); + const domWrapper = container.querySelector('.t-skeleton__row:nth-child(3)'); + expect(domWrapper.style.width).toBe('100px'); + }); + it(`props.rowCol is equal to [1, 2, [{ width: '100px', height: '35px' }, { width: '101px', height: '36px', marginLeft: '16px' }]]`, () => { + const { container } = getSkeletonDefaultMount(Skeleton, { + rowCol: [ + 1, + 2, + [ + { width: '100px', height: '35px' }, + { width: '101px', height: '36px', marginLeft: '16px' }, + ], + ], + }); + const domWrapper = container.querySelector('.t-skeleton__row:nth-child(3) .t-skeleton__col:first-child'); + expect(domWrapper.style.width).toBe('100px'); + const domWrapper1 = container.querySelector('.t-skeleton__row:nth-child(3) .t-skeleton__col:nth-child(2)'); + expect(domWrapper1.style.width).toBe('101px'); + expect(domWrapper1.style.height).toBe('36px'); + expect(domWrapper1.style.marginLeft).toBe('16px'); + }); + it('props.theme is equal text', () => { const { container } = getSkeletonDefaultMount(Skeleton, { theme: 'text' }); expect(container.querySelectorAll('.t-skeleton__row').length).toBe(1); From 5b64e93a330ad1cad8f8aada4b890bcd574e5841 Mon Sep 17 00:00:00 2001 From: lihuanIT <996203586@qq.com> Date: Tue, 7 Feb 2023 18:31:00 +0800 Subject: [PATCH 3/3] skeleton bug fix & unit tests (#1951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(switch): coverage (#1948) * test: skeleton props and test fix delay and loading props and test * test: function name change * test: hook variable name change * test: 新增手写测试用例 * test: delete comments * test: delete test notes --------- Co-authored-by: luo3house <96865086+luo3house@users.noreply.github.com> Co-authored-by: hayleyli --- site/test-coverage.js | 232 +++++++++--------- src/hooks/useCountUp.ts | 109 ++++++++ src/skeleton/Skeleton.tsx | 76 ++++-- src/skeleton/__tests__/mount.jsx | 27 +- src/skeleton/__tests__/skeleton.test.jsx | 30 +++ .../__tests__/vitest-skeleton.test.jsx | 35 ++- src/skeleton/_example/delay.jsx | 16 +- src/switch/__tests__/switch.test.tsx | 9 + test/snap/__snapshots__/csr.test.jsx.snap | 38 +-- 9 files changed, 381 insertions(+), 191 deletions(-) create mode 100644 src/hooks/useCountUp.ts create mode 100644 src/skeleton/__tests__/skeleton.test.jsx diff --git a/site/test-coverage.js b/site/test-coverage.js index 7a0c18d115..90f2643924 100644 --- a/site/test-coverage.js +++ b/site/test-coverage.js @@ -1,33 +1,33 @@ module.exports = { "Util": { - "statements": "57.95%", - "branches": "43.1%", - "functions": "65.49%", - "lines": "59.83%" + "statements": "57.94%", + "branches": "43.51%", + "functions": "65.48%", + "lines": "59.82%" }, "affix": { "statements": "87.3%", - "branches": "56.67%", + "branches": "56.66%", "functions": "87.5%", "lines": "88.52%" }, "alert": { - "statements": "97.06%", - "branches": "72.73%", + "statements": "97.05%", + "branches": "72.72%", "functions": "100%", "lines": "100%" }, "anchor": { "statements": "93.16%", - "branches": "66.67%", + "branches": "66.66%", "functions": "88%", - "lines": "97.17%" + "lines": "97.16%" }, "autoComplete": { "statements": "95.39%", "branches": "89.33%", - "functions": "97.06%", - "lines": "97.89%" + "functions": "97.05%", + "lines": "97.88%" }, "avatar": { "statements": "97.01%", @@ -42,10 +42,10 @@ module.exports = { "lines": "100%" }, "breadcrumb": { - "statements": "86.96%", + "statements": "86.95%", "branches": "57.69%", "functions": "83.33%", - "lines": "90.91%" + "lines": "90.9%" }, "button": { "statements": "100%", @@ -55,7 +55,7 @@ module.exports = { }, "calendar": { "statements": "75.98%", - "branches": "50.33%", + "branches": "50.32%", "functions": "70%", "lines": "78.57%" }, @@ -66,20 +66,20 @@ module.exports = { "lines": "100%" }, "cascader": { - "statements": "93.46%", - "branches": "71.74%", - "functions": "92.31%", - "lines": "95.88%" + "statements": "93.45%", + "branches": "71.73%", + "functions": "92.3%", + "lines": "95.87%" }, "checkbox": { "statements": "90.54%", - "branches": "83.02%", + "branches": "83.01%", "functions": "100%", - "lines": "92.86%" + "lines": "92.85%" }, "collapse": { "statements": "96.1%", - "branches": "78.95%", + "branches": "78.94%", "functions": "93.75%", "lines": "96.05%" }, @@ -96,28 +96,28 @@ module.exports = { "lines": "100%" }, "common": { - "statements": "94.34%", - "branches": "84.62%", + "statements": "94.33%", + "branches": "84.61%", "functions": "100%", - "lines": "97.92%" + "lines": "97.91%" }, "configProvider": { - "statements": "70.59%", - "branches": "66.67%", + "statements": "70.58%", + "branches": "66.66%", "functions": "25%", "lines": "68.75%" }, "datePicker": { "statements": "68%", - "branches": "46.76%", - "functions": "63.08%", - "lines": "72.4%" + "branches": "46.75%", + "functions": "63.07%", + "lines": "72.39%" }, "dialog": { "statements": "85.43%", "branches": "70.21%", "functions": "84.21%", - "lines": "88.41%" + "lines": "88.4%" }, "divider": { "statements": "100%", @@ -127,39 +127,39 @@ module.exports = { }, "drawer": { "statements": "86.44%", - "branches": "82.76%", - "functions": "61.54%", + "branches": "82.75%", + "functions": "61.53%", "lines": "89.09%" }, "dropdown": { "statements": "94.44%", "branches": "61.29%", - "functions": "84.62%", + "functions": "84.61%", "lines": "97.14%" }, "form": { - "statements": "45.57%", - "branches": "25%", - "functions": "39.82%", - "lines": "47.94%" + "statements": "72.35%", + "branches": "61.45%", + "functions": "61.94%", + "lines": "75.3%" }, "grid": { - "statements": "84.54%", + "statements": "84.53%", "branches": "74.24%", "functions": "90%", - "lines": "84.54%" + "lines": "84.53%" }, "guide": { "statements": "100%", - "branches": "94.12%", + "branches": "94.11%", "functions": "100%", "lines": "100%" }, "hooks": { - "statements": "47.5%", - "branches": "27.42%", - "functions": "55.26%", - "lines": "47.92%" + "statements": "51.48%", + "branches": "32.91%", + "functions": "55.31%", + "lines": "52.45%" }, "image": { "statements": "100%", @@ -168,34 +168,34 @@ module.exports = { "lines": "100%" }, "imageViewer": { - "statements": "75.54%", + "statements": "75.53%", "branches": "77.19%", "functions": "65.71%", - "lines": "75.94%" + "lines": "75.93%" }, "input": { "statements": "93.63%", - "branches": "92.73%", - "functions": "89.19%", - "lines": "93.96%" + "branches": "92.72%", + "functions": "89.18%", + "lines": "93.95%" }, "inputAdornment": { - "statements": "86.96%", - "branches": "54.55%", + "statements": "86.95%", + "branches": "54.54%", "functions": "100%", - "lines": "90.48%" + "lines": "90.47%" }, "inputNumber": { - "statements": "78.69%", + "statements": "78.68%", "branches": "66.23%", - "functions": "78.95%", - "lines": "82.46%" + "functions": "78.94%", + "lines": "82.45%" }, "layout": { - "statements": "91.49%", - "branches": "41.67%", + "statements": "91.48%", + "branches": "41.66%", "functions": "85.71%", - "lines": "95.56%" + "lines": "95.55%" }, "link": { "statements": "100%", @@ -206,37 +206,37 @@ module.exports = { "list": { "statements": "79.41%", "branches": "58.33%", - "functions": "66.67%", + "functions": "66.66%", "lines": "79.41%" }, "loading": { "statements": "86.25%", - "branches": "66.67%", + "branches": "66.66%", "functions": "78.57%", "lines": "89.47%" }, "locale": { - "statements": "73.08%", + "statements": "73.07%", "branches": "72.22%", "functions": "83.33%", "lines": "73.91%" }, "menu": { "statements": "85.82%", - "branches": "69.14%", + "branches": "69.13%", "functions": "83.33%", - "lines": "90.52%" + "lines": "90.51%" }, "message": { - "statements": "88.44%", + "statements": "88.43%", "branches": "87.8%", "functions": "64.1%", - "lines": "94.74%" + "lines": "94.73%" }, "notification": { "statements": "89.47%", "branches": "75%", - "functions": "86.96%", + "functions": "86.95%", "lines": "93.7%" }, "pagination": { @@ -247,8 +247,8 @@ module.exports = { }, "popconfirm": { "statements": "75%", - "branches": "53.85%", - "functions": "81.82%", + "branches": "53.84%", + "functions": "81.81%", "lines": "75%" }, "popup": { @@ -258,26 +258,26 @@ module.exports = { "lines": "94.44%" }, "progress": { - "statements": "88.24%", - "branches": "64.71%", + "statements": "88.23%", + "branches": "64.7%", "functions": "100%", - "lines": "88.24%" + "lines": "88.23%" }, "radio": { - "statements": "82.54%", + "statements": "82.53%", "branches": "45.45%", - "functions": "92.86%", - "lines": "81.67%" + "functions": "92.85%", + "lines": "81.66%" }, "rangeInput": { "statements": "76.62%", - "branches": "66.67%", + "branches": "66.66%", "functions": "50%", - "lines": "76.32%" + "lines": "76.31%" }, "rate": { - "statements": "96.23%", - "branches": "79.17%", + "statements": "96.22%", + "branches": "79.16%", "functions": "100%", "lines": "100%" }, @@ -289,19 +289,25 @@ module.exports = { }, "selectInput": { "statements": "98%", - "branches": "91.67%", + "branches": "91.66%", "functions": "100%", "lines": "100%" }, + "skeleton": { + "statements": "98.66%", + "branches": "89.65%", + "functions": "100%", + "lines": "98.64%" + }, "slider": { "statements": "89.47%", - "branches": "67.8%", - "functions": "92.86%", - "lines": "91.06%" + "branches": "67.79%", + "functions": "92.85%", + "lines": "91.05%" }, "space": { - "statements": "92.31%", - "branches": "92.31%", + "statements": "92.3%", + "branches": "92.3%", "functions": "100%", "lines": "91.89%" }, @@ -313,98 +319,98 @@ module.exports = { }, "swiper": { "statements": "72.13%", - "branches": "42.61%", + "branches": "42.6%", "functions": "85.71%", - "lines": "71.51%" + "lines": "71.5%" }, "switch": { - "statements": "92.59%", - "branches": "84%", + "statements": "96.29%", + "branches": "92%", "functions": "100%", - "lines": "96.15%" + "lines": "100%" }, "table": { - "statements": "48.81%", - "branches": "34.33%", - "functions": "45.57%", + "statements": "48.8%", + "branches": "34.32%", + "functions": "45.56%", "lines": "49.67%" }, "tabs": { - "statements": "90.91%", - "branches": "79.8%", + "statements": "90.9%", + "branches": "79.79%", "functions": "86.36%", "lines": "91.12%" }, "tag": { "statements": "97.56%", - "branches": "96.3%", + "branches": "96.29%", "functions": "100%", "lines": "100%" }, "tagInput": { - "statements": "85.98%", - "branches": "83.1%", + "statements": "85.97%", + "branches": "83.09%", "functions": "83.78%", "lines": "87.74%" }, "textarea": { "statements": "82.43%", - "branches": "58.54%", + "branches": "58.53%", "functions": "80.95%", "lines": "86.36%" }, "timePicker": { - "statements": "80.65%", + "statements": "80.64%", "branches": "67.16%", "functions": "72.22%", "lines": "82.02%" }, "timeline": { - "statements": "98.39%", - "branches": "88.14%", + "statements": "98.38%", + "branches": "88.13%", "functions": "100%", "lines": "98.33%" }, "tooltip": { "statements": "90.74%", - "branches": "64.71%", + "branches": "64.7%", "functions": "75%", - "lines": "90.57%" + "lines": "90.56%" }, "transfer": { - "statements": "86.07%", + "statements": "86.06%", "branches": "67.02%", - "functions": "84.29%", - "lines": "87.78%" + "functions": "84.28%", + "lines": "87.77%" }, "tree": { "statements": "51.87%", - "branches": "34.17%", + "branches": "34.16%", "functions": "51.28%", "lines": "52.79%" }, "treeSelect": { "statements": "95.17%", "branches": "86.44%", - "functions": "97.44%", + "functions": "97.43%", "lines": "95.62%" }, "upload": { "statements": "96.55%", "branches": "100%", - "functions": "88.89%", + "functions": "88.88%", "lines": "100%" }, "watermark": { - "statements": "95.24%", - "branches": "84.38%", + "statements": "95.23%", + "branches": "84.37%", "functions": "100%", "lines": "100%" }, "utils": { "statements": "74.07%", "branches": "79.41%", - "functions": "82.61%", - "lines": "73.08%" + "functions": "82.6%", + "lines": "73.07%" } }; diff --git a/src/hooks/useCountUp.ts b/src/hooks/useCountUp.ts new file mode 100644 index 0000000000..9282eab1db --- /dev/null +++ b/src/hooks/useCountUp.ts @@ -0,0 +1,109 @@ +const requestAnimationFrame = + (typeof window === 'undefined' ? false : window.requestAnimationFrame) || ((cb) => setTimeout(cb, 16.6)); +const cancelAnimationFrame = + (typeof window === 'undefined' ? false : window.cancelAnimationFrame) || ((id) => clearTimeout(id)); + +type CountUpProps = { + // 计时器计时上限 + maxTime?: number; + // 浏览器每次重绘前的回调 + onChange?: (currentTime?: number) => void; + // 计时结束回调行为 + onFinish?: () => void; +}; + +const useCountUp = (options: CountUpProps) => { + const { maxTime, onChange, onFinish } = options; + // 记录最新动画id + let rafId; + // 计时器起始时间 + let startTime; + // 计时器暂停时间 + let stopTime; + // 计时器启动后历经时长 + let currentTime; + // 是否计时中 + let counting = false; + + /** + * 暂停计时 + * @returns + */ + const pause = () => { + // 已经暂停后,屏蔽掉点击 + if (!counting) return; + + counting = false; + stopTime = performance.now(); + if (rafId) { + cancelAnimationFrame(rafId); + } + }; + + /** + * 停止计时 + */ + const stop = () => { + pause(); + onFinish?.(); + }; + + /** + * 浏览器下一次重绘之前更新动画帧所调用的函数 + */ + const step = () => { + // performance.now(): requestAnimationFrame()开始去执行回调函数的时刻 + currentTime = performance.now() - startTime; + onChange?.(currentTime); + + if (maxTime && Math.floor(currentTime) >= maxTime) { + stop(); + return; + } + rafId = requestAnimationFrame(step); + }; + + /** + * 启动计时器 + */ + const start = () => { + // 计时中 或者 曾经计时过想要重新开始计时,应该先点击一下 重置 再开始计时 + if (counting || currentTime) return; + + counting = true; + startTime = performance.now(); + rafId = requestAnimationFrame(step); + }; + + /** + * 计时器继续计时 + */ + const goOn = () => { + // 已经在计时中,屏蔽掉点击 + if (counting) return; + + counting = true; + startTime += performance.now() - stopTime; + rafId = requestAnimationFrame(step); + }; + + /** + * 重置计时器 + */ + const reset = () => { + stop(); + currentTime = 0; + startTime = 0; + stopTime = 0; + }; + + return { + start, + pause, + stop, + goOn, + reset, + }; +}; + +export default useCountUp; diff --git a/src/skeleton/Skeleton.tsx b/src/skeleton/Skeleton.tsx index bf7f33a5ff..5fa1473875 100644 --- a/src/skeleton/Skeleton.tsx +++ b/src/skeleton/Skeleton.tsx @@ -1,10 +1,11 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import isNumber from 'lodash/isNumber'; import classNames from 'classnames'; import { SkeletonRowCol, SkeletonRowColObj, TdSkeletonProps } from './type'; import { StyledProps, Styles } from '../common'; import useConfig from '../hooks/useConfig'; +import useCountUp from '../hooks/useCountUp'; import { pxCompat } from '../_util/helper'; import parseTNode from '../_util/parseTNode'; import { skeletonDefaultProps } from './defaultProps'; @@ -24,10 +25,35 @@ const ThemeMap: Record = { ], }; -const Skeleton = (props: SkeletonProps) => { - const { animation, loading, rowCol, theme, className, style, delay = 0, children } = props; +// 超过delay时长,loading仍为true时,为解决骨架屏一闪而过问题,默认骨架屏存在时长500ms +const DEFAULT_DURATION = 500; +// 统计loading时长的最小时间间隔 +const DEFAULT_MIN_INTERVAL = 16.7; +const Skeleton = (props: SkeletonProps) => { + const { animation, loading, rowCol, theme, className, style, children } = props; + let { delay = 0 } = props; + // delay最小值为统计loading时长的最小时间间隔,一般在loading时长大于34时,骨架屏才生效 + if (delay > 0 && delay < DEFAULT_MIN_INTERVAL) { + delay = DEFAULT_MIN_INTERVAL; + } + const loadingTime = useRef(0); + const [ctrlLoading, setCtrlLoading] = useState(loading); const { classPrefix } = useConfig(); + + const countupChange = (currentTime) => { + loadingTime.current = currentTime; + if (currentTime > delay) { + setCtrlLoading(true); + } + }; + const countupFinish = () => { + setCtrlLoading(false); + }; + const { start, stop } = useCountUp({ + onChange: countupChange, + onFinish: countupFinish, + }); const name = `${classPrefix}-skeleton`; // t-skeleton const renderCols = (_cols: Number | SkeletonRowColObj | Array) => { @@ -87,17 +113,36 @@ const Skeleton = (props: SkeletonProps) => { )); }; - const [ctrlLoading, setCtrlLoading] = useState(loading); + // 清除骨架屏 + const clearSkeleton = () => { + setCtrlLoading(false); + stop(); + }; useEffect(() => { - if (delay > 0 && !loading) { - const timeout = setTimeout(() => { - setCtrlLoading(loading); - }, delay); - return () => clearTimeout(timeout); + // 骨架屏无需展示 + if (!loading) { + // 加载时长超过delay时,需加载DEFAULT_DURATION时长的骨架屏 + if (delay > 0 && loadingTime.current > delay) { + setCtrlLoading(true); + const timeout = setTimeout(() => { + clearSkeleton(); + }, DEFAULT_DURATION); + return () => clearTimeout(timeout); + } + // 直接展示内容 + clearSkeleton(); + } else { + // 存在delay时,暂不展示骨架屏 + if (delay > 0) { + setCtrlLoading(false); + start(); + return stop; + } + // 直接展示骨架屏 + setCtrlLoading(true); } - - setCtrlLoading(loading); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [delay, loading]); if (!ctrlLoading) { @@ -105,13 +150,12 @@ const Skeleton = (props: SkeletonProps) => { } const childrenContent = []; - if (theme) { - childrenContent.push(renderRowCol(ThemeMap[theme])); - } + if (rowCol) { childrenContent.push(renderRowCol(rowCol)); - } - if (!theme && !rowCol) { + } else if (theme) { + childrenContent.push(renderRowCol(ThemeMap[theme])); + } else { // 什么都不传时,传入默认 rowCol childrenContent.push(renderRowCol([1, 1, 1, { width: '70%' }])); } diff --git a/src/skeleton/__tests__/mount.jsx b/src/skeleton/__tests__/mount.jsx index c82d80b2e7..20138b23fe 100644 --- a/src/skeleton/__tests__/mount.jsx +++ b/src/skeleton/__tests__/mount.jsx @@ -1,14 +1,21 @@ import React from 'react'; import { render } from '@test/utils'; -export function getSkeletonDefaultMount(Skeleton, props, events) { - return render( - -
-

- 骨架屏组件,是指当网络较慢时,在页面真实数据加载之前,给用户展示出页面的大致结构。 -

-
-
- ); +const getContent = (Skeleton, props, events) => +
+

+ 骨架屏组件,是指当网络较慢时,在页面真实数据加载之前,给用户展示出页面的大致结构。 +

+
+
; + +export function getSkeletonDefaultMount(...args) { + return render(getContent(...args)); +} + +export function getSkeletonInfo(...args) { + return { + mountedContent: render(getContent(...args)), + getContent + }; } diff --git a/src/skeleton/__tests__/skeleton.test.jsx b/src/skeleton/__tests__/skeleton.test.jsx new file mode 100644 index 0000000000..051226f5c0 --- /dev/null +++ b/src/skeleton/__tests__/skeleton.test.jsx @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * 该文件由脚本自动生成,如需修改请联系 PMC + * This file generated by scripts of tdesign-api. `npm run api:docs Skeleton React(PC) vitest,finalProject` + * If you need to modify this file, contact PMC first please. + */ +import React from 'react'; +import { fireEvent, vi, render, mockDelay, screen } from '@test/utils'; +import { Skeleton } from '..'; +import { getSkeletonDefaultMount, getSkeletonInfo } from './mount'; + +describe('Skeleton Component', () => { + it('props.delay: show loading delay 10ms', async () => { + const props = { delay: 10 }; + const { mountedContent, getContent } = getSkeletonInfo(Skeleton, props); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeFalsy(); + setTimeout(() => { + mountedContent.rerender(getContent(Skeleton, {...props, loading: false})); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeFalsy(); + }, 10); + setTimeout(() => { + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeFalsy(); + mountedContent.rerender(getContent(Skeleton, { ...props, loading: true })); + }, 20); + // <34 都不会有骨架屏 + await mockDelay(35); + mountedContent.rerender(getContent(Skeleton, { ...props, loading: false })); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeTruthy(); + }); +}); diff --git a/src/skeleton/__tests__/vitest-skeleton.test.jsx b/src/skeleton/__tests__/vitest-skeleton.test.jsx index 1cc39c038d..6ba9907775 100644 --- a/src/skeleton/__tests__/vitest-skeleton.test.jsx +++ b/src/skeleton/__tests__/vitest-skeleton.test.jsx @@ -5,9 +5,9 @@ * If you need to modify this file, contact PMC first please. */ import React from 'react'; -import { fireEvent, vi, render, mockDelay } from '@test/utils'; +import { fireEvent, vi, render, mockDelay, screen } from '@test/utils'; import { Skeleton } from '..'; -import { getSkeletonDefaultMount } from './mount'; +import { getSkeletonDefaultMount, getSkeletonInfo } from './mount'; describe('Skeleton Component', () => { ['gradient', 'flashed', 'none'].forEach((item) => { @@ -36,11 +36,24 @@ describe('Skeleton Component', () => { expect(container).toMatchSnapshot(); }); - it('props.delay: show loading delay 300ms', async () => { - const { container } = getSkeletonDefaultMount(Skeleton, { delay: 1000, loading: true }); - expect(container.querySelector('.t-skeleton__row')).toBeFalsy(); - await mockDelay(1000); - expect(container.querySelector('.t-skeleton__row')).toBeTruthy(); + it('props.delay: show loading delay 100ms', async () => { + const props = { delay: 100, loading: true }; + const { mountedContent, getContent } = getSkeletonInfo(Skeleton, props); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeFalsy(); + await mockDelay(50); + mountedContent.rerender(getContent(Skeleton, {...props, loading: false})); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeFalsy(); + await mockDelay(80); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeFalsy(); + + mountedContent.rerender(getContent(Skeleton, {...props, loading: true})); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeFalsy(); + await mockDelay(150); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeTruthy(); + mountedContent.rerender(getContent(Skeleton, {...props, loading: false})); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeTruthy(); + await mockDelay(650); + expect(mountedContent.container.querySelector('.t-skeleton__row')).toBeFalsy(); }); it('props.loading is equal true', () => { @@ -68,14 +81,14 @@ describe('Skeleton Component', () => { expect(container.querySelectorAll('.t-skeleton__row:nth-child(3) .t-skeleton__col').length).toBe(1); }); - it('props.rowCol is equal [1, 2, [{ width, height }, { width, height, marginLeft }]]', () => { + it('props.rowCol is equal [1, 2, [{ width: 100, height: 35 }, { width: 101, height: 36, marginLeft: 16 }]]', () => { const { container } = getSkeletonDefaultMount(Skeleton, { rowCol: [ 1, 2, [ - { width, height }, - { width, height, marginLeft }, + { width: 100, height: 35 }, + { width: 101, height: 36, marginLeft: 16 }, ], ], }); @@ -86,7 +99,7 @@ describe('Skeleton Component', () => { it(`props.rowCol is equal to [1, 1, { width: '100px' }]`, () => { const { container } = getSkeletonDefaultMount(Skeleton, { rowCol: [1, 1, { width: '100px' }] }); - const domWrapper = container.querySelector('.t-skeleton__row:nth-child(3)'); + const domWrapper = container.querySelector('.t-skeleton__row:nth-child(3) .t-skeleton__col'); expect(domWrapper.style.width).toBe('100px'); }); it(`props.rowCol is equal to [1, 2, [{ width: '100px', height: '35px' }, { width: '101px', height: '36px', marginLeft: '16px' }]]`, () => { diff --git a/src/skeleton/_example/delay.jsx b/src/skeleton/_example/delay.jsx index 83699148f5..e6c6419c14 100644 --- a/src/skeleton/_example/delay.jsx +++ b/src/skeleton/_example/delay.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { Skeleton, Switch } from 'tdesign-react'; const style = { @@ -10,12 +10,20 @@ const style = { }, }; +const CONTENT =
+

设置最短延迟响应时间,低于响应时间的操作不显示加载状态。

+
; + export default function BasicSkeleton() { const [checked, setChecked] = useState(true); + const [content, setContent] = useState(null); + const loadingTimeChecked = useRef(0); + const loadingTimeUnChecked = useRef(0); const onChange = (value) => { - console.log('value', value); + console.log('checked:', value, '勾选:', loadingTimeChecked.current, '取消勾选:', loadingTimeUnChecked.current); setChecked(value); + setContent(value ? null : CONTENT); }; return ( @@ -25,9 +33,7 @@ export default function BasicSkeleton() {
-
-

设置最短延迟响应时间,低于响应时间的操作不显示加载状态。

-
+ {content}
diff --git a/src/switch/__tests__/switch.test.tsx b/src/switch/__tests__/switch.test.tsx index 3395c3b60e..ad1dbb60d0 100644 --- a/src/switch/__tests__/switch.test.tsx +++ b/src/switch/__tests__/switch.test.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { render, fireEvent, vi } from '@test/utils'; import Switch from '../Switch'; +import log from '../../_common/js/log'; +import noop from '../../_util/noop'; describe('Switch 组件测试', () => { test('create', async () => { @@ -37,4 +39,11 @@ describe('Switch 组件测试', () => { fireEvent.click(container.firstChild); expect(clickFn).toBeCalledTimes(1); }); + + test('should log error if value is not in customValue', async () => { + const logSpy = vi.spyOn(log, 'error').mockImplementation(noop); + render(); + expect(logSpy).toBeCalledTimes(1); + logSpy.mockRestore(); + }); }); diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 4708886c59..91b97e0115 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -168736,13 +168736,6 @@ exports[`csr snapshot test > csr test src/skeleton/_example/advance.jsx 1`] = ` class="content" >
-
-
-
@@ -168991,13 +168984,6 @@ exports[`csr snapshot test > csr test src/skeleton/_example/advance.jsx 1`] = ` class="content" >
-
-
-
@@ -169606,17 +169592,7 @@ exports[`csr snapshot test > csr test src/skeleton/_example/delay.jsx 1`] = ` />
-
-
-
-
-
-
-
+
, @@ -169637,17 +169613,7 @@ exports[`csr snapshot test > csr test src/skeleton/_example/delay.jsx 1`] = ` />
-
-
-
-
-
-
-
+
, "debug": [Function],