fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
436 lines (337 loc) • 12.2 kB
text/typescript
import {
describe,
test,
expect,
beforeAll,
afterAll,
afterEach,
beforeEach,
} from 'vitest';
import { Canvas } from '../../canvas/Canvas';
import { IText } from './IText';
import { Group } from '../Group';
import { config } from '../../config';
import type {
ObjectPointerEvents,
TPointerEventInfo,
} from '../../EventTypeDefs';
import { Point } from '../../Point';
import { createPointerEvent } from '../../../test/utils';
describe('iText click interaction', () => {
let canvas: Canvas;
beforeAll(() => {
canvas = new Canvas(undefined, {
enableRetinaScaling: false,
});
});
afterAll(() => canvas.dispose());
afterEach(() => {
canvas.clear();
canvas.cancelRequestedRender();
});
test('doubleClickHandler', async () => {
const iText = new IText('test need some word\nsecond line');
iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');
iText.canvas = canvas;
let eventData = createPointerEvent({
target: canvas.upperCanvasEl,
clientX: 40,
clientY: 10,
});
iText.enterEditing();
iText.doubleClickHandler({
e: eventData,
} as unknown as TPointerEventInfo);
expect(iText.selectionStart, 'dblClick selection start is').toBe(0);
expect(iText.selectionEnd, 'dblClick selection end is').toBe(4);
eventData = createPointerEvent({
target: canvas.upperCanvasEl,
clientX: 40,
clientY: 60,
});
iText.doubleClickHandler({
e: eventData,
} as unknown as TPointerEventInfo);
expect(iText.selectionStart, 'second dblClick selection start is').toBe(20);
expect(iText.selectionEnd, 'second dblClick selection end is').toBe(26);
});
test('doubleClickHandler no editing', () => {
const iText = new IText('test need some word\nsecond line');
iText.canvas = canvas;
const eventData = createPointerEvent({
target: canvas.upperCanvasEl,
clientX: 40,
clientY: 10,
});
iText.doubleClickHandler({
e: eventData,
} as unknown as TPointerEventInfo);
expect(iText.selectionStart, 'dblClick selection start is').toBe(0);
expect(iText.selectionEnd, 'dblClick selection end is').toBe(0);
});
test('tripleClickHandler', async () => {
const iText = new IText('test need some word\nsecond line');
iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');
iText.canvas = canvas;
let eventData = createPointerEvent({
target: canvas.upperCanvasEl,
clientX: 40,
clientY: 10,
});
iText.enterEditing();
iText.tripleClickHandler({
e: eventData,
} as unknown as TPointerEventInfo);
expect(iText.selectionStart, 'tripleClick selection start is').toBe(0);
expect(iText.selectionEnd, 'tripleClick selection end is').toBe(19);
eventData = createPointerEvent({
target: canvas.upperCanvasEl,
clientX: 40,
clientY: 60,
});
iText.tripleClickHandler({
e: eventData,
} as unknown as TPointerEventInfo);
expect(iText.selectionStart, 'second tripleClick selection start is').toBe(
20,
);
expect(iText.selectionEnd, 'second tripleClick selection end is').toBe(31);
iText.exitEditing();
});
test('tripleClickHandler without editing', () => {
const iText = new IText('test need some word\nsecond line');
iText.canvas = canvas;
const eventData = createPointerEvent({
target: canvas.upperCanvasEl,
clientX: 40,
clientY: 10,
});
iText.tripleClickHandler({
e: eventData,
} as unknown as TPointerEventInfo);
expect(iText.selectionStart, 'tripleClick selection start is').toBe(0);
expect(iText.selectionEnd, 'tripleClick selection end is').toBe(0);
});
test('getSelectionStartFromPointer with scale', () => {
const eventData = createPointerEvent({
target: canvas.upperCanvasEl,
clientX: 70,
clientY: 10,
});
const iText = new IText('test need some word\nsecond line', {
scaleX: 3,
scaleY: 2,
canvas,
});
iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');
expect(iText.getSelectionStartFromPointer(eventData), 'index').toBe(2);
expect(
iText.getSelectionStartFromPointer({ ...eventData, clientY: 20 }),
'index',
).toBe(2);
iText.set({ scaleX: 0.5, scaleY: 0.25 });
iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');
expect(iText.getSelectionStartFromPointer(eventData), 'index').toBe(9);
expect(
iText.getSelectionStartFromPointer({ ...eventData, clientY: 20 }),
'index',
).toBe(29);
iText.set({ scaleX: 1, scaleY: 1 });
iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');
expect(iText.getSelectionStartFromPointer(eventData), 'index').toBe(5);
expect(
iText.getSelectionStartFromPointer({ ...eventData, clientY: 20 }),
'index',
).toBe(5);
});
test('mouse down aborts cursor animation', () => {
const iText = new IText('test need some word\nsecond line', {
canvas,
});
expect(iText._animateCursor, 'method is defined').toBeTypeOf('function');
let animate = 0;
let aborted = 0;
// @ts-expect-error -- overridden for test simplicity
iText._animateCursor = () => animate++;
iText.abortCursorAnimation = () => aborted++;
canvas.setActiveObject(iText);
iText.enterEditing();
iText._mouseDownHandler({
e: { target: canvas.upperCanvasEl },
} as unknown as ObjectPointerEvents['mousedown']);
expect(animate, 'called from enterEditing').toBe(1);
expect(aborted, 'called from render').toBe(1);
});
test('_mouseUpHandler on a selected object enter edit', () => {
const iText = new IText('test');
iText.initDelayedCursor = function () {};
iText.renderCursorOrSelection = function () {};
expect(iText.isEditing, 'iText not editing').toBe(false);
iText.canvas = canvas;
canvas._activeObject = undefined;
// @ts-expect-error -- protected member
iText.selected = true;
iText.mouseUpHandler({
e: {},
} as unknown as ObjectPointerEvents['mouseup']);
expect(iText.isEditing, 'iText entered editing').toBe(true);
iText.exitEditing();
});
test('_mouseUpHandler on a selected object does enter edit if there is an activeObject', () => {
const iText = new IText('test');
iText.initDelayedCursor = function () {};
iText.renderCursorOrSelection = function () {};
expect(iText.isEditing, 'iText not editing').toBe(false);
iText.canvas = canvas;
canvas._activeObject = new IText('test2');
// @ts-expect-error -- protected member
iText.selected = true;
iText.mouseUpHandler({
e: {},
} as unknown as ObjectPointerEvents['mouseup']);
expect(iText.isEditing, 'iText should not enter editing').toBe(false);
iText.exitEditing();
});
test('_mouseUpHandler on a selected text in a group does NOT enter editing', () => {
const iText = new IText('test');
iText.initDelayedCursor = function () {};
iText.renderCursorOrSelection = function () {};
expect(iText.isEditing, 'iText not editing').toBe(false);
const group = new Group([iText], { subTargetCheck: false });
canvas.add(group);
// @ts-expect-error -- protected member
iText.selected = true;
const evt = createPointerEvent({
clientX: 1,
clientY: 1,
target: canvas.upperCanvasEl,
});
canvas._cacheTransformEventData(evt);
canvas.__onMouseUp(evt);
// @ts-expect-error -- protected member
expect(canvas._targetInfo.target, 'group should be found as target').toBe(
group,
);
expect(iText.isEditing, 'iText should not enter editing').toBe(false);
iText.exitEditing();
canvas._resetTransformEventData();
});
test('_mouseUpHandler on a text in a group', () => {
const iText = new IText('test');
iText.initDelayedCursor = function () {};
iText.renderCursorOrSelection = function () {};
expect(iText.isEditing, 'iText not editing').toBe(false);
const group = new Group([iText], {
subTargetCheck: true,
interactive: true,
});
canvas.add(group);
// @ts-expect-error -- protected member
iText.selected = true;
canvas._onMouseUp(
createPointerEvent({
clientX: 1,
clientY: 1,
target: canvas.upperCanvasEl,
}),
);
expect(iText.isEditing, 'iText should enter editing').toBe(true);
iText.exitEditing();
group.interactive = false;
// @ts-expect-error -- protected member
iText.selected = true;
canvas._onMouseUp(
createPointerEvent({
clientX: 1,
clientY: 1,
target: canvas.upperCanvasEl,
}),
);
expect(iText.isEditing, 'iText should not enter editing').toBe(false);
});
test('_mouseUpHandler on a corner of selected text DOES NOT enter edit', () => {
const iText = new IText('test');
iText.initDelayedCursor = function () {};
iText.renderCursorOrSelection = function () {};
expect(iText.isEditing, 'iText not editing').toBe(false);
iText.canvas = canvas;
// @ts-expect-error -- protected member
iText.selected = true;
iText.__corner = 'mt';
iText.setCoords();
iText.mouseUpHandler({
e: {},
} as unknown as ObjectPointerEvents['mouseup']);
expect(iText.isEditing, 'iText should not enter editing').toBe(false);
iText.exitEditing();
canvas.renderAll();
});
[true, false].forEach((enableRetinaScaling) => {
describe(`enableRetinaScaling = ${enableRetinaScaling}`, () => {
let testCanvas: Canvas;
let eventData: TPointerEvent;
let iText: IText;
let count: number;
let countCanvas: number;
beforeAll(() => {
config.configure({ devicePixelRatio: 2 });
});
afterAll(() => {
config.restoreDefaults();
});
beforeEach(() => {
testCanvas = new Canvas(undefined, {
enableRetinaScaling,
});
eventData = createPointerEvent({
target: testCanvas.upperCanvasEl,
...(enableRetinaScaling
? { clientX: 60, clientY: 30 }
: { clientX: 30, clientY: 15 }),
});
count = 0;
countCanvas = 0;
iText = new IText('test test');
iText.setPositionByOrigin(new Point(0, 0), 'left', 'top');
testCanvas.add(iText);
testCanvas.on('text:selection:changed', () => {
countCanvas++;
});
iText.on('selection:changed', () => {
count++;
});
});
afterEach(() => testCanvas.dispose());
test(`click on editing itext make selection:changed fire`, async () => {
expect(
testCanvas.getActiveObject(),
'no active object exist',
).toBeUndefined();
expect(count, 'no selection:changed fired yet').toBe(0);
expect(countCanvas, 'no text:selection:changed fired yet').toBe(0);
testCanvas._onMouseDown(eventData);
testCanvas._onMouseUp(eventData);
expect(testCanvas.getActiveObject(), 'Itext got selected').toBe(iText);
expect(iText.isEditing, 'Itext is not editing yet').toBe(false);
expect(count, 'no selection:changed fired yet').toBe(0);
expect(countCanvas, 'no text:selection:changed fired yet').toBe(0);
expect(
iText.selectionStart,
'Itext did not set the selectionStart',
).toBe(0);
expect(iText.selectionEnd, 'Itext did not set the selectionend').toBe(
0,
);
// make a little delay or it will act as double click and select everything
await new Promise((resolve) => setTimeout(resolve, 500));
testCanvas._onMouseDown(eventData);
testCanvas._onMouseUp(eventData);
expect(iText.isEditing, 'Itext entered editing').toBe(true);
expect(iText.selectionStart, 'Itext set the selectionStart').toBe(2);
expect(iText.selectionEnd, 'Itext set the selectionend').toBe(2);
expect(count, 'no selection:changed fired yet').toBe(1);
expect(countCanvas, 'no text:selection:changed fired yet').toBe(1);
});
});
});
});