diff --git a/.jest.image.js b/.jest.image.js
index fb42545326..78191c45d4 100644
--- a/.jest.image.js
+++ b/.jest.image.js
@@ -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,
};
diff --git a/.jest.js b/.jest.js
index d71c4e8497..c3a1708cc4 100644
--- a/.jest.js
+++ b/.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/'],
diff --git a/.jest.node.js b/.jest.node.js
index 54d6fb852d..e4631d406e 100644
--- a/.jest.node.js
+++ b/.jest.node.js
@@ -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,
diff --git a/components/carousel/__tests__/image.test.ts b/components/carousel/__tests__/image.test.ts
index 30d204a801..4d1bdc6da2 100644
--- a/components/carousel/__tests__/image.test.ts
+++ b/components/carousel/__tests__/image.test.ts
@@ -1,5 +1,7 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Carousel image', () => {
- imageDemoTest('carousel');
+ imageDemoTest('carousel', {
+ ssr: true,
+ });
});
diff --git a/components/date-picker/__tests__/demo.test.tsx b/components/date-picker/__tests__/demo.test.tsx
index 162c3a5a26..d8c4e237bb 100644
--- a/components/date-picker/__tests__/demo.test.tsx
+++ b/components/date-picker/__tests__/demo.test.tsx
@@ -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) => ,
{
findRootElements: () => document.querySelectorAll('.ant-picker-range, .ant-picker-dropdown'),
diff --git a/components/progress/__tests__/image.test.ts b/components/progress/__tests__/image.test.ts
index ba763d18ec..e3298b732e 100644
--- a/components/progress/__tests__/image.test.ts
+++ b/components/progress/__tests__/image.test.ts
@@ -1,5 +1,7 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Progress image', () => {
- imageDemoTest('progress');
+ imageDemoTest('progress', {
+ ssr: true,
+ });
});
diff --git a/package.json b/package.json
index aac032334c..b1db1c7a11 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/scripts/visual-regression/build.ts b/scripts/visual-regression/build.ts
index 27e5842318..8bbdb75b52 100644
--- a/scripts/visual-regression/build.ts
+++ b/scripts/visual-regression/build.ts
@@ -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';
@@ -158,10 +157,7 @@ function generateReport(
const htmlReportLink = `${publicPath}/visualRegressionReport/report.html`;
- const addonFullReportDesc = `\n\nToo many visual-regression diffs found, please check Full Report for details`;
-
- // github action pr comment has limit of 65536 4-byte unicode characters
- const limit = 65536 - addonFullReportDesc.length;
+ const addonFullReportDesc = `\n\nCheck Full Report for details`;
let reportMdStr = `
${commonHeader}
@@ -174,7 +170,7 @@ ${commonHeader}
let fullVersionMd = reportMdStr;
- let addonFullReportDescAdded = false;
+ let diffCount = 0;
for (const badCase of badCases) {
const { filename, type } = badCase;
@@ -199,17 +195,16 @@ ${commonHeader}
lineReportMdStr += ' |\n';
}
- if (lineReportMdStr) {
- if (reportMdStr.length + lineReportMdStr.length < limit) {
- reportMdStr += lineReportMdStr;
- } else if (!addonFullReportDescAdded) {
- reportMdStr += addonFullReportDesc;
- addonFullReportDescAdded = true;
- }
- fullVersionMd += lineReportMdStr;
+ diffCount += 1;
+ if (diffCount <= 10) {
+ reportMdStr += lineReportMdStr;
}
+
+ fullVersionMd += lineReportMdStr;
}
+ reportMdStr += addonFullReportDesc;
+
// convert fullVersionMd to html
return [reportMdStr, md2Html(fullVersionMd)];
}
diff --git a/scripts/visual-regression/report-template.html b/scripts/visual-regression/report-template.html
index dc5b178f3f..96c3f4aac7 100644
--- a/scripts/visual-regression/report-template.html
+++ b/scripts/visual-regression/report-template.html
@@ -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;
}
diff --git a/tests/setup.js b/tests/setup.ts
similarity index 51%
rename from tests/setup.js
rename to tests/setup.ts
index e5da82ca53..1977c6b1a8 100644
--- a/tests/setup.js
+++ b/tests/setup.ts
@@ -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 = { -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 & 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;
diff --git a/tests/shared/imageTest.tsx b/tests/shared/imageTest.tsx
index 9c104987bd..c6dfd6b136 100644
--- a/tests/shared/imageTest.tsx
+++ b/tests/shared/imageTest.tsx
@@ -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('
', {
+ 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 = ``;
+ container = doc.querySelector('#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 = (
-
+
{themedComponent}
);
- 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,
});
});
});
diff --git a/tests/shared/rootPropsTest.tsx b/tests/shared/rootPropsTest.tsx
index 1f4ee413df..b38ce248c4 100644
--- a/tests/shared/rootPropsTest.tsx
+++ b/tests/shared/rootPropsTest.tsx
@@ -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 & Record,
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) => {
diff --git a/tests/utils.tsx b/tests/utils.tsx
index d3427d79e3..3355faf802 100644
--- a/tests/utils.tsx
+++ b/tests/utils.tsx
@@ -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(item?: T): asserts item is T {
expect(item).not.toBeUndefined();
@@ -33,7 +33,7 @@ const customRender = (ui: ReactElement, options?: Omit
render(ui, { wrapper: StrictMode, ...options });
export function renderHook(func: () => T): { result: React.RefObject } {
- const result = React.createRef();
+ const result = createRef();
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]);