Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/react-tests-mock-svg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
175 changes: 11 additions & 164 deletions packages/joint-react/__mocks__/jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@
// 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 });

// Mocks
// -----

/**
* @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`.
Expand All @@ -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,
};
1 change: 1 addition & 0 deletions packages/joint-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions packages/joint-react/src/presets/__tests__/anchors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions packages/joint-react/src/presets/__tests__/extra-coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -62,6 +63,14 @@ function NodeWithButton() {

const renderElement = () => <NodeWithButton />;

// 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 `<button>`.
* @param clickSpy - Spy for the button's React `onClick`.
Expand All @@ -87,7 +96,10 @@ async function renderNodeWithButton(
const paper = capturedPaper;
const button = capturedButton;
if (!paper || !button) throw new Error('paper or button not mounted');
return { paper, button };
// Typed explicitly: the narrowing above cannot see the assignments made while rendering.
const mounted: { paper: PaperView; button: HTMLButtonElement } = { paper, button };
mounted.paper.clientToLocalPoint = clientToLocalIdentity;
return mounted;
}

function dispatchAt(node: EventTarget, type: string, clientX: number, clientY: number) {
Expand Down
6 changes: 3 additions & 3 deletions packages/joint-react/src/store/__tests__/graph-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ describe('GraphStore', () => {

// The observer measures the same size, so it writes nothing to the graph.
const node = document.createElement('div');
const resizeObserverSpy = jest.spyOn(globalThis, 'ResizeObserver');
store.measurement.observe({ id: 'a', node });
const [callback] = (globalThis.ResizeObserver as jest.Mock).mock.calls.at(-1) as [
ResizeObserverCallback,
];
const [callback] = resizeObserverSpy.mock.calls.at(-1) as [ResizeObserverCallback];
resizeObserverSpy.mockRestore();
const entry = { target: node, borderBoxSize: [{ inlineSize: 120, blockSize: 40 }] };
callback([entry as unknown as ResizeObserverEntry], {} as ResizeObserver);
await flush();
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6064,6 +6064,7 @@ __metadata:
"@joint/core": "workspace:^"
"@joint/eslint-config": "workspace:*"
"@joint/layout-directed-graph": "workspace:^"
"@joint/mock-svg": "workspace:~"
"@reduxjs/toolkit": "npm:^2.11.0"
"@storybook/addon-a11y": "npm:^10.6.2"
"@storybook/addon-docs": "npm:^10.6.2"
Expand Down
Loading