test: Update image snapshot logic (#46529)

* test: mix snapshot

* test: clean up

* test: fix order

* test: fix test logic

* test: fix reset

* test: refactor code

* test: clean fetch

* chore: lint

* test: clean up

* test: delay check

* chore: refactor fetch

* docs: update diff html

* test: add ssr support

* chore: part use ssr

* chore: update style

* chore: slice of it

* docs: fix cut
This commit is contained in:
二货爱吃白萝卜 2023-12-21 10:30:08 +08:00 committed by GitHub
parent b071284027
commit eff720837c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 187 additions and 58 deletions

View File

@ -2,7 +2,7 @@ const { moduleNameMapper, transformIgnorePatterns } = require('./.jest');
// jest config for image snapshots // jest config for image snapshots
module.exports = { module.exports = {
setupFiles: ['./tests/setup.js'], setupFiles: ['./tests/setup.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'md'], moduleFileExtensions: ['ts', 'tsx', 'js', 'md'],
moduleNameMapper, moduleNameMapper,
transform: { transform: {
@ -19,5 +19,5 @@ module.exports = {
}, },
}, },
preset: 'jest-puppeteer', preset: 'jest-puppeteer',
testTimeout: 10000, testTimeout: 20000,
}; };

View File

@ -32,7 +32,7 @@ function getTestRegex(libDir) {
module.exports = { module.exports = {
verbose: true, verbose: true,
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
setupFiles: ['./tests/setup.js', 'jest-canvas-mock'], setupFiles: ['./tests/setup.ts', 'jest-canvas-mock'],
setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'], setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'],
modulePathIgnorePatterns: ['/_site/'], modulePathIgnorePatterns: ['/_site/'],

View File

@ -2,7 +2,7 @@ const { moduleNameMapper, transformIgnorePatterns } = require('./.jest');
// jest config for server render environment // jest config for server render environment
module.exports = { module.exports = {
setupFiles: ['./tests/setup.js'], setupFiles: ['./tests/setup.ts'],
setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'], setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'md'], moduleFileExtensions: ['ts', 'tsx', 'js', 'md'],
moduleNameMapper, moduleNameMapper,

View File

@ -1,5 +1,7 @@
import { imageDemoTest } from '../../../tests/shared/imageTest'; import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Carousel image', () => { describe('Carousel image', () => {
imageDemoTest('carousel'); imageDemoTest('carousel', {
ssr: true,
});
}); });

View File

@ -1,5 +1,6 @@
import dayjs from 'dayjs';
import * as React from 'react'; import * as React from 'react';
import dayjs from 'dayjs';
import demoTest, { rootPropsTest } from '../../../tests/shared/demoTest'; import demoTest, { rootPropsTest } from '../../../tests/shared/demoTest';
demoTest('date-picker', { skip: ['locale.tsx', 'component-token.tsx'], testRootProps: false }); demoTest('date-picker', { skip: ['locale.tsx', 'component-token.tsx'], testRootProps: false });
@ -10,7 +11,7 @@ rootPropsTest('date-picker', (DatePicker, props) => <DatePicker {...props} value
}); });
rootPropsTest( rootPropsTest(
'date-picker', ['date-picker', 'RangePicker'],
(DatePicker, props) => <DatePicker.RangePicker {...props} value={dayjs()} />, (DatePicker, props) => <DatePicker.RangePicker {...props} value={dayjs()} />,
{ {
findRootElements: () => document.querySelectorAll('.ant-picker-range, .ant-picker-dropdown'), findRootElements: () => document.querySelectorAll('.ant-picker-range, .ant-picker-dropdown'),

View File

@ -1,5 +1,7 @@
import { imageDemoTest } from '../../../tests/shared/imageTest'; import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Progress image', () => { describe('Progress image', () => {
imageDemoTest('progress'); imageDemoTest('progress', {
ssr: true,
});
}); });

View File

@ -93,7 +93,7 @@
"pretest": "npm run version && npm run component-changelog", "pretest": "npm run version && npm run component-changelog",
"test": "jest --config .jest.js --no-cache", "test": "jest --config .jest.js --no-cache",
"test-all": "sh -e ./scripts/test-all.sh", "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-node": "npm run version && jest --config .jest.node.js --no-cache",
"test:update": "jest --config .jest.js --no-cache -u", "test:update": "jest --config .jest.js --no-cache -u",
"token-meta": "tsx scripts/generate-token-meta.ts", "token-meta": "tsx scripts/generate-token-meta.ts",

