From 215d944109c6fbc11183152f65b4dde063809944 Mon Sep 17 00:00:00 2001 From: samuelgja Date: Thu, 29 Oct 2026 14:20:03 +0700 Subject: [PATCH] chore(joint-react): use @joint/mock-svg for the jest SVG mocks --- .changeset/react-tests-mock-svg.md | 2 + packages/joint-react/__mocks__/jest-setup.ts | 175 ++---------------- packages/joint-react/package.json | 1 + .../src/presets/__tests__/anchors.test.ts | 11 ++ .../presets/__tests__/extra-coverage.test.ts | 11 ++ .../paper-button-interactions.test.tsx | 14 +- .../src/store/__tests__/graph-store.test.ts | 6 +- yarn.lock | 1 + 8 files changed, 53 insertions(+), 168 deletions(-) create mode 100644 .changeset/react-tests-mock-svg.md diff --git a/.changeset/react-tests-mock-svg.md b/.changeset/react-tests-mock-svg.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/react-tests-mock-svg.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/joint-react/__mocks__/jest-setup.ts b/packages/joint-react/__mocks__/jest-setup.ts index 29102e11ef..9dd78e0418 100644 --- a/packages/joint-react/__mocks__/jest-setup.ts +++ b/packages/joint-react/__mocks__/jest-setup.ts @@ -4,6 +4,10 @@ // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; import { configure } from '@testing-library/react'; +// Installs the SVG globals JSDOM does not implement but JointJS relies on. +// Note: it mocks `checkVisibility()` on SVG elements only (an empty `getBBox()` +// reads as not rendered); HTML elements have none in JSDOM. +import '@joint/mock-svg'; configure({ reactStrictMode: true }); @@ -11,13 +15,8 @@ configure({ reactStrictMode: true }); // ----- /** - * @description Mock method which is not implemented in JSDOM - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGPathElement - */ -globalThis.SVGPathElement = jest.fn(); - -/** - * @description Minimal DOMMatrix polyfill — JSDOM doesn't ship it. + * @description Minimal DOMMatrix polyfill — JSDOM doesn't ship it, and + * `@joint/mock-svg` does not mock it. * Always returns the identity matrix. Tests don't validate transform math; * they only need the constructor to not throw and produce a/b/c/d/e/f * fields readable by `V.createSVGMatrix`. @@ -36,162 +35,10 @@ Object.defineProperty(globalThis, 'DOMMatrix', { value: MockDOMMatrix, }); -/** - * @description Mock SVGAngle which is used for sanity checks in Vectorizer library - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGAngle - */ -Object.defineProperty(globalThis, 'SVGAngle', { - writable: true, - value: jest.fn().mockImplementation(() => SVGAngle), -}); - +// `@joint/mock-svg` installs its mocks once per test file. Tests that drive +// their own `ResizeObserver` replace the global, so restore the library one +// before every test to keep each test isolated from the previous one. +const { ResizeObserver: MockResizeObserver } = globalThis; beforeEach(() => { - /** - * @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver - */ - globalThis.ResizeObserver = jest.fn().mockImplementation(() => ({ - observe: jest.fn(), - unobserve: jest.fn(), - disconnect: jest.fn(), - })); - - /** - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGSVGElement/createSVGMatrix - */ - Object.defineProperty(globalThis.SVGSVGElement.prototype, 'createSVGMatrix', { - writable: true, - value: jest.fn().mockImplementation(() => SVGMatrix), - }); - - /** - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGTransform - */ - Object.defineProperty(globalThis.SVGSVGElement.prototype, 'createSVGTransform', { - writable: true, - value: jest.fn().mockImplementation(() => SVGTransform), - }); - - /** - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGPoint - */ - Object.defineProperty(globalThis.SVGSVGElement.prototype, 'createSVGPoint', { - writable: true, - value: jest.fn().mockImplementation(() => SVGPoint), - }); - - /** - * @description used in `util.breakText()` method - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGTextContentElement/getComputedTextLength - */ - Object.defineProperty(globalThis.SVGElement.prototype, 'getComputedTextLength', { - writable: true, - value: jest.fn().mockImplementation(() => 0), - }); - - /** - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGGraphicsElement/getScreenCTM - * Note: JSDOM SVGGraphicsElement does not encompass all SVG elements that might be needed, - * whereas SVGElement provides broader compatibility. - */ - Object.defineProperty(globalThis.SVGElement.prototype, 'getScreenCTM', { - writable: true, - value: jest.fn().mockImplementation(() => SVGMatrix), - }); - - /** - * @description used in `util.breakText()` method - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGGraphicsElement/getBBox - */ - Object.defineProperty(globalThis.SVGElement.prototype, 'getBBox', { - writable: true, - value: jest.fn().mockImplementation(() => SVGRect), - }); - - /** - * @description Mock checkVisibility method which is not implemented in JSDOM - * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility - */ - Object.defineProperty(globalThis.Element.prototype, 'checkVisibility', { - writable: true, - value: jest.fn().mockImplementation(() => true), - }); + globalThis.ResizeObserver = MockResizeObserver; }); - -// Interfaces -// ---------- - -/** - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGAngle - */ -const SVGAngle = { - SVG_ANGLETYPE_UNKNOWN: 0, - SVG_ANGLETYPE_UNSPECIFIED: 1, - SVG_ANGLETYPE_DEG: 2, - SVG_ANGLETYPE_RAD: 3, - SVG_ANGLETYPE_GRAD: 4, -}; - -/** - * @description SVGMatrix is deprecated, we should use DOMMatrix instead - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGMatrix - */ -const SVGMatrix = { - a: 0, - b: 0, - c: 0, - d: 0, - e: 0, - f: 0, - flipX: jest.fn().mockImplementation(() => SVGMatrix), - flipY: jest.fn().mockImplementation(() => SVGMatrix), - inverse: jest.fn().mockImplementation(() => SVGMatrix), - multiply: jest.fn().mockImplementation(() => SVGMatrix), - rotate: jest.fn().mockImplementation(() => SVGMatrix), - rotateFromVector: jest.fn().mockImplementation(() => SVGMatrix), - scale: jest.fn().mockImplementation(() => SVGMatrix), - scaleNonUniform: jest.fn().mockImplementation(() => SVGMatrix), - skewX: jest.fn().mockImplementation(() => SVGMatrix), - skewY: jest.fn().mockImplementation(() => SVGMatrix), - translate: jest.fn().mockImplementation(() => SVGMatrix), -}; - -/** - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGTransform - */ -const SVGTransform = { - type: 0, - angle: 0, - matrix: SVGMatrix, - SVG_TRANSFORM_UNKNOWN: 0, - SVG_TRANSFORM_MATRIX: 1, - SVG_TRANSFORM_TRANSLATE: 2, - SVG_TRANSFORM_SCALE: 3, - SVG_TRANSFORM_ROTATE: 4, - SVG_TRANSFORM_SKEWX: 5, - SVG_TRANSFORM_SKEWY: 6, - setMatrix: jest.fn(), - setRotate: jest.fn(), - setScale: jest.fn(), - setSkewX: jest.fn(), - setSkewY: jest.fn(), - setTranslate: jest.fn(), -}; - -/** - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGPoint - */ -const SVGPoint = { - x: 0, - y: 0, - matrixTransform: jest.fn().mockImplementation(() => SVGPoint), -}; - -/** - * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGRect - */ -const SVGRect = { - x: 0, - y: 0, - width: 0, - height: 0, -}; diff --git a/packages/joint-react/package.json b/packages/joint-react/package.json index d039bd864c..2a963922e4 100644 --- a/packages/joint-react/package.json +++ b/packages/joint-react/package.json @@ -92,6 +92,7 @@ "@jest/globals": "30.1.2", "@joint/eslint-config": "workspace:*", "@joint/layout-directed-graph": "workspace:^", + "@joint/mock-svg": "workspace:~", "@reduxjs/toolkit": "^2.11.0", "@storybook/addon-a11y": "^10.6.2", "@storybook/addon-docs": "^10.6.2", diff --git a/packages/joint-react/src/presets/__tests__/anchors.test.ts b/packages/joint-react/src/presets/__tests__/anchors.test.ts index fa21d3ccb3..0accc2735c 100644 --- a/packages/joint-react/src/presets/__tests__/anchors.test.ts +++ b/packages/joint-react/src/presets/__tests__/anchors.test.ts @@ -75,6 +75,17 @@ function setupPaper(targetWithPort = false, portSide: PortSide = 'left'): Setup }; } +// `@joint/mock-svg` counts a node as part of the render tree only when its mocked +// `getBBox()` is non-empty, and the mock is always empty. These suites measure magnets +// through the DOM-measured branches, so every node is treated as rendered. +let checkVisibilitySpy: jest.SpyInstance; +beforeEach(() => { + checkVisibilitySpy = jest.spyOn(SVGElement.prototype, 'checkVisibility').mockReturnValue(true); +}); +afterEach(() => { + checkVisibilitySpy.mockRestore(); +}); + describe('presets / anchors / centerAnchor', () => { it('returns center for root element magnet (uses model geometry path)', () => { const ctx = setupPaper(); diff --git a/packages/joint-react/src/presets/__tests__/extra-coverage.test.ts b/packages/joint-react/src/presets/__tests__/extra-coverage.test.ts index e299e3d3fe..c51ad711c4 100644 --- a/packages/joint-react/src/presets/__tests__/extra-coverage.test.ts +++ b/packages/joint-react/src/presets/__tests__/extra-coverage.test.ts @@ -8,6 +8,17 @@ import { boundaryPoint, anchorPoint } from '../connection-points'; import { LinkView } from '../link-view'; import { Paper } from '../paper'; +// `@joint/mock-svg` counts a node as part of the render tree only when its mocked +// `getBBox()` is non-empty, and the mock is always empty. These suites measure magnets +// through the DOM-measured branches, so every node is treated as rendered. +let checkVisibilitySpy: jest.SpyInstance; +beforeEach(() => { + checkVisibilitySpy = jest.spyOn(SVGElement.prototype, 'checkVisibility').mockReturnValue(true); +}); +afterEach(() => { + checkVisibilitySpy.mockRestore(); +}); + describe('presets / element-ports / elementPort — group-positioned', () => { it('omits position when neither cx nor cy is supplied', () => { const port = elementPort({ shape: 'rect' }); diff --git a/packages/joint-react/src/presets/__tests__/paper-button-interactions.test.tsx b/packages/joint-react/src/presets/__tests__/paper-button-interactions.test.tsx index e0f7a2ca35..91770b1236 100644 --- a/packages/joint-react/src/presets/__tests__/paper-button-interactions.test.tsx +++ b/packages/joint-react/src/presets/__tests__/paper-button-interactions.test.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react'; import { render, waitFor } from '@testing-library/react'; +import { g, type dia } from '@joint/core'; import { GraphProvider, Paper } from '../../components'; import { usePaper } from '../../hooks/use-paper'; import type { PaperView } from '../../mvc/paper'; @@ -62,6 +63,14 @@ function NodeWithButton() { const renderElement = () => ; +// jsdom has no layout, so `@joint/mock-svg` maps every client point to the origin. +// The paper sits unscaled at the client origin, so client and local points are equal. +function clientToLocalIdentity(x: number, y: number): g.Point; +function clientToLocalIdentity(point: dia.Point): g.Point; +function clientToLocalIdentity(xOrPoint: number | dia.Point, y?: number): g.Point { + return typeof xOrPoint === 'number' ? new g.Point(xOrPoint, y) : new g.Point(xOrPoint); +} + /** * Mount a paper holding one element whose markup contains a `