From ea105fcc7c6df3423a605461a3e4d4d1d4bf1546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=BE=F0=9D=92=96=F0=9D=92=99=F0=9D=92=89?= Date: Tue, 7 Jan 2025 11:34:37 +0800 Subject: [PATCH 01/15] chore: add local visual regression testing (#52236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add local visual regression * chore: add blog * chore: update english version * chore: 不用考虑移动,都改为 copy 即可 * chore: update * chore: update * Revert "chore: update" This reverts commit 4dbc5d45e96f66dfe5be85fb45adb345a575a228. * chore: update --- docs/blog/visual-regression.en-US.md | 49 +++++ docs/blog/visual-regression.zh-CN.md | 49 +++++ package.json | 3 + scripts/visual-regression/build.ts | 34 ++-- scripts/visual-regression/local.ts | 255 +++++++++++++++++++++++++++ 5 files changed, 377 insertions(+), 13 deletions(-) create mode 100644 docs/blog/visual-regression.en-US.md create mode 100644 docs/blog/visual-regression.zh-CN.md create mode 100644 scripts/visual-regression/local.ts diff --git a/docs/blog/visual-regression.en-US.md b/docs/blog/visual-regression.en-US.md new file mode 100644 index 0000000000..ce1c31f24e --- /dev/null +++ b/docs/blog/visual-regression.en-US.md @@ -0,0 +1,49 @@ +--- +title: 👀 Visual Regression Testing +date: 2025-01-05 +author: Wxh16144 +--- + +Visual Regression Testing is a software testing technique that focuses on detecting visual changes and differences in the user interface of web applications or websites. It involves capturing screenshots of web pages at different stages of development and comparing them to identify any unexpected visual regressions caused by code changes or updates. + +## Baseline Screenshots + +The main goal of Ant Design's visual regression testing is to detect visual changes in components and avoid visual issues introduced by PR changes. We use [jest-puppeteer](https://jestjs.io/docs/puppeteer) as our testing framework. By combining Puppeteer with Jest, we take screenshots of each component demo and compare them with baseline screenshots. + +You can find visual regression test code in `__tests__/image.test.ts` under each component. You can run visual screenshots in the antd repository using the following command: + +```bash +npm run test:image # Screenshots will be saved in the imageSnapshots directory. For specific component screenshots, use: npm run test:image -- components/button +``` + +## Visual Regression Solutions + +### Argos + +Initially, we used [Argos](https://argos-ci.com/) as our visual regression testing solution. However, Argos changed their pricing strategy, and with antd triggering visual regression tests on every PR, comparing nearly 6,000 screenshots each time, the cost became unsustainable for us. + +### Self-hosted + +We built our own visual regression testing solution using jest-puppeteer mentioned earlier. We take screenshots of each component demo using four themes: `dark`, `light`, `compact`, and `cssVar`, then upload these screenshots to [Alibaba Cloud OSS](https://www.aliyun.com/product/oss) as baseline screenshots. + +Using GitHub Actions for continuous integration, we automatically capture and upload screenshots to OSS whenever the base branch code changes, ensuring the baseline screenshots stay up-to-date. + +For branches requiring visual regression testing, we use [pixelmatch](https://github.com/mapbox/pixelmatch) to compare current screenshots with baseline screenshots. If differences are found, difference screenshots are generated, and the difference report is uploaded to OSS. + +Further leveraging GitHub Actions, we implement baseline screenshot comparison in PRs. If visual differences are detected, the CI uploads the difference screenshots and report to OSS, displays the visual differences in the PR, and marks it as failed, requiring developers to fix the issues. + +![https://github.com/ant-design/ant-design/pull/52210#issuecomment-2567659292](https://github.com/user-attachments/assets/8a5c4e0a-3686-4b1c-aa32-930505173abe) + +## Local Visual Regression Testing + +When developing locally and preparing to submit a PR contribution, we can run visual regression tests in advance using the following command: + +```bash +npm run test:visual-regression:local # Follow the prompts to select components for visual regression testing +``` + +## References + +- For visual regression CI implementation, refer to [.github/workflows/visual-regression-\*.yml](https://github.com/search?q=repo%3Aant-design%2Fant-design%20path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F%20Visual%20Regression&type=code) +- For baseline screenshot implementation, refer to [tests/shared/imageTest.tsx](https://github.com/ant-design/ant-design/blob/46a8eff/tests/shared/imageTest.tsx#L38) +- For visual regression test code implementation, refer to [scripts/visual-regression](https://github.com/ant-design/ant-design/tree/46a8eff/scripts/visual-regression) diff --git a/docs/blog/visual-regression.zh-CN.md b/docs/blog/visual-regression.zh-CN.md new file mode 100644 index 0000000000..78f252caaf --- /dev/null +++ b/docs/blog/visual-regression.zh-CN.md @@ -0,0 +1,49 @@ +--- +title: 👀 视觉回归测试 +date: 2025-01-05 +author: Wxh16144 +--- + +视觉回归测试(Visual Regression Testing)是一种软件测试技术,专注于检测 Web 应用程序或网站的用户界面中的视觉变化和差异, 它涉及在不同的开发阶段捕获网页的屏幕截图,并进行比较,用来识别由代码更改或更新引起的任何意外的视觉回归。 + +## 基准截图 + +Ant Design 视觉回归测试的主要目标是检测组件的视觉变化,避免 PR 改动引入的视觉问题。我们使用 [jest-puppeteer](https://jestjs.io/docs/puppeteer) 作为测试框架。将 Puppeteer 与 Jest 结合使用,对每一个组件 Demo 进行截图,然后与基准截图进行比较。 + +可以看到每个组件下的 `__tests__/image.test.ts` 里面包含了视觉回归测试的代码, 你可以通过以下命令在 antd 仓库中运行视觉截图: + +```bash +npm run test:image # 截图将会保存在 imageSnapshots 目录下, 指定组件截图可以使用 npm run test:image -- components/button +``` + +## 视觉回归方案 + +### Argos + +早期使用我们使用 [Argos](https://argos-ci.com/) 作为视觉回归测试的方案,但是 Argos 修改了收费策略,antd 在每次 PR 中都会触发视觉回归测试,每次对比将近 6,000 张截图,这样的费用是我们无法承受的。 + +### Self-hosted + +我们自建了视觉回归测试的方案,利用前面提到的 jest-puppeteer,将每一个组件的 Demo 分别使用 `dark`、`light`、`compact` 以及 `cssVar` 四种主题进行截图,然后将截图上传到 [阿里云 OSS](https://www.aliyun.com/product/oss) 中,作为基准截图。 + +利用 GitHub Actions 持续集成,可以在每次基准分支的代码变动时,自动截图并上传到 OSS 中,这样就保证了基准截图的实时性。 + +对于需要进行视觉回归测试的分支,我们使用 [pixelmatch](https://github.com/mapbox/pixelmatch) 将当前截图与基准截图进行比较,如果有差异,将会生成差异截图,并将差异报告上传到 OSS 中。 + +进一步利用 GitHub Actions,实现在 PR 中对比基准截图,如果有视觉差异,CI 会将 PR 中的差异截图和报告上传到 OSS 中,在 PR 中展示视觉差异,同时标记为失败,需要开发者进行修复。 + +![https://github.com/ant-design/ant-design/pull/52210#issuecomment-2567659292](https://github.com/user-attachments/assets/8a5c4e0a-3686-4b1c-aa32-930505173abe) + +## 本地视觉回归测试 + +在本地开发,准备提交 PR 贡献时,我们也可以通过以下命令来事先进行视觉回归测试: + +```bash +npm run test:visual-regression:local # 按照提示选择组件进行视觉回归测试 +``` + +## 以上 + +- 视觉回归持续集成方案,可以参考 [.github/workflows/visual-regression-\*.yml](https://github.com/search?q=repo%3Aant-design%2Fant-design%20path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F%20Visual%20Regression&type=code) +- 基准截图实现,可以参考 [tests/shared/imageTest.tsx](https://github.com/ant-design/ant-design/blob/46a8eff/tests/shared/imageTest.tsx#L38) +- 视觉回归测试的代码实现,可以参考 [scripts/visual-regression](https://github.com/ant-design/ant-design/tree/46a8eff/scripts/visual-regression) diff --git a/package.json b/package.json index b5e289e745..59689ec8e9 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "test:site-update": "npm run site && npm run test:site -- -u", "test:update": "jest --config .jest.js --no-cache -u", "test:visual-regression": "tsx scripts/visual-regression/build.ts", + "test:visual-regression:local": "tsx scripts/visual-regression/local.ts", "token:meta": "tsx scripts/generate-token-meta.ts", "token:statistic": "tsx scripts/collect-token-statistic.ts", "tsc": "tsc --noEmit", @@ -234,6 +235,7 @@ "dotenv": "^16.4.5", "dumi": "~2.4.17", "dumi-plugin-color-chunk": "^1.1.2", + "env-paths": "^3.0.0", "eslint": "^9.15.0", "eslint-plugin-compat": "^6.0.1", "eslint-plugin-jest": "^28.9.0", @@ -274,6 +276,7 @@ "open": "^10.1.0", "ora": "^8.1.0", "p-all": "^5.0.0", + "package-manager-detector": "^0.2.8", "pixelmatch": "^6.0.0", "pngjs": "^7.0.0", "prettier": "^3.4.1", diff --git a/scripts/visual-regression/build.ts b/scripts/visual-regression/build.ts index e9da53be35..838afdb389 100644 --- a/scripts/visual-regression/build.ts +++ b/scripts/visual-regression/build.ts @@ -270,13 +270,21 @@ function generateReport( return [mdStr, markdown2Html(mdStr)]; } - let reportMdStr = ` -${commonHeader} -${fullReport} - + const summaryHeader = ''; + const tableHeader = ` | Expected (Branch ${targetBranch}) | Actual (Current PR) | Diff | | --- | --- | --- | - `.trim(); + `.trim(); + + let reportMdStr = [ + commonHeader, + isLocalEnv ? false : `${fullReport}`, + summaryHeader, + '\n', + tableHeader, + ] + .filter(Boolean) + .join('\n'); reportMdStr += '\n'; @@ -316,9 +324,9 @@ ${fullReport} // tips for comment `Pass Visual Diff` will pass the CI if (!passed) { const summaryLine = [ - changedCount > 0 && `🔄 **${changedCount}** changed`, - removedCount > 0 && `🛑 **${removedCount}** removed`, - addedCount > 0 && `🆕 **${addedCount}** added`, + changedCount > 0 && `🔄 \`${changedCount}\` changed`, + removedCount > 0 && `🛑 \`${removedCount}\` removed`, + addedCount > 0 && `🆕 \`${addedCount}\` added`, ] .filter(Boolean) .join(', '); @@ -333,6 +341,9 @@ ${fullReport} ] .filter(Boolean) .join('\n'); + + reportMdStr = reportMdStr.replace(summaryHeader, `> **📊 Summary:** ${summaryLine}`); + fullVersionMd = fullVersionMd.replace(summaryHeader, `> **📊 Summary:** ${summaryLine}`); } // convert fullVersionMd to html @@ -411,7 +422,6 @@ async function boot() { const currentImgExists = await fse.exists(currentImgPath); if (!currentImgExists) { console.log(chalk.red(`⛔️ Missing image: ${compareImgName}\n`)); - // base img would use twice so we cannot move it await fse.copy(baseImgPath, path.join(baseImgReportDir, compareImgName)); return { type: 'removed', @@ -432,10 +442,8 @@ async function boot() { chalk.yellow(compareImgName), `${(mismatchedPxPercent * 100).toFixed(2)}%\n`, ); - // copy/move compare imgs(x2) to report dir - // base img would use twice so we cannot move it await fse.copy(baseImgPath, path.join(baseImgReportDir, compareImgName)); - await fse.move(currentImgPath, path.join(currentImgReportDir, compareImgName)); + await fse.copy(currentImgPath, path.join(currentImgReportDir, compareImgName)); return { type: 'changed', @@ -473,7 +481,7 @@ async function boot() { } const newImgTask = newImgs.map((newImg) => async () => { - await fse.move( + await fse.copy( path.join(currentImgSourceDir, newImg), path.resolve(currentImgReportDir, newImg), ); diff --git a/scripts/visual-regression/local.ts b/scripts/visual-regression/local.ts new file mode 100644 index 0000000000..407601dc67 --- /dev/null +++ b/scripts/visual-regression/local.ts @@ -0,0 +1,255 @@ +/** + * 本地运行视觉回归测试 + */ +import path from 'path'; +import fs from 'fs-extra'; +import simpleGit from 'simple-git'; +import envPaths from 'env-paths'; +import fg from 'fast-glob'; +import minimist from 'minimist'; +import { Readable } from 'stream'; +import { finished } from 'stream/promises'; +import { extract } from 'tar'; +import { Octokit } from '@octokit/rest'; +import { spawnSync } from 'child_process'; +import difference from 'lodash/difference'; +import open from 'open'; +import { select, input, checkbox, confirm } from '@inquirer/prompts'; +import { detectSync, resolveCommand } from 'package-manager-detector'; + +const ROOT = path.resolve(__dirname, '../../'); +// ==================== 环境变量 ==================== +const GITHUB_TOKEN = process.env.GITHUB_ACCESS_TOKEN; +const GITHUB_OWNER = process.env.GITHUB_OWNER || 'ant-design'; +const GITHUB_REPO = process.env.GITHUB_REPO || 'ant-design'; + +// ==================== 阿里云 OSS 配置 ==================== +const ALI_OSS_BUCKET = process.env.ALI_OSS_BUCKET || 'antd-visual-diff'; +const ALI_OSS_REGION = process.env.ALI_OSS_REGION || 'oss-accelerate'; +const OSS_DOMAIN = `https://${ALI_OSS_BUCKET}.${ALI_OSS_REGION}.aliyuncs.com`; + +// ==================== 本地存储路径 ==================== +const _VISUAL_STORE_PATH = envPaths('visual-regression').cache; +const STORE_PATH = path.join(_VISUAL_STORE_PATH, GITHUB_OWNER, GITHUB_REPO); + +// ==================== 初始化 ==================== +fs.ensureDirSync(STORE_PATH); +const git = simpleGit(ROOT); +const octokit = new Octokit({ auth: GITHUB_TOKEN }); +const packageManager = detectSync({ cwd: ROOT }); +const components = fg.sync('components/*/index.ts[x]', { cwd: ROOT }).reduce((acc, file) => { + const basePath = path.dirname(file); + if ( + [ + fs.existsSync(path.join(basePath, 'index.en-US.md')), + fs.existsSync(path.join(basePath, 'demo')), + fs.existsSync(path.join(basePath, '__tests__')), + ].every(Boolean) + ) { + acc.push(basePath); + } + + return acc; +}, [] as string[]); + +// ==================== scripts ==================== +const imagesTestsScript = 'test:image'; +const visualTestsScript = 'test:visual-regression'; + +async function parseArgs() { + const argv = minimist(process.argv.slice(2)); + let baseRef = argv['base-ref']; + + const { latest } = await git.log(); + + if (!baseRef) { + baseRef = await select({ + message: '📚 请选择基准分支', + default: 'master', + choices: [ + 'master', + 'feature', + 'next', + // '✍️ Custom Input', // 临时关闭自定义 + ], + }); + + if (baseRef.endsWith('Custom Input')) { + baseRef = await input({ + message: '📚 请输入基准分支', + default: 'master', + }); + } + } + + return { + baseRef, + currentRef: latest?.hash.slice(0, 8) || '', + }; +} + +// 获取 commit sha +async function getCommitSha(ref: string) { + const { data } = await octokit.repos.getCommit({ + owner: GITHUB_OWNER, + repo: GITHUB_REPO, + ref, + }); + + return data.sha; +} + +// 获取 oss branch hash +async function getOssBranchHash(branch: string) { + const uri = new URL(`${OSS_DOMAIN}/${branch}/visual-regression-ref.txt`); + + const res = await fetch(uri.toString()); + const text = await res.text(); + return text.trim(); +} + +function runImageTests(args: string[]) { + const { command, args: realArgs } = resolveCommand(packageManager!.agent, 'run', args)!; + spawnSync(command, realArgs, { + stdio: 'inherit', + env: { + ...process.env, + LOCAL: 'true', // 总是本地运行 + }, + }); +} + +async function downloadVisualSnapshots(sha: string) { + const uri = new URL(`${OSS_DOMAIN}/${sha}/imageSnapshots.tar.gz`); + const tarPath = path.join(STORE_PATH, `imageSnapshots-${sha}.tar.gz`); + + if (fs.existsSync(tarPath) && fs.statSync(tarPath).size > 10 * 1024 * 1024) { + console.log(`📦 视觉回归快照已存在,跳过下载`); + } else { + console.log(`📦 正在下载视觉回归快照`); + const res = await fetch(uri); + if (!res.ok || res.status !== 200) { + throw new Error(`Download file failed: ${new URL(uri).href}`); + } + // @ts-ignore + const body = Readable.fromWeb(res.body); + await finished(body.pipe(fs.createWriteStream(tarPath))); + + if (fs.statSync(tarPath).size < 10 * 1024 * 1024) { + console.log(`📦 下载完成 ${tarPath}`); + } + } + + return tarPath; +} + +async function run() { + const args = await parseArgs(); + const { baseRef, currentRef } = args; + + const baseSha = await getCommitSha(baseRef); + + if (baseSha === currentRef) { + console.log(` +👋 你好像没有提交任何代码,不需要进行视觉回归测试。 +或者你可以切到你提交 PR 的分支上进行本地测试。 +`); + } + + const visualSha = await getOssBranchHash(baseRef); + + if (baseSha !== visualSha) { + console.warn( + ` +⚠️ 基准分支提交和 oss 分支提交不一致,可能会导致视觉回归测试不准确 +- 基准分支提交:${baseSha} [${baseRef}] +- oss 分支提交:${visualSha} [${baseRef}] + `.trim(), + ); + } + + const basePath = path.join(ROOT, `imageSnapshots-${baseRef}`); // 本地基准快照存储路径 + const targetPath = path.join(ROOT, 'imageSnapshots'); // 本地目标快照存储路径 + + // ==================== 生成目标快照(选择组件 ================== + let appliedComponents: 'all' | string[]; + + const selected = await checkbox({ + message: '📚 请选择需要测试的组件,不建议选择全部【全量快照生成需要耗费很长时间】\n', + pageSize: Math.floor(components.length / 4), + loop: false, + theme: { helpMode: 'always' }, + choices: components.map((component) => ({ + value: component, + checked: component.endsWith('components/button'), // 默认选中 button + })), + }); + + if (selected.length === 0 || difference(components, selected).length === 0) { + appliedComponents = 'all'; + } else { + appliedComponents = selected; + } + + // ==================== 生成目标快照(运行快照测试 ================== + const needRun = await confirm({ + message: '📚 是否进行快照截图?【如果你已经运行过了,可以忽略】', + default: true, + }); + + if (needRun) { + fs.emptyDirSync(targetPath); + runImageTests([imagesTestsScript, ...(appliedComponents === 'all' ? [] : appliedComponents)]); + } else { + fs.ensureDirSync(targetPath); + } + + // ==================== 下载基准快照 ================== + const visualTarPath = await downloadVisualSnapshots(visualSha); + + // 解压 tar 包 + fs.emptyDirSync(basePath); + await extract({ + strip: 1, + file: visualTarPath, + C: basePath, + }); + + if (appliedComponents !== 'all') { + // components/avatar => avatar + const componentNames = appliedComponents.map((component) => path.basename(component)); + + console.log(`🧹 正在清理基准快照`); + + const files = fs.readdirSync(basePath); + files.forEach((file) => { + // 删除不在选择范围内的组件 + if (!componentNames.some((name) => file.startsWith(name))) { + fs.removeSync(path.join(basePath, file)); + } + }); + } + + // ==================== 对比快照 ================== + const reportFile = path.join(ROOT, 'visualRegressionReport', 'report.html'); + fs.emptyDirSync(path.dirname(reportFile)); + // https://github.com/ant-design/ant-design/wiki/Development#run-visual-regression-diff-locally + runImageTests([visualTestsScript, `--base-ref=${baseRef}`, `--pr-id=local`]); + + // ==================== 提示 ================== + console.log(`🎉 本地视觉回归测试完成, 报告: ${path.relative(process.cwd(), reportFile)}`); + + const needOpen = await confirm({ + message: '📚 是否打开报告查看?', + default: true, + }); + + if (needOpen) { + open(reportFile); + } +} + +run().catch((e) => { + console.error(e); + process.exit(1); +}); From 8b99ad6199fb58de0645e0c5047049ac7921ebab Mon Sep 17 00:00:00 2001 From: thinkasany <480968828@qq.com> Date: Tue, 7 Jan 2025 11:58:41 +0800 Subject: [PATCH 02/15] docs: repair img display for change doc (#52273) * docs(changelog): repair img display * fix * fix --- CHANGELOG.en-US.md | 6 ++++-- CHANGELOG.zh-CN.md | 23 +++++++++++++++-------- scripts/generate-component-changelog.ts | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 7b4b27e394..d53c85db64 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -22,8 +22,10 @@ tag: vVERSION - 🔥 TreeSelect support `maxCount` to limit the maximum number of selections. [#51759](https://github.com/ant-design/ant-design/pull/51759) [@aojunhao123](https://github.com/aojunhao123) - 🔥 Modal `width` support responsive size. [#51653](https://github.com/ant-design/ant-design/pull/51653) [@zombieJ](https://github.com/zombieJ) - 🔥 Splitter support `lazy` mode. [#51557](https://github.com/ant-design/ant-design/pull/51557) [@OysterD3](https://github.com/OysterD3) -- 🔥 Button `color` support full color palette. [#51550](https://github.com/ant-design/ant-design/pull/51550) [@OysterD3](https://github.com/OysterD3) Button Colors -- 🆕 Button support `loadingIcon` to customize loading icon. [#51758](https://github.com/ant-design/ant-design/pull/51758) [@zhangchao-wooc](https://github.com/zhangchao-wooc) +- Button + - 🔥 Button `color` support full color palette. [#51550](https://github.com/ant-design/ant-design/pull/51550) [@OysterD3](https://github.com/OysterD3) + Button Colors + - 🆕 Button support `loading={{ icon: ReactNode }}` to customize loading icon. [#51758](https://github.com/ant-design/ant-design/pull/51758) [@zhangchao-wooc](https://github.com/zhangchao-wooc) - Menu - 🐞 Fix Menu `extra` font size and vertical align issue. [#52217](https://github.com/ant-design/ant-design/pull/52217) [@guoyunhe](https://github.com/guoyunhe) - 🆕 Menu add token `subMenuItemSelectedColor` to resolve submenu title color being overrided by `itemSelectedColor`. [#52182](https://github.com/ant-design/ant-design/pull/52182) [@afc163](https://github.com/afc163) diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index e54fc62c17..76e0038dd5 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -22,8 +22,10 @@ tag: vVERSION - 🔥 TreeSelect 新增 `maxCount` 属性以限制最大选择数量。[#51759](https://github.com/ant-design/ant-design/pull/51759) [@aojunhao123](https://github.com/aojunhao123) - 🔥 Modal `width` 支持响应式尺寸。[#51653](https://github.com/ant-design/ant-design/pull/51653) [@zombieJ](https://github.com/zombieJ) - 🔥 Splitter 增加 `lazy` 模式。[#51557](https://github.com/ant-design/ant-design/pull/51557) [@OysterD3](https://github.com/OysterD3) -- 🔥 Button `color` 属性支持完整色板。[#51550](https://github.com/ant-design/ant-design/pull/51550) [@OysterD3](https://github.com/OysterD3) Button Colors -- 🆕 Button 组件新增 `loadingIcon` 以自定义加载图标。[#51758](https://github.com/ant-design/ant-design/pull/51758) [@zhangchao-wooc](https://github.com/zhangchao-wooc) +- Button + - 🔥 Button `color` 属性支持完整色板。[#51550](https://github.com/ant-design/ant-design/pull/51550) [@OysterD3](https://github.com/OysterD3) + Button Colors + - 🆕 Button 组件新增 `loading={{ icon: ReactNode }}` 以自定义加载图标。[#51758](https://github.com/ant-design/ant-design/pull/51758) [@zhangchao-wooc](https://github.com/zhangchao-wooc) - Menu - 🆕 Menu 新增 token `subMenuItemSelectedColor`,避免 `itemSelectedColor` 覆盖子菜单标题样式。[#52182](https://github.com/ant-design/ant-design/pull/52182) [@afc163](https://github.com/afc163) - 🐞 修复 Menu `extra` 字体大小和垂直居中对齐问题。[#52217](https://github.com/ant-design/ant-design/pull/52217) [@guoyunhe](https://github.com/guoyunhe) @@ -52,7 +54,7 @@ tag: vVERSION - 🐞 修复 Slider 当 `tipFormatter` 未定义时导致崩溃的问题。[#52184](https://github.com/ant-design/ant-design/pull/52184) [@thinkasany](https://github.com/thinkasany) - 💄 优化 Collapse 聚焦样式以及折叠项圆角。[#52086](https://github.com/ant-design/ant-design/pull/52086) [@aojunhao123](https://github.com/aojunhao123) - ⌨️ 为 Radio.Group 添加默认 `name` 属性以提升无障碍体验。[#52076](https://github.com/ant-design/ant-design/pull/52076) [@aojunhao123](https://github.com/aojunhao123) -- ⌨️ Input.Search添加默认 `type=search` 类型。[#52083](https://github.com/ant-design/ant-design/pull/52083) [@Kaikiat1126](https://github.com/Kaikiat1126) +- ⌨️ Input.Search 添加默认 `type=search` 类型。[#52083](https://github.com/ant-design/ant-design/pull/52083) [@Kaikiat1126](https://github.com/Kaikiat1126) - ⌨️ 优化 Tabs 键盘操作时的焦点样式。[#52002](https://github.com/ant-design/ant-design/pull/52002) [@aojunhao123](https://github.com/aojunhao123) - Segmented - ⌨️ 优化 Segmented 聚焦样式以提升无障碍体验。[#51934](https://github.com/ant-design/ant-design/pull/51934) [@aojunhao123](https://github.com/aojunhao123) @@ -278,17 +280,21 @@ tag: vVERSION `2024-09-22` -- 🔥 **全新 Splitter 区域分割组件**,自由拖拽调整区域大小。[#50038](https://github.com/ant-design/ant-design/pull/50038) [@wanpan11](https://github.com/wanpan11) Splitter +- 🔥 **全新 Splitter 区域分割组件**,自由拖拽调整区域大小。[#50038](https://github.com/ant-design/ant-design/pull/50038) [@wanpan11](https://github.com/wanpan11) + Splitter - Button - - 🔥 Button 支持 `variant` 变体和 `color` 颜色属性,以支持更多组合样式。[#50051](https://github.com/ant-design/ant-design/pull/50051) [@coding-ice](https://github.com/coding-ice) Button + - 🔥 Button 支持 `variant` 变体和 `color` 颜色属性,以支持更多组合样式。[#50051](https://github.com/ant-design/ant-design/pull/50051) [@coding-ice](https://github.com/coding-ice) + Button - 💄 Button 添加 `textColor`、`textHoverColor` 和 `textActiveColor` 三个 token。[#47870](https://github.com/ant-design/ant-design/pull/47870) [@madocto](https://github.com/madocto) - FloatButton - - 🆕 FloatButton 组件支持 `placement` 属性,支持从四个方向弹出菜单。(实现方式改为 `position: absolute` + flex 布局,可能会对你现有的布局造成 breaking change,请注意兼容)[#50407](https://github.com/ant-design/ant-design/pull/50407) [@li-jia-nan](https://github.com/li-jia-nan) float button + - 🆕 FloatButton 组件支持 `placement` 属性,支持从四个方向弹出菜单。(实现方式改为 `position: absolute` + flex 布局,可能会对你现有的布局造成 breaking change,请注意兼容)[#50407](https://github.com/ant-design/ant-design/pull/50407) [@li-jia-nan](https://github.com/li-jia-nan) + float button - 💄 统一 FloatButton 和 FloatButton.Group 的按钮圆角。[#50513](https://github.com/ant-design/ant-design/pull/50513) [@Layouwen](https://github.com/Layouwen) - 💄 FloatButton 组件的 `z-index` 加入 `useZIndex` 管理,兼容弹层类组件。[#50311](https://github.com/ant-design/ant-design/pull/50311) [@li-jia-nan](https://github.com/li-jia-nan) - 🆕 FloatButton 支持传入 `htmlType` 属性。[#50892](https://github.com/ant-design/ant-design/pull/50892) [@li-jia-nan](https://github.com/li-jia-nan) - Menu - - 🆕 Menu.Item 和 Dropdown 的 menu 支持 `extra` 属性。[#50431](https://github.com/ant-design/ant-design/pull/50431) [@coding-ice](https://github.com/coding-ice) menu extra + - 🆕 Menu.Item 和 Dropdown 的 menu 支持 `extra` 属性。[#50431](https://github.com/ant-design/ant-design/pull/50431) [@coding-ice](https://github.com/coding-ice) + menu extra - 🐞 修复 Menu `popupStyle` 在 SubMenu 上失效的问题。[#50922](https://github.com/ant-design/ant-design/pull/50922) [@Wxh16144](https://github.com/Wxh16144) - Table - 🆕 Table 列支持配置 `minWidth` 属性。[#50416](https://github.com/ant-design/ant-design/pull/50416) [@linxianxi](https://github.com/linxianxi) @@ -319,7 +325,8 @@ tag: vVERSION - 💄 Select 组件新增一些 token 以支持自定义 hover 和 focus 样式。[#50951](https://github.com/ant-design/ant-design/pull/50951) [@kiner-tang](https://github.com/kiner-tang) - 🐞 修复 Select 搜索模式下搜索词内容覆盖右侧图标的问题。[#50917](https://github.com/ant-design/ant-design/pull/50917) [@yezhonghu0503](https://github.com/yezhonghu0503) - 🐞 修复 Select 同时启用 `allowClear` 和 `variant="filled"` 时清除图标多余的白色背景的问题。[#50916](https://github.com/ant-design/ant-design/pull/50916) [@thinkasany](https://github.com/thinkasany) -- 🆕 Segmented 新增 `vertical` 属性以支持垂直模式,并优化了可访问性。[#50708](https://github.com/ant-design/ant-design/pull/50708) [@liangchaofei](https://github.com/liangchaofei) Segmented vertical demo +- 🆕 Segmented 新增 `vertical` 属性以支持垂直模式,并优化了可访问性。[#50708](https://github.com/ant-design/ant-design/pull/50708) [@liangchaofei](https://github.com/liangchaofei) + Segmented vertical demo - 🆕 Radio.Group 支持 `block` 属性以撑满一行。[#50828](https://github.com/ant-design/ant-design/pull/50828) [@yuanliu147](https://github.com/yuanliu147) - 🆕 ConfigProvider 支持配置 Splitter 组件的 `className` 和 `style` 属性。[#50855](https://github.com/ant-design/ant-design/pull/50855) [@li-jia-nan](https://github.com/li-jia-nan) - 🆕 Image 新增 `onActive` 到 `toolbarRender` 以切换图片 。[#50812](https://github.com/ant-design/ant-design/pull/50812) [@madocto](https://github.com/madocto) diff --git a/scripts/generate-component-changelog.ts b/scripts/generate-component-changelog.ts index 51d221e882..1b6160c8b6 100644 --- a/scripts/generate-component-changelog.ts +++ b/scripts/generate-component-changelog.ts @@ -144,7 +144,7 @@ const miscKeys = [ } // Filter not is changelog - if (!line.trim().startsWith('-') && !line.includes('github.')) { + if (!line.trim().startsWith('-') && !line.includes('github.') && !line.includes('img')) { continue; } From 8408880911747352ab4fed78e487584acc843427 Mon Sep 17 00:00:00 2001 From: jjlstruggle <73511098+jjlstruggle@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:06:14 +0800 Subject: [PATCH 03/15] fix(Splitter): try recover history size when collapsed (#52222) * feat: try recover history size when collapsed * update: update shouldUseCache condition * fix: it should recover history size when collapse back * test: add Splitter test use fallback size when collapse back * Apply suggestions from code review Signed-off-by: afc163 * Update components/splitter/hooks/useResize.ts Signed-off-by: afc163 * Update components/splitter/hooks/useResize.ts Signed-off-by: afc163 --------- Signed-off-by: afc163 Co-authored-by: sagajiang@tencent.com Co-authored-by: afc163 --- components/splitter/__tests__/index.test.tsx | 53 ++++++++++++++++++-- components/splitter/hooks/useResize.ts | 21 +++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/components/splitter/__tests__/index.test.tsx b/components/splitter/__tests__/index.test.tsx index 37dd98e50d..96ea3f8d42 100644 --- a/components/splitter/__tests__/index.test.tsx +++ b/components/splitter/__tests__/index.test.tsx @@ -475,7 +475,7 @@ describe('Splitter', () => { expectClick(container.querySelector('.ant-splitter-bar-collapse-start')!, [50, 50]); }); - it('collapsible with min', async () => { + it('collapsible with cache', async () => { const onResize = jest.fn(); const onResizeEnd = jest.fn(); @@ -509,8 +509,55 @@ describe('Splitter', () => { onResize.mockReset(); onResizeEnd.mockReset(); fireEvent.click(container.querySelector('.ant-splitter-bar-collapse-end')!); - expect(onResize).toHaveBeenCalledWith([5, 95]); - expect(onResizeEnd).toHaveBeenCalledWith([5, 95]); + expect(onResize).toHaveBeenCalledWith([20, 80]); + expect(onResizeEnd).toHaveBeenCalledWith([20, 80]); + expect(container.querySelector('.ant-splitter-bar-dragger-disabled')).toBeFalsy(); + + // Collapse right + onResize.mockReset(); + onResizeEnd.mockReset(); + fireEvent.click(container.querySelector('.ant-splitter-bar-collapse-end')!); + expect(onResize).toHaveBeenCalledWith([100, 0]); + expect(onResizeEnd).toHaveBeenCalledWith([100, 0]); + expect(container.querySelector('.ant-splitter-bar-dragger-disabled')).toBeTruthy(); + }); + + it('collapsible with fallback', async () => { + const onResize = jest.fn(); + const onResizeEnd = jest.fn(); + + const { container } = render( + , + ); + + await resizeSplitter(); + + // Collapse left + fireEvent.click(container.querySelector('.ant-splitter-bar-collapse-start')!); + expect(onResize).toHaveBeenCalledWith([0, 100]); + expect(onResizeEnd).toHaveBeenCalledWith([0, 100]); + expect(container.querySelector('.ant-splitter-bar-dragger-disabled')).toBeTruthy(); + + // Collapse back + onResize.mockReset(); + onResizeEnd.mockReset(); + fireEvent.click(container.querySelector('.ant-splitter-bar-collapse-end')!); + expect(onResize).toHaveBeenCalledWith([2.5, 97.5]); + expect(onResizeEnd).toHaveBeenCalledWith([2.5, 97.5]); expect(container.querySelector('.ant-splitter-bar-dragger-disabled')).toBeFalsy(); // Collapse right diff --git a/components/splitter/hooks/useResize.ts b/components/splitter/hooks/useResize.ts index 5d33b5f539..98760b4c83 100644 --- a/components/splitter/hooks/useResize.ts +++ b/components/splitter/hooks/useResize.ts @@ -29,6 +29,7 @@ export default function useResize( // Real px sizes const [cacheSizes, setCacheSizes] = React.useState([]); + const cacheCollapsedSize = React.useRef([]); /** * When start drag, check the direct is `start` or `end`. @@ -129,6 +130,7 @@ export default function useResize( // Collapse directly currentSizes[currentIndex] = 0; currentSizes[targetIndex] += currentSize; + cacheCollapsedSize.current[index] = currentSize; } else { const totalSize = currentSize + targetSize; @@ -141,8 +143,23 @@ export default function useResize( const limitEnd = Math.min(currentSizeMax, totalSize - targetSizeMin); const halfOffset = (limitEnd - limitStart) / 2; - currentSizes[currentIndex] -= halfOffset; - currentSizes[targetIndex] += halfOffset; + const targetCacheCollapsedSize = cacheCollapsedSize.current[index]; + const currentCacheCollapsedSize = totalSize - targetCacheCollapsedSize; + + const shouldUseCache = + targetCacheCollapsedSize && + targetCacheCollapsedSize <= targetSizeMax && + targetCacheCollapsedSize >= targetSizeMin && + currentCacheCollapsedSize <= currentSizeMax && + currentCacheCollapsedSize >= currentSizeMin; + + if (shouldUseCache) { + currentSizes[targetIndex] = targetCacheCollapsedSize; + currentSizes[currentIndex] = currentCacheCollapsedSize; + } else { + currentSizes[currentIndex] -= halfOffset; + currentSizes[targetIndex] += halfOffset; + } } updateSizes(currentSizes); From 9974e8cf5f55f57550e8c01de9c13b2cba74eece Mon Sep 17 00:00:00 2001 From: xiangcai <135000151+jin19980928@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:17:45 +0800 Subject: [PATCH 04/15] docs: Replace open in tab code (#52277) Co-authored-by: WB01676250 --- .../builtins/Previewer/CodeBlockButton.tsx | 69 +++++++++++++++++++ .../builtins/Previewer/CodePreviewer.tsx | 13 ++-- .dumi/theme/common/styles/Demo.tsx | 7 +- .dumi/theme/locales/en-US.json | 2 +- .dumi/theme/locales/zh-CN.json | 2 +- .dumirc.ts | 7 ++ 6 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 .dumi/theme/builtins/Previewer/CodeBlockButton.tsx diff --git a/.dumi/theme/builtins/Previewer/CodeBlockButton.tsx b/.dumi/theme/builtins/Previewer/CodeBlockButton.tsx new file mode 100644 index 0000000000..1c50a538cc --- /dev/null +++ b/.dumi/theme/builtins/Previewer/CodeBlockButton.tsx @@ -0,0 +1,69 @@ +import React, { Suspense, useEffect, useState } from 'react'; +import { Tooltip } from 'antd'; +import { FormattedMessage } from 'dumi'; + +import { ping } from '../../utils'; + +let pingDeferrer: PromiseLike; + +function useShowCodeBlockButton() { + const [showCodeBlockButton, setShowCodeBlockButton] = useState(false); + + useEffect(() => { + pingDeferrer ??= new Promise((resolve) => { + ping((status) => { + if (status !== 'timeout' && status !== 'error') { + return resolve(true); + } + + return resolve(false); + }); + }); + pingDeferrer.then(setShowCodeBlockButton); + }, []); + + return showCodeBlockButton; +} + +interface CodeBlockButtonProps { + title?: string; + dependencies: Record; + jsx: string; +} + +const CodeBlockButton: React.FC = ({ title, dependencies = {}, jsx }) => { + const showCodeBlockButton = useShowCodeBlockButton(); + + const codeBlockPrefillConfig = { + title: `${title} - antd@${dependencies.antd}`, + js: `${ + /import React(\D*)from 'react';/.test(jsx) ? '' : `import React from 'react';\n` + }import { createRoot } from 'react-dom/client';\n${jsx.replace( + /export default/, + 'const ComponentDemo =', + )}\n\ncreateRoot(mountNode).render();\n`, + css: '', + json: JSON.stringify({ name: 'antd-demo', dependencies }, null, 2), + }; + + return showCodeBlockButton ? ( + }> +
+ codeblock { + openHituCodeBlock(JSON.stringify(codeBlockPrefillConfig)); + }} + /> +
+
+ ) : null; +}; + +export default (props: CodeBlockButtonProps) => ( + + + +); diff --git a/.dumi/theme/builtins/Previewer/CodePreviewer.tsx b/.dumi/theme/builtins/Previewer/CodePreviewer.tsx index fd7b6ec0e4..f453f44b13 100644 --- a/.dumi/theme/builtins/Previewer/CodePreviewer.tsx +++ b/.dumi/theme/builtins/Previewer/CodePreviewer.tsx @@ -8,7 +8,6 @@ import classNames from 'classnames'; import { FormattedMessage, useLiveDemo, useSiteData } from 'dumi'; import LZString from 'lz-string'; -import RiddleButton from './RiddleButton'; import useLocation from '../../../hooks/useLocation'; import BrowserFrame from '../../common/BrowserFrame'; import ClientOnly from '../../common/ClientOnly'; @@ -20,6 +19,7 @@ import ExternalLinkIcon from '../../icons/ExternalLinkIcon'; import DemoContext from '../../slots/DemoContext'; import type { SiteContextProps } from '../../slots/SiteContext'; import SiteContext from '../../slots/SiteContext'; +import CodeBlockButton from './CodeBlockButton'; import type { AntdPreviewerProps } from './Previewer'; const { ErrorBoundary } = Alert; @@ -95,7 +95,7 @@ const CodePreviewer: React.FC = (props) => { const entryName = 'index.tsx'; const entryCode = asset.dependencies[entryName].value; - + const previewDemo = useRef(null); const demoContainer = useRef(null); const { @@ -253,6 +253,7 @@ const CodePreviewer: React.FC = (props) => { .join(';'), js_pre_processor: 'typescript', }; + // Reorder source code let parsedSourceCode = suffix === 'tsx' ? entryCode : jsx; let importReactContent = "import React from 'react';"; @@ -403,13 +404,7 @@ createRoot(document.getElementById('container')).render(); - + }> { cursor: pointer; } - &-riddle { - width: 14px; - height: 14px; + &-codeblock { + width: 16px; + height: 16px; overflow: hidden; border: 0; cursor: pointer; + max-width: 100% !important; } &-codesandbox { diff --git a/.dumi/theme/locales/en-US.json b/.dumi/theme/locales/en-US.json index 41dd070306..370f67cb8b 100644 --- a/.dumi/theme/locales/en-US.json +++ b/.dumi/theme/locales/en-US.json @@ -35,7 +35,7 @@ "app.demo.codepen": "Open in CodePen", "app.demo.codesandbox": "Open in CodeSandbox", "app.demo.stackblitz": "Open in Stackblitz", - "app.demo.riddle": "Open in Riddle", + "app.demo.codeblock": "Open in Hitu", "app.demo.separate": "Open in a new window", "app.demo.online": "Online Address", "app.home.introduce": "A design system for enterprise-level products. Create an efficient and enjoyable work experience.", diff --git a/.dumi/theme/locales/zh-CN.json b/.dumi/theme/locales/zh-CN.json index 1cefada772..83f8f55242 100644 --- a/.dumi/theme/locales/zh-CN.json +++ b/.dumi/theme/locales/zh-CN.json @@ -35,7 +35,7 @@ "app.demo.codepen": "在 CodePen 中打开", "app.demo.codesandbox": "在 CodeSandbox 中打开", "app.demo.stackblitz": "在 Stackblitz 中打开", - "app.demo.riddle": "在 Riddle 中打开", + "app.demo.codeblock": "在海兔中打开", "app.demo.separate": "在新窗口打开", "app.demo.online": "线上地址", "app.home.introduce": "企业级产品设计体系,创造高效愉悦的工作体验", diff --git a/.dumirc.ts b/.dumirc.ts index 57672e5d8e..e0098081af 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -7,6 +7,12 @@ import rehypeAntd from './.dumi/rehypeAntd'; import remarkAntd from './.dumi/remarkAntd'; import { version } from './package.json'; +const codeBlockJs = + 'https://renderoffice.a' + + 'lipay' + + 'objects.com/p' + + '/yuyan/180020010001206410/parseFileData.js'; + export default defineConfig({ plugins: ['dumi-plugin-color-chunk'], @@ -183,6 +189,7 @@ export default defineConfig({ document.documentElement.className += isZhCN(pathname) ? 'zh-cn' : 'en-us'; })(); `, + codeBlockJs, ], scripts: [ { From 3ee1fec047880a83b1e5242ec9b11bdb09ad1cef Mon Sep 17 00:00:00 2001 From: EmilyyyLiu <100924403+EmilyyyLiu@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:00:47 +0800 Subject: [PATCH 05/15] =?UTF-8?q?chore:=20upgrade=20[rc-tree]=20to=20[~5.1?= =?UTF-8?q?3.0],upgrade=20[rc-cascader]=20to=20[~3.33=E2=80=A6=20(#52274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: upgrade [rc-tree] to [~5.13.0],upgrade [rc-cascader] to [~3.33.0],upgrade [ rc-tree-select] to [~5.27.0] * chore:升级版本后更新快照 --------- Co-authored-by: 刘欢 --- .../__snapshots__/components.test.tsx.snap | 40 +++++----- .../__snapshots__/demo-extend.test.ts.snap | 2 +- .../__snapshots__/demo-extend.test.ts.snap | 6 +- .../__snapshots__/demo-extend.test.ts.snap | 6 +- .../__snapshots__/demo-extend.test.ts.snap | 16 ++-- .../__snapshots__/demo-extend.test.ts.snap | 12 +-- .../__tests__/__snapshots__/demo.test.ts.snap | 12 +-- .../__snapshots__/demo-extend.test.ts.snap | 48 +++++------ .../__snapshots__/demo-extend.test.ts.snap | 80 +++++++++---------- .../__tests__/__snapshots__/demo.test.ts.snap | 80 +++++++++---------- .../__snapshots__/directory.test.tsx.snap | 42 +++++----- .../__snapshots__/index.test.tsx.snap | 10 +-- package.json | 6 +- 13 files changed, 180 insertions(+), 180 deletions(-) diff --git a/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap b/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap index c7759d39e7..442d8f0ee8 100644 --- a/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap +++ b/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap @@ -41520,7 +41520,7 @@ exports[`ConfigProvider components Tree configProvider 1`] = ` >