fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
977 lines (842 loc) • 29.7 kB
text/typescript
import '../../vitest.extend';
import { Textbox } from './Textbox';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
import { Canvas } from '../canvas/Canvas';
import { stylesFromArray } from '../util';
import { FabricText } from './Text/Text';
import { IText } from './IText/IText';
import type { TPointerEvent } from '../EventTypeDefs';
import { Point } from '../Point';
describe('Textbox', () => {
let canvas: Canvas;
beforeAll(() => {
canvas = new Canvas();
});
afterEach(() => {
canvas.clear();
});
it('fromObject', async () => {
const textbox = await Textbox.fromObject({
text: 'The quick \nbrown \nfox',
});
expect(textbox).toMatchObjectSnapshot();
expect(textbox).toMatchObjectSnapshot({ includeDefaultValues: false });
});
it('toObject with styles', () => {
const textbox = new Textbox('The quick \nbrown \nfox', {
width: 120,
styles: {
'0': {
'5': { fill: 'red' },
'6': { fill: 'red' },
'7': { fill: 'red' },
'8': { fill: 'red' },
},
'1': {
'3': { underline: true },
'4': { underline: true },
'5': { underline: true },
},
'2': {
'0': { underline: true },
'1': { underline: true },
},
},
});
expect(textbox).toMatchObjectSnapshot();
});
it('stylesToArray edge case', () => {
const textbox = new Textbox('The quick \nbrown \nfox', {
width: 120,
styles: {
'0': {
'5': { fill: 'red' },
'6': { fill: 'red' },
'7': { fill: 'red' },
'8': { fill: 'red' },
'9': { fill: 'red' },
'10': { fill: 'red' },
},
'2': {
'0': { fill: 'red' },
},
},
});
expect(textbox.toObject().styles).toMatchSnapshot();
});
it('fromObject with styles', async () => {
const textbox = new Textbox('The quick \nbrown \nfox', {
width: 120,
styles: {
'0': {
'5': { fill: 'red' },
'6': { fill: 'red' },
'7': { fill: 'red' },
'8': { fill: 'red' },
},
'1': {
'3': { underline: true },
'4': { underline: true },
'5': { underline: true },
},
'2': {
'0': { underline: true },
'1': { underline: true },
},
},
});
const textbox2 = await Textbox.fromObject(textbox.toObject());
expect(textbox2.toObject()).toEqual(textbox.toObject());
expect(textbox2.styles !== textbox.styles).toBeTruthy();
for (const a in textbox2.styles) {
for (const b in textbox2.styles[a]) {
expect(textbox2.styles[a][b] !== textbox.styles[a][b]).toBeTruthy();
expect(textbox2.styles[a][b]).toEqual(textbox.styles[a][b]);
}
}
});
it('constructor', () => {
const textbox = new Textbox('test');
expect(textbox, 'should be instance of Textbox').toBeInstanceOf(Textbox);
expect(textbox, 'should be instance of IText').toBeInstanceOf(IText);
expect(textbox, 'should be instance of FabricText').toBeInstanceOf(
FabricText,
);
});
it('constructor with width', () => {
const textbox = new Textbox('test', { width: 400 });
expect(textbox.width, 'width is taken by contstructor').toBe(400);
});
it('constructor with width too small', () => {
const textbox = new Textbox('test', { width: 5 });
expect(
Math.round(textbox.width),
'width is calculated by constructor',
).toBe(56);
});
it('initial properties', () => {
const textbox = new Textbox('test');
expect(textbox.text, 'text value should be set').toBe('test');
expect(
textbox.constructor,
'constructor type should be Textbox',
).toHaveProperty('type', 'Textbox');
expect(textbox.styles, 'styles should be empty object').toEqual({});
expect(
Textbox.cacheProperties.indexOf('width') > -1,
'width is in cacheProperties',
).toBeTruthy();
});
it('isEndOfWrapping', () => {
const textbox = new Textbox('a q o m s g\np q r s t w', {
width: 70,
});
expect(
textbox.isEndOfWrapping(0),
'first line is not end of wrapping',
).toBe(false);
expect(
textbox.isEndOfWrapping(1),
'second line is not end of wrapping',
).toBe(false);
expect(
textbox.isEndOfWrapping(2),
'line before an hard break is end of wrapping',
).toBe(true);
expect(textbox.isEndOfWrapping(3), 'line 3 is not end of wrapping').toBe(
false,
);
expect(textbox.isEndOfWrapping(4), 'line 4 is not end of wrapping').toBe(
false,
);
expect(textbox.isEndOfWrapping(5), 'last line is end of wrapping').toBe(
true,
);
});
it('_removeExtraneousStyles', () => {
const textbox = new Textbox('a q o m s g\np q r s t w', {
width: 40,
styles: {
0: { 0: { fontSize: 4 } },
1: { 0: { fontSize: 4 } },
2: { 0: { fontSize: 4 } },
3: { 0: { fontSize: 4 } },
4: { 0: { fontSize: 4 } },
5: { 0: { fontSize: 4 } },
},
});
expect(textbox.styles[3], 'style line 3 exists').toEqual({
0: { fontSize: 4 },
});
expect(textbox.styles[4], 'style line 4 exists').toEqual({
0: { fontSize: 4 },
});
expect(textbox.styles[5], 'style line 5 exists').toEqual({
0: { fontSize: 4 },
});
textbox._removeExtraneousStyles();
expect(textbox.styles[2], 'style line 2 has been removed').toBeUndefined();
expect(textbox.styles[3], 'style line 3 has been removed').toBeUndefined();
expect(textbox.styles[4], 'style line 4 has been removed').toBeUndefined();
expect(textbox.styles[5], 'style line 5 has been removed').toBeUndefined();
});
it('isEmptyStyles', () => {
const textbox = new Textbox('x x', {
width: 5,
styles: { 0: { 0: { fill: 'red' } } },
});
expect(textbox._textLines.length, 'lines are wrapped').toBe(2);
expect(
textbox._unwrappedTextLines.length,
'there is only one text line',
).toBe(1);
// @ts-expect-error -- TODO: check if lineIndex should be optional?
expect(textbox.isEmptyStyles(), 'style is not empty').toBe(false);
expect(textbox.isEmptyStyles(0), 'style is not empty at line 0').toBe(
false,
);
expect(textbox.isEmptyStyles(1), 'style is empty at line 1').toBe(true);
});
it('isEmptyStyles does not crash on null styles', () => {
const textbox = new Textbox('x x', { width: 5 });
textbox.styles = {};
expect(textbox._textLines.length, 'lines are wrapped').toBe(2);
expect(
textbox._unwrappedTextLines.length,
'there is only one text line',
).toBe(1);
expect(textbox.isEmptyStyles(1), 'style is empty').toBe(true);
});
it('isEmptyStyles alternate lines', () => {
const textbox = new Textbox('xa xb xc xd xe\nya yb', {
width: 5,
styles: {
0: {
0: { fill: 'red' },
1: { fill: 'blue' },
9: { fill: 'red' },
10: { fill: 'blue' },
},
1: { 3: { fill: 'red' }, 4: { fill: 'blue' } },
},
});
expect(textbox._textLines.length, 'lines are wrapped').toBe(7);
expect(
textbox._unwrappedTextLines.length,
'there is only one text line',
).toBe(2);
// @ts-expect-error -- TODO: check why lineIndex is mandatory but test doesn't provide it
expect(textbox.isEmptyStyles(), 'style is not empty').toBe(false);
expect(textbox.isEmptyStyles(0), 'style is not empty at line 0').toBe(
false,
);
expect(textbox.isEmptyStyles(1), 'style is empty at line 1').toBe(true);
expect(textbox.isEmptyStyles(2), 'style is empty at line 2').toBe(true);
expect(textbox.isEmptyStyles(3), 'style is empty at line 3').toBe(false);
expect(textbox.isEmptyStyles(4), 'style is empty at line 4').toBe(true);
expect(textbox.isEmptyStyles(5), 'style is empty at line 5').toBe(true);
expect(textbox.isEmptyStyles(6), 'style is empty at line 6').toBe(false);
});
it('wrapping with charspacing', () => {
const textbox = new Textbox('xa xb xc xd xe ya yb id', {
width: 190,
});
expect(textbox.textLines[0], 'first line match expectations').toBe(
'xa xb xc xd',
);
textbox.charSpacing = 100;
textbox.initDimensions();
expect(
textbox.textLines[0],
'first line match expectations spacing 100',
).toBe('xa xb xc');
textbox.charSpacing = 300;
textbox.initDimensions();
expect(
textbox.textLines[0],
'first line match expectations spacing 300',
).toBe('xa xb');
textbox.charSpacing = 800;
textbox.initDimensions();
expect(
textbox.textLines[0],
'first line match expectations spacing 800',
).toBe('xa');
});
it('wrapping with splitByGrapheme and styles', () => {
const value = 'xaxbxcxdeyaybid';
const textbox = new Textbox(value, {
width: 190,
splitByGrapheme: true,
styles: stylesFromArray(
[
{
style: {
fontWeight: 'bold',
fontSize: 64,
},
start: 0,
end: 9,
},
],
value,
),
});
expect(
textbox.textLines,
'lines match splitByGrapheme with styles',
).toEqual(['xaxbx', 'cxdeyay', 'bid']);
});
it('wrapping with largestWordWidth and styles', () => {
const value = 'xaxbxc xdeyayb id sdgjhgsdg';
const textbox = new Textbox(value, {
width: 190,
styles: stylesFromArray(
[
{
style: {
fontWeight: 'bold',
fontSize: 64,
},
start: 0,
end: 10,
},
],
value,
),
});
expect(
textbox.textLines,
'lines match largestWordWidth with styles',
).toEqual(['xaxbxc', 'xdeyayb', 'id', 'sdgjhgsdg']);
});
it('wrapping with charspacing and splitByGrapheme positive', () => {
const textbox = new Textbox('xaxbxcxdeyaybid', {
width: 190,
splitByGrapheme: true,
charSpacing: 400,
});
expect(
textbox.textLines,
'lines match splitByGrapheme charSpacing 400',
).toEqual(['xaxbx', 'cxdey', 'aybid']);
});
it('wrapping with charspacing and splitByGrapheme negative', () => {
const textbox = new Textbox('xaxbxcxdeyaybid', {
width: 190,
splitByGrapheme: true,
charSpacing: -100,
});
expect(
textbox.textLines,
'lines match splitByGrapheme charSpacing -100',
).toEqual(['xaxbxcxdeyay', 'bid']);
});
it('Measure words', () => {
const textbox = new Textbox('word word\nword\nword', { width: 300 });
const { wordsData, largestWordWidth } = textbox.getGraphemeDataForRender(
textbox.textLines,
);
expect(wordsData[0], 'All words have the same length line 0').toEqual([
{ word: ['w', 'o', 'r', 'd'], width: largestWordWidth },
{ word: ['w', 'o', 'r', 'd'], width: largestWordWidth },
]);
expect(wordsData[1], 'All words have the same length line1').toEqual([
{ word: ['w', 'o', 'r', 'd'], width: largestWordWidth },
]);
expect(Math.round(largestWordWidth), 'largest word is 82').toBe(82);
});
it('Measure words with styles', () => {
const textbox = new Textbox('word word\nword\nword', { width: 300 });
textbox.styles = {
0: {
5: {
fontSize: 100,
},
6: {
fontSize: 100,
},
7: {
fontSize: 100,
},
8: {
fontSize: 100,
},
},
2: {
0: {
fontSize: 200,
},
1: {
fontSize: 200,
},
2: {
fontSize: 200,
},
3: {
fontSize: 200,
},
},
};
const { wordsData, largestWordWidth } = textbox.getGraphemeDataForRender(
textbox.textLines,
);
expect(Math.round(wordsData[0][0].width), 'unstyle word is 82 wide').toBe(
82,
);
expect(Math.round(wordsData[0][1].width), 'unstyle word is 206 wide').toBe(
206,
);
expect(wordsData[2], 'All words have the same length line1').toEqual([
{ word: ['w', 'o', 'r', 'd'], width: largestWordWidth },
]);
expect(Math.round(largestWordWidth), 'largest word is 411').toBe(411);
});
it('wrapping with different things', () => {
const textbox = new Textbox('xa xb\txc\rxd xe ya yb id', {
width: 16,
});
expect(textbox.textLines[0], '0 line match expectations').toBe('xa');
expect(textbox.textLines[1], '1 line match expectations').toBe('xb');
expect(textbox.textLines[2], '2 line match expectations').toBe('xc');
expect(textbox.textLines[3], '3 line match expectations').toBe('xd');
expect(textbox.textLines[4], '4 line match expectations').toBe('xe');
expect(textbox.textLines[5], '5 line match expectations').toBe('ya');
expect(textbox.textLines[6], '6 line match expectations').toBe('yb');
});
it('wrapping with splitByGrapheme', () => {
const textbox = new Textbox('xaxbxcxdxeyaybid', {
width: 1,
splitByGrapheme: true,
});
expect(
textbox.textLines[0],
'0 line match expectations splitByGrapheme',
).toBe('x');
expect(
textbox.textLines[1],
'1 line match expectations splitByGrapheme',
).toBe('a');
expect(
textbox.textLines[2],
'2 line match expectations splitByGrapheme',
).toBe('x');
expect(
textbox.textLines[3],
'3 line match expectations splitByGrapheme',
).toBe('b');
expect(
textbox.textLines[4],
'4 line match expectations splitByGrapheme',
).toBe('x');
expect(
textbox.textLines[5],
'5 line match expectations splitByGrapheme',
).toBe('c');
});
it('wrapping with custom space', () => {
const textbox = new Textbox('xa xb xc xd xe ya yb id', {
width: 2000,
});
const wordsData = textbox.getGraphemeDataForRender([
'xa xb xc xd xe ya yb id',
]);
const line1 = textbox._wrapLine(0, 100, wordsData, 0);
const expected1 = [
['x', 'a', ' ', 'x', 'b'],
['x', 'c', ' ', 'x', 'd'],
['x', 'e', ' ', 'y', 'a'],
['y', 'b', ' ', 'i', 'd'],
];
expect(line1, 'line1 match expected').toEqual(expected1);
expect(textbox.dynamicMinWidth, 'texbox width is 40').toBe(40);
const line2 = textbox._wrapLine(0, 100, wordsData, 50);
const expected2 = [
['x', 'a'],
['x', 'b'],
['x', 'c'],
['x', 'd'],
['x', 'e'],
['y', 'a'],
['y', 'b'],
['i', 'd'],
];
expect(line2, 'line2 match expected').toEqual(expected2);
expect(textbox.dynamicMinWidth, 'texbox width is 90').toBe(90);
});
it('wrapping an empty line', () => {
const textbox = new Textbox('', {
width: 10,
});
const wordsData = textbox.getGraphemeDataForRender(['']);
const line1 = textbox._wrapLine(0, 100, wordsData, 0);
expect(line1, 'wrapping without splitByGrapheme').toEqual([[]]);
textbox.splitByGrapheme = true;
const line2 = textbox._wrapLine(0, 100, wordsData, 0);
expect(line2, 'wrapping with splitByGrapheme').toEqual([[]]);
});
it('wrapping respects max line width', () => {
const a = 'xaxbxc xdxeyaybid xaxbxc';
const b = 'xaxbxcxdxeyaybidxaxbxcxdxeyaybid';
[true, false].forEach((order) => {
[true, false].forEach((space) => {
const ordered = order ? [a, b] : [b, a];
const text = ordered.join(space ? ' ' : '\n');
const { _textLines: lines } = new Textbox(text);
expect(lines, `max line width should be respected for ${text}`).toEqual(
ordered.map((line) => line.split('')),
);
});
});
});
it('texbox will change width from the mr corner', () => {
const text = new Textbox('xa xb xc xd xe ya yb id', { strokeWidth: 0 });
text.setPositionByOrigin(new Point(0, 0), 'left', 'top');
canvas.add(text);
canvas.setActiveObject(text);
const eventStub = {
clientX: text.width,
clientY: text.oCoords.mr.corner.tl.y + 1,
type: 'mousedown',
target: canvas.upperCanvasEl,
} as unknown as TPointerEvent & { clientX: number; clientY: number };
const originalWidth = text.width;
canvas._onMouseDown(eventStub);
canvas._onMouseMove({
...eventStub,
clientX: eventStub.clientX + 20,
clientY: eventStub.clientY,
type: 'mousemove',
});
canvas._onMouseUp({
...eventStub,
clientX: eventStub.clientX + 20,
clientY: eventStub.clientY,
type: 'mouseup',
});
expect(text.width, 'width increased').toBe(originalWidth + 20);
});
it('texbox will change width from the ml corner', () => {
const text = new Textbox('xa xb xc xd xe ya yb id', {
strokeWidth: 0,
left: 40,
});
text.setPositionByOrigin(new Point(40, 0), 'left', 'top');
canvas.add(text);
canvas.setActiveObject(text);
const eventStub = {
clientX: text.left - text.width / 2,
clientY: text.oCoords.ml.corner.tl.y + 2,
type: 'mousedown',
target: canvas.upperCanvasEl,
} as unknown as TPointerEvent & { clientX: number; clientY: number };
const originalWidth = text.width;
canvas._onMouseDown(eventStub);
canvas._onMouseMove({
...eventStub,
clientX: eventStub.clientX - 20,
clientY: eventStub.clientY,
type: 'mousemove',
});
canvas._onMouseUp({
...eventStub,
clientX: eventStub.clientX + 20,
clientY: eventStub.clientY,
type: 'mouseup',
});
expect(text.width, 'width increased').toBe(originalWidth + 20);
});
it('_removeExtraneousStyles for textbox', () => {
const iText = new Textbox('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('get2DCursorLocation with splitByGrapheme', () => {
const iText = new Textbox('aaaaaaaaaaaaaaaaaaaaaaaa', {
width: 60,
splitByGrapheme: true,
});
let loc = iText.get2DCursorLocation();
expect(loc.lineIndex, 'initial cursor line should be 0').toBe(0);
expect(loc.charIndex, 'initial cursor position should be 0').toBe(0);
iText.selectionStart = iText.selectionEnd = 4;
loc = iText.get2DCursorLocation();
expect(loc.lineIndex, 'selection end 4 line 1').toBe(1);
expect(loc.charIndex, 'selection end 4 char 1').toBe(1);
iText.selectionStart = iText.selectionEnd = 7;
loc = iText.get2DCursorLocation();
expect(loc.lineIndex, 'selection end 7 line 2').toBe(2);
expect(loc.charIndex, 'selection end 7 char 1').toBe(1);
iText.selectionStart = iText.selectionEnd = 14;
loc = iText.get2DCursorLocation();
expect(loc.lineIndex, 'selection end 14 line 4').toBe(4);
expect(loc.charIndex, 'selection end 14 char 2').toBe(2);
});
it('missingNewlineOffset with splitByGrapheme', () => {
const textbox = new Textbox('aaa\naaaaaa\na\naaaaaaaaaaaa\n aaa', {
width: 80,
splitByGrapheme: true,
});
const expected = {
lines: ['aaa', 'aaaa', 'aa', 'a', 'aaaa', 'aaaa', 'aaaa', ' aaa'],
hardBreaks: [1, 0, 1, 1, 0, 0, 1, 1],
cursor: [
{ selection: 1, lineIndex: 0, charIndex: 1 }, // a|aa
{ selection: 4, lineIndex: 1, charIndex: 0 }, // |aaaa
{ selection: 9, lineIndex: 2, charIndex: 1 }, // a|a
{ selection: 11, lineIndex: 3, charIndex: 0 }, // |a
{ selection: 14, lineIndex: 4, charIndex: 1 }, // a|aaa
{ selection: 20, lineIndex: 5, charIndex: 3 }, // aaa|a
{ selection: 22, lineIndex: 6, charIndex: 1 }, // a|aaa
{ selection: 29, lineIndex: 7, charIndex: 3 }, // aa|a
],
};
expect(textbox.textLines, 'wrap line by width').toEqual(expected.lines);
for (let i = 0; i < expected.hardBreaks.length; i++) {
const offset = textbox.missingNewlineOffset(i);
expect(
offset,
`line ${i} expect missingNewlineOffset: ${expected.hardBreaks[i]}`,
).toBe(expected.hardBreaks[i]);
}
let loc = textbox.get2DCursorLocation();
expect(loc.lineIndex, 'initial cursor line should be 0').toBe(0);
expect(loc.charIndex, 'initial cursor position should be 0').toBe(0);
for (let i = 0; i < expected.cursor.length; i++) {
const { selection, lineIndex, charIndex } = expected.cursor[i];
textbox.selectionStart = textbox.selectionEnd = selection;
loc = textbox.get2DCursorLocation();
expect(
loc.lineIndex,
`selection end ${selection} line ${lineIndex}`,
).toBe(lineIndex);
expect(
loc.charIndex,
`selection end ${selection} char ${charIndex}`,
).toBe(charIndex);
}
});
it('missingNewlineOffset with normal split 1', () => {
const textbox = new Textbox('aaa\naaaaaa\na\naaaaaaaaaaaa\n aaa', {
width: 80,
});
const expected = {
lines: ['aaa', 'aaaaaa', 'a', 'aaaaaaaaaaaa', ' aaa'],
hardBreaks: [1, 1, 1, 1, 1], // it has to always return 1
cursor: [
{ selection: 1, lineIndex: 0, charIndex: 1 }, // a|aa
{ selection: 4, lineIndex: 1, charIndex: 0 }, // |aaaaaa
{ selection: 12, lineIndex: 2, charIndex: 1 }, // a|
{ selection: 22, lineIndex: 3, charIndex: 9 }, // aaaaaaaaa|aaa
{ selection: 28, lineIndex: 4, charIndex: 2 }, // a|aa
],
};
expect(textbox.textLines, 'wrap by largestWordWidth').toEqual(
expected.lines,
);
for (let i = 0; i < expected.hardBreaks.length; i++) {
const offset = textbox.missingNewlineOffset(i);
expect(offset, `line ${i} expect ${expected.hardBreaks[i]}`).toBe(
expected.hardBreaks[i],
);
}
let loc = textbox.get2DCursorLocation();
expect(loc.lineIndex, 'initial cursor line should be 0').toBe(0);
expect(loc.charIndex, 'initial cursor position should be 0').toBe(0);
for (let i = 0; i < expected.cursor.length; i++) {
const { selection, lineIndex, charIndex } = expected.cursor[i];
textbox.selectionStart = textbox.selectionEnd = selection;
loc = textbox.get2DCursorLocation();
expect(
loc.lineIndex,
`selection end ${selection} line ${lineIndex}`,
).toBe(lineIndex);
expect(
loc.charIndex,
`selection end ${selection} char ${charIndex}`,
).toBe(charIndex);
}
});
it('missingNewlineOffset with normal split and short word', () => {
const textbox = new Textbox(
'aaa\naaaaaa \na\naaaaaaa aaaaa\n aaa',
{
width: 80,
},
);
const expected = {
lines: ['aaa', 'aaaaaa ', ' ', 'a', 'aaaaaaa', 'aaaaa', ' aaa'],
hardBreaks: [1, 1, 1, 1, 1, 1, 1], // Note: currently, lineIndex 2 and 4 no hardBreak but still removed a space
cursor: [
{ selection: 1, lineIndex: 0, charIndex: 1 }, // a|aa
{ selection: 4, lineIndex: 1, charIndex: 0 }, // |aaaaaa
{ selection: 13, lineIndex: 2, charIndex: 1 }, // 8 space
{ selection: 22, lineIndex: 3, charIndex: 1 }, // a|
{ selection: 29, lineIndex: 4, charIndex: 6 }, // aaaaaa|a
{ selection: 32, lineIndex: 5, charIndex: 1 }, // a|aaaa
{ selection: 38, lineIndex: 6, charIndex: 1 }, // |aaa
],
};
expect(textbox.textLines, 'wrap by largestWordWidth').toEqual(
expected.lines,
);
for (let i = 0; i < expected.hardBreaks.length; i++) {
const offset = textbox.missingNewlineOffset(i);
expect(offset, `line ${i} expect ${expected.hardBreaks[i]}`).toBe(
expected.hardBreaks[i],
);
}
let loc = textbox.get2DCursorLocation();
expect(loc.lineIndex, 'initial cursor line should be 0').toBe(0);
expect(loc.charIndex, 'initial cursor position should be 0').toBe(0);
for (let i = 0; i < expected.cursor.length; i++) {
const { selection, lineIndex, charIndex } = expected.cursor[i];
textbox.selectionStart = textbox.selectionEnd = selection;
loc = textbox.get2DCursorLocation();
expect(
loc.lineIndex,
`selection end ${selection} line ${lineIndex}`,
).toBe(lineIndex);
expect(
loc.charIndex,
`selection end ${selection} char ${charIndex}`,
).toBe(charIndex);
}
});
it('_getLineStyle', () => {
const textbox = new Textbox('aaa aaq ggg gg\noee eee', {
styles: {
1: { 0: { fontSize: 4 } },
},
width: 80,
});
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(0), 'wrapped line 0 has no style').toBe(false);
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(1), 'wrapped line 1 has no style').toBe(false);
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(4), 'wrapped line 2 has style').toBe(true);
});
it('_setLineStyle', () => {
const textbox = new Textbox('aaa aaq ggg gg\noee eee', {
styles: {
1: { 0: { fontSize: 4 } },
},
width: 80,
});
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(0), 'wrapped line 0 has no style').toBe(false);
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(1), 'wrapped line 1 has no style').toBe(false);
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(2), 'wrapped line 2 has no style').toBe(false);
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(3), 'wrapped line 3 has no style').toBe(false);
expect(textbox.styles[0], 'style is undefined').toBeUndefined();
// @ts-expect-error -- protected member
textbox._setLineStyle(0);
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(0), 'wrapped line 0 has style').toBe(true);
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(1), 'wrapped line 1 has style').toBe(true);
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(2), 'wrapped line 2 has style').toBe(true);
// @ts-expect-error -- protected member
expect(textbox._getLineStyle(3), 'wrapped line 3 has style').toBe(true);
expect(textbox.styles[0], 'style is an empty object').toEqual({});
});
it('_deleteStyleDeclaration', () => {
const text = 'aaa aaq ggg gg oee eee';
const styles: Record<PropertyKey, any> = {};
for (let index = 0; index < text.length; index++) {
styles[index] = { fontSize: 4 };
}
const textbox = new Textbox(text, {
styles: { 0: styles },
width: 5,
});
// @ts-expect-error -- protected member
expect(textbox._deleteStyleDeclaration, 'function exists').toBeTypeOf(
'function',
);
// @ts-expect-error -- protected member
textbox._deleteStyleDeclaration(2, 2);
expect(textbox.styles[0][10], 'style has been removed').toBeUndefined();
});
it('_setStyleDeclaration', () => {
const text = 'aaa aaq ggg gg oee eee';
const styles: Record<PropertyKey, any> = {};
for (let index = 0; index < text.length; index++) {
styles[index] = { fontSize: 4 };
}
const textbox = new Textbox(text, {
styles: { 0: styles },
width: 5,
});
// @ts-expect-error -- protected member
expect(textbox._setStyleDeclaration, 'function exists').toBeTypeOf(
'function',
);
const newStyle = { fontSize: 10 };
// @ts-expect-error -- protected member
textbox._setStyleDeclaration(2, 2, newStyle);
expect(textbox.styles[0][10], 'style has been changed').toBe(newStyle);
});
it('styleHas', () => {
const textbox = new Textbox('aaa aaq ggg gg oee eee', {
styles: {
0: {
0: { fontSize: 4 },
1: { fontSize: 4 },
2: { fontSize: 4 },
4: { fontFamily: 'Arial' },
5: { fontFamily: 'Arial' },
6: { fontFamily: 'Arial' },
},
},
width: 5,
});
// @ts-expect-error -- TODO: check why lineIndex is mandatory but test doesn't provide it
expect(textbox.styleHas('fontSize'), 'style has fontSize').toBe(true);
expect(
textbox.styleHas('fontSize', 0),
'style has fontSize on line 0',
).toBe(true);
// @ts-expect-error -- TODO: check why lineIndex is mandatory but test doesn't provide it
expect(textbox.styleHas('fontFamily'), 'style has fontFamily').toBe(true);
expect(
textbox.styleHas('fontFamily', 1),
'style has fontFamily on line 1',
).toBe(true);
});
it('The same text does not need to be wrapped.', () => {
const str = '0123456789';
const measureTextbox = new Textbox(str, {
fontSize: 20,
splitByGrapheme: false,
});
const newTextbox = new Textbox(str, {
width: measureTextbox.width,
fontSize: 20,
splitByGrapheme: true,
});
expect(newTextbox.textLines.length, 'The same text is not wrapped').toBe(
measureTextbox.textLines.length,
);
});
});