UNPKG

fabric

Version:

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

1,341 lines (1,118 loc) 38.1 kB
import { roundSnapshotOptions } from '../../../vitest.extend'; import { cache } from '../../cache'; import { config } from '../../config'; import { Path } from '../Path'; import { FabricText } from './Text'; import { describe, expect, it, afterEach } from 'vitest'; import { FabricObject, getFabricDocument, IText, Textbox, version, } from '../../../fabric'; import { toFixed } from '../../util'; const CHAR_WIDTH = 20; const REFERENCE_TEXT_OBJECT = { version: version, type: 'Text', originX: 'center', originY: 'center', left: 0, top: 0, width: CHAR_WIDTH, height: 45.2, fill: 'rgb(0,0,0)', stroke: null, strokeWidth: 1, strokeDashArray: null, strokeLineCap: 'butt', strokeDashOffset: 0, strokeLineJoin: 'miter', strokeMiterLimit: 4, scaleX: 1, scaleY: 1, angle: 0, flipX: false, flipY: false, opacity: 1, shadow: null, visible: true, backgroundColor: '', text: 'x', fontSize: 40, fontWeight: 'normal', fontFamily: 'Times New Roman', fontStyle: 'normal', lineHeight: 1.16, underline: false, overline: false, linethrough: false, textAlign: 'left', textBackgroundColor: '', fillRule: 'nonzero', paintFirst: 'fill', globalCompositeOperation: 'source-over', skewX: 0, skewY: 0, charSpacing: 0, styles: [], path: undefined, strokeUniform: false, direction: 'ltr', pathStartOffset: 0, pathSide: 'left', pathAlign: 'baseline', textDecorationThickness: 66.667, }; function createTextObject() { return new FabricText('x'); } describe('FabricText', () => { afterEach(() => { config.restoreDefaults(); }); it('toObject', async () => { expect(new FabricText('text').toObject()).toMatchObjectSnapshot(); }); it('fromObject', async () => { expect((await FabricText.fromObject({ text: 'text' })).toObject()).toEqual( new FabricText('text').toObject(), ); }); describe('measuring, splitting', () => { it('measuring', () => { cache.clearFontCache(); const zwc = '\u200b'; const text = new FabricText(''); const style = text.getCompleteStyleDeclaration(0, 0); const measurement = text._measureChar('a', style, zwc, style); expect(measurement).toMatchSnapshot(roundSnapshotOptions); expect(measurement).toEqual(text._measureChar('a', style, zwc, style)); }); it('splits into lines', () => { const text = new FabricText('test foo bar-baz\nqux'); expect(text._splitTextIntoLines(text.text)).toMatchSnapshot(); }); }); it('toSVG with NUM_FRACTION_DIGITS', async () => { const text = await FabricText.fromObject({ left: 60.5, top: 23.1, text: 'xxxxxx', styles: [ { fill: 'red' }, { fill: 'blue' }, { fill: 'green' }, { fill: 'yellow' }, { fill: 'pink' }, ].map((style, index) => ({ style, start: index, end: index + 1 })), }); config.configure({ NUM_FRACTION_DIGITS: 1 }); expect(text.toSVG()).toMatchSVGSnapshot(); config.configure({ NUM_FRACTION_DIGITS: 3 }); expect(text.toSVG()).toMatchSVGSnapshot(); }); it('toSVG with a path', async () => { const path = new Path('M 10 10 H 50 V 60', { fill: '', stroke: 'red' }); const text = new FabricText( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', { scaleX: 2, scaleY: 2, left: 1061, top: 46.2 }, ); const plainSvg = text.toSVG(); text.path = path; const svg = text.toSVG(); expect(text.toSVG()).toMatchSVGSnapshot(); expect(svg.includes(plainSvg)).toBe(false); }); it('subscript/superscript', async () => { const text = await FabricText.fromObject({ text: 'xxxxxx', styles: [ { stroke: 'black', fill: 'blue' }, { fill: 'blue' }, { fontSize: 4, deltaY: 20 }, { stroke: 'black', fill: 'blue' }, { fill: 'blue' }, { fontSize: 4, deltaY: 20 }, ].map((style, index) => ({ style, start: index, end: index + 1 })), }); text.setSuperscript(1, 2); text.setSuperscript(2, 3); text.setSubscript(3, 4); text.setSubscript(4, 5); expect(text.toObject().styles).toMatchSnapshot(); }); it('constructor', () => { expect(Text, 'Text class should exist').toBeTruthy(); const text = createTextObject(); expect(text, 'Text instance should be created').toBeTruthy(); expect(text, 'should be instance of FabricText').toBeInstanceOf(FabricText); expect(text, 'should be instance of FabricObject').toBeInstanceOf( FabricObject, ); expect(text.constructor, 'type should be Text').toHaveProperty( 'type', 'Text', ); expect(text.get('text'), 'text should be "x"').toBe('x'); }); it('toString', () => { const text = createTextObject(); expect(text.toString, 'toString should be a function').toBeTypeOf( 'function', ); expect(text.toString(), 'toString should return expected string').toBe( '#<Text (1): { "text": "x", "fontFamily": "Times New Roman" }>', ); }); it('_getFontDeclaration', () => { const text = createTextObject(); expect( text._getFontDeclaration, 'has a private method _getFontDeclaration', ).toBeTypeOf('function'); let fontDecl = text._getFontDeclaration(); expect(fontDecl, 'it returns a string').toBeTypeOf('string'); expect(fontDecl, 'default font declaration').toBe( 'normal normal 40px "Times New Roman"', ); text.fontFamily = '"Times New Roman"'; fontDecl = text._getFontDeclaration(); expect(fontDecl, 'font declaration with double quotes').toBe( 'normal normal 40px "Times New Roman"', ); text.fontFamily = "'Times New Roman'"; fontDecl = text._getFontDeclaration(); expect(fontDecl, 'font declaration with single quotes').toBe( "normal normal 40px 'Times New Roman'", ); fontDecl = text._getFontDeclaration({ fontFamily: 'Arial' }); expect(fontDecl, 'passed style should take precedence').toBe( 'normal normal 40px "Arial"', ); }); it('_getFontDeclaration with coma', () => { const text = createTextObject(); text.fontFamily = 'Arial, sans-serif'; const fontDecl = text._getFontDeclaration(); expect(fontDecl, 'if multiple font name detected no quotes added').toBe( 'normal normal 40px Arial, sans-serif', ); }); it.each(FabricText.genericFonts)( '_getFontDeclaration with genericFont: %s', (fontName) => { const text = createTextObject(); text.fontFamily = fontName; let fontDecl = text._getFontDeclaration(); expect(fontDecl, 'it does not quote genericFont').toBe( 'normal normal 40px ' + fontName, ); text.fontFamily = fontName.toUpperCase(); fontDecl = text._getFontDeclaration(); expect(fontDecl, 'it uses a non case sensitive logic').toBe( 'normal normal 40px ' + fontName.toUpperCase(), ); }, ); it('complexity', () => { const text = createTextObject(); expect(text.complexity, 'complexity should be a function').toBeTypeOf( 'function', ); expect(text.complexity(), 'complexity should be 1').toBe(1); }); it('set', () => { const text = createTextObject(); expect(text.set, 'set should be a function').toBeTypeOf('function'); expect(text.set('text', 'bar'), 'should be chainable').toBe(text); text.set({ left: 1234, top: 2345, angle: 55 }); expect(text.get('left'), 'left should be set').toBe(1234); expect(text.get('top'), 'top should be set').toBe(2345); expect(text.get('angle'), 'angle should be set').toBe(55); }); it('lineHeight with single line', () => { const text = createTextObject(); text.text = 'text with one line'; text.lineHeight = 2; text.initDimensions(); const height = text.height; text.lineHeight = 0.5; text.initDimensions(); const heightNew = text.height; expect(height, 'text height does not change with one single line').toBe( heightNew, ); }); it('lineHeight with multi line', () => { const text = createTextObject(); text.text = 'text with\ntwo lines'; text.lineHeight = 0.1; text.initDimensions(); const height = text.height; const minimumHeight = text.fontSize * text._fontSizeMult; expect( height > minimumHeight, 'text height is always bigger than minimum Height', ).toBe(true); }); it('set with "hash"', () => { const text = createTextObject(); text.set({ opacity: 0.123, fill: 'red', fontFamily: 'blah' }); expect(text.opacity, 'opacity should be set').toBe(0.123); expect(text.fill, 'fill should be set').toBe('red'); expect(text.fontFamily, 'fontFamily should be set').toBe('blah'); }); it('get bounding rect after init', () => { const string = 'Some long text, the quick brown fox jumps over the lazy dog etc... blah blah blah'; const text = new FabricText(string, { left: 30, top: 30, fill: '#ffffff', fontSize: 24, fontWeight: 'normal', fontFamily: 'Arial', originY: 'bottom', }); const br = text.getBoundingRect(); text.setCoords(); const br2 = text.getBoundingRect(); expect( br, 'text bounding box is the same before and after calling setCoords', ).toEqual(br2); }); it('Text.fromElement', async () => { config.configure({ NUM_FRACTION_DIGITS: 2 }); expect( FabricText.fromElement, 'fromElement should be a function', ).toBeTypeOf('function'); const elText = getFabricDocument().createElement('text'); elText.textContent = 'x'; const text = await FabricText.fromElement(elText); expect(text, 'should be instance of FabricText').toBeInstanceOf(FabricText); const expectedObject = { ...REFERENCE_TEXT_OBJECT, left: 0, top: -14.05, width: 8, height: 18.08, fontSize: 16, originX: 'center', }; expect(text.toObject(), 'parsed object is what expected').toEqual( expectedObject, ); }); it('fromElement with custom attributes', async () => { config.configure({ NUM_FRACTION_DIGITS: 2 }); const namespace = 'http://www.w3.org/2000/svg'; const elTextWithAttrs = getFabricDocument().createElementNS( namespace, 'text', ); elTextWithAttrs.textContent = 'x'; elTextWithAttrs.setAttributeNS(namespace, 'x', String(10)); elTextWithAttrs.setAttributeNS(namespace, 'y', String(20)); elTextWithAttrs.setAttributeNS(namespace, 'fill', 'rgb(255,255,255)'); elTextWithAttrs.setAttributeNS(namespace, 'opacity', String(0.45)); elTextWithAttrs.setAttributeNS(namespace, 'stroke', 'blue'); elTextWithAttrs.setAttributeNS(namespace, 'stroke-width', String(3)); elTextWithAttrs.setAttributeNS(namespace, 'stroke-dasharray', '5, 2'); elTextWithAttrs.setAttributeNS(namespace, 'stroke-linecap', 'round'); elTextWithAttrs.setAttributeNS(namespace, 'stroke-linejoin', 'bevel'); elTextWithAttrs.setAttributeNS(namespace, 'stroke-miterlimit', String(5)); elTextWithAttrs.setAttributeNS(namespace, 'font-family', 'Monaco'); elTextWithAttrs.setAttributeNS(namespace, 'font-style', 'italic'); elTextWithAttrs.setAttributeNS(namespace, 'font-weight', 'bold'); elTextWithAttrs.setAttributeNS(namespace, 'font-size', '123'); elTextWithAttrs.setAttributeNS(namespace, 'letter-spacing', '1em'); elTextWithAttrs.setAttributeNS(namespace, 'text-decoration', 'underline'); elTextWithAttrs.setAttributeNS(namespace, 'text-anchor', 'middle'); const textWithAttrs = await FabricText.fromElement(elTextWithAttrs); // temp workaround for text objects not obtaining width under node textWithAttrs.width = CHAR_WIDTH; expect(textWithAttrs, 'should be instance of FabricText').toBeInstanceOf( FabricText, ); const expectedObject = { ...REFERENCE_TEXT_OBJECT, /* left varies slightly due to node-canvas rendering */ left: toFixed(textWithAttrs.left + '', 2), top: -88.03, width: CHAR_WIDTH, height: 138.99, fill: 'rgb(255,255,255)', opacity: 0.45, stroke: 'blue', strokeWidth: 3, strokeDashArray: [5, 2], strokeLineCap: 'round', strokeLineJoin: 'bevel', strokeMiterLimit: 5, fontFamily: 'Monaco', paintFirst: 'fill', fontStyle: 'italic', charSpacing: 1000, fontWeight: 'bold', fontSize: 123, underline: true, }; expect( textWithAttrs.toObject(), 'textWithAttrs should match expected object', ).toEqual(expectedObject); }); it('dimensions after text change', () => { const text = new FabricText('x'); expect(text.width, 'initial width should be CHAR_WIDTH').toBe(CHAR_WIDTH); text.set('text', 'xx'); expect(text.width, 'width after change should be 2 * CHAR_WIDTH').toBe( CHAR_WIDTH * 2, ); }); it('dimensions without text', () => { const text = new FabricText(''); expect(text.width, 'width should be 2').toBe(2); }); it('setting fontFamily', () => { const text = new FabricText('x'); text.set('fontFamily', 'foobar'); expect(text.get('fontFamily'), 'fontFamily should be foobar').toBe( 'foobar', ); text.set('fontFamily', '"Arial Black", Arial'); expect( text.get('fontFamily'), 'fontFamily with quotes should be preserved', ).toBe('"Arial Black", Arial'); }); it('text styleHas', () => { const text = new FabricText('xxxxxx\nx y'); text.styles = {}; expect(text.styleHas, 'styleHas should be a function').toBeTypeOf( 'function', ); expect(text.styleHas('stroke'), 'the text style has no stroke').toBe(false); text.styles = { 1: { 0: { stroke: 'red' } } }; expect(text.styleHas('stroke'), 'the text style has stroke').toBe(true); }); it('text cleanStyle', () => { const text = new FabricText('xxxxxx\nx y'); text.styles = { 1: { 0: { stroke: 'red' } } }; text.stroke = 'red'; expect(text.cleanStyle, 'cleanStyle should be a function').toBeTypeOf( 'function', ); text.cleanStyle('stroke'); expect( text.styles[1], 'the style has been cleaned since stroke was equal to text property', ).toBe(undefined); text.styles = { 1: { 0: { stroke: 'blue' } } }; text.stroke = 'red'; text.cleanStyle('stroke'); expect(text.styles[1][0].stroke, 'nothing to clean, style untouched').toBe( 'blue', ); }); it('text cleanStyle with different sub styles styles', () => { const text = new FabricText('xxxxxx\nx y'); text.styles = { 1: { 0: { fill: 'red' }, 1: { stroke: 'red' }, 2: { stroke: 'blue' } }, }; text.stroke = 'red'; text.cleanStyle('stroke'); expect(text.stroke, 'the stroke stays red').toBe('red'); expect( text.styles[1][0].fill, "the style has not been changed since it's a different property", ).toBe('red'); expect( text.styles[1][0].stroke, 'the style has been cleaned since stroke was equal to text property', ).toBe(undefined); expect(text.styles[1][1], 'the style remains undefined').toBe(undefined); expect(text.styles[1][2].stroke, 'the style remains unchanged').toBe( 'blue', ); }); it('text cleanStyle with undefined and set styles', () => { const text = new FabricText('xxxxxx\nx y'); text.styles = { 1: { 1: { stroke: 'red' }, 3: { stroke: 'red' } } }; text.stroke = 'red'; text.cleanStyle('stroke'); expect(text.stroke, 'the stroke stays red').toBe('red'); expect( text.styles[1], 'the style has been cleaned since stroke was equal to text property', ).toBe(undefined); }); it('text cleanStyle with empty styles', () => { const text = new FabricText('xxxxxx\nx y'); text.styles = { 1: { 0: {}, 1: {} }, 2: {}, 3: { 4: {} } }; // @ts-expect-error -- intentional text.cleanStyle('any'); expect( text.styles[1], 'the style has been cleaned since there was no useful information', ).toBe(undefined); expect( text.styles[2], 'the style has been cleaned since there was no useful information', ).toBe(undefined); expect( text.styles[3], 'the style has been cleaned since there was no useful information', ).toBe(undefined); }); it('text cleanStyle with full style', () => { const text = new FabricText('xxx'); text.styles = { 0: { 0: { fill: 'blue' }, 1: { fill: 'blue' }, 2: { fill: 'blue' } }, }; text.fill = 'black'; text.cleanStyle('fill'); expect(text.fill, 'the fill has been changed to blue').toBe('blue'); expect(text.styles[0], 'all the style has been removed').toBe(undefined); }); it('text cleanStyle with no relevant style', () => { const text = new FabricText('xxx'); text.styles = { 0: { // @ts-expect-error -- dummy value 0: { other: 'value1' }, // @ts-expect-error -- dummy value 1: { other: 'value2' }, // @ts-expect-error -- dummy value 2: { other: 'value3' }, }, }; text.fill = 'black'; text.cleanStyle('fill'); expect(text.fill, 'the fill remains black').toBe('black'); expect(text.styles[0][0], 'style remains the same').toHaveProperty( 'other', 'value1', ); expect(text.styles[0][0], 'style remains undefined').not.toHaveProperty( 'full', ); expect(text.styles[0][1], 'style remains the same').toHaveProperty( 'other', 'value2', ); expect(text.styles[0][1], 'style remains undefined').not.toHaveProperty( 'full', ); expect(text.styles[0][2], 'style remains the same').toHaveProperty( 'other', 'value3', ); expect(text.styles[0][2], 'style remains undefined').not.toHaveProperty( 'full', ); }); it('text removeStyle with some style', () => { const text = new FabricText('xxx'); text.styles = { 0: { 0: { stroke: 'black', fill: 'blue' }, 1: { fill: 'blue' }, 2: { fill: 'blue' }, }, }; expect(text.removeStyle, 'removeStyle should be a function').toBeTypeOf( 'function', ); text.fill = 'red'; text.removeStyle('fill'); expect(text.fill, 'the fill has not been changed').toBe('red'); expect( text.styles[0][0].stroke, 'the non fill part of the style is still there', ).toBe('black'); expect( text.styles[0][0].fill, 'the fill part of the style has been removed', ).toBe(undefined); text.styles = { 0: { 0: { fill: 'blue' }, 1: { fill: 'blue' }, 2: { fill: 'blue' } }, }; text.removeStyle('fill'); expect(text.styles[0], 'the styles got empty and has been removed').toBe( undefined, ); }); it('text toObject removes empty style object', () => { const text = new FabricText('xxx'); text.styles = { 0: { 0: {} } }; const obj = text.toObject(); expect(obj.styles, 'empty style object has been removed').toEqual([]); }); it('text toObject can handle style objects with only a textBackgroundColor property', () => { const text = new FabricText('xxx'); text.styles = { 0: { 0: { textBackgroundColor: 'blue' } } }; const obj = text.toObject(); expect( obj.styles, 'styles with only a textBackgroundColor property do not throw an error', ).toEqual([{ start: 0, end: 1, style: { textBackgroundColor: 'blue' } }]); }); it('getFontCache works with fontWeight numbers', () => { const text = new FabricText('xxx', { fontWeight: 400 }); text.initDimensions(); const charCache = cache.charWidthsCache.get(text.fontFamily.toLowerCase()); const cacheProp = text.fontStyle + '_400'; expect( charCache && charCache.has(cacheProp), '400 is converted to string', ).toBe(true); }); it('getFontCache is case insensitive', () => { const text = new FabricText('xxx', { fontWeight: 'BOld', fontStyle: 'NormaL', }); const text2 = new FabricText('xxx', { fontWeight: 'bOLd', fontStyle: 'nORMAl', }); text.initDimensions(); text2.initDimensions(); const fothCache = cache.getFontCache(text); const cache2 = cache.getFontCache(text2); expect(fothCache, 'you get the same cache').toBe(cache2); }); it('getSelectionStyles with no arguments', () => { const iText = new FabricText('test foo bar-baz\nqux', { styles: { 0: { // @ts-expect-error -- typescript does not recognize textDecoration prop 0: { textDecoration: 'underline' }, // @ts-expect-error -- typescript does not recognize textDecoration prop 2: { textDecoration: 'overline' }, 4: { textBackgroundColor: '#ffc' }, }, 1: { 0: { fill: 'red' }, 1: { fill: 'green' }, 2: { fill: 'blue' }, }, }, }); expect( iText.getSelectionStyles, 'getSelectionStyles should be a function', ).toBeTypeOf('function'); expect( iText.getSelectionStyles(0), 'should return empty array with no selection', ).toEqual([]); }); it('getSelectionStyles with 2 args', () => { const iText = new FabricText('test foo bar-baz\nqux', { styles: { 0: { // @ts-expect-error -- typescript does not recognize textDecoration prop 0: { textDecoration: 'underline' }, // @ts-expect-error -- typescript does not recognize textDecoration prop 2: { textDecoration: 'overline' }, 4: { textBackgroundColor: '#ffc' }, }, 1: { 0: { fill: 'red' }, 1: { fill: 'green' }, 2: { fill: 'blue' }, }, }, }); expect( iText.getSelectionStyles(0, 5), 'should return correct styles', ).toEqual([ { textDecoration: 'underline' }, {}, { textDecoration: 'overline' }, {}, { textBackgroundColor: '#ffc' }, ]); expect( iText.getSelectionStyles(2, 2), 'should return empty array when no styles defined', ).toEqual([]); }); it('setSelectionStyles', () => { const iText = new FabricText('test foo bar-baz\nqux', { styles: { 0: { 0: { fill: '#112233' }, 2: { stroke: '#223344' }, }, }, }); expect( iText.setSelectionStyles, 'setSelectionStyles should be a function', ).toBeTypeOf('function'); iText.setSelectionStyles( { fill: 'red', stroke: 'yellow', }, 0, ); expect( iText.styles[0][0], 'styles should not be changed without selection', ).toEqual({ fill: '#112233', }); iText.setSelectionStyles( { fill: 'red', stroke: 'yellow', }, 0, 1, ); expect( iText.styles[0][0], 'styles should be changed with selection', ).toEqual({ fill: 'red', stroke: 'yellow', }); iText.setSelectionStyles( { fill: '#998877', stroke: 'yellow', }, 2, 3, ); expect( iText.styles[0][2], 'styles should be changed with another selection', ).toEqual({ fill: '#998877', stroke: 'yellow', }); }); it('getStyleAtPosition', () => { const iText = new FabricText('test foo bar-baz\nqux', { styles: { 0: { // @ts-expect-error -- typescript does not recognize textDecoration prop 0: { textDecoration: 'underline' }, // @ts-expect-error -- typescript does not recognize textDecoration prop 2: { textDecoration: 'overline' }, 4: { textBackgroundColor: '#ffc' }, }, 1: { 0: { fill: 'red' }, 1: { fill: 'green' }, 2: { fill: 'blue' }, }, }, }); expect( iText.getStyleAtPosition, 'getStyleAtPosition should be a function', ).toBeTypeOf('function'); expect( iText.getStyleAtPosition(2), 'should return style at position 2', ).toEqual({ textDecoration: 'overline' }); expect( iText.getStyleAtPosition(1), 'should return empty object at position 1', ).toEqual({}); expect( iText.getStyleAtPosition(18), 'should return style at position 18', ).toEqual({ fill: 'green' }); }); it('getStyleAtPosition complete', () => { const iText = new FabricText('test foo bar-baz\nqux', { styles: { 0: { 0: { underline: true }, 2: { overline: true }, 4: { textBackgroundColor: '#ffc' }, }, 1: { 0: { fill: 'red' }, 1: { fill: 'green' }, 2: { fill: 'blue' }, }, }, }); const expectedStyle0 = { stroke: null, strokeWidth: 1, fill: 'rgb(0,0,0)', fontFamily: 'Times New Roman', fontSize: 40, fontWeight: 'normal', fontStyle: 'normal', underline: true, overline: false, linethrough: false, textBackgroundColor: '', deltaY: 0, textDecorationThickness: 66.667, }; const expectedStyle2 = { stroke: null, strokeWidth: 1, fill: 'rgb(0,0,0)', fontFamily: 'Times New Roman', fontSize: 40, fontWeight: 'normal', fontStyle: 'normal', underline: false, overline: true, linethrough: false, textBackgroundColor: '', deltaY: 0, textDecorationThickness: 66.667, }; expect( iText.getStyleAtPosition, 'getStyleAtPosition should be a function', ).toBeTypeOf('function'); expect(iText.getStyleAtPosition(0, true), 'styles do match at 0').toEqual( expectedStyle0, ); expect(iText.getStyleAtPosition(2, true), 'styles do match at 2').toEqual( expectedStyle2, ); }); it('getSvgSpanStyles produces correct output', () => { const iText = new IText('test foo bar-baz'); const styleObject = { fill: 'red', strokeWidth: 30, fontFamily: 'Verdana', fontSize: 25, }; // @ts-expect-error -- TODO: this is added by the mixing, can the types be improved here? const styleString = iText.getSvgSpanStyles(styleObject); const expected = "stroke-width: 30; font-family: 'Verdana'; font-size: 25px; fill: rgb(255,0,0); "; expect(styleString, 'style is as expected').toBe(expected); }); it('getSvgSpanStyles produces correct output with useWhiteSpace', () => { const iText = new IText('test foo bar-baz'); const styleObject = { fill: 'red', strokeWidth: 30, fontFamily: 'Verdana', fontSize: 25, }; // @ts-expect-error -- TODO: this is added by the mixing, can the types be improved here? const styleString = iText.getSvgSpanStyles(styleObject, true); const expected = "stroke-width: 30; font-family: 'Verdana'; font-size: 25px; fill: rgb(255,0,0); white-space: pre; "; expect(styleString, 'style is as expected').toBe(expected); }); it('getSvgTextDecoration with overline true produces correct output', () => { const iText = new IText('test foo bar-baz'); const styleObject = { overline: true, }; // @ts-expect-error -- TODO: this is added by the mixing, can the types be improved here? const styleString = iText.getSvgTextDecoration(styleObject); const expected = 'overline'; expect(styleString, 'style is as expected').toBe(expected); }); it('getSvgTextDecoration with overline underline true produces correct output', () => { const iText = new IText('test foo bar-baz'); const styleObject = { overline: true, underline: true, }; // @ts-expect-error -- TODO: this is added by the mixing, can the types be improved here? const styleString = iText.getSvgTextDecoration(styleObject); const expected = 'overline underline'; expect(styleString, 'style is as expected with overline underline').toBe( expected, ); }); it('getSvgTextDecoration with overline underline linethrough true produces correct output', () => { const iText = new IText('test foo bar-baz'); const styleObject = { overline: true, underline: true, linethrough: true, }; // @ts-expect-error -- TODO: this is added by the mixing, can the types be improved here? const styleString = iText.getSvgTextDecoration(styleObject); const expected = 'overline underline line-through'; expect(styleString, 'style is as expected with all decorations').toBe( expected, ); }); it('getHeightOfLine measures height of a line', () => { const text = new FabricText('xxx\n'); const height1 = text.getHeightOfLine(0); const height2 = text.getHeightOfLine(1); expect(Math.round(height1), 'height of line with text').toBe(52); expect(Math.round(height2), 'height of empty line').toBe(52); expect(height1, 'both lines should have same height').toBe(height2); }); it('_measureChar handles 0 width chars', () => { cache.clearFontCache(); const zwc = '\u200b'; const text = new FabricText(''); const style = text.getCompleteStyleDeclaration(0, 0); const box = text._measureChar('a', style, zwc, style); const box2 = text._measureChar('a', style, zwc, style); expect( cache.charWidthsCache .get(text.fontFamily.toLowerCase()) ?.get('normal_normal') ?.get(zwc), 'zwc is a 0 width char', ).toBe(0); expect(box.kernedWidth, 'measurements should be consistent').toBe( box2.kernedWidth, ); }); it('_deleteStyleDeclaration', () => { const text = new FabricText('aaa aaq ggg gg oee eee', { styles: { 0: { 0: { fontSize: 4 }, 1: { fontSize: 4 }, 2: { fontSize: 4 }, 3: { fontSize: 4 }, 4: { fontSize: 4 }, 5: { fontSize: 4 }, 6: { fontSize: 4 }, 7: { fontSize: 4 }, 8: { fontSize: 4 }, 9: { fontSize: 4 }, 10: { fontSize: 4 }, 11: { fontSize: 4 }, 12: { fontSize: 4 }, 13: { fontSize: 4 }, 14: { fontSize: 4 }, 15: { fontSize: 4 }, 16: { fontSize: 4 }, }, }, width: 5, }); // @ts-expect-error -- protected member text._deleteStyleDeclaration(0, 10); expect(text.styles[0][10], 'style has been removed').toBe(undefined); }); it('_setStyleDeclaration', () => { const text = new FabricText('aaa aaq ggg gg oee eee', { styles: { 0: { 0: { fontSize: 4 }, 1: { fontSize: 4 }, 2: { fontSize: 4 }, 3: { fontSize: 4 }, 4: { fontSize: 4 }, 5: { fontSize: 4 }, 6: { fontSize: 4 }, 7: { fontSize: 4 }, 8: { fontSize: 4 }, 9: { fontSize: 4 }, 10: { fontSize: 4 }, 11: { fontSize: 4 }, 12: { fontSize: 4 }, 13: { fontSize: 4 }, 14: { fontSize: 4 }, 15: { fontSize: 4 }, 16: { fontSize: 4 }, }, }, width: 5, }); // @ts-expect-error -- protected member expect(text._setStyleDeclaration, 'function exists').toBeTypeOf('function'); const newStyle = { fontSize: 10 }; // @ts-expect-error -- protected member text._setStyleDeclaration(0, 10, newStyle); expect(text.styles[0][10], 'style has been changed').toBe(newStyle); }); it('styleHas', () => { const textbox = new Textbox('aaa\naaq ggg gg oee eee', { styles: { 0: { 0: { fontSize: 4 }, 1: { fontSize: 4 }, 2: { fontSize: 4 }, }, 1: { 0: { fontFamily: 'Arial' }, 1: { fontFamily: 'Arial' }, 2: { fontFamily: 'Arial' }, }, }, width: 5, }); // @ts-expect-error -- TODO: should lineIndex be made an optional argument? test fails if it is provided but it is mandatory according to the types expect(textbox.styleHas('fontSize'), 'style has fontSize').toBe(true); expect( textbox.styleHas('fontSize', 0), 'style has fontSize on line 0', ).toBe(true); expect( textbox.styleHas('fontSize', 1), 'style does not have fontSize on line 1', ).toBe(false); // @ts-expect-error -- TODO: should lineIndex be made an optional argument? test fails if it is provided but it is mandatory according to the types expect(textbox.styleHas('fontFamily'), 'style has fontFamily').toBe(true); expect( textbox.styleHas('fontFamily', 0), 'style does not have fontFamily on line 0', ).toBe(false); expect( textbox.styleHas('fontFamily', 1), 'style has fontFamily on line 1', ).toBe(true); }); it('text with a path', () => { const text = new FabricText('a', { path: new Path('M0 0 h 100 v 100 h -100 z'), }); expect(text.path, 'text has a path').toBeTruthy(); expect( text.path!.segmentsInfo, 'text has segmentsInfo calculated', ).toBeTruthy(); expect(text.width, 'text width equals path width').toBe(100); expect(text.height, 'text height equals path height').toBe(100); }); it('text with a path toObject', () => { const text = new FabricText('a', { path: new Path('M0 0 h 100 v 100 h -100 z'), }); const toObject = text.toObject(); expect(toObject.path, 'export has a path').toBeTruthy(); }); it('text with a path fromObject', async () => { const text = new FabricText('a', { path: new Path('M0 0 h 100 v 100 h -100 z'), }); const toObject = text.toObject(); const text2 = await FabricText.fromObject(toObject); expect(text2.path!.constructor, 'the path is restored').toHaveProperty( 'type', 'Path', ); expect(text2.path, 'the path is a path').toBeInstanceOf(Path); expect(toObject.path, 'the input has still a path property').toBeTruthy(); }); it('cacheProperties for text', () => { expect( FabricText.cacheProperties.join('-'), 'cache properties include text-specific ones', ).toBe( 'fill-stroke-strokeWidth-strokeDashArray-width-height-paintFirst-strokeUniform-strokeLineCap-strokeDashOffset-strokeLineJoin-strokeMiterLimit-backgroundColor-clipPath-fontSize-fontWeight-fontFamily-fontStyle-lineHeight-text-charSpacing-textAlign-styles-path-pathStartOffset-pathSide-pathAlign-underline-overline-linethrough-textBackgroundColor-direction-textDecorationThickness', ); }); it('_getLineLeftOffset', () => { const text = new FabricText('long line of text\nshort'); expect(text._getLineLeftOffset(1), 'with align left is 0').toBe(0); text.textAlign = 'right'; expect( Math.round(text._getLineLeftOffset(1)), 'with align right is diff between width and lineWidth', ).toBe(174); text.textAlign = 'center'; expect( Math.round(text._getLineLeftOffset(1)), 'with align center is split in 2', ).toBe(87); text.textAlign = 'justify'; expect(text._getLineLeftOffset(1), 'with align justify is 0').toBe(0); text.textAlign = 'justify-center'; expect( text._getLineLeftOffset(0), 'is zero for any line but not the last center', ).toBe(0); expect(Math.round(text._getLineLeftOffset(1)), 'like align center').toBe( 87, ); text.textAlign = 'justify-left'; expect( text._getLineLeftOffset(0), 'is zero for any line but not the last left', ).toBe(0); expect(text._getLineLeftOffset(1), 'like align left').toBe(0); text.textAlign = 'justify-right'; expect( text._getLineLeftOffset(0), 'is zero for any line but not the last right', ).toBe(0); expect(Math.round(text._getLineLeftOffset(1)), 'like align right').toBe( 174, ); }); it('_getLineLeftOffset with direction rtl', () => { const text = new FabricText('long line of text\nshort'); text.direction = 'rtl'; expect( Math.round(text._getLineLeftOffset(1)), 'with align left is diff between width and lineWidth, negative', ).toBe(-174); text.textAlign = 'right'; expect(text._getLineLeftOffset(1), 'with align right is 0').toBe(0); text.textAlign = 'center'; expect( Math.round(text._getLineLeftOffset(1)), 'with align center is split in 2', ).toBe(-87); text.textAlign = 'justify'; expect(text._getLineLeftOffset(1), 'with align justify is 0').toBe(0); text.textAlign = 'justify-center'; expect( text._getLineLeftOffset(0), 'is zero for any line but not the last center', ).toBe(0); expect(Math.round(text._getLineLeftOffset(1)), 'like align center').toBe( -87, ); text.textAlign = 'justify-left'; expect( text._getLineLeftOffset(0), 'is zero for any line but not the last left', ).toBe(0); expect( Math.round(text._getLineLeftOffset(1)), 'like align left with rtl', ).toBe(-174); text.textAlign = 'justify-right'; expect( text._getLineLeftOffset(0), 'is zero for any line but not the last right', ).toBe(0); expect(text._getLineLeftOffset(1), 'like align right with rtl').toBe(0); }); describe('measuring, splitting', () => { it('measuring a single char', () => { cache.clearFontCache(); const text = new FabricText(''); const style = text.getCompleteStyleDeclaration(0, 0); const measurement1 = text._measureChar('a', style, '', style); const measurement2 = text._measureChar('a', style, '', style); expect(measurement1).toEqual(measurement2); const cacheKeys = cache.charWidthsCache .get(text.fontFamily.toLowerCase()) ?.get('normal_normal') ?.keys(); expect(cacheKeys?.next().value).not.toBe('undefineda'); expect(cacheKeys?.next().done).toBe(true); }); }); });