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 `