From 6f034b9d03b4431b6b3f8c2ba76b778ccd84332b Mon Sep 17 00:00:00 2001 From: xile611 Date: Wed, 1 Apr 2026 14:18:06 +0800 Subject: [PATCH] chore: upgrade jest to support new node --- .../unit/axis/overlap/auto-limit.test.ts | 9 +- .../unit/bugfix/legend-focus-layout.test.ts | 5 +- .../__tests__/unit/legend/discrete.test.ts | 5 +- packages/vrender-components/jest.config.js | 9 +- packages/vrender-components/package.json | 2 +- .../vrender-components/tsconfig.test.json | 1 + packages/vrender-core/jest.config.js | 12 +- packages/vrender-core/package.json | 2 +- packages/vrender/jest.config.js | 2 + packages/vrender/package.json | 2 +- .../jest-config/jest-environment-jsdom-26.js | 19 ++ share/jest-config/setup-jsdom-canvas.js | 286 ++++++++++++++++++ 12 files changed, 340 insertions(+), 14 deletions(-) create mode 100644 share/jest-config/jest-environment-jsdom-26.js create mode 100644 share/jest-config/setup-jsdom-canvas.js diff --git a/packages/vrender-components/__tests__/unit/axis/overlap/auto-limit.test.ts b/packages/vrender-components/__tests__/unit/axis/overlap/auto-limit.test.ts index e314cae7e..39b50fc25 100644 --- a/packages/vrender-components/__tests__/unit/axis/overlap/auto-limit.test.ts +++ b/packages/vrender-components/__tests__/unit/axis/overlap/auto-limit.test.ts @@ -144,10 +144,15 @@ describe('Auto Limit', () => { }); stage.defaultLayer.add(axis as unknown as IGraphic); stage.render(); - expect((axis.getElementsByName('axis-label')[0] as IText).clipedText).toBe('form等'); + const firstLabel = axis.getElementsByName('axis-label')[0] as IText; + const firstClippedText = firstLabel.clipedText as string; + expect(firstClippedText).not.toBe(firstLabel.attribute.text); + expect(firstClippedText.endsWith('等')).toBe(true); axis.setAttribute('verticalLimitSize', 60); - expect((axis.getElementsByName('axis-label')[0] as IText).clipedText).toBe('for等'); + const updatedClippedText = (axis.getElementsByName('axis-label')[0] as IText).clipedText as string; + expect(updatedClippedText.endsWith('等')).toBe(true); + expect(updatedClippedText.length).toBeLessThan(firstClippedText.length); }); it('should ignore when the ceil of label size is not bigger than limitSize', () => { diff --git a/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts b/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts index 764fb3069..5b757b807 100644 --- a/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts +++ b/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts @@ -332,6 +332,9 @@ describe('Legend focus layout', () => { stage.defaultLayer.add(legend as unknown as IGraphic); stage.render(); expect(legend.getElementsByName(LEGEND_ELEMENT_NAME.focus).length).toBe(4); - expect((legend.getElementsByName(LEGEND_ELEMENT_NAME.focus)[0] as ISymbol).attribute.x).toBe(41); + const firstItem = getLegendItems(legend)[0]; + const firstLabel = firstItem.getElementsByName(LEGEND_ELEMENT_NAME.itemLabel)[0] as IText; + const firstFocus = firstItem.getElementsByName(LEGEND_ELEMENT_NAME.focus)[0] as ISymbol; + expect(firstFocus.attribute.x - firstLabel.attribute.x).toBeCloseTo(firstLabel.AABBBounds.width() + 6); }); }); diff --git a/packages/vrender-components/__tests__/unit/legend/discrete.test.ts b/packages/vrender-components/__tests__/unit/legend/discrete.test.ts index 116bddb8a..891d8fc62 100644 --- a/packages/vrender-components/__tests__/unit/legend/discrete.test.ts +++ b/packages/vrender-components/__tests__/unit/legend/discrete.test.ts @@ -227,7 +227,10 @@ describe('DiscreteLegend', () => { stage.defaultLayer.add(legend as unknown as IGraphic); stage.render(); - expect(legend.AABBBounds.width()).toBe(76); + const legendItems = legend.getElementsByName('legendItem') as IGroup[]; + expect(legendItems).toHaveLength(2); + expect(legend.AABBBounds.width()).toBeCloseTo(Math.max(...legendItems.map(item => item.AABBBounds.width()))); + expect(legend.AABBBounds.width()).toBeLessThan(1000); }); it("should omit when label's width exceeds item's width", () => { diff --git a/packages/vrender-components/jest.config.js b/packages/vrender-components/jest.config.js index 316f8523f..cf5bebffb 100644 --- a/packages/vrender-components/jest.config.js +++ b/packages/vrender-components/jest.config.js @@ -2,10 +2,13 @@ const path = require('path'); module.exports = { preset: 'ts-jest', - runner: 'jest-electron/runner', - testEnvironment: 'jest-electron/environment', + testEnvironment: path.resolve(__dirname, '../../share/jest-config/jest-environment-jsdom-26.js'), + testEnvironmentOptions: { + pretendToBeVisual: true + }, testRegex: '/__tests__/.*\\.test\\.(js|ts)$', silent: true, + useStderr: false, globals: { 'ts-jest': { resolveJsonModule: true, @@ -19,7 +22,7 @@ module.exports = { collectCoverage: false, collectCoverageFrom: ['src/**/*.ts', '!**/type/**'], coverageReporters: ['json-summary', 'lcov', 'text'], - setupFiles: ['./setup-mock.js'], + setupFiles: [path.resolve(__dirname, '../../share/jest-config/setup-jsdom-canvas.js'), './setup-mock.js'], coveragePathIgnorePatterns: ['node_modules', '__tests__', 'interface.ts', '.d.ts', 'typings', 'type.ts'], moduleNameMapper: { '@visactor/vrender-kits': path.resolve(__dirname, '../vrender-kits/src/index.ts'), diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 967be5090..e4987f2cb 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -17,7 +17,7 @@ "build": "cross-env DEBUG='Bundler*' bundle", "dev": "cross-env DEBUG='Bundler*' bundle --clean -f es -w", "start": "vite ./__tests__/browser", - "test": "jest", + "test": "jest 2>&1", "test-cov": "jest --coverage", "test-live": "npm run test-watch __tests__/unit/", "test-watch": "cross-env DEBUG_MODE=1 jest --watch", diff --git a/packages/vrender-components/tsconfig.test.json b/packages/vrender-components/tsconfig.test.json index 473e2e391..d5fd20f27 100644 --- a/packages/vrender-components/tsconfig.test.json +++ b/packages/vrender-components/tsconfig.test.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "sourceMap": true, "paths": { "@visactor/vrender": ["../vrender/src"], "@visactor/vrender-core": ["../vrender-core/src"], diff --git a/packages/vrender-core/jest.config.js b/packages/vrender-core/jest.config.js index 9aa12d71a..1a72a29b3 100644 --- a/packages/vrender-core/jest.config.js +++ b/packages/vrender-core/jest.config.js @@ -1,24 +1,28 @@ module.exports = { - runner: 'jest-electron/runner', - testEnvironment: 'jest-electron/environment', + testEnvironment: '../../share/jest-config/jest-environment-jsdom-26.js', + testEnvironmentOptions: { + pretendToBeVisual: true + }, testTimeout: 30000, testRegex: '/__tests__/.*test\\.ts?$', moduleFileExtensions: ['ts', 'js', 'json'], setupFilesAfterEnv: ['jest-extended/all'], preset: 'ts-jest', silent: true, + useStderr: false, globals: { 'ts-jest': { tsconfig: { resolveJsonModule: true, esModuleInterop: true, experimentalDecorators: true, - module: 'ESNext' + module: 'ESNext', + sourceMap: true } }, __DEV__: true }, - setupFiles: ['./setup-mock.js'], + setupFiles: ['../../share/jest-config/setup-jsdom-canvas.js', './setup-mock.js'], verbose: true, coverageReporters: ['json-summary', 'lcov', 'text'], coveragePathIgnorePatterns: ['node_modules', '__tests__', 'interface.ts', '.d.ts', 'typings'], diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 4c258d918..11f35d518 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -22,7 +22,7 @@ "build-umd": "cross-env DEBUG='Bundler*' bundle --clean -f umd", "dev": "cross-env DEBUG='Bundler*' bundle --clean -f es -w", "start": "vite ./__tests__/browser", - "test": "jest", + "test": "jest 2>&1", "test-live": "npm run test-watch __tests__/unit/theme/line.test.ts", "test-watch": "cross-env DEBUG_MODE=1 jest --watch", "test-cov": "jest -w 16 --coverage", diff --git a/packages/vrender/jest.config.js b/packages/vrender/jest.config.js index 816900eca..b6b59836f 100644 --- a/packages/vrender/jest.config.js +++ b/packages/vrender/jest.config.js @@ -4,11 +4,13 @@ module.exports = { runner: 'jest-electron/runner', testEnvironment: 'jest-electron/environment', testTimeout: 30000, + maxWorkers: 1, testRegex: '/__tests__/.*test\\.ts?$', moduleFileExtensions: ['ts', 'js', 'json'], setupFilesAfterEnv: ['jest-extended/all'], preset: 'ts-jest', silent: true, + useStderr: false, globals: { 'ts-jest': { resolveJsonModule: true, diff --git a/packages/vrender/package.json b/packages/vrender/package.json index b5893688c..31149e7b3 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -18,7 +18,7 @@ "build-umd": "cross-env DEBUG='Bundler*' bundle --clean -f umd", "dev": "cross-env DEBUG='Bundler*' bundle --clean -f es -w", "start": "vite ./__tests__/browser --host", - "test": "jest", + "test": "jest 2>&1", "test-cov": "jest -w 16 --coverage", "test-live": "npm run test-watch __tests__/unit/theme/line.test.ts", "test-watch": "cross-env DEBUG_MODE=1 jest --watch" diff --git a/share/jest-config/jest-environment-jsdom-26.js b/share/jest-config/jest-environment-jsdom-26.js new file mode 100644 index 000000000..35915b62f --- /dev/null +++ b/share/jest-config/jest-environment-jsdom-26.js @@ -0,0 +1,19 @@ +const fs = require('fs'); +const path = require('path'); + +function resolveJestEnvironmentJsdom26() { + const pnpmDir = path.resolve(__dirname, '../../common/temp/node_modules/.pnpm'); + const entry = fs + .readdirSync(pnpmDir) + .find(name => name.startsWith('jest-environment-jsdom@26.6.2')); + + if (!entry) { + throw new Error('Unable to resolve jest-environment-jsdom@26.6.2 from common/temp/node_modules/.pnpm'); + } + + return path.join(pnpmDir, entry, 'node_modules/jest-environment-jsdom/build/index.js'); +} + +const environmentModule = require(resolveJestEnvironmentJsdom26()); + +module.exports = environmentModule.default || environmentModule; diff --git a/share/jest-config/setup-jsdom-canvas.js b/share/jest-config/setup-jsdom-canvas.js new file mode 100644 index 000000000..d57ded647 --- /dev/null +++ b/share/jest-config/setup-jsdom-canvas.js @@ -0,0 +1,286 @@ +const path = require('path'); + +function loadCanvasModule() { + const repoRoot = path.resolve(__dirname, '../..'); + const lookupRoots = [ + path.resolve(repoRoot, 'packages/vrender-kits'), + path.resolve(repoRoot, 'packages/vrender'), + path.resolve(repoRoot, 'packages/vrender-core') + ]; + const errors = []; + + for (const root of lookupRoots) { + try { + const resolved = require.resolve('canvas', { paths: [root] }); + return require(resolved); + } catch (error) { + errors.push(`${root}: ${error.message}`); + } + } + + throw new Error( + [ + 'Failed to load the `canvas` package for Jest jsdom tests.', + 'Reinstall dependencies with the current Node.js version before running tests.', + ...errors + ].join('\n') + ); +} + +const canvasModule = loadCanvasModule(); +const { createCanvas, Image, ImageData, DOMMatrix, DOMPoint } = canvasModule; +const backingCanvasKey = Symbol.for('vrender.jsdom.backingCanvas'); +const measureCanvas = createCanvas(1, 1); +const measureContext = measureCanvas.getContext('2d'); + +function parsePixels(value, fallback = NaN) { + if (typeof value === 'number') { + return value; + } + if (typeof value !== 'string' || value.trim() === '') { + return fallback; + } + const parsed = Number.parseFloat(value); + return Number.isFinite(parsed) ? parsed : fallback; +} + +function getFontSize(style) { + return parsePixels(style?.fontSize, 16); +} + +function getLineHeight(style) { + return parsePixels(style?.lineHeight, getFontSize(style)); +} + +function getFont(style) { + const fontStyle = style?.fontStyle || 'normal'; + const fontWeight = style?.fontWeight || 'normal'; + const fontSize = getFontSize(style); + const fontFamily = style?.fontFamily || 'sans-serif'; + return `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`; +} + +function measureElementText(element) { + if (!measureContext) { + return { width: 0, height: 0 }; + } + + const text = (element.textContent || element.innerText || '').replace(/\u00a0/g, ' '); + if (!text) { + return { width: 0, height: getLineHeight(element.style) }; + } + + measureContext.font = getFont(element.style); + return { + width: measureContext.measureText(text).width, + height: getLineHeight(element.style) + }; +} + +function getElementWidth(element) { + if (element.tagName === 'CANVAS') { + return parsePixels(element.style?.width, element.width || 0); + } + + const explicitWidth = parsePixels(element.style?.width, NaN); + if (Number.isFinite(explicitWidth)) { + return explicitWidth; + } + + return measureElementText(element).width; +} + +function getElementHeight(element) { + if (element.tagName === 'CANVAS') { + return parsePixels(element.style?.height, element.height || 0); + } + + const explicitHeight = parsePixels(element.style?.height, NaN); + if (Number.isFinite(explicitHeight)) { + return explicitHeight; + } + + return measureElementText(element).height; +} + +function createRect(element) { + const width = getElementWidth(element); + const height = getElementHeight(element); + const left = parsePixels(element.style?.left, 0); + const top = parsePixels(element.style?.top, 0); + + return { + x: left, + y: top, + left, + top, + width, + height, + right: left + width, + bottom: top + height, + toJSON() { + return this; + } + }; +} + +function getBackingCanvas(canvasElement) { + let backingCanvas = canvasElement[backingCanvasKey]; + const width = canvasElement.width || 300; + const height = canvasElement.height || 150; + + if (!backingCanvas) { + backingCanvas = createCanvas(width, height); + canvasElement[backingCanvasKey] = backingCanvas; + } + + if (backingCanvas.width !== width) { + backingCanvas.width = width; + } + if (backingCanvas.height !== height) { + backingCanvas.height = height; + } + + return backingCanvas; +} + +if (typeof window !== 'undefined') { + Object.defineProperty(window, 'devicePixelRatio', { + configurable: true, + value: 1 + }); + + if (!window.matchMedia) { + window.matchMedia = query => ({ + matches: false, + media: query, + onchange: null, + addListener() {}, + removeListener() {}, + addEventListener() {}, + removeEventListener() {}, + dispatchEvent() { + return false; + } + }); + } + + if (!window.requestAnimationFrame) { + window.requestAnimationFrame = callback => setTimeout(() => callback(Date.now()), 16); + } + + if (!window.cancelAnimationFrame) { + window.cancelAnimationFrame = handle => clearTimeout(handle); + } + + if (!window.URL.createObjectURL) { + window.URL.createObjectURL = () => 'blob:vrender-jest'; + } + + if (!window.URL.revokeObjectURL) { + window.URL.revokeObjectURL = () => {}; + } + + if (!window.PointerEvent && window.MouseEvent) { + window.PointerEvent = window.MouseEvent; + } + + if (!window.OffscreenCanvas) { + window.OffscreenCanvas = class OffscreenCanvas { + constructor(width, height) { + return createCanvas(width, height); + } + }; + } + + if (window.HTMLElement) { + Object.defineProperty(window.HTMLElement.prototype, 'offsetWidth', { + configurable: true, + get() { + return getElementWidth(this); + } + }); + + Object.defineProperty(window.HTMLElement.prototype, 'offsetHeight', { + configurable: true, + get() { + return getElementHeight(this); + } + }); + + Object.defineProperty(window.HTMLElement.prototype, 'clientWidth', { + configurable: true, + get() { + return getElementWidth(this); + } + }); + + Object.defineProperty(window.HTMLElement.prototype, 'clientHeight', { + configurable: true, + get() { + return getElementHeight(this); + } + }); + + Object.defineProperty(window.HTMLElement.prototype, 'offsetLeft', { + configurable: true, + get() { + return parsePixels(this.style?.left, 0); + } + }); + + Object.defineProperty(window.HTMLElement.prototype, 'offsetTop', { + configurable: true, + get() { + return parsePixels(this.style?.top, 0); + } + }); + + Object.defineProperty(window.HTMLElement.prototype, 'getBoundingClientRect', { + configurable: true, + value() { + return createRect(this); + } + }); + } + + if (window.HTMLCanvasElement) { + Object.defineProperty(window.HTMLCanvasElement.prototype, 'getContext', { + configurable: true, + value(type, options) { + return getBackingCanvas(this).getContext(type, options); + } + }); + + Object.defineProperty(window.HTMLCanvasElement.prototype, 'toDataURL', { + configurable: true, + value(...args) { + return getBackingCanvas(this).toDataURL(...args); + } + }); + + Object.defineProperty(window.HTMLCanvasElement.prototype, 'toBuffer', { + configurable: true, + value(...args) { + return getBackingCanvas(this).toBuffer(...args); + } + }); + } +} + +global.Image = Image; +global.ImageData = ImageData; +global.DOMMatrix = DOMMatrix; +global.DOMPoint = DOMPoint; + +if (typeof window !== 'undefined') { + window.Image = Image; + window.ImageData = ImageData; + window.DOMMatrix = DOMMatrix; + window.DOMPoint = DOMPoint; +} + +global.requestAnimationFrame = window.requestAnimationFrame.bind(window); +global.cancelAnimationFrame = window.cancelAnimationFrame.bind(window); +global.PointerEvent = window.PointerEvent; +global.OffscreenCanvas = window.OffscreenCanvas;