View File

@ -1,23 +1,22 @@
/* eslint-disable compat/compat */ /* eslint-disable compat/compat */
/* eslint-disable no-console, no-await-in-loop, import/no-extraneous-dependencies, lodash/import-scope, no-restricted-syntax */ /* 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 fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path';
import { Readable } from 'stream'; import { Readable } from 'stream';
import { finished } from 'stream/promises'; 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 chalk from 'chalk';
import fse from 'fs-extra';
import _ from 'lodash'; import _ from 'lodash';
import minimist from 'minimist';
import pixelmatch from 'pixelmatch'; import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs'; import { PNG } from 'pngjs';
import { remark } from 'remark';
import remarkGfm from 'remark-gfm';
import remarkHtml from 'remark-html';
import sharp from 'sharp'; import sharp from 'sharp';
import { assert } from 'console'; import tar from 'tar';
const ALI_OSS_BUCKET = 'antd-visual-diff'; const ALI_OSS_BUCKET = 'antd-visual-diff';
@ -158,10 +157,7 @@ function generateReport(
const htmlReportLink = `${publicPath}/visualRegressionReport/report.html`; 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`; const addonFullReportDesc = `\n\nCheck <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;
let reportMdStr = ` let reportMdStr = `
${commonHeader} ${commonHeader}
@ -174,7 +170,7 @@ ${commonHeader}
let fullVersionMd = reportMdStr; let fullVersionMd = reportMdStr;
let addonFullReportDescAdded = false; let diffCount = 0;
for (const badCase of badCases) { for (const badCase of badCases) {
const { filename, type } = badCase; const { filename, type } = badCase;
@ -199,16 +195,15 @@ ${commonHeader}
lineReportMdStr += ' |\n'; lineReportMdStr += ' |\n';
} }
if (lineReportMdStr) { diffCount += 1;
if (reportMdStr.length + lineReportMdStr.length < limit) { if (diffCount <= 10) {
reportMdStr += lineReportMdStr; reportMdStr += lineReportMdStr;
} else if (!addonFullReportDescAdded) {
reportMdStr += addonFullReportDesc;
addonFullReportDescAdded = true;
} }
fullVersionMd += lineReportMdStr; fullVersionMd += lineReportMdStr;
} }
}
reportMdStr += addonFullReportDesc;
// convert fullVersionMd to html // convert fullVersionMd to html
return [reportMdStr, md2Html(fullVersionMd)]; return [reportMdStr, md2Html(fullVersionMd)];

View File

@ -16,6 +16,7 @@
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed;
} }
th, th,
@ -26,6 +27,20 @@
vertical-align: top; vertical-align: top;
} }
td img {
max-width: 100%;
}
th,
td {
width: 10%;
}
th+th,
td+td {
width: 30%;
}
th { th {
background-color: #f2f2f2; background-color: #f2f2f2;
} }

View File

@ -1,5 +1,9 @@
/* eslint-disable no-console */ /* eslint-disable no-console, import/prefer-default-export */
const util = require('util'); import util from 'util';
import type { DOMWindow } from 'jsdom';
// import { fillWindowEnv } from './utils';
const React = require('react'); const React = require('react');
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -20,17 +24,21 @@ console.error = (...args) => {
} }
}; };
/* eslint-disable global-require */ type Writeable<T> = { -readonly [P in keyof T]: T[P] };
if (typeof window !== 'undefined') {
global.window.resizeTo = (width, height) => { // This function can not move to external file since jest setup not support
global.window.innerWidth = width || global.window.innerWidth; export function fillWindowEnv(window: Window | DOMWindow) {
global.window.innerHeight = height || global.window.innerHeight; const win = window as Writeable<Window> & typeof globalThis;
global.window.dispatchEvent(new Event('resize'));
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 // ref: https://github.com/ant-design/ant-design/issues/18774
if (!window.matchMedia) { if (!win.matchMedia) {
Object.defineProperty(global.window, 'matchMedia', { Object.defineProperty(win, 'matchMedia', {
writable: true, writable: true,
configurable: true, configurable: true,
value: jest.fn((query) => ({ value: jest.fn((query) => ({
@ -44,11 +52,19 @@ if (typeof window !== 'undefined') {
// Fix css-animation or rc-motion deps on these // 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/react-component/motion/blob/9c04ef1a210a4f3246c9becba6e33ea945e00669/src/util/motion.ts#L27-L35
// https://github.com/yiminghe/css-animation/blob/a5986d73fd7dfce75665337f39b91483d63a4c8c/src/Event.js#L44 // https://github.com/yiminghe/css-animation/blob/a5986d73fd7dfce75665337f39b91483d63a4c8c/src/Event.js#L44
window.AnimationEvent = window.AnimationEvent || window.Event; win.AnimationEvent = win.AnimationEvent || win.Event;
window.TransitionEvent = window.TransitionEvent || window.Event; win.TransitionEvent = win.TransitionEvent || win.Event;
// ref: https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom // ref: https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
// ref: https://github.com/jsdom/jsdom/issues/2524 // ref: https://github.com/jsdom/jsdom/issues/2524
Object.defineProperty(window, 'TextEncoder', { writable: true, value: util.TextEncoder }); Object.defineProperty(win, 'TextEncoder', { writable: true, value: util.TextEncoder });
Object.defineProperty(window, 'TextDecoder', { writable: true, value: util.TextDecoder }); 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;

View File

@ -1,15 +1,20 @@
import path from 'path';
import React from 'react'; import React from 'react';
// Reference: https://github.com/ant-design/ant-design/pull/24003#discussion_r427267386 // Reference: https://github.com/ant-design/ant-design/pull/24003#discussion_r427267386
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs'; import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import path from 'path';
import { globSync } from 'glob'; import { globSync } from 'glob';
import { configureToMatchImageSnapshot } from 'jest-image-snapshot'; import { configureToMatchImageSnapshot } from 'jest-image-snapshot';
import { JSDOM } from 'jsdom';
import MockDate from 'mockdate'; import MockDate from 'mockdate';
import ReactDOMServer from 'react-dom/server'; import ReactDOMServer from 'react-dom/server';
import { App, ConfigProvider, theme } from '../../components'; import { App, ConfigProvider, theme } from '../../components';
import { fillWindowEnv } from '../setup';
import { render } from '../utils';
jest.mock('../../components/grid/hooks/useBreakpoint', () => () => ({}));
const toMatchImageSnapshot = configureToMatchImageSnapshot({ const toMatchImageSnapshot = configureToMatchImageSnapshot({
customSnapshotsDir: `${process.cwd()}/imageSnapshots`, customSnapshotsDir: `${process.cwd()}/imageSnapshots`,
@ -27,6 +32,7 @@ const themes = {
interface ImageTestOptions { interface ImageTestOptions {
onlyViewport?: boolean; onlyViewport?: boolean;
splitTheme?: boolean; splitTheme?: boolean;
ssr?: boolean;
} }
// eslint-disable-next-line jest/no-export // eslint-disable-next-line jest/no-export
@ -35,6 +41,74 @@ export default function imageTest(
identifier: string, identifier: string,
options: ImageTestOptions, 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) { function test(name: string, suffix: string, themedComponent: React.ReactElement) {
it(name, async () => { it(name, async () => {
await jestPuppeteer.resetPage(); await jestPuppeteer.resetPage();
@ -55,14 +129,30 @@ export default function imageTest(
const cache = createCache(); const cache = createCache();
const emptyStyleHolder = doc.createElement('div');
const element = ( const element = (
<StyleProvider cache={cache}> <StyleProvider cache={cache} container={emptyStyleHolder}>
<App>{themedComponent}</App> <App>{themedComponent}</App>
</StyleProvider> </StyleProvider>
); );
const html = ReactDOMServer.renderToString(element); let html: string;
const styleStr = extractStyle(cache); 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( await page.evaluate(
(innerHTML, ssrStyle) => { (innerHTML, ssrStyle) => {
@ -141,6 +231,8 @@ type Options = {
skip?: boolean | string[]; skip?: boolean | string[];
onlyViewport?: boolean | string[]; onlyViewport?: boolean | string[];
splitTheme?: 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 // eslint-disable-next-line jest/no-export
@ -168,6 +260,7 @@ export function imageDemoTest(component: string, options: Options = {}) {
splitTheme: splitTheme:
options.splitTheme === true || options.splitTheme === true ||
(Array.isArray(options.splitTheme) && options.splitTheme.some((c) => file.endsWith(c))), (Array.isArray(options.splitTheme) && options.splitTheme.some((c) => file.endsWith(c))),
ssr: options.ssr,
}); });
}); });
}); });

View File

@ -1,5 +1,6 @@
/* eslint-disable global-require, import/no-dynamic-require, jest/no-export */ /* eslint-disable global-require, import/no-dynamic-require, jest/no-export */
import React from 'react'; import React from 'react';
import ConfigProvider from '../../components/config-provider'; import ConfigProvider from '../../components/config-provider';
import { render, waitFakeTimer } from '../utils'; import { render, waitFakeTimer } from '../utils';
import { TriggerMockContext } from './demoTestContext'; import { TriggerMockContext } from './demoTestContext';
@ -20,14 +21,17 @@ function isSingleNode(node: any): node is Element {
} }
export default function rootPropsTest( export default function rootPropsTest(
component: string, component: string | string[],
customizeRender?: ( customizeRender?: (
component: React.ComponentType<any> & Record<string, any>, component: React.ComponentType<any> & Record<string, any>,
props: any, props: any,
) => React.ReactNode, ) => React.ReactNode,
options?: Options, 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})` : ''; const name = options?.name ? `(${options.name})` : '';
describe(`RootProps${name}`, () => { describe(`RootProps${name}`, () => {
@ -36,6 +40,7 @@ export default function rootPropsTest(
beforeEach(() => { beforeEach(() => {
passed = false; passed = false;
jest.useFakeTimers(); jest.useFakeTimers();
document.body.innerHTML = '';
}); });
afterEach(() => { afterEach(() => {
@ -46,7 +51,7 @@ export default function rootPropsTest(
jest.useRealTimers(); jest.useRealTimers();
}); });
it('rootClassName', async () => { it(['rootClassName', subComponentName].filter((v) => v).join(' '), async () => {
const rootClassName = 'TEST_ROOT_CLS'; const rootClassName = 'TEST_ROOT_CLS';
if (options?.beforeRender) { if (options?.beforeRender) {
@ -104,7 +109,7 @@ export default function rootPropsTest(
expect(childList.length).toBeGreaterThan(0); expect(childList.length).toBeGreaterThan(0);
if (options?.expectCount) { if (options?.expectCount) {
expect(childList.length).toBe(options.expectCount); expect(childList).toHaveLength(options.expectCount);
} }
childList.forEach((ele) => { childList.forEach((ele) => {

View File

@ -1,10 +1,10 @@
import type { ReactElement } from 'react';
import React, { createRef, StrictMode } from 'react';
import type { RenderOptions } from '@testing-library/react'; import type { RenderOptions } from '@testing-library/react';
import { act, render } from '@testing-library/react'; import { act, render } from '@testing-library/react';
import MockDate from 'mockdate'; import MockDate from 'mockdate';
import { _rs as onEsResize } from 'rc-resize-observer/es/utils/observerUtil'; import { _rs as onEsResize } from 'rc-resize-observer/es/utils/observerUtil';
import { _rs as onLibResize } from 'rc-resize-observer/lib/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 { export function assertsExist<T>(item?: T): asserts item is T {
expect(item).not.toBeUndefined(); expect(item).not.toBeUndefined();
@ -33,7 +33,7 @@ const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>
render(ui, { wrapper: StrictMode, ...options }); render(ui, { wrapper: StrictMode, ...options });
export function renderHook<T>(func: () => T): { result: React.RefObject<T> } { export function renderHook<T>(func: () => T): { result: React.RefObject<T> } {
const result = React.createRef<T>(); const result = createRef<T>();
const Demo: React.FC = () => { const Demo: React.FC = () => {
(result as any).current = func(); (result as any).current = func();
@ -58,7 +58,7 @@ export { pureRender, customRender as render };
export const triggerResize = (target: Element) => { export const triggerResize = (target: Element) => {
const originGetBoundingClientRect = target.getBoundingClientRect; const originGetBoundingClientRect = target.getBoundingClientRect;
target.getBoundingClientRect = () => ({ width: 510, height: 903 } as DOMRect); target.getBoundingClientRect = () => ({ width: 510, height: 903 }) as DOMRect;
act(() => { act(() => {
onLibResize([{ target } as ResizeObserverEntry]); onLibResize([{ target } as ResizeObserverEntry]);