mirror of
https://github.com/ant-design/ant-design.git
synced 2024-12-18 11:18:14 +08:00
commit
1aa5bc1d68
@ -1,4 +1,5 @@
|
||||
{
|
||||
"installCommand": "npm-install",
|
||||
"sandboxes": ["antd-reproduction-template-forked-jyh2k9"],
|
||||
"node": "18"
|
||||
}
|
||||
|
@ -54,15 +54,18 @@ jobs:
|
||||
permissions:
|
||||
actions: read # for dawidd6/action-download-artifact to query and download artifacts
|
||||
runs-on: ubuntu-latest
|
||||
needs: upstream-workflow-summary
|
||||
needs: [upstream-workflow-summary]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# We need get persist key first
|
||||
- name: Download Visual Regression Ref
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: ${{ github.event.workflow_run.workflow_id }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: visual-regression-persist-ref
|
||||
name: visual-regression-master-ref
|
||||
|
||||
# Save visual-regression ref to output
|
||||
- name: Extra Visual Regression Ref
|
||||
@ -78,36 +81,20 @@ jobs:
|
||||
name: image-snapshots
|
||||
path: ./
|
||||
|
||||
- name: Persist to Snapshot Repo
|
||||
- name: Persist Image Snapshot to OSS
|
||||
if: github.ref_name == 'master'
|
||||
id: persist
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ANTD_IMAGE_SNAP_REPO_TOKEN }}
|
||||
# should push to snapshot repo firstly
|
||||
# push the single folder to the repo
|
||||
ALI_OSS_AK_ID: ${{ secrets.ALI_OSS_AK_ID }}
|
||||
ALI_OSS_AK_SECRET: ${{ secrets.ALI_OSS_AK_SECRET }}
|
||||
ALI_OSS_BUCKET: ${{ secrets.ALI_OSS_BUCKET }}
|
||||
run: |
|
||||
git clone https://$GITHUB_TOKEN@github.com/ant-design/antd-image-snapshots.git
|
||||
rm package.json
|
||||
npm i ali-oss --no-save
|
||||
|
||||
echo "✅ Clone Finished"
|
||||
echo "🤖 Uploading"
|
||||
node scripts/visual-regression-upload.js ./imageSnapshots.tar.gz --ref=${{ github.sha }}
|
||||
node scripts/visual-regression-upload.js ./visual-regression-ref.txt --ref=${{ github.ref_name }}
|
||||
|
||||
rm antd-image-snapshots/*.txt
|
||||
mv visual-regression-ref.txt antd-image-snapshots/
|
||||
|
||||
rm -rf antd-image-snapshots/imageSnapshots/*
|
||||
tar -xzvf imageSnapshots.tar.gz -C antd-image-snapshots/imageSnapshots
|
||||
|
||||
echo "✅ Changes Finished"
|
||||
|
||||
cd antd-image-snapshots
|
||||
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
|
||||
git commit -a -m 'feat: update snapshot from ${{steps.visuall-regression.outputs.id}}'
|
||||
|
||||
echo "✅ Commit Finished"
|
||||
|
||||
git push --prune https://vaugsX:$GITHUB_TOKEN@github.com/ant-design/antd-image-snapshots.git +refs/remotes/origin/*:refs/heads/*
|
||||
|
||||
echo "✅ Push Finished"
|
||||
echo "✅ Uploaded"
|
||||
|
@ -90,5 +90,5 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: visual-regression-persist-ref
|
||||
name: visual-regression-master-ref
|
||||
path: ./visual-regression-ref.txt
|
||||
|
@ -88,14 +88,14 @@ const InternalForm: React.ForwardRefRenderFunction<FormInstance, FormProps> = (p
|
||||
return requiredMark;
|
||||
}
|
||||
|
||||
if (contextForm && contextForm.requiredMark !== undefined) {
|
||||
return contextForm.requiredMark;
|
||||
}
|
||||
|
||||
if (hideRequiredMark) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (contextForm && contextForm.requiredMark !== undefined) {
|
||||
return contextForm.requiredMark;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [hideRequiredMark, requiredMark, contextForm]);
|
||||
|
||||
|
@ -1089,7 +1089,8 @@ describe('Form', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('legacy hideRequiredMark', () => {
|
||||
describe('legacy hideRequiredMark', () => {
|
||||
it('should work', () => {
|
||||
const { container } = render(
|
||||
<Form hideRequiredMark role="form">
|
||||
<Form.Item name="light" label="light" required>
|
||||
@ -1101,6 +1102,33 @@ describe('Form', () => {
|
||||
expect(container.querySelector('form')!).toHaveClass('ant-form-hide-required-mark');
|
||||
});
|
||||
|
||||
it('priority should be higher than CP', () => {
|
||||
const { container, rerender } = render(
|
||||
<ConfigProvider form={{ requiredMark: true }}>
|
||||
<Form hideRequiredMark role="form">
|
||||
<Form.Item name="light" label="light" required>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('form')!).toHaveClass('ant-form-hide-required-mark');
|
||||
|
||||
rerender(
|
||||
<ConfigProvider form={{ requiredMark: undefined }}>
|
||||
<Form hideRequiredMark role="form">
|
||||
<Form.Item name="light" label="light" required>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('form')!).toHaveClass('ant-form-hide-required-mark');
|
||||
});
|
||||
});
|
||||
|
||||
it('form should support disabled', () => {
|
||||
const App: React.FC = () => (
|
||||
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 14 }} layout="horizontal" disabled>
|
||||
|
@ -75,7 +75,6 @@ const genSpinStyle: GenerateStyle<SpinToken> = (token: SpinToken): CSSObject =>
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
opacity: 0,
|
||||
visibility: 'hidden',
|
||||
transition: `all ${token.motionDurationMid}`,
|
||||
|
@ -202,6 +202,10 @@ const InternalTable = <RecordType extends AnyObject = AnyObject>(
|
||||
const prefixCls = getPrefixCls('table', customizePrefixCls);
|
||||
const dropdownPrefixCls = getPrefixCls('dropdown', customizeDropdownPrefixCls);
|
||||
|
||||
const [, token] = useToken();
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
const [wrapCSSVar, hashId] = useStyle(prefixCls, rootCls);
|
||||
|
||||
const mergedExpandable: ExpandableConfig<RecordType> = {
|
||||
childrenColumnName: legacyChildrenColumnName,
|
||||
expandIconColumnIndex,
|
||||
@ -349,6 +353,7 @@ const InternalTable = <RecordType extends AnyObject = AnyObject>(
|
||||
mergedColumns,
|
||||
onFilterChange,
|
||||
getPopupContainer: getPopupContainer || getContextPopupContainer,
|
||||
rootClassName: classNames(rootClassName, rootCls),
|
||||
});
|
||||
const mergedData = getFilterData(sortedData, filterStates);
|
||||
|
||||
@ -538,10 +543,6 @@ const InternalTable = <RecordType extends AnyObject = AnyObject>(
|
||||
};
|
||||
}
|
||||
|
||||
const [, token] = useToken();
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
const [wrapCSSVar, hashId] = useStyle(prefixCls, rootCls);
|
||||
|
||||
const wrapperClassNames = classNames(
|
||||
rootCls,
|
||||
`${prefixCls}-wrapper`,
|
||||
|
@ -133,6 +133,7 @@ export interface FilterDropdownProps<RecordType> {
|
||||
locale: TableLocale;
|
||||
getPopupContainer?: GetPopupContainer;
|
||||
filterResetToDefaultFilteredValue?: boolean;
|
||||
rootClassName?: string;
|
||||
}
|
||||
|
||||
function wrapStringListType(keys?: FilterKey) {
|
||||
@ -154,6 +155,7 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
|
||||
locale,
|
||||
children,
|
||||
getPopupContainer,
|
||||
rootClassName,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@ -524,6 +526,7 @@ function FilterDropdown<RecordType>(props: FilterDropdownProps<RecordType>) {
|
||||
onOpenChange={onVisibleChange}
|
||||
getPopupContainer={getPopupContainer}
|
||||
placement={direction === 'rtl' ? 'bottomLeft' : 'bottomRight'}
|
||||
rootClassName={rootClassName}
|
||||
>
|
||||
<span
|
||||
role="button"
|
||||
|
@ -76,6 +76,7 @@ function injectFilter<RecordType>(
|
||||
triggerFilter: (filterState: FilterState<RecordType>) => void,
|
||||
getPopupContainer?: GetPopupContainer,
|
||||
pos?: string,
|
||||
rootClassName?: string,
|
||||
): ColumnsType<RecordType> {
|
||||
return columns.map((column, index) => {
|
||||
const columnPos = getColumnPos(index, pos);
|
||||
@ -103,6 +104,7 @@ function injectFilter<RecordType>(
|
||||
triggerFilter={triggerFilter}
|
||||
locale={locale}
|
||||
getPopupContainer={getPopupContainer}
|
||||
rootClassName={rootClassName}
|
||||
>
|
||||
{renderColumnTitle(column.title, renderProps)}
|
||||
</FilterDropdown>
|
||||
@ -122,6 +124,7 @@ function injectFilter<RecordType>(
|
||||
triggerFilter,
|
||||
getPopupContainer,
|
||||
columnPos,
|
||||
rootClassName,
|
||||
),
|
||||
};
|
||||
}
|
||||
@ -184,6 +187,7 @@ interface FilterConfig<RecordType> {
|
||||
filterStates: FilterState<RecordType>[],
|
||||
) => void;
|
||||
getPopupContainer?: GetPopupContainer;
|
||||
rootClassName?: string;
|
||||
}
|
||||
|
||||
const getMergedColumns = <RecordType extends unknown>(
|
||||
@ -203,6 +207,7 @@ function useFilter<RecordType>({
|
||||
onFilterChange,
|
||||
getPopupContainer,
|
||||
locale: tableLocale,
|
||||
rootClassName,
|
||||
}: FilterConfig<RecordType>): [
|
||||
TransformColumns<RecordType>,
|
||||
FilterState<RecordType>[],
|
||||
@ -282,6 +287,8 @@ function useFilter<RecordType>({
|
||||
tableLocale,
|
||||
triggerFilter,
|
||||
getPopupContainer,
|
||||
undefined,
|
||||
rootClassName,
|
||||
);
|
||||
|
||||
return [transformColumns, mergedFilterStates, filters];
|
||||
|
@ -5,7 +5,7 @@ order: 5
|
||||
title: Internationalization
|
||||
---
|
||||
|
||||
The default language of `antd@2.x` is currently English. If you wish to use other languages, follow the instructions below.
|
||||
The default language of `antd` is currently English. If you wish to use other languages, follow the instructions below.
|
||||
|
||||
## ConfigProvider
|
||||
|
||||
|
@ -98,7 +98,8 @@
|
||||
"test:update": "jest --config .jest.js --no-cache -u",
|
||||
"token-meta": "tsx scripts/generate-token-meta.ts",
|
||||
"tsc": "tsc --noEmit",
|
||||
"version": "tsx scripts/generate-version.ts"
|
||||
"version": "tsx scripts/generate-version.ts",
|
||||
"npm-install": "npm install"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": "biome format --write",
|
||||
@ -183,6 +184,7 @@
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/ali-oss": "^6.16.11",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/gtag.js": "^0.0.18",
|
||||
"@types/http-server": "^0.12.4",
|
||||
@ -209,6 +211,7 @@
|
||||
"@types/warning": "^3.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"ali-oss": "^6.18.1",
|
||||
"antd-img-crop": "^4.17.0",
|
||||
"antd-style": "^3.5.2",
|
||||
"antd-token-previewer": "^2.0.5",
|
||||
|
@ -1,5 +0,0 @@
|
||||
| expected | actual | diff |
|
||||
| --- | --- | --- |
|
||||
| ![master image name](test/fixtures/4a.png) | ![pr image name](test/fixtures/4b.png) | ![diff](test/fixtures/4diff.png) |
|
||||
| ![master image name](test/fixtures/3a.png) | ![pr image name](test/fixtures/3b.png) | ![diff](test/fixtures/3diff.png) |
|
||||
| ![master image name](test/fixtures/6a.png) | ![pr image name](test/fixtures/6b.png) | ![diff](test/fixtures/6diff.png) |
|
121
scripts/visual-regression-upload.js
Normal file
121
scripts/visual-regression-upload.js
Normal file
@ -0,0 +1,121 @@
|
||||
/* eslint-disable no-restricted-syntax, no-console */
|
||||
// Attention: use all node builtin modules except `ali-oss`
|
||||
// Must keep our ak/sk safe
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const OSS = require('ali-oss');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { assert } = require('console');
|
||||
|
||||
// node scripts/visual-regression-upload.js ./visualRegressionReport.tar.gz --ref=pr-id
|
||||
// node scripts/visual-regression-upload.js ./imageSnapshots.tar.gz --ref=master-commitId
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 2) {
|
||||
console.error('Usage: node scripts/visual-regression-upload.js <tarFilePath> --ref=<refValue>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the tar file path and ref value from the cli arguments
|
||||
* @param {string[]} cliArgs
|
||||
*/
|
||||
function parseArgs(cliArgs) {
|
||||
const filepath = cliArgs[0];
|
||||
let refValue = '';
|
||||
|
||||
for (let i = 1; i < cliArgs.length; i++) {
|
||||
if (cliArgs[i].startsWith('--ref=')) {
|
||||
refValue = cliArgs[i].substring(6);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [filepath, refValue];
|
||||
}
|
||||
|
||||
async function walkDir(dirPath) {
|
||||
const fileList = [];
|
||||
|
||||
const files = await fs.promises.readdir(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const fileStat = fs.statSync(filePath);
|
||||
|
||||
if (fileStat.isDirectory()) {
|
||||
// Recursively call this func for subdirs
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
fileList.push(...(await walkDir(filePath)));
|
||||
} else {
|
||||
fileList.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('ali-oss')} client
|
||||
* @param {*} filePath
|
||||
* @param {*} refValue
|
||||
*/
|
||||
async function uploadFile(client, filePath, refValue) {
|
||||
const headers = {
|
||||
// https://help.aliyun.com/zh/oss/user-guide/object-acl
|
||||
'x-oss-object-acl': 'public-read',
|
||||
// https://help.aliyun.com/zh/oss/developer-reference/prevent-objects-from-being-overwritten-by-objects-that-have-the-same-names-3
|
||||
'x-oss-forbid-overwrite': 'false',
|
||||
};
|
||||
|
||||
console.log('Uploading file: %s', filePath);
|
||||
try {
|
||||
const targetFilePath = path.relative(process.cwd(), filePath);
|
||||
const r1 = await client.put(`${refValue}/${targetFilePath}`, filePath, { headers });
|
||||
console.log('Uploading file successfully: %s', r1.name);
|
||||
} catch (err) {
|
||||
console.error('Uploading file failed: %s', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
const [filepath, refValue] = parseArgs(args);
|
||||
assert(filepath, 'filepath is required');
|
||||
assert(refValue, 'refValue is required');
|
||||
|
||||
const fileOrFolderName = filepath;
|
||||
// check if exists
|
||||
const filePath = path.resolve(process.cwd(), fileOrFolderName);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error('File not exists: %s', filePath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new OSS({
|
||||
endpoint: 'oss-cn-shanghai.aliyuncs.com',
|
||||
accessKeyId: process.env.ALI_OSS_AK_ID,
|
||||
accessKeySecret: process.env.ALI_OSS_AK_SECRET,
|
||||
bucket: process.env.ALI_OSS_BUCKET,
|
||||
});
|
||||
|
||||
// if is a file then upload it directly
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isFile()) {
|
||||
await uploadFile(client, filePath, refValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
const fileList = await walkDir(filePath);
|
||||
for (const file of fileList) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await uploadFile(client, file, refValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boot();
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||
// 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 MockDate from 'mockdate';
|
||||
@ -29,7 +30,11 @@ interface ImageTestOptions {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line jest/no-export
|
||||
export default function imageTest(component: React.ReactElement, options: ImageTestOptions) {
|
||||
export default function imageTest(
|
||||
component: React.ReactElement,
|
||||
identifier: string,
|
||||
options: ImageTestOptions,
|
||||
) {
|
||||
function test(name: string, themedComponent: React.ReactElement) {
|
||||
it(name, async () => {
|
||||
await jestPuppeteer.resetPage();
|
||||
@ -80,7 +85,9 @@ export default function imageTest(component: React.ReactElement, options: ImageT
|
||||
fullPage: !options.onlyViewport,
|
||||
});
|
||||
|
||||
expect(image).toMatchImageSnapshot();
|
||||
expect(image).toMatchImageSnapshot({
|
||||
customSnapshotIdentifier: `${identifier}-${name.replace(/\s/g, '-')}`,
|
||||
});
|
||||
|
||||
MockDate.reset();
|
||||
page.off('request', onRequestHandle);
|
||||
@ -96,7 +103,7 @@ export default function imageTest(component: React.ReactElement, options: ImageT
|
||||
</div>,
|
||||
);
|
||||
test(
|
||||
`[CSS Var] component image screenshot should correct ${key}`,
|
||||
`component image screenshot should correct ${key}.css-var`,
|
||||
<div style={{ background: key === 'dark' ? '#000' : '', padding: `24px 12px` }} key={key}>
|
||||
<div>CSS Var</div>
|
||||
<ConfigProvider theme={{ algorithm, cssVar: true }}>{component}</ConfigProvider>
|
||||
@ -115,7 +122,7 @@ export default function imageTest(component: React.ReactElement, options: ImageT
|
||||
</>,
|
||||
);
|
||||
test(
|
||||
`[CSS Var] component image screenshot should correct`,
|
||||
`component image screenshot should correct.css-var`,
|
||||
<>
|
||||
<div>CSS Var</div>
|
||||
{Object.entries(themes).map(([key, algorithm]) => (
|
||||
@ -151,7 +158,7 @@ export function imageDemoTest(component: string, options: Options = {}) {
|
||||
if (typeof Demo === 'function') {
|
||||
Demo = <Demo />;
|
||||
}
|
||||
imageTest(Demo, {
|
||||
imageTest(Demo, `${component}-${path.basename(file, '.tsx')}`, {
|
||||
onlyViewport:
|
||||
options.onlyViewport === true ||
|
||||
(Array.isArray(options.onlyViewport) &&
|
||||
|
Loading…
Reference in New Issue
Block a user