Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skeleton Unit Tests #1891

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 119 additions & 113 deletions site/test-coverage.js

Large diffs are not rendered by default.

109 changes: 109 additions & 0 deletions src/hooks/useCountUp.ts
Original file line number Diff line number Diff line change
@@ -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;
80 changes: 62 additions & 18 deletions src/skeleton/Skeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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';

export type SkeletonProps = TdSkeletonProps & StyledProps & { children: React.ReactNode };
export type SkeletonProps = TdSkeletonProps & StyledProps;

const ThemeMap: Record<TdSkeletonProps['theme'], SkeletonRowCol> = {
text: [1],
Expand All @@ -24,10 +25,35 @@ const ThemeMap: Record<TdSkeletonProps['theme'], SkeletonRowCol> = {
],
};

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<SkeletonRowColObj>) => {
Expand Down Expand Up @@ -87,31 +113,49 @@ 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) {
return <>{children}</>;
return <>{children || props.content}</>;
}

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%' }]));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Vitest Snapshot v1

exports[`Skeleton Component > props.children works fine 1`] = `
<div>
<span
class="custom-node"
>
TNode
</span>
</div>
`;

exports[`Skeleton Component > props.content works fine 1`] = `
<div>
<span
class="custom-node"
>
TNode
</span>
</div>
`;
21 changes: 21 additions & 0 deletions src/skeleton/__tests__/mount.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { render } from '@test/utils';

const getContent = (Skeleton, props, events) => <Skeleton {...props} {...events}>
<div className='main-content'>
<p>
骨架屏组件,是指当网络较慢时,在页面真实数据加载之前,给用户展示出页面的大致结构。
</p>
</div>
</Skeleton>;

export function getSkeletonDefaultMount(...args) {
return render(getContent(...args));
}

export function getSkeletonInfo(...args) {
return {
mountedContent: render(getContent(...args)),
getContent
};
}
30 changes: 30 additions & 0 deletions src/skeleton/__tests__/skeleton.test.jsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
10 changes: 0 additions & 10 deletions src/skeleton/__tests__/skeleton.test.tsx

This file was deleted.

Loading