import path from 'path'; import React from 'react'; // Reference: https://github.com/ant-design/ant-design/pull/24003#discussion_r427267386 import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs'; import { extractStaticStyle } from 'antd-style'; import dayjs from 'dayjs'; import fse from 'fs-extra'; import { globSync } from 'glob'; import { JSDOM } from 'jsdom'; import MockDate from 'mockdate'; import type { HTTPRequest } from 'puppeteer'; import ReactDOMServer from 'react-dom/server'; import { App, ConfigProvider, theme } from '../../components'; import { fillWindowEnv } from '../setup'; import { render } from '../utils'; import { TriggerMockContext } from './demoTestContext'; jest.mock('../../components/grid/hooks/useBreakpoint', () => () => ({})); const snapshotPath = path.join(process.cwd(), 'imageSnapshots'); fse.ensureDirSync(snapshotPath); const themes = { default: theme.defaultAlgorithm, dark: theme.darkAlgorithm, compact: theme.compactAlgorithm, }; interface ImageTestOptions { onlyViewport?: boolean; ssr?: boolean; openTriggerClassName?: string; } // eslint-disable-next-line jest/no-export export default function imageTest( component: React.ReactElement, identifier: string, options: ImageTestOptions, ) { let doc: Document; let container: HTMLDivElement; beforeAll(async () => { const dom = new JSDOM('<!DOCTYPE html><body></body></html>', { 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 unknown as typeof ResizeObserver; // Fake promise not called global.fetch = function mockFetch() { return { then() { return this; }, catch() { return this; }, finally() { return this; }, }; } as unknown as typeof fetch; // Fake matchMedia win.matchMedia = (() => ({ matches: false, addListener: jest.fn(), removeListener: jest.fn(), })) as unknown as typeof matchMedia; // Fill window fillWindowEnv(win); await page.setRequestInterception(true); }); beforeEach(() => { doc.body.innerHTML = `<div id="root"></div>`; container = doc.querySelector<HTMLDivElement>('#root')!; }); function test(name: string, suffix: string, themedComponent: React.ReactElement) { it(name, async () => { await page.setViewport({ width: 800, height: 600 }); const onRequestHandle = (request: HTTPRequest) => { if (['image'].includes(request.resourceType())) { request.abort(); } else { request.continue(); } }; const { openTriggerClassName } = options; const requestListener = (request: any) => onRequestHandle(request as HTTPRequest); MockDate.set(dayjs('2016-11-22').valueOf()); page.on('request', requestListener); await page.goto(`file://${process.cwd()}/tests/index.html`); await page.addStyleTag({ path: `${process.cwd()}/components/style/reset.css` }); await page.addStyleTag({ content: '*{animation: none!important;}' }); const cache = createCache(); const emptyStyleHolder = doc.createElement('div'); let element = ( <StyleProvider cache={cache} container={emptyStyleHolder}> <App>{themedComponent}</App> </StyleProvider> ); // Do inject open trigger if (openTriggerClassName) { element = ( <TriggerMockContext.Provider value={{ popupVisible: true }}> {element} </TriggerMockContext.Provider> ); } let html: string; let styleStr: string; if (options.ssr) { html = ReactDOMServer.renderToString(element); styleStr = extractStyle(cache) + extractStaticStyle(html).map((item) => item.tag); } else { const { unmount } = render(element, { container, }); html = container.innerHTML; styleStr = extractStyle(cache) + extractStaticStyle(html).map((item) => item.tag); // We should extract style before unmount unmount(); } if (openTriggerClassName) { styleStr += `<style> .${openTriggerClassName} { position: relative !important; left: 0 !important; top: 0 !important; opacity: 1 !important; display: inline-block !important; vertical-align: top !important; } </style>`; } await page.evaluate( (innerHTML: string, ssrStyle: string, triggerClassName?: string) => { const root = document.querySelector<HTMLDivElement>('#root')!; root.innerHTML = innerHTML; const head = document.querySelector<HTMLElement>('head')!; head.innerHTML += ssrStyle; // Inject open trigger with block style if (triggerClassName) { document.querySelectorAll<HTMLElement>(`.${triggerClassName}`).forEach((node) => { const blockStart = document.createElement('div'); const blockEnd = document.createElement('div'); node.parentNode?.insertBefore(blockStart, node); node.parentNode?.insertBefore(blockEnd, node.nextSibling); }); } }, html, styleStr, openTriggerClassName || '', ); if (!options.onlyViewport) { // Get scroll height of the rendered page and set viewport const bodyHeight = await page.evaluate(() => document.body.scrollHeight); await page.setViewport({ width: 800, height: bodyHeight }); } const image = await page.screenshot({ fullPage: !options.onlyViewport, }); await fse.writeFile(path.join(snapshotPath, `${identifier}${suffix}.png`), image); MockDate.reset(); page.off('request', requestListener); }); } Object.entries(themes).forEach(([key, algorithm]) => { const configTheme = { algorithm, token: { fontFamily: 'Arial', }, }; test( `component image screenshot should correct ${key}`, `.${key}`, <div style={{ background: key === 'dark' ? '#000' : '', padding: `24px 12px` }} key={key}> <ConfigProvider theme={configTheme}>{component}</ConfigProvider> </div>, ); test( `[CSS Var] component image screenshot should correct ${key}`, `.${key}.css-var`, <div style={{ background: key === 'dark' ? '#000' : '', padding: `24px 12px` }} key={key}> <ConfigProvider theme={{ ...configTheme, cssVar: true }}>{component}</ConfigProvider> </div>, ); }); } type Options = { skip?: boolean | string[]; onlyViewport?: boolean | string[]; /** Use SSR render instead. Only used when the third part deps component */ ssr?: boolean; /** Open Trigger to check the popup render */ openTriggerClassName?: string; }; // eslint-disable-next-line jest/no-export export function imageDemoTest(component: string, options: Options = {}) { let describeMethod = options.skip === true ? describe.skip : describe; const files = globSync(`./components/${component}/demo/*.tsx`).filter( (file) => !file.includes('_semantic'), ); files.forEach((file) => { if (Array.isArray(options.skip) && options.skip.some((c) => file.endsWith(c))) { describeMethod = describe.skip; } else { describeMethod = describe; } describeMethod(`Test ${file} image`, () => { let Demo = require(`../../${file}`).default; if (typeof Demo === 'function') { Demo = <Demo />; } imageTest(Demo, `${component}-${path.basename(file, '.tsx')}`, { onlyViewport: options.onlyViewport === true || (Array.isArray(options.onlyViewport) && options.onlyViewport.some((c) => file.endsWith(c))), ssr: options.ssr, openTriggerClassName: options.openTriggerClassName, }); }); }); }