fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
1,227 lines (1,046 loc) • 34.5 kB
text/typescript
import { Canvas } from '../../canvas/Canvas';
import { Group } from '../Group';
import { IText } from './IText';
import { Point } from '../../Point';
import {
describe,
expect,
vi,
beforeAll,
afterAll,
beforeEach,
afterEach,
it,
} from 'vitest';
import { FabricText } from '../Text/Text';
import { version } from '../../../package.json';
import { getFabricWindow } from '../../env';
import { config } from '../../config';
import { LEFT, LTR } from '../../constants';
const ITEXT_OBJECT = {
version: version,
type: 'IText',
originX: 'left' as const,
originY: 'top' as const,
left: 0,
top: 0,
width: 20,
height: 45.2,
fill: 'rgb(0,0,0)',
stroke: null,
strokeWidth: 1,
strokeDashArray: null,
strokeLineCap: 'butt' as const,
strokeDashOffset: 0,
strokeLineJoin: 'miter' as const,
strokeMiterLimit: 4,
scaleX: 1,
scaleY: 1,
angle: 0,
flipX: false,
flipY: false,
opacity: 1,
shadow: null,
visible: true,
text: 'x',
fontSize: 40,
fontWeight: 'normal',
fontFamily: 'Times New Roman',
fontStyle: 'normal',
lineHeight: 1.3,
underline: false,
overline: false,
linethrough: false,
textAlign: LEFT,
backgroundColor: '',
textBackgroundColor: '',
fillRule: 'nonzero' as const,
paintFirst: 'fill' as const,
globalCompositeOperation: 'source-over' as const,
skewX: 0,
skewY: 0,
charSpacing: 0,
styles: [],
strokeUniform: false,
path: undefined,
direction: LTR,
pathStartOffset: 0,
pathSide: LEFT,
pathAlign: 'baseline' as const,
textDecorationThickness: 66.67,
};
describe('IText', () => {
let canvas: Canvas;
beforeEach(() => {
canvas = new Canvas();
});
afterEach(() => {
canvas.clear();
canvas.cancelRequestedRender();
config.clearFonts();
});
describe('cursor drawing width', () => {
it.each([
{ scale: 1, zoom: 1, textScale: 1, angle: 0, textAngle: 0 },
{ scale: 1, zoom: 50, textScale: 2, angle: 0, textAngle: 0 },
{ scale: 200, zoom: 1, textScale: 2, angle: 45, textAngle: 0 },
{ scale: 200, zoom: 1, textScale: 1, angle: 0, textAngle: 0 },
{ scale: 200, zoom: 50, textScale: 1, angle: 30, textAngle: 30 },
{ scale: 200, zoom: 1 / 200, textScale: 1, angle: 0, textAngle: 0 },
{ scale: 200, zoom: 1 / 200, textScale: 2, angle: 0, textAngle: 90 },
])(
'group scaled by $scale and rotated by $angle , text scaled by $textScale and rotated by $textAngle, and canvas zoomed by $zoom',
({ scale, zoom, textScale, angle, textAngle }) => {
const text = new IText('testing', {
cursorWidth: 100,
angle: textAngle,
scaleX: textScale,
scaleY: textScale,
});
const group = new Group([text]);
group.set({ scaleX: scale, scaleY: scale, angle });
group.setCoords();
const fillRect = vi.fn();
const getZoom = vi.fn().mockReturnValue(zoom);
const mockContext = { fillRect };
const mockCanvas = { contextTop: mockContext, getZoom };
Object.assign(text, {
canvas: mockCanvas,
});
text.renderCursorAt(1);
const call = fillRect.mock.calls[0];
expect({ width: call[2], height: call[3] }).toMatchSnapshot({
cloneDeepWith: (value: unknown) =>
typeof value === 'number' ? value.toFixed(3) : undefined,
});
},
);
});
describe('Interaction with mouse and editing', () => {
it('_mouseDownHandlerBefore set up selected property', () => {
const iText = new IText('test need some word\nsecond line');
iText.canvas = new Canvas();
// @ts-expect-error -- protected member
expect(iText.selected).toBe(undefined);
// @ts-expect-error -- mock events
iText._mouseDownHandler({ e: { button: 0 }, alreadySelected: false });
// @ts-expect-error -- protected member
expect(iText.selected).toBe(false);
// @ts-expect-error -- mock events
iText._mouseDownHandler({ e: {}, alreadySelected: true });
// @ts-expect-error -- protected member
expect(iText.selected).toBe(true);
});
});
it('constructor', () => {
const iText = new IText('test');
expect(iText, 'should be instance of IText').toBeInstanceOf(IText);
expect(iText, 'should be instance of FabricText').toBeInstanceOf(
FabricText,
);
});
it('initial properties', () => {
const iText = new IText('test');
expect(iText, 'should be instance of IText').toBeInstanceOf(IText);
expect(iText.text, 'text should be set to test').toBe('test');
expect(
iText.constructor,
'constructor type should be IText',
).toHaveProperty('type', 'IText');
expect(iText.styles, 'styles should be empty object').toEqual({});
});
it('fromObject', async () => {
expect(IText.fromObject, 'fromObject should be a function').toBeTypeOf(
'function',
);
const iText = await IText.fromObject(ITEXT_OBJECT);
expect(iText, 'should be instance of IText').toBeInstanceOf(IText);
expect(iText.toObject(), 'object should match the reference').toEqual(
ITEXT_OBJECT,
);
});
it('lineHeight with single line', () => {
const text = new IText('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 = new IText('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',
).toBeTruthy();
});
it('toObject', () => {
const stylesObject = {
0: {
0: { fill: 'red' },
1: { textDecoration: 'underline' },
},
};
const stylesArray = [
{
start: 0,
end: 1,
style: { fill: 'red' },
},
{
start: 1,
end: 2,
style: { textDecoration: 'underline' },
},
];
const iText = new IText('test', {
// @ts-expect-error -- partial style
styles: stylesObject,
});
expect(iText.toObject, 'toObject should be a function').toBeTypeOf(
'function',
);
const obj = iText.toObject();
expect(
obj.styles,
'object styles array should match expected styles',
).toEqual(stylesArray);
expect(obj.styles[0], 'object styles should be a clone').not.toBe(
stylesArray[0],
);
expect(obj.styles[1], 'object styles should be a clone').not.toBe(
stylesArray[1],
);
expect(obj.styles[0].style, 'object style should be a clone').not.toBe(
stylesArray[0].style,
);
expect(obj.styles[1].style, 'object style should be a clone').not.toBe(
stylesArray[1].style,
);
expect(obj.styles[0], 'object style should match reference').toEqual(
stylesArray[0],
);
expect(obj.styles[1], 'object style should match reference').toEqual(
stylesArray[1],
);
expect(obj.styles[0].style, 'object style should match reference').toEqual(
stylesArray[0].style,
);
expect(obj.styles[1].style, 'object style should match reference').toEqual(
stylesArray[1].style,
);
});
it('setSelectionStart', () => {
const iText = new IText('test');
expect(
iText.setSelectionStart,
'setSelectionStart should be a function',
).toBeTypeOf('function');
expect(iText.selectionStart, 'initial selectionStart should be 0').toBe(0);
iText.setSelectionStart(3);
expect(iText.selectionStart, 'selectionStart should be set to 3').toBe(3);
expect(iText.selectionEnd, 'selectionEnd should remain 0').toBe(0);
});
it('empty itext', () => {
const iText = new IText('');
expect(iText.width, 'width should equal cursorWidth').toBe(
iText.cursorWidth,
);
});
it('setSelectionEnd', () => {
const iText = new IText('test');
expect(
iText.setSelectionEnd,
'setSelectionEnd should be a function',
).toBeTypeOf('function');
expect(iText.selectionEnd, 'initial selectionEnd should be 0').toBe(0);
iText.setSelectionEnd(3);
expect(iText.selectionEnd, 'selectionEnd should be set to 3').toBe(3);
expect(iText.selectionStart, 'selectionStart should remain 0').toBe(0);
});
it('get2DCursorLocation', () => {
const iText = new IText('test\nfoo\nbarbaz');
let loc = iText.get2DCursorLocation();
expect(loc.lineIndex, 'initial line index should be 0').toBe(0);
expect(loc.charIndex, 'initial char index should be 0').toBe(0);
// 'tes|t'
iText.selectionStart = iText.selectionEnd = 3;
loc = iText.get2DCursorLocation();
expect(loc.lineIndex, 'line index for cursor position 3 should be 0').toBe(
0,
);
expect(loc.charIndex, 'char index for cursor position 3 should be 3').toBe(
3,
);
// test
// fo|o
iText.selectionStart = iText.selectionEnd = 7;
loc = iText.get2DCursorLocation();
expect(loc.lineIndex, 'line index for cursor position 7 should be 1').toBe(
1,
);
expect(loc.charIndex, 'char index for cursor position 7 should be 2').toBe(
2,
);
// test
// foo
// barba|z
iText.selectionStart = iText.selectionEnd = 14;
loc = iText.get2DCursorLocation();
expect(loc.lineIndex, 'line index for cursor position 14 should be 2').toBe(
2,
);
expect(loc.charIndex, 'char index for cursor position 14 should be 5').toBe(
5,
);
});
it('isEmptyStyles', () => {
let iText = new IText('test');
expect(
iText.isEmptyStyles(),
'styles should be empty initially',
).toBeTruthy();
iText = new IText('test', {
styles: {
0: {
0: {},
},
1: {
0: {},
1: {},
},
},
});
expect(
iText.isEmptyStyles(),
'styles with empty objects should be considered empty',
).toBeTruthy();
iText = new IText('test', {
styles: {
0: {
0: {},
},
1: {
0: { fill: 'red' },
1: {},
},
},
});
expect(
iText.isEmptyStyles(),
'styles with properties should not be considered empty',
).toBeFalsy();
});
it('selectAll', () => {
const iText = new IText('test');
iText.selectAll();
expect(
iText.selectionStart,
'selectionStart should be 0 after selectAll',
).toBe(0);
expect(iText.selectionEnd, 'selectionEnd should be 4 after selectAll').toBe(
4,
);
iText.selectionStart = 1;
iText.selectionEnd = 2;
iText.selectAll();
expect(
iText.selectionStart,
'selectionStart should be 0 after second selectAll',
).toBe(0);
expect(
iText.selectionEnd,
'selectionEnd should be 4 after second selectAll',
).toBe(4);
expect(iText.selectAll(), 'selectAll should be chainable').toBe(iText);
});
it('getSelectedText', () => {
const iText = new IText('test\nfoobarbaz');
iText.selectionStart = 1;
iText.selectionEnd = 10;
expect(iText.getSelectedText(), 'should return the selected text').toBe(
'est\nfooba',
);
iText.selectionStart = iText.selectionEnd = 3;
expect(
iText.getSelectedText(),
'should return empty string when selection is collapsed',
).toBe('');
});
it('enterEditing, exitEditing', () => {
const iText = new IText('test');
expect(iText.enterEditing, 'enterEditing should be a function').toBeTypeOf(
'function',
);
expect(iText.exitEditing, 'exitEditing should be a function').toBeTypeOf(
'function',
);
expect(
iText.isEditing,
'should not be in editing mode initially',
).toBeFalsy();
iText.enterEditing();
expect(
iText.isEditing,
'should be in editing mode after enterEditing',
).toBeTruthy();
iText.exitEditing();
expect(
iText.isEditing,
'should not be in editing mode after exitEditing',
).toBeFalsy();
iText.abortCursorAnimation();
});
it('enterEditing, exitEditing and saved props', () => {
const iText = new IText('test');
const _savedProps = {
hasControls: iText.hasControls,
borderColor: iText.borderColor,
lockMovementX: iText.lockMovementX,
lockMovementY: iText.lockMovementY,
hoverCursor: iText.hoverCursor,
selectable: iText.selectable,
defaultCursor: iText.canvas && iText.canvas.defaultCursor,
moveCursor: iText.canvas && iText.canvas.moveCursor,
};
iText.enterEditing();
// @ts-expect-error -- protected member
expect(iText._savedProps, 'iText saves a copy of important props').toEqual(
_savedProps,
);
expect(iText.selectable, 'selectable is set to false').toBe(false);
expect(iText.hasControls, 'hasControls is set to false').toBe(false);
expect(iText.lockMovementX, 'lockMovementX is set to true').toBe(true);
expect(
// @ts-expect-error -- protected member
iText._savedProps!.lockMovementX,
'lockMovementX is set to false originally',
).toBe(false);
iText.set({ hasControls: true, lockMovementX: true });
expect(iText.hasControls, 'hasControls is still set to false').toBe(false);
expect(
// @ts-expect-error -- protected member
iText._savedProps!.lockMovementX,
'lockMovementX should have been set to true',
).toBe(true);
iText.exitEditing();
// @ts-expect-error -- protected member
expect(iText._savedProps, 'removed ref').toBeFalsy();
iText.abortCursorAnimation();
expect(iText.selectable, 'selectable is set back to true').toBe(true);
expect(iText.hasControls, 'hasControls is set back to true').toBe(true);
expect(
iText.lockMovementX,
'lockMovementX is set back to true, after changing saved props',
).toBe(true);
iText.selectable = false;
iText.enterEditing();
iText.exitEditing();
expect(iText.selectable, 'selectable is set back to initial value').toBe(
false,
);
iText.abortCursorAnimation();
});
it('event firing', () => {
const iText = new IText('test');
let enter = 0;
let exit = 0;
let modify = 0;
function countEnter() {
enter++;
}
function countExit() {
exit++;
}
function countModify() {
modify++;
}
iText.on('editing:entered', countEnter);
iText.on('editing:exited', countExit);
iText.on('modified', countModify);
expect(iText.enterEditing, 'enterEditing should be a function').toBeTypeOf(
'function',
);
expect(iText.exitEditing, 'exitEditing should be a function').toBeTypeOf(
'function',
);
iText.enterEditing();
expect(enter, 'editing:entered event should have fired once').toBe(1);
expect(exit, 'editing:exited event should not have fired').toBe(0);
expect(modify, 'modified event should not have fired').toBe(0);
iText.exitEditing();
expect(enter, 'editing:entered event count should remain 1').toBe(1);
expect(exit, 'editing:exited event should have fired once').toBe(1);
expect(modify, 'modified event should not have fired').toBe(0);
iText.enterEditing();
expect(enter, 'editing:entered event should have fired again').toBe(2);
expect(exit, 'editing:exited event count should remain 1').toBe(1);
expect(modify, 'modified event should not have fired').toBe(0);
iText.text = 'Test+';
iText.exitEditing();
expect(enter, 'editing:entered event count should remain 2').toBe(2);
expect(exit, 'editing:exited event should have fired again').toBe(2);
expect(modify, 'modified event should have fired once').toBe(1);
iText.abortCursorAnimation();
});
it('insertNewlineStyleObject', () => {
const iText = new IText('test\n2');
expect(
iText.insertNewlineStyleObject,
'insertNewlineStyleObject should be a function',
).toBeTypeOf('function');
iText.insertNewlineStyleObject(0, 4, 1);
expect(iText.styles, 'does not insert empty styles').toEqual({});
iText.styles = { 1: { 0: { fill: 'blue' } } };
iText.insertNewlineStyleObject(0, 4, 1);
expect(iText.styles, 'correctly shift styles').toEqual({
2: { 0: { fill: 'blue' } },
});
});
it('insertNewlineStyleObject with existing style', () => {
const iText = new IText('test\n2');
iText.styles = { 0: { 3: { fill: 'red' } }, 1: { 0: { fill: 'blue' } } };
iText.insertNewlineStyleObject(0, 4, 3);
expect(iText.styles[4], 'correctly shift styles 3 lines').toEqual({
0: { fill: 'blue' },
});
expect(iText.styles[3], 'correctly copied previous style line 3').toEqual({
0: { fill: 'red' },
});
expect(iText.styles[2], 'correctly copied previous style line 2').toEqual({
0: { fill: 'red' },
});
expect(iText.styles[1], 'correctly copied previous style line 1').toEqual({
0: { fill: 'red' },
});
});
it('shiftLineStyles', () => {
const iText = new IText('test\ntest\ntest', {
styles: {
1: {
0: { fill: 'red' },
1: { fill: 'red' },
2: { fill: 'red' },
3: { fill: 'red' },
},
},
});
expect(
iText.shiftLineStyles,
'shiftLineStyles should be a function',
).toBeTypeOf('function');
iText.shiftLineStyles(0, +1);
expect(iText.styles, 'styles should shift down one line').toEqual({
2: {
0: { fill: 'red' },
1: { fill: 'red' },
2: { fill: 'red' },
3: { fill: 'red' },
},
});
iText.shiftLineStyles(0, -1);
expect(iText.styles, 'styles should shift back to original line').toEqual({
1: {
0: { fill: 'red' },
1: { fill: 'red' },
2: { fill: 'red' },
3: { fill: 'red' },
},
});
});
it('selectWord', () => {
const iText = new IText('test foo bar-baz\n\nqux');
expect(iText.selectWord, 'selectWord should be a function').toBeTypeOf(
'function',
);
iText.selectWord(1);
expect(
iText.selectionStart,
'selection should start at beginning of word',
).toBe(0);
expect(iText.selectionEnd, 'selection should end at end of word').toBe(4);
iText.selectWord(7);
expect(
iText.selectionStart,
'selection should start at beginning of word',
).toBe(5);
expect(iText.selectionEnd, 'selection should end at end of word').toBe(8);
iText.selectWord(17);
expect(iText.selectionStart, 'selection should be on newline').toBe(17);
expect(iText.selectionEnd, 'selection should be on newline').toBe(17);
});
it('selectLine', () => {
const iText = new IText('test foo bar-baz\nqux');
expect(iText.selectLine, 'selectLine should be a function').toBeTypeOf(
'function',
);
iText.selectLine(6);
expect(
iText.selectionStart,
'selection should start at beginning of line',
).toBe(0);
expect(iText.selectionEnd, 'selection should end at end of line').toBe(16);
iText.selectLine(18);
expect(
iText.selectionStart,
'selection should start at beginning of line',
).toBe(17);
expect(iText.selectionEnd, 'selection should end at end of line').toBe(20);
});
it('findWordBoundaryLeft', () => {
const iText = new IText('test foo bar-baz\nqux');
expect(
iText.findWordBoundaryLeft,
'findWordBoundaryLeft should be a function',
).toBeTypeOf('function');
expect(
iText.findWordBoundaryLeft(3),
'boundary for position 3 (tes|t) should be 0',
).toBe(0);
expect(
iText.findWordBoundaryLeft(20),
'boundary for position 20 (qux|) should be 17',
).toBe(17);
expect(
iText.findWordBoundaryLeft(6),
'boundary for position 6 (f|oo) should be 5',
).toBe(5);
expect(
iText.findWordBoundaryLeft(11),
'boundary for position 11 (ba|r-baz) should be 9',
).toBe(9);
});
it('findWordBoundaryRight', () => {
const iText = new IText('test foo bar-baz\nqux');
expect(
iText.findWordBoundaryRight,
'findWordBoundaryRight should be a function',
).toBeTypeOf('function');
expect(
iText.findWordBoundaryRight(3),
'boundary for position 3 (tes|t) should be 4',
).toBe(4);
expect(
iText.findWordBoundaryRight(17),
'boundary for position 17 (|qux) should be 20',
).toBe(20);
expect(
iText.findWordBoundaryRight(6),
'boundary for position 6 (f|oo) should be 8',
).toBe(8);
expect(
iText.findWordBoundaryRight(11),
'boundary for position 11 (ba|r-baz) should be 16',
).toBe(16);
});
it('findLineBoundaryLeft', () => {
const iText = new IText('test foo bar-baz\nqux');
expect(
iText.findLineBoundaryLeft,
'findLineBoundaryLeft should be a function',
).toBeTypeOf('function');
expect(
iText.findLineBoundaryLeft(3),
'boundary for position 3 (tes|t) should be 0',
).toBe(0);
expect(
iText.findLineBoundaryLeft(20),
'boundary for position 20 (qux|) should be 17',
).toBe(17);
});
it('findLineBoundaryRight', () => {
const iText = new IText('test foo bar-baz\nqux');
expect(
iText.findLineBoundaryRight,
'findLineBoundaryRight should be a function',
).toBeTypeOf('function');
expect(
iText.findLineBoundaryRight(3),
'boundary for position 3 (tes|t) should be 16',
).toBe(16);
expect(
iText.findLineBoundaryRight(17),
'boundary for position 17 (|qux) should be 20',
).toBe(20);
});
it('getSelectionStyles with no arguments', () => {
const iText = new IText('test foo bar-baz\nqux', {
styles: {
0: {
// @ts-expect-error -- TODO: check if this is really unstandard prop?
0: { textDecoration: 'underline' },
// @ts-expect-error -- TODO: check if this is really unstandard 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');
iText.selectionStart = 0;
iText.selectionEnd = 0;
expect(
iText.getSelectionStyles(),
'should return empty array when no selection',
).toEqual([]);
iText.selectionStart = 2;
iText.selectionEnd = 3;
expect(
iText.getSelectionStyles(),
'should return styles for the selected character',
).toEqual([
{
textDecoration: 'overline',
},
]);
iText.selectionStart = 17;
iText.selectionEnd = 18;
expect(
iText.getSelectionStyles(),
'should return styles for character at position 17',
).toEqual([
{
fill: 'red',
},
]);
});
it('getSelectionStyles with 2 args', () => {
const iText = new IText('test foo bar-baz\nqux', {
styles: {
0: {
// @ts-expect-error -- TODO: check if this is really unstandard prop?
0: { textDecoration: 'underline' },
// @ts-expect-error -- TODO: check if this is really unstandard prop?
2: { textDecoration: 'overline' },
4: { textBackgroundColor: '#ffc' },
},
1: {
0: { fill: 'red' },
1: { fill: 'green' },
2: { fill: 'blue' },
},
},
});
expect(
iText.getSelectionStyles(0, 2),
'should return styles for positions 0 and 1',
).toEqual([{ textDecoration: 'underline' }, {}]);
});
it('setSelectionStyles', () => {
const iText = new IText('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',
});
expect(
iText.styles[0][0],
'styles should not change without selection',
).toEqual({
fill: '#112233',
});
iText.selectionEnd = 0;
iText.selectionEnd = 1;
iText.setSelectionStyles({
fill: 'red',
stroke: 'yellow',
});
expect(
iText.styles[0][0],
'styles should be applied to character at position 0',
).toEqual({
fill: 'red',
stroke: 'yellow',
});
iText.selectionStart = 2;
iText.selectionEnd = 3;
iText.setSelectionStyles({
fill: '#998877',
stroke: 'yellow',
});
expect(
iText.styles[0][2],
'styles should be applied to character at position 2',
).toEqual({
fill: '#998877',
stroke: 'yellow',
});
});
it('getCurrentCharFontSize', () => {
const iText = new IText('test foo bar-baz\nqux', {
styles: {
0: {
0: { fontSize: 20 },
1: { fontSize: 60 },
},
},
});
expect(
iText.getCurrentCharFontSize,
'getCurrentCharFontSize should be a function',
).toBeTypeOf('function');
iText.selectionStart = 0;
expect(
iText.getCurrentCharFontSize(),
'should return fontSize of character at position 0',
).toBe(20);
iText.selectionStart = 1;
expect(
iText.getCurrentCharFontSize(),
'should return fontSize of character at position 1',
).toBe(20);
iText.selectionStart = 2;
expect(
iText.getCurrentCharFontSize(),
'should return fontSize of character at position 2',
).toBe(60);
iText.selectionStart = 3;
expect(
iText.getCurrentCharFontSize(),
'should return default fontSize when style not defined',
).toBe(40);
});
it('getCurrentCharColor', () => {
const iText = new IText('test foo bar-baz\nqux', {
styles: {
0: {
0: { fill: 'red' },
1: { fill: 'green' },
},
},
fill: '#333',
});
expect(
iText.getCurrentCharColor,
'getCurrentCharColor should be a function',
).toBeTypeOf('function');
iText.selectionStart = 0;
expect(
iText.getCurrentCharColor(),
'should return color of character at position 0',
).toBe('red');
iText.selectionStart = 1;
expect(
iText.getCurrentCharColor(),
'should return color of character at position 1',
).toBe('red');
iText.selectionStart = 2;
expect(
iText.getCurrentCharColor(),
'should return color of character at position 2',
).toBe('green');
iText.selectionStart = 3;
expect(
iText.getCurrentCharColor(),
'should return default color when style not defined',
).toBe('#333');
});
it('toSVGWithFonts', () => {
const iText = new IText('test foo bar-baz\nqux', {
styles: {
0: {
0: { fill: '#112233' },
2: { stroke: '#223344', fontFamily: 'Engagement' },
// @ts-expect-error -- TODO: check if this is really unstandard prop?
3: { backgroundColor: '#00FF00' },
},
},
fontFamily: 'Plaster',
});
config.addFonts({
Engagement: 'path-to-engagement-font-file',
Plaster: 'path-to-plaster-font-file',
});
canvas.add(iText);
expect(iText.toSVG, 'toSVG should be a function').toBeTypeOf('function');
const parser = new (getFabricWindow().DOMParser)();
const svgString = canvas.toSVG();
const doc = parser.parseFromString(svgString, 'image/svg+xml');
// @ts-expect-error -- data is not typed
const style = doc.getElementsByTagName('style')[0].firstChild!.data;
expect(style, 'SVG style should contain font definitions').toBe(
"\n\t\t@font-face {\n\t\t\tfont-family: 'Plaster';\n\t\t\tsrc: url('path-to-plaster-font-file');\n\t\t}\n\t\t@font-face {\n\t\t\tfont-family: 'Engagement';\n\t\t\tsrc: url('path-to-engagement-font-file');\n\t\t}\n",
);
});
it('toSVGWithFontsInGroups', () => {
const iText1 = new IText('test foo bar-baz\nqux', {
styles: {
0: {
0: { fill: '#112233' },
2: { stroke: '#223344', fontFamily: 'Lacquer' },
// @ts-expect-error -- custom prop
3: { backgroundColor: '#00FF00' },
},
},
fontFamily: 'Plaster',
});
const iText2 = new IText('test foo bar-baz\nqux\n2', {
styles: {
0: {
0: { fill: '#112233', fontFamily: 'Engagement' },
2: { stroke: '#223344' },
// @ts-expect-error -- custom prop
3: { backgroundColor: '#00FF00' },
},
},
fontFamily: 'Poppins',
});
config.addFonts({
Engagement: 'path-to-engagement-font-file',
Plaster: 'path-to-plaster-font-file',
Poppins: 'path-to-poppins-font-file',
Lacquer: 'path-to-lacquer-font-file',
});
const subGroup = new Group([iText1]);
const group = new Group([subGroup, iText2]);
canvas.add(group);
expect(iText1.toSVG, 'toSVG should be a function').toBeTypeOf('function');
expect(iText2.toSVG, 'toSVG should be a function').toBeTypeOf('function');
const parser = new (getFabricWindow().DOMParser)();
const svgString = canvas.toSVG();
const doc = parser.parseFromString(svgString, 'image/svg+xml');
// @ts-expect-error -- data is not typed
const style = doc.getElementsByTagName('style')[0].firstChild!.data;
expect(style, 'SVG style should contain all font definitions').toBe(
"\n\t\t@font-face {\n\t\t\tfont-family: 'Plaster';\n\t\t\tsrc: url('path-to-plaster-font-file');\n\t\t}\n\t\t@font-face {\n\t\t\tfont-family: 'Lacquer';\n\t\t\tsrc: url('path-to-lacquer-font-file');\n\t\t}\n\t\t@font-face {\n\t\t\tfont-family: 'Poppins';\n\t\t\tsrc: url('path-to-poppins-font-file');\n\t\t}\n\t\t@font-face {\n\t\t\tfont-family: 'Engagement';\n\t\t\tsrc: url('path-to-engagement-font-file');\n\t\t}\n",
);
});
it('space wrap attribute', () => {
const iText = new IText('test foo bar-baz\nqux');
iText.enterEditing();
expect(
iText.hiddenTextarea!.wrap,
'HiddenTextarea needs wrap off attribute',
).toBe('off');
iText.abortCursorAnimation();
});
it('_removeExtraneousStyles', () => {
const iText = new IText('a\nqqo', {
styles: {
0: { 0: { fontSize: 4 } },
1: { 0: { fontSize: 4 } },
2: { 0: { fontSize: 4 } },
3: { 0: { fontSize: 4 } },
4: { 0: { fontSize: 4 } },
},
});
expect(iText.styles[3], 'style line 3 exists').toEqual({
0: { fontSize: 4 },
});
expect(iText.styles[4], 'style line 4 exists').toEqual({
0: { fontSize: 4 },
});
iText._removeExtraneousStyles();
expect(iText.styles[3], 'style line 3 has been removed').toBeUndefined();
expect(iText.styles[4], 'style line 4 has been removed').toBeUndefined();
});
it('dispose', () => {
const iText = new IText('a');
const cursorState = () =>
// @ts-expect-error -- protected members
[iText._currentTickState, iText._currentTickCompleteState].some(
(cursorAnimation) => cursorAnimation && !cursorAnimation.isDone(),
);
iText.enterEditing();
expect(cursorState(), 'should have been started').toBeTruthy();
iText.dispose();
expect(iText.isEditing, 'should have been aborted').toBe(false);
expect(cursorState(), 'should have been aborted').toBe(false);
});
describe('IText and retina scaling', () => {
beforeAll(() => {
config.configure({ devicePixelRatio: 2 });
});
afterAll(() => {
config.restoreDefaults();
});
[true, false].forEach((enableRetinaScaling) => {
it(`hiddenTextarea does not move DOM, enableRetinaScaling ${enableRetinaScaling}`, () => {
const iText = new IText('a', { fill: '#ffffff', fontSize: 50 });
const canvas2 = new Canvas(undefined, {
width: 800,
height: 800,
renderOnAddRemove: false,
enableRetinaScaling,
});
canvas2.setDimensions({ width: 100, height: 100 }, { cssOnly: true });
canvas2.cancelRequestedRender();
iText.setPositionByOrigin(new Point(400, 400), 'left', 'top');
canvas2.add(iText);
Object.defineProperty(canvas2.upperCanvasEl, 'clientWidth', {
get: function () {
return this._clientWidth;
},
set: function (value) {
return (this._clientWidth = value);
},
});
Object.defineProperty(canvas2.upperCanvasEl, 'clientHeight', {
get: function () {
return this._clientHeight;
},
set: function (value) {
return (this._clientHeight = value);
},
});
// @ts-expect-error -- not recognized by typescript
canvas2.upperCanvasEl._clientWidth = 100;
// @ts-expect-error -- not recognized by typescript
canvas2.upperCanvasEl._clientHeight = 100;
iText.enterEditing();
canvas2.cancelRequestedRender();
expect(
Math.round(parseInt(iText.hiddenTextarea!.style.top)),
'top is scaled with CSS',
).toBe(57);
expect(
Math.round(parseInt(iText.hiddenTextarea!.style.left)),
'left is scaled with CSS',
).toBe(50);
iText.exitEditing();
canvas2.cancelRequestedRender();
// @ts-expect-error -- not recognized by typescript
canvas2.upperCanvasEl._clientWidth = 200;
// @ts-expect-error -- not recognized by typescript
canvas2.upperCanvasEl._clientHeight = 200;
iText.enterEditing();
canvas2.cancelRequestedRender();
expect(
Math.round(parseInt(iText.hiddenTextarea!.style.top)),
'top is scaled with CSS',
).toBe(114);
expect(
Math.round(parseInt(iText.hiddenTextarea!.style.left)),
'left is scaled with CSS',
).toBe(100);
iText.exitEditing();
canvas2.cancelRequestedRender();
});
});
});
});