mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-29 18:50:00 +08:00
chore: merge master into feature
This commit is contained in:
commit
a09ed506d3
@ -87,12 +87,16 @@ jobs:
|
||||
# Execute visual regression diff task and zip then
|
||||
# output as visualRegressionReport.tar.gz
|
||||
- name: visual regression diff
|
||||
env:
|
||||
EVENT_NUMBER: ${{ github.event.number }}
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
npm run visual-regression -- --pr-id=${{ github.event.number }} --base-ref=${{ github.base_ref}}
|
||||
npm run visual-regression -- --pr-id=$EVENT_NUMBER --base-ref=$BASE_REF
|
||||
|
||||
# Upload report in `visualRegressionReport`
|
||||
- name: upload report artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: visual-regression-report
|
||||
path: visualRegressionReport.tar.gz
|
||||
|
@ -17,9 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
jobs: ${{ steps.visual_diff_build_job_status.outputs.result }}
|
||||
build-success: ${{ steps.visual_diff_build_job_status.outputs.build-success }}
|
||||
build-failure: ${{ steps.visual_diff_build_job_status.outputs.build-failure }}
|
||||
job-status: ${{ steps.visual_diff_build_job_status.outputs.job-status }}
|
||||
build-status: ${{ steps.visual_diff_build_job_status.outputs.build-status }}
|
||||
steps:
|
||||
- name: summary jobs status
|
||||
uses: actions/github-script@v6
|
||||
@ -47,9 +45,7 @@ jobs:
|
||||
console.log('visual-diff report job status: %s', jobs['visual-diff report']);
|
||||
|
||||
// set output
|
||||
core.setOutput('build-success', jobs['visual-diff report'] === 'success');
|
||||
core.setOutput('build-failure', jobs['visual-diff report'] === 'failure');
|
||||
core.setOutput('job-status', jobs['visual-diff report']);
|
||||
core.setOutput('build-status', jobs['visual-diff report']);
|
||||
return jobs;
|
||||
|
||||
download-visual-regression-report:
|
||||
@ -79,7 +75,8 @@ jobs:
|
||||
|
||||
# Download report artifact
|
||||
- name: download report artifact
|
||||
if: ${{ fromJSON(needs.upstream-workflow-summary.outputs.build-success) }}
|
||||
id: download_report
|
||||
if: ${{ needs.upstream-workflow-summary.outputs.build-status == 'success' || needs.upstream-workflow-summary.outputs.build-status == 'failure' }}
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: ${{ github.event.workflow_run.workflow_id }}
|
||||
@ -92,6 +89,7 @@ jobs:
|
||||
env:
|
||||
ALI_OSS_AK_ID: ${{ secrets.ALI_OSS_AK_ID }}
|
||||
ALI_OSS_AK_SECRET: ${{ secrets.ALI_OSS_AK_SECRET }}
|
||||
PR_ID: ${{ steps.pr.outputs.id }}
|
||||
run: |
|
||||
mkdir ./visualRegressionReport
|
||||
tar -xzvf visualRegressionReport.tar.gz -C ./visualRegressionReport
|
||||
@ -102,7 +100,7 @@ jobs:
|
||||
echo "✅ Install `ali-oss` Finished"
|
||||
|
||||
echo "🤖 Uploading"
|
||||
node scripts/visual-regression/upload.js ./visualRegressionReport --ref=pr-${{ steps.pr.outputs.id }}
|
||||
node scripts/visual-regression/upload.js ./visualRegressionReport --ref=pr-$PR_ID
|
||||
echo "✅ Uploaded"
|
||||
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
@ -122,13 +120,16 @@ jobs:
|
||||
number: ${{ steps.pr.outputs.id }}
|
||||
|
||||
- name: failed comment
|
||||
if: ${{ fromJSON(needs.upstream-workflow-summary.outputs.build-failure) || steps.report.outcome == 'failure' || failure() }}
|
||||
if: ${{ steps.download_report.outcome == 'failure' || steps.report.outcome == 'failure' || failure() }}
|
||||
uses: actions-cool/maintain-one-comment@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body: |
|
||||
## Visual-Regression Diff Failed
|
||||
Visual-diff report job conclusion: ${{ fromJSON(needs.upstream-workflow-summary.outputs.job-status) }}
|
||||
## Visual Regression Build for PR #${{ steps.pr.outputs.id }} Failed ❌
|
||||
Potential causes:
|
||||
- `upstream workflow` status: ${{ needs.upstream-workflow-summary.outputs.build-status }}
|
||||
- `download report artifact` status: ${{ steps.download_report.outcome }}
|
||||
- `report upload` status: ${{ steps.report.outcome }}
|
||||
<!-- VISUAL_DIFF_REGRESSION_HOOK -->
|
||||
body-include: '<!-- VISUAL_DIFF_REGRESSION_HOOK -->'
|
||||
number: ${{ steps.pr.outputs.id }}
|
||||
|
@ -87,18 +87,18 @@ jobs:
|
||||
|
||||
- name: Persist Image Snapshot to OSS
|
||||
if: github.event.workflow_run.event == 'push' && (github.event.workflow_run.head_branch == 'master' || github.event.workflow_run.head_branch == 'feature')
|
||||
id: persist
|
||||
continue-on-error: true
|
||||
env:
|
||||
ALI_OSS_AK_ID: ${{ secrets.ALI_OSS_AK_ID }}
|
||||
ALI_OSS_AK_SECRET: ${{ secrets.ALI_OSS_AK_SECRET }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
run: |
|
||||
rm package.json
|
||||
npm i ali-oss --no-save
|
||||
echo "✅ Install `ali-oss` Finished"
|
||||
|
||||
echo "🤖 Uploading"
|
||||
node scripts/visual-regression/upload.js ./imageSnapshots.tar.gz --ref=${{ github.event.workflow_run.head_sha }}
|
||||
node scripts/visual-regression/upload.js ./visual-regression-ref.txt --ref=${{ github.event.workflow_run.head_branch }}
|
||||
node scripts/visual-regression/upload.js ./imageSnapshots.tar.gz --ref=$HEAD_SHA
|
||||
node scripts/visual-regression/upload.js ./visual-regression-ref.txt --ref=$HEAD_BRANCH
|
||||
|
||||
echo "✅ Uploaded"
|
||||
|
@ -2,7 +2,7 @@ const { moduleNameMapper, transformIgnorePatterns } = require('./.jest');
|
||||
|
||||
// jest config for image snapshots
|
||||
module.exports = {
|
||||
setupFiles: ['./tests/setup.js'],
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'md'],
|
||||
moduleNameMapper,
|
||||
transform: {
|
||||
@ -19,5 +19,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
preset: 'jest-puppeteer',
|
||||
testTimeout: 10000,
|
||||
testTimeout: 20000,
|
||||
};
|
||||
|
2
.jest.js
2
.jest.js
@ -32,7 +32,7 @@ function getTestRegex(libDir) {
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
testEnvironment: 'jsdom',
|
||||
setupFiles: ['./tests/setup.js', 'jest-canvas-mock'],
|
||||
setupFiles: ['./tests/setup.ts', 'jest-canvas-mock'],
|
||||
setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'],
|
||||
modulePathIgnorePatterns: ['/_site/'],
|
||||
|
@ -2,7 +2,7 @@ const { moduleNameMapper, transformIgnorePatterns } = require('./.jest');
|
||||
|
||||
// jest config for server render environment
|
||||
module.exports = {
|
||||
setupFiles: ['./tests/setup.js'],
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'md'],
|
||||
moduleNameMapper,
|
||||
|
@ -16,6 +16,26 @@ tag: vVERSION
|
||||
|
||||
---
|
||||
|
||||
## 5.12.4
|
||||
|
||||
`2023-12-19`
|
||||
|
||||
- 🐞 Fix DatePicker style in `cssVar` mode. [#46526](https://github.com/ant-design/ant-design/pull/46526)
|
||||
|
||||
## 5.12.3
|
||||
|
||||
`2023-12-18`
|
||||
|
||||
- 💄 Fix Tag that style would be missing in SSR. [#46500](https://github.com/ant-design/ant-design/pull/46500) [@MadCcc](https://github.com/MadCcc)
|
||||
- 🐞 Upload `disabled` prop should not affect download icon. [#46454](https://github.com/ant-design/ant-design/pull/46454)
|
||||
- 💄 Upload.Dragger add vertical padding style. [#46457](https://github.com/ant-design/ant-design/pull/46457)
|
||||
- 🐞 Fix Upload actions color issue. [#46456](https://github.com/ant-design/ant-design/pull/46456)
|
||||
- 🐞 Fix Form with `getValueProps` not working with value update. [#46445](https://github.com/ant-design/ant-design/pull/46445)
|
||||
- 💄 Fix Checkbox style when customize `token.lineWidth`. [#46431](https://github.com/ant-design/ant-design/pull/46431)
|
||||
- 🐞 Fix Select that custom token make `padding` broken. [#46427](https://github.com/ant-design/ant-design/pull/46427) [@MadCcc](https://github.com/MadCcc)
|
||||
- 🐞 Fix Message that token overrides not work in `cssVar` mode. [#46415](https://github.com/ant-design/ant-design/pull/46415) [@MadCcc](https://github.com/MadCcc)
|
||||
- 💄 Fix Flex component don't apply extra style. [#46404](https://github.com/ant-design/ant-design/pull/46404) [@li-jia-nan](https://github.com/li-jia-nan)
|
||||
|
||||
## 5.12.2
|
||||
|
||||
`2023-12-11`
|
||||
|
@ -16,6 +16,26 @@ tag: vVERSION
|
||||
|
||||
---
|
||||
|
||||
## 5.12.4
|
||||
|
||||
`2023-12-19`
|
||||
|
||||
- 🐞 修复 DatePicker 组件在 CSS 变量模式下的样式错乱问题。[#46526](https://github.com/ant-design/ant-design/pull/46526)
|
||||
|
||||
## 5.12.3
|
||||
|
||||
`2023-12-18`
|
||||
|
||||
- 💄 修复 Tag 组件在 SSR 场景下的样式丢失问题。[#46500](https://github.com/ant-design/ant-design/pull/46500) [@MadCcc](https://github.com/MadCcc)
|
||||
- 🐞 Upload 的 `disabled` 属性不再对下载按钮生效。[#46454](https://github.com/ant-design/ant-design/pull/46454)
|
||||
- 💄 Upload.Dragger 增加一个水平内边距。[#46457](https://github.com/ant-design/ant-design/pull/46457)
|
||||
- 🐞 修复 Upload `actions` 的颜色问题。[#46456](https://github.com/ant-design/ant-design/pull/46456)
|
||||
- 🐞 修复 Form 使用 `getValueProps` 展示值无法更新的问题。[#46445](https://github.com/ant-design/ant-design/pull/46445)
|
||||
- 💄 修复 Checkbox 自定义 `token.lineWidth` 时勾选箭头错位问题。[#46431](https://github.com/ant-design/ant-design/pull/46431)
|
||||
- 🐞 修复 Select 组件定制 token 会让 padding 失效的问题。[#46427](https://github.com/ant-design/ant-design/pull/46427) [@MadCcc](https://github.com/MadCcc)
|
||||
- 🐞 修复 Message 在 `cssVar` 模式下覆盖组件 token 无效的问题。[#46415](https://github.com/ant-design/ant-design/pull/46415) [@MadCcc](https://github.com/MadCcc)
|
||||
- 💄 Flex 组件不应该应用额外的样式。[#46404](https://github.com/ant-design/ant-design/pull/46404) [@li-jia-nan](https://github.com/li-jia-nan)
|
||||
|
||||
## 5.12.2
|
||||
|
||||
`2023-12-11`
|
||||
|
@ -6,20 +6,11 @@
|
||||
|
||||
一套企业级 UI 设计语言和 React 组件库。
|
||||
|
||||
[![CI status][github-action-image]][github-action-url]
|
||||
[![codecov][codecov-image]][codecov-url]
|
||||
[![NPM version][npm-image]][npm-url]
|
||||
[![NPM downloads][download-image]][download-url]
|
||||
[![CI status][github-action-image]][github-action-url] [![codecov][codecov-image]][codecov-url] [![NPM version][npm-image]][npm-url] [![NPM downloads][download-image]][download-url]
|
||||
|
||||
[![][bundlephobia-image]][bundlephobia-url]
|
||||
[![][bundlesize-js-image]][unpkg-js-url]
|
||||
[![FOSSA Status][fossa-image]][fossa-url]
|
||||
[![][bundlephobia-image]][bundlephobia-url] [![][bundlesize-js-image]][unpkg-js-url] [![FOSSA Status][fossa-image]][fossa-url]
|
||||
|
||||
[![Follow Twitter][twitter-image]][twitter-url]
|
||||
[![Renovate status][renovate-image]][renovate-dashboard-url]
|
||||
[![][issues-helper-image]][issues-helper-url]
|
||||
[![dumi][dumi-image]][dumi-url]
|
||||
[![Issues need help][help-wanted-image]][help-wanted-url]
|
||||
[![Follow Twitter][twitter-image]][twitter-url] [![Renovate status][renovate-image]][renovate-dashboard-url] [![][issues-helper-image]][issues-helper-url] [![dumi][dumi-image]][dumi-url] [![Issues need help][help-wanted-image]][help-wanted-url]
|
||||
|
||||
[更新日志](./CHANGELOG-zh-CN.md) · [报告问题][github-issues-url] · [特性需求][github-issues-url] · [English](./README.md) · 中文
|
||||
|
||||
@ -171,8 +162,6 @@ $ npm start
|
||||
|
||||
## 🤝 参与共建 [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
|
||||
|
||||
ome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
|
||||
|
||||
<!-- Copy-paste in your Readme.md file -->
|
||||
|
||||
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=34526884&limit=30" target="_blank" style="display: block" align="center">
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { imageDemoTest } from '../../../tests/shared/imageTest';
|
||||
|
||||
describe('Carousel image', () => {
|
||||
imageDemoTest('carousel');
|
||||
imageDemoTest('carousel', {
|
||||
ssr: true,
|
||||
});
|
||||
});
|
||||
|
@ -32,7 +32,7 @@ import type {
|
||||
TriggerType,
|
||||
} from './interface';
|
||||
import useStyle from './style';
|
||||
import { customizePrefixCls, genAlphaColor, generateColor, getAlphaColor } from './util';
|
||||
import { genAlphaColor, generateColor, getAlphaColor } from './util';
|
||||
|
||||
export type ColorPickerProps = Omit<
|
||||
RcColorPickerProps,
|
||||
@ -91,6 +91,7 @@ const ColorPicker: CompoundedComponent = (props) => {
|
||||
className,
|
||||
size: customizeSize,
|
||||
rootClassName,
|
||||
prefixCls: customizePrefixCls,
|
||||
styles,
|
||||
disabledAlpha = false,
|
||||
onFormatChange,
|
||||
|
@ -2,8 +2,6 @@ import type { ColorGenInput } from '@rc-component/color-picker';
|
||||
import type { Color } from './color';
|
||||
import { ColorFactory } from './color';
|
||||
|
||||
export const customizePrefixCls = 'ant-color-picker';
|
||||
|
||||
export const generateColor = (color: ColorGenInput<Color>): Color => {
|
||||
if (color instanceof ColorFactory) {
|
||||
return color;
|
||||
|
@ -12543,6 +12543,111 @@ exports[`ConfigProvider components Collapse prefixCls 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components ColorPicker configProvider 1`] = `
|
||||
<div
|
||||
class="config-color-picker-trigger"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block-inner"
|
||||
style="background: rgb(22, 119, 255);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components ColorPicker configProvider componentDisabled 1`] = `
|
||||
<div
|
||||
class="config-color-picker-trigger config-color-picker-trigger-disabled"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block-inner"
|
||||
style="background: rgb(22, 119, 255);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components ColorPicker configProvider componentSize large 1`] = `
|
||||
<div
|
||||
class="config-color-picker-trigger config-color-picker-lg"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block-inner"
|
||||
style="background: rgb(22, 119, 255);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components ColorPicker configProvider componentSize middle 1`] = `
|
||||
<div
|
||||
class="config-color-picker-trigger"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block-inner"
|
||||
style="background: rgb(22, 119, 255);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components ColorPicker configProvider componentSize small 1`] = `
|
||||
<div
|
||||
class="config-color-picker-trigger config-color-picker-sm"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block"
|
||||
>
|
||||
<div
|
||||
class="config-color-picker-color-block-inner"
|
||||
style="background: rgb(22, 119, 255);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components ColorPicker normal 1`] = `
|
||||
<div
|
||||
class="ant-color-picker-trigger"
|
||||
>
|
||||
<div
|
||||
class="ant-color-picker-color-block"
|
||||
>
|
||||
<div
|
||||
class="ant-color-picker-color-block-inner"
|
||||
style="background: rgb(22, 119, 255);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components ColorPicker prefixCls 1`] = `
|
||||
<div
|
||||
class="prefix-ColorPicker-trigger"
|
||||
>
|
||||
<div
|
||||
class="prefix-ColorPicker-color-block"
|
||||
>
|
||||
<div
|
||||
class="prefix-ColorPicker-color-block-inner"
|
||||
style="background: rgb(22, 119, 255);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfigProvider components DatePicker DatePicker configProvider 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import React from 'react';
|
||||
|
||||
import ConfigProvider from '..';
|
||||
import { render } from '../../../tests/utils';
|
||||
import Alert from '../../alert';
|
||||
import Anchor from '../../anchor';
|
||||
import AutoComplete from '../../auto-complete';
|
||||
@ -16,6 +18,7 @@ import Carousel from '../../carousel';
|
||||
import Cascader from '../../cascader';
|
||||
import Checkbox from '../../checkbox';
|
||||
import Collapse from '../../collapse';
|
||||
import ColorPicker from '../../color-picker';
|
||||
import DatePicker from '../../date-picker';
|
||||
import Divider from '../../divider';
|
||||
import Drawer from '../../drawer';
|
||||
@ -39,7 +42,6 @@ import Select from '../../select';
|
||||
import Skeleton from '../../skeleton';
|
||||
import type { SliderTooltipProps } from '../../slider';
|
||||
import Slider from '../../slider';
|
||||
import { render } from '../../../tests/utils';
|
||||
import Spin from '../../spin';
|
||||
import Statistic from '../../statistic';
|
||||
import Steps from '../../steps';
|
||||
@ -233,6 +235,9 @@ describe('ConfigProvider', () => {
|
||||
</Collapse>
|
||||
));
|
||||
|
||||
// ColorPicker
|
||||
testPair('ColorPicker', (props) => <ColorPicker {...props} />);
|
||||
|
||||
// DatePicker
|
||||
describe('DatePicker', () => {
|
||||
testPair('DatePicker', (props) => (
|
||||
|
@ -1,5 +1,6 @@
|
||||
import dayjs from 'dayjs';
|
||||
import * as React from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import demoTest, { rootPropsTest } from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('date-picker', { skip: ['locale.tsx', 'component-token.tsx'], testRootProps: false });
|
||||
@ -10,7 +11,7 @@ rootPropsTest('date-picker', (DatePicker, props) => <DatePicker {...props} value
|
||||
});
|
||||
|
||||
rootPropsTest(
|
||||
'date-picker',
|
||||
['date-picker', 'RangePicker'],
|
||||
(DatePicker, props) => <DatePicker.RangePicker {...props} value={dayjs()} />,
|
||||
{
|
||||
findRootElements: () => document.querySelectorAll('.ant-picker-range, .ant-picker-dropdown'),
|
||||
|
@ -123,7 +123,7 @@ Form field component for data bidirectional binding, validation, layout, and so
|
||||
| dependencies | Set the dependency field. See [below](#dependencies) | [NamePath](#namepath)\[] | - | |
|
||||
| extra | The extra prompt message. It is similar to help. Usage example: to display error message and prompt message at the same time | ReactNode | - | |
|
||||
| getValueFromEvent | Specify how to get value from event or other onChange arguments | (..args: any\[]) => any | - | |
|
||||
| getValueProps | Additional props with sub component | (value: any) => any | - | 4.2.0 |
|
||||
| getValueProps | Additional props with sub component (It's not recommended to generate dynamic function prop by `getValueProps`. Please pass it to child component directly) | (value: any) => Record<string, any> | - | 4.2.0 |
|
||||
| hasFeedback | Used with `validateStatus`, this option specifies the validation status icon. Recommended to be used only with `Input`. Also, It can get feedback icons via icons prop. | boolean \| { icons: [FeedbackIcons](#feedbackicons) } | false | icons: 5.9.0 |
|
||||
| help | The prompt message. If not provided, the prompt message will be generated by the validation rule. | ReactNode | - | |
|
||||
| hidden | Whether to hide Form.Item (still collect and validate value) | boolean | false | 4.4.0 |
|
||||
|
@ -124,7 +124,7 @@ const validateMessages = {
|
||||
| dependencies | 设置依赖字段,说明[见下](#dependencies) | [NamePath](#namepath)\[] | - | |
|
||||
| extra | 额外的提示信息,和 `help` 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 | ReactNode | - | |
|
||||
| getValueFromEvent | 设置如何将 event 的值转换成字段值 | (..args: any\[]) => any | - | |
|
||||
| getValueProps | 为子元素添加额外的属性 | (value: any) => any | - | 4.2.0 |
|
||||
| getValueProps | 为子元素添加额外的属性 (不建议通过 `getValueProps` 生成动态函数 prop,请直接将其传递给子组件) | (value: any) => Record<string, any> | - | 4.2.0 |
|
||||
| hasFeedback | 配合 `validateStatus` 属性使用,展示校验状态图标,建议只配合 Input 组件使用 此外,它还可以通过 Icons 属性获取反馈图标。 | boolean \| { icons: [FeedbackIcons](#feedbackicons) } | false | icons: 5.9.0 |
|
||||
| help | 提示信息,如不设置,则会根据校验规则自动生成 | ReactNode | - | |
|
||||
| hidden | 是否隐藏字段(依然会收集和校验字段) | boolean | false | 4.4.0 |
|
||||
|
@ -125,7 +125,7 @@ Supports all props of `Input`.
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| visible | Whether the password is show or hide | boolean | false | 4.24.0 |
|
||||
| onVisibleChange | Callback executed when visibility of the password is changed | boolean | - | 4.24.0 |
|
||||
| onVisibleChange | Callback executed when visibility of the password is changed | (visible) => void | - | 4.24.0 |
|
||||
|
||||
#### Input Methods
|
||||
|
||||
|
@ -124,9 +124,9 @@ interface CountConfig {
|
||||
#### VisibilityToggle
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --------------- | -------------------- | ------- | ------- | ------- |
|
||||
| --------------- | -------------------- | ----------------- | ------- | ------- |
|
||||
| visible | 用于手动控制密码显隐 | boolean | false | 4.24 |
|
||||
| onVisibleChange | 显隐密码的回调 | boolean | - | 4.24 |
|
||||
| onVisibleChange | 显隐密码的回调 | (visible) => void | - | 4.24 |
|
||||
|
||||
#### Input Methods
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { imageDemoTest } from '../../../tests/shared/imageTest';
|
||||
|
||||
describe('Progress image', () => {
|
||||
imageDemoTest('progress');
|
||||
imageDemoTest('progress', {
|
||||
ssr: true,
|
||||
});
|
||||
});
|
||||
|
@ -12,9 +12,9 @@ const customIcons: Record<number, React.ReactNode> = {
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<Rate defaultValue={2} character={({ index }: { index: number }) => index + 1} />
|
||||
<Rate defaultValue={2} character={({ index = 0 }) => index + 1} />
|
||||
<br />
|
||||
<Rate defaultValue={3} character={({ index }: { index: number }) => customIcons[index + 1]} />
|
||||
<Rate defaultValue={3} character={({ index = 0 }) => customIcons[index + 1]} />
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs';
|
||||
|
||||
import type { AliasToken } from '../theme/internal';
|
||||
import type { TokenWithCommonCls } from '../theme/util/genComponentStyleHook';
|
||||
import type { ArrowToken } from './roundedArrow';
|
||||
@ -24,7 +25,9 @@ export function getArrowOffsetToken(options: {
|
||||
}
|
||||
|
||||
function isInject(valid: boolean, code: CSSObject): CSSObject {
|
||||
if (!valid) return {};
|
||||
if (!valid) {
|
||||
return {};
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
|
127
docs/blog/build-ghost.en-US.md
Normal file
127
docs/blog/build-ghost.en-US.md
Normal file
@ -0,0 +1,127 @@
|
||||
---
|
||||
title: A build ghost
|
||||
date: 2023-12-20
|
||||
author: zombieJ
|
||||
---
|
||||
|
||||
In the maintenance of antd-mobile, We meet an annoying ghost. It rarely appears when building locally, but it almost always appears in the github workflow. After a lot of tossing, We finally found its trace.
|
||||
|
||||
### CI Failed...again
|
||||
|
||||
For antd-mobile's CI, there is a task to check the build artifacts, which will prompt the file size changes. But in recent months, this task often fails to build, as shown in the following figure:
|
||||
|
||||
![CI failed](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*XSAESJ3_HWgAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
Check the log, we will get the error message of CSS file:
|
||||
|
||||
![Unknown word](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*2ybATYq9l2oAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
It seems that the error occurred when building 2x style (antd-mobile will generate 2x style for high-definition screen):
|
||||
|
||||
```log
|
||||
[09:44:16] Using gulpfile ~/work/ant-design-mobile/ant-design-mobile/gulpfile.js
|
||||
[09:44:16] Starting 'default'...
|
||||
[09:44:16] Starting 'clean'...
|
||||
[09:44:17] Finished 'clean' after 286 ms
|
||||
[09:44:17] Starting 'buildES'...
|
||||
[09:44:26] Finished 'buildES' after 8.77 s
|
||||
[09:44:26] Starting 'buildCJS'...
|
||||
[09:44:27] Finished 'buildCJS' after 1.72 s
|
||||
[09:44:27] Starting 'buildDeclaration'...
|
||||
[09:44:27] Starting 'buildStyle'...
|
||||
[09:44:28] Finished 'buildStyle' after 682 ms
|
||||
[09:44:34] Finished 'buildDeclaration' after 6.5 s
|
||||
[09:44:34] Starting 'copyAssets'...
|
||||
[09:44:34] Finished 'copyAssets' after 2.37 ms
|
||||
[09:44:34] Starting 'copyMetaFiles'...
|
||||
[09:44:34] Finished 'copyMetaFiles' after 4.64 ms
|
||||
[09:44:34] Starting 'generatePackageJSON'...
|
||||
[09:44:34] Finished 'generatePackageJSON' after 2.72 ms
|
||||
[09:44:34] Starting 'buildBundles'...
|
||||
[09:44:45] Finished 'buildBundles' after 11 s
|
||||
[09:44:45] Starting 'init2xFolder'...
|
||||
[09:44:46] Finished 'init2xFolder' after 811 ms
|
||||
[09:44:46] Starting 'build2xCSS'...
|
||||
[09:44:46] 'build2xCSS' errored after 126 ms
|
||||
[09:44:46] CssSyntaxError in plugin "gulp-postcss"
|
||||
```
|
||||
|
||||
The `style.css` of `build2xCSS` comes from the artifact of `buildStyle`, so it can be determined that there is a problem in the `buildStyle` task. After checking the corresponding file `/lib/bundle/style.css`, we found the following content:
|
||||
|
||||
![Break Lines](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*5NqFR6_nkhwAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
The first line of `style.css` is the compressed style, and then the incomplete uncompressed style. Compared with the successful artifact, it will be found that the style after the second line is unexpected:
|
||||
|
||||
![Success Style](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xnDRT5SDVvMAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
Check the uncompressed content, we will find that these contents already exist in the previous compressed content:
|
||||
|
||||
![Duplicated Content](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wShGRJ16U1AAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
It is speculated that the uncompressed content was generated first during the build, and then the compressed operation was performed. But there is an asynchronous problem, the second task started to execute before the first task was completed, resulting in the duplication of content. What's even more bizarre is that if it is an asynchronous problem, the error file content generated on CI is surprisingly consistent. No matter how many times it is built, as long as it fails, it must be the same content.
|
||||
|
||||
### Concurrent problem
|
||||
|
||||
Check the `gulpfile.js` file, we found that `buildStyle` uses vite to build. Considering that it may be a problem with the build version, so we upgraded vite from `3.x` to `5.x`, but the problem still exists. So check the relevant configuration:
|
||||
|
||||
```tsx
|
||||
{
|
||||
root: process.cwd(),
|
||||
mode: env,
|
||||
logLevel: 'silent',
|
||||
define: { 'process.env.NODE_ENV': `"${env}"` },
|
||||
build: {
|
||||
cssTarget: 'chrome61',
|
||||
lib: {
|
||||
formats,
|
||||
...
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
dir: './lib/bundle',
|
||||
globals: {
|
||||
'react': 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
},
|
||||
},
|
||||
},
|
||||
minify: isProd ? 'esbuild' : false,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Though closing the `logLevel: 'silent'` configuration, we can see more log content after rebuilding:
|
||||
|
||||
![Bundle Result](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*efjVR4DG_ysAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
We are close. When building, antd-mobile will create three copies of `es`, `cjs`, and `umd` through `lib.formats`. And each `format` will generate a `style.css` file. If it is just to overwrite the file, it should only waste extra build resources, and the compressed `style.css` will always be overwritten, and there should be no problem of simultaneous overwriting. Let's check the part that calls vite to build:
|
||||
|
||||
```tsx
|
||||
async function buildBundles(cb) {
|
||||
const envs = ['development', 'production'];
|
||||
const configs = envs.map((env) =>
|
||||
getViteConfigForPackage({
|
||||
env,
|
||||
formats: ['es', 'cjs', 'umd'],
|
||||
external: ['react', 'react-dom'],
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(configs.map((config) => vite.build(config)));
|
||||
cb && cb();
|
||||
}
|
||||
```
|
||||
|
||||
That's it. `Promise.all` is used to build concurrently, and vite's build is asynchronous. This causes a competition problem for `style.css`. The rollup called by vite will clean up the files and then perform write operations. Since the compressed style needs to be uglified, it is always slower than the uncompressed version. When rollup has finished cleaning and starts writing files, although the first part of the uncompressed version is deleted due to cleaning, the subsequent content is still written, while the compressed version starts writing from the beginning. When both are written, an error will occur and the content will be consistent under each CI build. The fix is also very simple, just change it to sequential execution:
|
||||
|
||||
```tsx
|
||||
for (const config of configs) {
|
||||
await vite.build(config);
|
||||
}
|
||||
```
|
||||
|
||||
(Of course, subsequent optimizations are also needed for the script. Skip the unnecessary `style.css` generation)
|
||||
|
||||
### That's all
|
||||
|
||||
With the performance changes of github CI, the ghost that was originally difficult to encounter has become stable and reproducible, which is quite interesting. This also gives us the opportunity to locate the problem.
|
127
docs/blog/build-ghost.zh-CN.md
Normal file
127
docs/blog/build-ghost.zh-CN.md
Normal file
@ -0,0 +1,127 @@
|
||||
---
|
||||
title: 一个构建的幽灵
|
||||
date: 2023-12-20
|
||||
author: zombieJ
|
||||
---
|
||||
|
||||
在 antd-mobile 的维护过程中,遇到了一个恼人的幽灵。它在本地构建时几乎不会出现,但是在 github 的 workflow 中,却几乎每次都会出现。在经过一番折腾后,终于找到了它的踪迹。
|
||||
|
||||
### CI 又挂了
|
||||
|
||||
在 antd-mobile 的 CI 中,有一个任务会对构建产物进行检查,会对文件大小变化进行提示。但是这个任务在最近几个月中,经常会出现构建失败的情况,如下图所示:
|
||||
|
||||
![CI failed](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*XSAESJ3_HWgAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
查看日志,会得到 CSS 文件报错的信息:
|
||||
|
||||
![Unknown word](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*2ybATYq9l2oAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
从构建流程看,似乎是在 2x 构建时报错(antd-mobile 会额外打一份 2x 的样式以适配高清屏):
|
||||
|
||||
```log
|
||||
[09:44:16] Using gulpfile ~/work/ant-design-mobile/ant-design-mobile/gulpfile.js
|
||||
[09:44:16] Starting 'default'...
|
||||
[09:44:16] Starting 'clean'...
|
||||
[09:44:17] Finished 'clean' after 286 ms
|
||||
[09:44:17] Starting 'buildES'...
|
||||
[09:44:26] Finished 'buildES' after 8.77 s
|
||||
[09:44:26] Starting 'buildCJS'...
|
||||
[09:44:27] Finished 'buildCJS' after 1.72 s
|
||||
[09:44:27] Starting 'buildDeclaration'...
|
||||
[09:44:27] Starting 'buildStyle'...
|
||||
[09:44:28] Finished 'buildStyle' after 682 ms
|
||||
[09:44:34] Finished 'buildDeclaration' after 6.5 s
|
||||
[09:44:34] Starting 'copyAssets'...
|
||||
[09:44:34] Finished 'copyAssets' after 2.37 ms
|
||||
[09:44:34] Starting 'copyMetaFiles'...
|
||||
[09:44:34] Finished 'copyMetaFiles' after 4.64 ms
|
||||
[09:44:34] Starting 'generatePackageJSON'...
|
||||
[09:44:34] Finished 'generatePackageJSON' after 2.72 ms
|
||||
[09:44:34] Starting 'buildBundles'...
|
||||
[09:44:45] Finished 'buildBundles' after 11 s
|
||||
[09:44:45] Starting 'init2xFolder'...
|
||||
[09:44:46] Finished 'init2xFolder' after 811 ms
|
||||
[09:44:46] Starting 'build2xCSS'...
|
||||
[09:44:46] 'build2xCSS' errored after 126 ms
|
||||
[09:44:46] CssSyntaxError in plugin "gulp-postcss"
|
||||
```
|
||||
|
||||
而 `build2xCSS` 的 `style.css` 来源于 `buildStyle` 的产物,所以可以确定是 `buildStyle` 任务中出现了问题。在查看对应的文件 `/lib/bundle/style.css` 后,发现了如下的内容:
|
||||
|
||||
![Break Lines](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*5NqFR6_nkhwAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
`style.css` 第一行为压缩的样式,而后是不完整的未压缩的样式。对比成功的产物会发现第二行往后的样式是非预期的内容:
|
||||
|
||||
![Success Style](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xnDRT5SDVvMAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
而根据未压缩的内容进行查询,会发现这些内容在之前的压缩内容中已经存在了:
|
||||
|
||||
![Duplicated Content](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wShGRJ16U1AAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
因而猜测可能是构建时,先生成了未压缩的内容,然后又进行了压缩操作。但是又存在异步问题,第二个任务在第一个未完成时就开始执行了,导致了内容的重复。更诡异的是,如果是异步问题,CI 上生成的错误文件内容却出奇的一致。无论构建多少次,只要是失败的就必定是相同的内容。
|
||||
|
||||
### 并发问题
|
||||
|
||||
在查看了 `gulpfile.js` 文件后,发现 `buildStyle` 使用的是 vite 构建。考虑到可能是构建版本的问题,所以将 vite 的版本从 `3.x` 升级到了 `5.x`,但是问题依旧存在。于是又看了一下相关配置:
|
||||
|
||||
```tsx
|
||||
{
|
||||
root: process.cwd(),
|
||||
mode: env,
|
||||
logLevel: 'silent',
|
||||
define: { 'process.env.NODE_ENV': `"${env}"` },
|
||||
build: {
|
||||
cssTarget: 'chrome61',
|
||||
lib: {
|
||||
formats,
|
||||
...
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
dir: './lib/bundle',
|
||||
globals: {
|
||||
'react': 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
},
|
||||
},
|
||||
},
|
||||
minify: isProd ? 'esbuild' : false,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
通过关闭 `logLevel: 'silent'` 配置后再次构建,我们可以看到更多的日志内容:
|
||||
|
||||
![Bundle Result](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*efjVR4DG_ysAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
看来接近答案了,antd-mobile 在构建时会通过 `lib.formats` 创建 `es`、`cjs`、`umd` 三份副本。而每个 `format` 都会生成一次 `style.css` 文件。如果仅是不断覆盖文件,那应该只是额外的浪费了构建资源而已,最后总是会被压缩的 `style.css` 覆盖掉,不应该出现同时覆盖的问题。于是去看了一下调用 vite 构建的部分:
|
||||
|
||||
```tsx
|
||||
async function buildBundles(cb) {
|
||||
const envs = ['development', 'production'];
|
||||
const configs = envs.map((env) =>
|
||||
getViteConfigForPackage({
|
||||
env,
|
||||
formats: ['es', 'cjs', 'umd'],
|
||||
external: ['react', 'react-dom'],
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(configs.map((config) => vite.build(config)));
|
||||
cb && cb();
|
||||
}
|
||||
```
|
||||
|
||||
原来是使用了 `Promise.all` 来并发构建,而 vite 的构建是异步的。这使得 `style.css` 存在竞争问题。vite 调用的 rollup 会对文件进行清除,然后进行写操作。由于压缩样式需要进行 uglify,所以它总是慢于非压缩版本。当 rollup 都执行完清理操作开始写文件后,非压缩版本虽然前面一部分由于清理被删除但是后续内容仍然继续被写入,而压缩版本则从头开始写入。当两者都写入完毕后,就会出现错误并且内容却在每次 CI 构建下都一致的情况。修复也很简单,直接改成顺序执行即可:
|
||||
|
||||
```tsx
|
||||
for (const config of configs) {
|
||||
await vite.build(config);
|
||||
}
|
||||
```
|
||||
|
||||
(当然,后续还需要对脚本进行优化。使其跳过非必要的 `style.css` 样式生成)
|
||||
|
||||
### 以上
|
||||
|
||||
随着 github CI 的性能变化,原本很难遇到的幽灵反而变得可以稳定重现,颇为有趣。从而也使得我们有机会可以定位到问题之所在。
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "antd",
|
||||
"version": "5.12.2",
|
||||
"version": "5.12.4",
|
||||
"description": "An enterprise-class UI design language and React components implementation",
|
||||
"keywords": [
|
||||
"ant",
|
||||
@ -93,7 +93,7 @@
|
||||
"pretest": "npm run version && npm run component-changelog",
|
||||
"test": "jest --config .jest.js --no-cache",
|
||||
"test-all": "sh -e ./scripts/test-all.sh",
|
||||
"test-image": "jest --config .jest.image.js --no-cache -i -u",
|
||||
"test-image": "jest --config .jest.image.js --no-cache -i -u --forceExit",
|
||||
"test-node": "npm run version && jest --config .jest.node.js --no-cache",
|
||||
"test:update": "jest --config .jest.js --no-cache -u",
|
||||
"token-meta": "tsx scripts/generate-token-meta.ts",
|
||||
@ -154,7 +154,7 @@
|
||||
"rc-table": "~7.36.0",
|
||||
"rc-tabs": "~12.15.0",
|
||||
"rc-textarea": "~1.6.3",
|
||||
"rc-tooltip": "~6.1.2",
|
||||
"rc-tooltip": "~6.1.3",
|
||||
"rc-tree": "~5.8.2",
|
||||
"rc-tree-select": "~5.15.0",
|
||||
"rc-upload": "~4.3.5",
|
||||
@ -241,7 +241,7 @@
|
||||
"eslint-plugin-markdown": "^3.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-unicorn": "^49.0.0",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fetch-jsonp": "^1.3.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
@ -275,7 +275,7 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-notifier": "^10.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"open": "^9.1.0",
|
||||
"open": "^10.0.0",
|
||||
"pixelmatch": "^5.3.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
|
@ -52,6 +52,7 @@ const DEPRECIATED_VERSION = {
|
||||
'5.11.1': ['https://github.com/ant-design/ant-design/issues/45883'],
|
||||
'5.11.2': ['https://github.com/ant-design/ant-design/issues/46005'],
|
||||
'5.11.4': ['https://github.com/ant-design/ant-design/pull/46103'],
|
||||
'5.12.3': ['https://github.com/ant-design/ant-design/issues/46525'],
|
||||
} as const;
|
||||
|
||||
function matchDeprecated(v: string) {
|
||||
|
@ -1,23 +1,22 @@
|
||||
/* eslint-disable compat/compat */
|
||||
/* eslint-disable no-console, no-await-in-loop, import/no-extraneous-dependencies, lodash/import-scope, no-restricted-syntax */
|
||||
import path from 'path';
|
||||
import { assert } from 'console';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { finished } from 'stream/promises';
|
||||
|
||||
import { remark } from 'remark';
|
||||
import remarkHtml from 'remark-html';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import minimist from 'minimist';
|
||||
import tar from 'tar';
|
||||
import fse from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import fse from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
import minimist from 'minimist';
|
||||
import pixelmatch from 'pixelmatch';
|
||||
import { PNG } from 'pngjs';
|
||||
import { remark } from 'remark';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkHtml from 'remark-html';
|
||||
import sharp from 'sharp';
|
||||
import { assert } from 'console';
|
||||
import tar from 'tar';
|
||||
|
||||
const ALI_OSS_BUCKET = 'antd-visual-diff';
|
||||
|
||||
@ -63,7 +62,7 @@ const compareScreenshots = async (
|
||||
diffPng.pack().pipe(fs.createWriteStream(diffImagePath));
|
||||
}
|
||||
|
||||
return (mismatchedPixels / (targetWidth * targetHeight)) * 100;
|
||||
return mismatchedPixels / (targetWidth * targetHeight);
|
||||
};
|
||||
|
||||
const readPngs = (dir: string) => fs.readdirSync(dir).filter((n) => n.endsWith('.png'));
|
||||
@ -108,6 +107,10 @@ async function downloadBaseSnapshots(ref: string, targetDir: string) {
|
||||
interface IBadCase {
|
||||
type: 'removed' | 'changed';
|
||||
filename: string;
|
||||
/**
|
||||
* 0 - 1
|
||||
*/
|
||||
weight: number;
|
||||
}
|
||||
|
||||
function md2Html(md: string) {
|
||||
@ -135,12 +138,14 @@ function generateReport(
|
||||
): [string, string] {
|
||||
const publicPath = isLocalEnv ? path.resolve(__dirname, '../..') : `${ossDomain}/pr-${prId}`;
|
||||
|
||||
const passed = badCases.length === 0;
|
||||
|
||||
const commonHeader = `
|
||||
## Visual Regression Report for PR #${prId}
|
||||
## Visual Regression Report for PR #${prId} ${passed ? 'Passed ✅' : 'Failed ❌'}
|
||||
> **Target branch:** ${targetBranch} (${targetRef})
|
||||
`.trim();
|
||||
|
||||
if (badCases.length === 0) {
|
||||
if (passed) {
|
||||
const mdStr = [
|
||||
commonHeader,
|
||||
'------------------------',
|
||||
@ -152,10 +157,7 @@ function generateReport(
|
||||
|
||||
const htmlReportLink = `${publicPath}/visualRegressionReport/report.html`;
|
||||
|
||||
const addonFullReportDesc = `\n\nToo many visual-regression diffs found, please check <a href="${htmlReportLink}" target="_blank">Full Report</a> for details`;
|
||||
|
||||
// github action pr comment has limit of 65536 4-byte unicode characters
|
||||
const limit = 65536 - addonFullReportDesc.length;
|
||||
const addonFullReportDesc = `\n\nCheck <a href="${htmlReportLink}" target="_blank">Full Report</a> for details`;
|
||||
|
||||
let reportMdStr = `
|
||||
${commonHeader}
|
||||
@ -168,7 +170,7 @@ ${commonHeader}
|
||||
|
||||
let fullVersionMd = reportMdStr;
|
||||
|
||||
let addonFullReportDescAdded = false;
|
||||
let diffCount = 0;
|
||||
|
||||
for (const badCase of badCases) {
|
||||
const { filename, type } = badCase;
|
||||
@ -193,16 +195,15 @@ ${commonHeader}
|
||||
lineReportMdStr += ' |\n';
|
||||
}
|
||||
|
||||
if (lineReportMdStr) {
|
||||
if (reportMdStr.length + lineReportMdStr.length < limit) {
|
||||
diffCount += 1;
|
||||
if (diffCount <= 10) {
|
||||
reportMdStr += lineReportMdStr;
|
||||
} else if (!addonFullReportDescAdded) {
|
||||
reportMdStr += addonFullReportDesc;
|
||||
addonFullReportDescAdded = true;
|
||||
}
|
||||
|
||||
fullVersionMd += lineReportMdStr;
|
||||
}
|
||||
}
|
||||
|
||||
reportMdStr += addonFullReportDesc;
|
||||
|
||||
// convert fullVersionMd to html
|
||||
return [reportMdStr, md2Html(fullVersionMd)];
|
||||
@ -213,6 +214,7 @@ async function boot() {
|
||||
|
||||
const baseImgSourceDir = path.resolve(__dirname, `../../imageSnapshots-${targetBranch}`);
|
||||
|
||||
/* --- prepare stage --- */
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Preparing image snapshots from latest \`${targetBranch}\` branch for pr \`${prId}\`\n`,
|
||||
@ -220,11 +222,11 @@ async function boot() {
|
||||
);
|
||||
await fse.ensureDir(baseImgSourceDir);
|
||||
|
||||
const targetRef = await getBranchLatestRef(targetBranch);
|
||||
assert(targetRef, `Missing ref from ${targetBranch}`);
|
||||
const targetCommitSha = await getBranchLatestRef(targetBranch);
|
||||
assert(targetCommitSha, `Missing commit sha from ${targetBranch}`);
|
||||
|
||||
if (!isLocalEnv) {
|
||||
await downloadBaseSnapshots(targetRef, baseImgSourceDir);
|
||||
await downloadBaseSnapshots(targetCommitSha, baseImgSourceDir);
|
||||
} else if (!fse.existsSync(baseImgSourceDir)) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
@ -252,24 +254,8 @@ async function boot() {
|
||||
console.log('\n');
|
||||
|
||||
const baseImgFileList = readPngs(baseImgSourceDir);
|
||||
const currentImgFileList = readPngs(currentImgSourceDir);
|
||||
|
||||
const deletedImgs = _.difference(baseImgFileList, currentImgFileList);
|
||||
if (deletedImgs.length) {
|
||||
console.log(
|
||||
chalk.red('⛔️ Missing images compare to %s:\n%s'),
|
||||
targetBranch,
|
||||
prettyList(deletedImgs),
|
||||
);
|
||||
console.log('\n');
|
||||
}
|
||||
// ignore new images
|
||||
const newImgs = _.difference(currentImgFileList, baseImgFileList);
|
||||
if (newImgs.length) {
|
||||
console.log(chalk.green('🆕 Added images:\n'), prettyList(newImgs));
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
/* --- compare stage --- */
|
||||
const badCases: IBadCase[] = [];
|
||||
|
||||
// compare cssinjs and css-var png from pr
|
||||
@ -295,6 +281,7 @@ async function boot() {
|
||||
badCases.push({
|
||||
type: 'removed',
|
||||
filename: compareImgName,
|
||||
weight: 1,
|
||||
});
|
||||
await fse.copy(baseImgPath, path.join(baseImgReportDir, compareImgName));
|
||||
continue;
|
||||
@ -310,7 +297,7 @@ async function boot() {
|
||||
console.log(
|
||||
'Mismatched pixels for:',
|
||||
chalk.yellow(compareImgName),
|
||||
`${mismatchedPxPercent.toFixed(2)}%\n`,
|
||||
`${(mismatchedPxPercent * 100).toFixed(2)}%\n`,
|
||||
);
|
||||
// copy compare imgs(x2) to report dir
|
||||
await fse.copy(baseImgPath, path.join(baseImgReportDir, compareImgName));
|
||||
@ -319,6 +306,7 @@ async function boot() {
|
||||
badCases.push({
|
||||
type: 'changed',
|
||||
filename: compareImgName,
|
||||
weight: mismatchedPxPercent,
|
||||
});
|
||||
} else {
|
||||
console.log('Passed for: %s\n', chalk.green(compareImgName));
|
||||
@ -326,15 +314,16 @@ async function boot() {
|
||||
}
|
||||
}
|
||||
|
||||
if (badCases.length) {
|
||||
console.log(chalk.red('⛔️ Failed cases:\n'), prettyList(badCases.map((i) => i.filename)));
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
/* --- generate report stage --- */
|
||||
const jsonl = badCases.map((i) => JSON.stringify(i)).join('\n');
|
||||
// write jsonl and markdown report to diffImgDir
|
||||
await fse.writeFile(path.join(reportDir, './report.jsonl'), jsonl);
|
||||
const [reportMdStr, reportHtmlStr] = generateReport(badCases, targetBranch, targetRef, prId);
|
||||
const [reportMdStr, reportHtmlStr] = generateReport(
|
||||
badCases,
|
||||
targetBranch,
|
||||
targetCommitSha,
|
||||
prId,
|
||||
);
|
||||
await fse.writeFile(path.join(reportDir, './report.md'), reportMdStr);
|
||||
const htmlTemplate = await fse.readFile(path.join(__dirname, './report-template.html'), 'utf8');
|
||||
|
||||
@ -353,6 +342,33 @@ async function boot() {
|
||||
},
|
||||
await fse.readdir(reportDir),
|
||||
);
|
||||
|
||||
const currentImgFileList = readPngs(currentImgSourceDir);
|
||||
/* --- text report stage --- */
|
||||
console.log(
|
||||
chalk.blue(`📊 Text report from pr #${prId} comparing to ${targetBranch}@${targetCommitSha}\n`),
|
||||
);
|
||||
// new images
|
||||
const newImgs = _.difference(currentImgFileList, baseImgFileList);
|
||||
if (newImgs.length) {
|
||||
console.log(chalk.green(`🆕 ${newImgs.length} images added from this pr`));
|
||||
console.log(chalk.green('🆕 Added images list:\n'));
|
||||
console.log(prettyList(newImgs));
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
if (!badCases.length) {
|
||||
console.log(chalk.green('🎉 All passed!'));
|
||||
console.log('\n');
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedBadCases = badCases.sort((a, b) => b.weight - a.weight);
|
||||
console.log(chalk.red('⛔️ Failed cases:\n'));
|
||||
console.log(prettyList(sortedBadCases.map((i) => `[${i.type}] ${i.filename}`)));
|
||||
console.log('\n');
|
||||
// let job failed
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
boot();
|
||||
|
@ -16,6 +16,7 @@
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th,
|
||||
@ -26,6 +27,20 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
td img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
th+th,
|
||||
td+td {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
/* eslint-disable no-console */
|
||||
const util = require('util');
|
||||
/* eslint-disable no-console, import/prefer-default-export */
|
||||
import util from 'util';
|
||||
import type { DOMWindow } from 'jsdom';
|
||||
|
||||
// import { fillWindowEnv } from './utils';
|
||||
|
||||
const React = require('react');
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
@ -20,17 +24,21 @@ console.error = (...args) => {
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-disable global-require */
|
||||
if (typeof window !== 'undefined') {
|
||||
global.window.resizeTo = (width, height) => {
|
||||
global.window.innerWidth = width || global.window.innerWidth;
|
||||
global.window.innerHeight = height || global.window.innerHeight;
|
||||
global.window.dispatchEvent(new Event('resize'));
|
||||
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
// This function can not move to external file since jest setup not support
|
||||
export function fillWindowEnv(window: Window | DOMWindow) {
|
||||
const win = window as Writeable<Window> & typeof globalThis;
|
||||
|
||||
win.resizeTo = (width, height) => {
|
||||
win.innerWidth = width || win.innerWidth;
|
||||
win.innerHeight = height || win.innerHeight;
|
||||
win.dispatchEvent(new Event('resize'));
|
||||
};
|
||||
global.window.scrollTo = () => {};
|
||||
win.scrollTo = () => {};
|
||||
// ref: https://github.com/ant-design/ant-design/issues/18774
|
||||
if (!window.matchMedia) {
|
||||
Object.defineProperty(global.window, 'matchMedia', {
|
||||
if (!win.matchMedia) {
|
||||
Object.defineProperty(win, 'matchMedia', {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: jest.fn((query) => ({
|
||||
@ -44,11 +52,19 @@ if (typeof window !== 'undefined') {
|
||||
// Fix css-animation or rc-motion deps on these
|
||||
// https://github.com/react-component/motion/blob/9c04ef1a210a4f3246c9becba6e33ea945e00669/src/util/motion.ts#L27-L35
|
||||
// https://github.com/yiminghe/css-animation/blob/a5986d73fd7dfce75665337f39b91483d63a4c8c/src/Event.js#L44
|
||||
window.AnimationEvent = window.AnimationEvent || window.Event;
|
||||
window.TransitionEvent = window.TransitionEvent || window.Event;
|
||||
win.AnimationEvent = win.AnimationEvent || win.Event;
|
||||
win.TransitionEvent = win.TransitionEvent || win.Event;
|
||||
|
||||
// ref: https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
// ref: https://github.com/jsdom/jsdom/issues/2524
|
||||
Object.defineProperty(window, 'TextEncoder', { writable: true, value: util.TextEncoder });
|
||||
Object.defineProperty(window, 'TextDecoder', { writable: true, value: util.TextDecoder });
|
||||
Object.defineProperty(win, 'TextEncoder', { writable: true, value: util.TextEncoder });
|
||||
Object.defineProperty(win, 'TextDecoder', { writable: true, value: util.TextDecoder });
|
||||
}
|
||||
|
||||
/* eslint-disable global-require */
|
||||
if (typeof window !== 'undefined') {
|
||||
fillWindowEnv(window);
|
||||
}
|
||||
|
||||
global.requestAnimationFrame = global.requestAnimationFrame || global.setTimeout;
|
||||
global.cancelAnimationFrame = global.cancelAnimationFrame || global.clearTimeout;
|
@ -1,15 +1,20 @@
|
||||
import path from 'path';
|
||||
import React from 'react';
|
||||
// Reference: https://github.com/ant-design/ant-design/pull/24003#discussion_r427267386
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
|
||||
import dayjs from 'dayjs';
|
||||
import path from 'path';
|
||||
import { globSync } from 'glob';
|
||||
import { configureToMatchImageSnapshot } from 'jest-image-snapshot';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import MockDate from 'mockdate';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
import { App, ConfigProvider, theme } from '../../components';
|
||||
import { fillWindowEnv } from '../setup';
|
||||
import { render } from '../utils';
|
||||
|
||||
jest.mock('../../components/grid/hooks/useBreakpoint', () => () => ({}));
|
||||
|
||||
const toMatchImageSnapshot = configureToMatchImageSnapshot({
|
||||
customSnapshotsDir: `${process.cwd()}/imageSnapshots`,
|
||||
@ -27,6 +32,7 @@ const themes = {
|
||||
interface ImageTestOptions {
|
||||
onlyViewport?: boolean;
|
||||
splitTheme?: boolean;
|
||||
ssr?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line jest/no-export
|
||||
@ -35,6 +41,74 @@ export default function imageTest(
|
||||
identifier: string,
|
||||
options: ImageTestOptions,
|
||||
) {
|
||||
let doc: Document;
|
||||
let container: HTMLDivElement;
|
||||
|
||||
beforeAll(() => {
|
||||
const dom = new JSDOM('<!DOCTYPE html><body></body></p>', {
|
||||
url: 'http://localhost/',
|
||||
});
|
||||
const win = dom.window;
|
||||
doc = win.document;
|
||||
|
||||
(global as any).window = win;
|
||||
|
||||
// Fill env
|
||||
const keys = [
|
||||
...Object.keys(win),
|
||||
'HTMLElement',
|
||||
'SVGElement',
|
||||
'ShadowRoot',
|
||||
'Element',
|
||||
'File',
|
||||
'Blob',
|
||||
].filter((key) => !(global as any)[key]);
|
||||
|
||||
keys.forEach((key) => {
|
||||
(global as any)[key] = win[key];
|
||||
});
|
||||
|
||||
// Fake Resize Observer
|
||||
global.ResizeObserver = function FakeResizeObserver() {
|
||||
return {
|
||||
observe() {},
|
||||
unobserve() {},
|
||||
disconnect() {},
|
||||
};
|
||||
} as any;
|
||||
|
||||
// Fake promise not called
|
||||
global.fetch = function mockFetch() {
|
||||
return {
|
||||
then() {
|
||||
return this;
|
||||
},
|
||||
catch() {
|
||||
return this;
|
||||
},
|
||||
finally() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
} as any;
|
||||
|
||||
// Fake matchMedia
|
||||
win.matchMedia = () =>
|
||||
({
|
||||
matches: false,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
}) as any;
|
||||
|
||||
// Fill window
|
||||
fillWindowEnv(win);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
doc.body.innerHTML = `<div id="root"></div>`;
|
||||
container = doc.querySelector<HTMLDivElement>('#root')!;
|
||||
});
|
||||
|
||||
function test(name: string, suffix: string, themedComponent: React.ReactElement) {
|
||||
it(name, async () => {
|
||||
await jestPuppeteer.resetPage();
|
||||
@ -55,14 +129,30 @@ export default function imageTest(
|
||||
|
||||
const cache = createCache();
|
||||
|
||||
const emptyStyleHolder = doc.createElement('div');
|
||||
|
||||
const element = (
|
||||
<StyleProvider cache={cache}>
|
||||
<StyleProvider cache={cache} container={emptyStyleHolder}>
|
||||
<App>{themedComponent}</App>
|
||||
</StyleProvider>
|
||||
);
|
||||
|
||||
const html = ReactDOMServer.renderToString(element);
|
||||
const styleStr = extractStyle(cache);
|
||||
let html: string;
|
||||
let styleStr: string;
|
||||
|
||||
if (options.ssr) {
|
||||
html = ReactDOMServer.renderToString(element);
|
||||
styleStr = extractStyle(cache);
|
||||
} else {
|
||||
const { unmount } = render(element, {
|
||||
container,
|
||||
});
|
||||
html = container.innerHTML;
|
||||
styleStr = extractStyle(cache);
|
||||
|
||||
// We should extract style before unmount
|
||||
unmount();
|
||||
}
|
||||
|
||||
await page.evaluate(
|
||||
(innerHTML, ssrStyle) => {
|
||||
@ -141,6 +231,8 @@ type Options = {
|
||||
skip?: boolean | string[];
|
||||
onlyViewport?: boolean | string[];
|
||||
splitTheme?: boolean | string[];
|
||||
/** Use SSR render instead. Only used when the third part deps component */
|
||||
ssr?: boolean;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line jest/no-export
|
||||
@ -168,6 +260,7 @@ export function imageDemoTest(component: string, options: Options = {}) {
|
||||
splitTheme:
|
||||
options.splitTheme === true ||
|
||||
(Array.isArray(options.splitTheme) && options.splitTheme.some((c) => file.endsWith(c))),
|
||||
ssr: options.ssr,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* eslint-disable global-require, import/no-dynamic-require, jest/no-export */
|
||||
import React from 'react';
|
||||
|
||||
import ConfigProvider from '../../components/config-provider';
|
||||
import { render, waitFakeTimer } from '../utils';
|
||||
import { TriggerMockContext } from './demoTestContext';
|
||||
@ -20,14 +21,17 @@ function isSingleNode(node: any): node is Element {
|
||||
}
|
||||
|
||||
export default function rootPropsTest(
|
||||
component: string,
|
||||
component: string | string[],
|
||||
customizeRender?: (
|
||||
component: React.ComponentType<any> & Record<string, any>,
|
||||
props: any,
|
||||
) => React.ReactNode,
|
||||
options?: Options,
|
||||
) {
|
||||
const Component = require(`../../components/${component}`).default as any;
|
||||
const componentNames = Array.isArray(component) ? component : [component];
|
||||
const [componentName, subComponentName] = componentNames;
|
||||
|
||||
const Component = require(`../../components/${componentName}`).default as any;
|
||||
const name = options?.name ? `(${options.name})` : '';
|
||||
|
||||
describe(`RootProps${name}`, () => {
|
||||
@ -36,6 +40,7 @@ export default function rootPropsTest(
|
||||
beforeEach(() => {
|
||||
passed = false;
|
||||
jest.useFakeTimers();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -46,7 +51,7 @@ export default function rootPropsTest(
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('rootClassName', async () => {
|
||||
it(['rootClassName', subComponentName].filter((v) => v).join(' '), async () => {
|
||||
const rootClassName = 'TEST_ROOT_CLS';
|
||||
|
||||
if (options?.beforeRender) {
|
||||
@ -104,7 +109,7 @@ export default function rootPropsTest(
|
||||
|
||||
expect(childList.length).toBeGreaterThan(0);
|
||||
if (options?.expectCount) {
|
||||
expect(childList.length).toBe(options.expectCount);
|
||||
expect(childList).toHaveLength(options.expectCount);
|
||||
}
|
||||
|
||||
childList.forEach((ele) => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { createRef, StrictMode } from 'react';
|
||||
import type { RenderOptions } from '@testing-library/react';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import MockDate from 'mockdate';
|
||||
import { _rs as onEsResize } from 'rc-resize-observer/es/utils/observerUtil';
|
||||
import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { StrictMode } from 'react';
|
||||
|
||||
export function assertsExist<T>(item?: T): asserts item is T {
|
||||
expect(item).not.toBeUndefined();
|
||||
@ -33,7 +33,7 @@ const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>
|
||||
render(ui, { wrapper: StrictMode, ...options });
|
||||
|
||||
export function renderHook<T>(func: () => T): { result: React.RefObject<T> } {
|
||||
const result = React.createRef<T>();
|
||||
const result = createRef<T>();
|
||||
|
||||
const Demo: React.FC = () => {
|
||||
(result as any).current = func();
|
||||
@ -58,7 +58,7 @@ export { pureRender, customRender as render };
|
||||
export const triggerResize = (target: Element) => {
|
||||
const originGetBoundingClientRect = target.getBoundingClientRect;
|
||||
|
||||
target.getBoundingClientRect = () => ({ width: 510, height: 903 } as DOMRect);
|
||||
target.getBoundingClientRect = () => ({ width: 510, height: 903 }) as DOMRect;
|
||||
|
||||
act(() => {
|
||||
onLibResize([{ target } as ResizeObserverEntry]);
|
||||
|
Loading…
Reference in New Issue
Block a user