UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

153 lines (130 loc) 4.68 kB
import { expect, Snapshots } from 'vitest'; import { cloneDeepWith } from 'es-toolkit/compat'; import type { FabricObject } from './src/shapes/Object/Object'; import type { TMat2D } from './src/typedefs'; import type { ExtendedOptions, ObjectOptions } from './vitest'; import type { FabricImage } from './dist-extensions'; const SVG_RE = /(SVGID|CLIPPATH|imageCrop)_[0-9]+/gm; const SVG_XLINK_HREF_RE = /xlink:href="([^"]*)"/gm; function basename(link: string) { return link.split(/[\\/]/).pop()?.replace(/"/gm, '') || ''; } function replaceLinks(value: string) { return (value.match(SVG_XLINK_HREF_RE) ?? []).reduce( (acc, match) => acc.replace(match, `xlink:href="assets/${basename(match)}"`), value, ); } export function sanitizeSVG(value: string) { return replaceLinks(value).replace(SVG_RE, 'SVGID'); } function looksLikeFabricObject(val: unknown): val is FabricObject { return ( !!val && typeof val === 'object' && // all FabricObjects implement these three members typeof (val as any).toObject === 'function' && typeof (val as any).render === 'function' && typeof (val as any).constructor?.type === 'string' ); } export function isJSDOM(): boolean { return 'jsdom' in globalThis; } export const roundSnapshotOptions = { cloneDeepWith: (value: unknown) => { if (typeof value === 'number') { return Math.round(value); } }, }; const { toMatchSnapshot } = Snapshots; expect.extend({ toMatchObjectSnapshot( received: unknown, { cloneDeepWith: customiser, includeDefaultValues, ...properties }: ObjectOptions = {}, hint?: string, ) { let snap = received as Record<string, unknown>; if (looksLikeFabricObject(received)) { const restore = received.includeDefaultValues; received.includeDefaultValues = includeDefaultValues ?? restore; snap = received.toObject(); received.includeDefaultValues = restore; } delete snap.version; const value = cloneDeepWith(snap, (v, k, obj, stack) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const c = (customiser as any)?.(v, k, obj, stack); if (c !== undefined) return c; if ((k === 'width' || k === 'height') && typeof v === 'number') return Math.round(v * 100) / 100; // Normalize uid-based ids which vary by test execution order if (k === 'id' && typeof v === 'number') return 0; }); return toMatchSnapshot.call(this, value, properties, hint); }, toMatchSnapshot( received: unknown, { cloneDeepWith: customiser, ...rest }: ExtendedOptions = {}, hint?: string, ) { const value = customiser ? cloneDeepWith(received, customiser) : received; return Object.keys(rest).length > 0 ? toMatchSnapshot.call(this, value, rest, hint) : toMatchSnapshot.call(this, value, hint); }, toMatchSVGSnapshot(received: string, hint?: string) { return toMatchSnapshot.call(this, sanitizeSVG(received), hint); }, toSameImageObject(actual: FabricImage, expected: FabricImage) { const normalizedActual = { ...actual, // @ts-expect-error -- src is protected in FabricImage class src: basename(actual.src), }; const normalizedExpected = { ...expected, // @ts-expect-error -- src is protected in FabricImage class src: basename(expected.src), }; const pass = this.equals(normalizedActual, normalizedExpected); return { pass, message: () => pass ? `Expected image object to not equal the reference` : `Expected image object to equal the reference\nReceived: ${JSON.stringify(normalizedActual)}\nExpected: ${JSON.stringify(normalizedExpected)}`, }; }, toEqualSVG(actual: string, expected: string) { const sanitizedActual = sanitizeSVG(actual); const sanitizedExpected = sanitizeSVG(expected); const pass = sanitizedActual === sanitizedExpected; return { pass, message: () => pass ? `Expected SVG to not equal the normalized reference` : `Expected SVG to equal the normalized reference\nReceived: ${sanitizedActual}\nExpected: ${sanitizedExpected}`, }; }, toEqualRoundedMatrix(actual: TMat2D, expected: TMat2D, precision = 10) { const error = Math.pow(10, -precision); return { message: () => { return `expected ${this.utils.printReceived( actual, )} to be rounded to ${this.utils.printExpected( expected.map((x) => Math.round(x / error) * error), )}`; }, pass: actual.every((x, i) => Math.abs(x - expected[i]) < error), }; }, });