From 432e1aded6a53eccfbea9cd3cbaa464b38e52f1e Mon Sep 17 00:00:00 2001 From: Shachar <34343793+ShaMan123@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:19:08 +0200 Subject: [PATCH] tests(): migrate target hit tests to jest and drag and drop tests to playwright (#9333) --- CHANGELOG.md | 1 + e2e/tests/text/drag&drop/index.html | 2 + e2e/tests/text/drag&drop/index.spec.ts | 282 ++++++- .../index.spec.ts-snapshots/3-events.json | 338 ++++++++ .../index.spec.ts-snapshots/4-events.json | 221 +++++ .../index.spec.ts-snapshots/5-events.json | 221 +++++ .../6-drop-textarea.png | Bin 0 -> 45511 bytes .../index.spec.ts-snapshots/6-events.json | 50 ++ ...drop-textarea-to-B-lor-dolor-fabrictur.png | Bin 0 -> 45869 bytes .../index.spec.ts-snapshots/7-events.json | 207 +++++ ...draggableTextDelegate-isActive-events.json | 1 + ...ag-draggableTextDelegate-start-events.json | 74 ++ .../disabling-drag-onDragStart-events.json | 1 + .../disabling-drop-A-events.json | 128 +++ .../disabling-drop-B-events.json | 128 +++ .../drag-image-em---tge.json | 508 ++++++++++- .../drag-image-fabric.json | 98 ++- e2e/tests/text/drag&drop/index.ts | 133 ++- e2e/utils/ObjectUtil.ts | 2 +- e2e/utils/TextUtil.ts | 9 + .../__snapshots__/eventData.test.ts.snap | 4 +- src/canvas/__tests__/eventData.test.ts | 792 +++++++++++++++++- test/unit/canvas.js | 386 --------- test/unit/canvas_events.js | 64 -- test/unit/draggable_text.js | 613 -------------- 25 files changed, 3098 insertions(+), 1165 deletions(-) create mode 100644 e2e/tests/text/drag&drop/index.html create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/3-events.json create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/4-events.json create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/5-events.json create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/6-drop-textarea.png create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/6-events.json create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/7-drop-textarea-to-B-lor-dolor-fabrictur.png create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/7-events.json create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-draggableTextDelegate-isActive-events.json create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-draggableTextDelegate-start-events.json create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-onDragStart-events.json create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drop-A-events.json create mode 100644 e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drop-B-events.json delete mode 100644 test/unit/draggable_text.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e74e609cc0..5a23e716a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- tests(): migrate target hit tests to jest and drag and drop test to playwright [#9333](https://github.com/fabricjs/fabric.js/pull/9333) - fix(SVGParser): avoid crashing on SVG that use @import css feature [#9602](https://github.com/fabricjs/fabric.js/pull/9602) - fix(): compositionEnd event handler is not registered correctly. (regression from f91362c ) [#9610](https://github.com/fabricjs/fabric.js/pull/9610) - ci(): Add a test case from the multiple selection use case for groups [#9599](https://github.com/fabricjs/fabric.js/pull/9599) diff --git a/e2e/tests/text/drag&drop/index.html b/e2e/tests/text/drag&drop/index.html new file mode 100644 index 00000000000..a7a979605e1 --- /dev/null +++ b/e2e/tests/text/drag&drop/index.html @@ -0,0 +1,2 @@ + + diff --git a/e2e/tests/text/drag&drop/index.spec.ts b/e2e/tests/text/drag&drop/index.spec.ts index bcebbfbf3a4..db7c957e305 100644 --- a/e2e/tests/text/drag&drop/index.spec.ts +++ b/e2e/tests/text/drag&drop/index.spec.ts @@ -1,16 +1,19 @@ import type { Locator, Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; +import type { IText } from '../../../..'; import setup from '../../../setup'; +import { TextUtil } from '../../../utils/TextUtil'; import { binaryToBuffer } from '../../../utils/binaryToBuffer'; +import { CanvasUtil } from '../../../utils/CanvasUtil'; setup(); const dragA = 'fabric'; const dragB = 'em ipsum\ndolor\nsit Amet2\nconsectge'; -test('Drag & Drop', async ({ page }) => { +const selectFabricInA = (page: Page) => { const canvas = page.locator('canvas').nth(1); - await test.step(`select "${dragA}" in A`, async () => { + return test.step(`select "${dragA}" in A`, async () => { await canvas.click({ position: { x: 130, @@ -25,17 +28,65 @@ test('Drag & Drop', async ({ page }) => { }, }); }); - await test.step('drag & drop to end', async () => { +}; + +const readEventStream = async (page: Page) => { + const data = await new CanvasUtil(page).executeInBrowser((canvas) => + canvas.readEventStream() + ); + return JSON.stringify(data, null, 2); +}; + +test('Drag & Drop', async ({ page }) => { + const canvas = page.locator('canvas').nth(1); + const textarea = page.locator('#textarea'); + const a = new TextUtil(page, 'a'); + const b = new TextUtil(page, 'b'); + + await selectFabricInA(page); + + await test.step('click sets the cursor', async () => { + await page.mouse.move(130, 50); await page.mouse.down(); - await page.mouse.move(0, 140, { steps: 40 }); - await page.mouse.move(435, 55, { steps: 40 }); + expect( + await a.executeInBrowser((text) => [ + text['draggableTextDelegate'].isActive(), + text.shouldStartDragging(), + ]) + ).toEqual([true, true]); + expect(await a.isCursorActive()).toBeFalsy(); + await page.mouse.up(); + expect( + await a.executeInBrowser((text) => [ + text['draggableTextDelegate'].isActive(), + text.shouldStartDragging(), + ]) + ).toEqual([false, false]); + await a.expectObjectToMatch({ + selectionStart: 3, + selectionEnd: 3, + }); + expect(await a.isCursorActive()).toBeTruthy(); + }); + + await selectFabricInA(page); + + // clean the stream + await readEventStream(page); + + await test.step('drag A & drop on self at end', async () => { + await page.mouse.down(); + await page.mouse.move(0, 140, { steps: 10 }); + await page.mouse.move(435, 55, { steps: 10 }); expect( await canvas.screenshot(), `1. drag "${dragA}" over "lor|em" (A => B)` ).toMatchSnapshot({ name: '1.drag-fabric-over-lor|em.png', }); - await page.mouse.move(240, 140, { steps: 40 }); + await page.mouse.move(400, 70, { steps: 10 }); + await page.mouse.move(250, 130, { steps: 10 }); + await page.mouse.move(240, 140, { steps: 10 }); expect( await canvas.screenshot(), `2. before dropping "${dragA}" => "sandbox|" (A => A)` @@ -49,6 +100,9 @@ test('Drag & Drop', async ({ page }) => { ).toMatchSnapshot({ name: '3.drop-fabric-after-sandbox.png', }); + expect(await readEventStream(page)).toMatchSnapshot({ + name: '3.events.json', + }); }); await test.step(`drag & drop to B(3) = "lor|${dragA}|em"`, async () => { @@ -62,12 +116,18 @@ test('Drag & Drop', async ({ page }) => { y: 55, }, }); + expect(await page.evaluate(() => document.activeElement)).toBe( + await b.executeInBrowser((text) => text.hiddenTextarea) + ); expect( await canvas.screenshot(), `4. drag & drop "${dragA}" => "lor|${dragA}|em" (A => B(3))` ).toMatchSnapshot({ name: '4.drop--lor|fabric|em.png', }); + expect(await readEventStream(page)).toMatchSnapshot({ + name: '4.events.json', + }); }); await test.step('select B', async () => { @@ -80,6 +140,7 @@ test('Drag & Drop', async ({ page }) => { await page.mouse.down(); await page.mouse.move(580, 300, { steps: 40 }); await page.mouse.up(); + await readEventStream(page); }); await test.step(`drag & drop to A(4) = ".js |${dragB}|sandbox"`, async () => { @@ -93,16 +154,166 @@ test('Drag & Drop', async ({ page }) => { y: 55, }, }); + expect(await page.evaluate(() => document.activeElement)).toBe( + await a.executeInBrowser((text) => text.hiddenTextarea) + ); expect( await canvas.screenshot(), `5. drag & drop "${dragB}" => ".js |${dragB}|sandbox" (B => A(4))` ).toMatchSnapshot({ name: '5..js |em ips.png', }); + expect(await readEventStream(page)).toMatchSnapshot({ + name: '5.events.json', + }); + }); + + await test.step(`drag & drop to textarea = ".js |${dragB}|sandbox"`, async () => { + await canvas.dragTo(textarea, { + sourcePosition: { + x: 120, + y: 55, + }, + }); + // expect(await page.evaluate(() => document.activeElement)).toBe( + // await textarea.elementHandle() + // ); + expect( + await page.screenshot(), + `6. drag & drop ".js |${dragB}|sandbox" to textarea (A => textarea)` + ).toMatchSnapshot({ + name: '6.drop-textarea.png', + }); + expect(await readEventStream(page)).toMatchSnapshot({ + name: '6.events.json', + }); + }); + + await test.step(`drag & drop "dolor" from textarea to B = "lor|dolor|fabrictur"`, async () => { + await page.mouse.move(25, 527); + await page.mouse.dblclick(25, 527); + await page.mouse.down(); + await page.mouse.move(25, 527); + await page.mouse.move(25, 550, { steps: 10 }); + await page.mouse.move(435, 55, { steps: 10 }); + await canvas.hover({ position: { x: 435, y: 55 } }); + await page.mouse.up(); + expect(await page.evaluate(() => document.activeElement)).toBe( + await b.executeInBrowser((text) => text.hiddenTextarea) + ); + expect( + await page.screenshot(), + `7. drag & drop "dolor" to B = "lor|dolor|fabrictur" (textarea => B)` + ).toMatchSnapshot({ + name: '7.drop-textarea-to-B-lor|dolor|fabrictur.png', + }); + expect(await readEventStream(page)).toMatchSnapshot({ + name: '7.events.json', + }); + }); +}); + +for (const options of [ + { + disabled: 'onDragStart', + exec: (text: IText) => (text.onDragStart = () => false), + expected: { + text: 'fabric.js sandbox', + selectionStart: 0, + selectionEnd: 6, + }, + }, + { + disabled: 'draggableTextDelegate#start', + exec: (text: IText) => (text['draggableTextDelegate'].start = () => false), + expected: { + text: 'fabric.js sandbox', + selectionStart: 3, + selectionEnd: 17, + }, + }, + { + disabled: 'draggableTextDelegate#isActive', + exec: (text: IText) => + (text['draggableTextDelegate'].isActive = () => false), + expected: { + text: 'fabric.js sandbox', + selectionStart: 0, + selectionEnd: 6, + }, + }, +] as const) { + test(`Disabling Drag & Drop by disabling ${options.disabled}`, async ({ + page, + }) => { + const a = new TextUtil(page, 'a'); + await test.step('disable dragging', () => a.executeInBrowser(options.exec)); + await selectFabricInA(page); + await readEventStream(page); + + await test.step('drag to end of text', async () => { + await page.mouse.down(); + await page.mouse.move(240, 140, { steps: 40 }); + a.expectObjectToMatch(options.expected); + expect(await readEventStream(page)).toMatchSnapshot({ + name: `disabling-drag-${options.disabled}.events.json`, + }); + }); + }); +} + +test('Disabling Drop', async ({ page }) => { + const canvas = page.locator('canvas').nth(1); + const a = new TextUtil(page, 'a'); + const b = new TextUtil(page, 'b'); + await test.step('disable dropping', () => { + a.executeInBrowser((text) => (text.canDrop = () => false)); + b.executeInBrowser((text) => (text.canDrop = () => false)); + }); + + await test.step('drop A on self', async () => { + await selectFabricInA(page); + await readEventStream(page); + await canvas.dragTo(canvas, { + sourcePosition: { + x: 130, + y: 40, + }, + targetPosition: { + x: 240, + y: 140, + }, + }); + await a.expectObjectToMatch({ text: 'fabric.js sandbox' }); + expect(await readEventStream(page)).toMatchSnapshot({ + name: 'disabling-drop-A.events.json', + }); + }); + + await test.step('drop A on B', async () => { + await selectFabricInA(page); + await readEventStream(page); + await canvas.dragTo(canvas, { + sourcePosition: { + x: 130, + y: 40, + }, + targetPosition: { + x: 435, + y: 55, + }, + }); + await a.expectObjectToMatch({ text: 'fabric.js sandbox' }); + await b.expectObjectToMatch({ + text: 'lorem ipsum\ndolor\nsit Amet2\nconsectgetur', + }); + expect(await readEventStream(page)).toMatchSnapshot({ + name: 'disabling-drop-B.events.json', + }); }); }); -async function waitForDragImage( +async function waitForDataTransfer( page: Page, canvas: Locator, { x, y }: { x: number; y: number } @@ -116,11 +327,28 @@ async function waitForDragImage( }); await page.mouse.down(); const dataTransfer = await page.evaluateHandle(() => - Object.defineProperty(new DataTransfer(), 'setDragImage', { - value: (image, x, y) => - window.dispatchEvent( - new CustomEvent('drag:image', { detail: { image, x, y } }) - ), + Object.defineProperties(new DataTransfer(), { + __data: { + value: {}, + }, + setDragImage: { + value(image, x, y) { + window.dispatchEvent( + new CustomEvent('drag:data', { + detail: { image, x, y, data: this.__data }, + }) + ); + }, + }, + setData: { + value(type, value) { + let out = value; + try { + out = JSON.parse(value); + } catch (error) {} + this.__data[type] = out; + }, + }, }) ); return [ @@ -128,21 +356,25 @@ async function waitForDragImage( page .evaluate( () => - new Promise<{ image: string; x: number; y: number }>((resolve) => + new Promise<{ + image: string; + x: number; + y: number; + data: Record; + }>((resolve) => window.addEventListener( - 'drag:image', - ({ detail: { image, x, y } }) => + 'drag:data', + ({ detail: { image, ...rest } }) => resolve({ image: image.toDataURL(`image/png`, 1), - x, - y, + ...rest, }), { once: true } ) ) ) - .then(({ x, y, image }) => { - return [binaryToBuffer(image), { x, y }] as const; + .then(({ x, y, image, ...data }) => { + return [binaryToBuffer(image), { x, y }, data] as const; }), ] as const; }); @@ -162,18 +394,18 @@ test('Drag Image A', async ({ page }) => { }); await test.step('start dragging', async () => { - const [dragEvent, trigger] = await waitForDragImage(page, canvas, { + const [dragEvent, trigger] = await waitForDataTransfer(page, canvas, { x: 130, y: 40, }); await canvas.dispatchEvent('dragstart', dragEvent); - const [image, position] = await trigger; + const [image, position, data] = await trigger; expect(image, `drag image A: "${dragA}"`).toMatchSnapshot({ name: 'drag-image-fabric.png', maxDiffPixelRatio: 0.03, }); expect( - JSON.stringify(position, null, 2), + JSON.stringify({ position, ...data }, null, 2), `drag image A position: "${dragA}"` ).toMatchSnapshot({ name: 'drag-image-fabric.json', @@ -199,17 +431,17 @@ test('Drag Image B', async ({ page }) => { }); await test.step('start dragging', async () => { - const [dragEvent, trigger] = await waitForDragImage(page, canvas, { + const [dragEvent, trigger] = await waitForDataTransfer(page, canvas, { x: 500, y: 280, }); await canvas.dispatchEvent('dragstart', dragEvent); - const [image, position] = await trigger; + const [image, position, data] = await trigger; expect(image, `drag image B: "${dragB}"`).toMatchSnapshot({ name: 'drag-image-em---tge.png', }); expect( - JSON.stringify(position, null, 2), + JSON.stringify({ position, ...data }, null, 2), `drag image B position: "${dragB}"` ).toMatchSnapshot({ name: 'drag-image-em---tge.json', diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/3-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/3-events.json new file mode 100644 index 00000000000..c349f283871 --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/3-events.json @@ -0,0 +1,338 @@ +[ + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "canvas" + ], + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "a" + ], + [ + "dragenter", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a" + }, + "canvas" + ], + [ + "dragenter", + { + "subTargets": [], + "dragSource": "a", + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 117, + "y": 50 + }, + "scenePoint": { + "x": 117, + "y": 50 + }, + "pointer": { + "x": 117, + "y": 50 + }, + "absolutePointer": { + "x": 117, + "y": 50 + } + }, + "a" + ], + [ + "dragleave", + { + "subTargets": [], + "dragSource": "a", + "canDrop": false, + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 39, + "y": 110 + }, + "scenePoint": { + "x": 39, + "y": 110 + }, + "pointer": { + "x": 39, + "y": 110 + }, + "absolutePointer": { + "x": 39, + "y": 110 + } + }, + "a" + ], + [ + "dragenter", + { + "subTargets": [], + "dragSource": "a", + "canDrop": false, + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 87, + "y": 123 + }, + "scenePoint": { + "x": 87, + "y": 123 + }, + "pointer": { + "x": 87, + "y": 123 + }, + "absolutePointer": { + "x": 87, + "y": 123 + } + }, + "a" + ], + [ + "dragleave", + { + "subTargets": [], + "dragSource": "a", + "canDrop": false, + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 261, + "y": 89 + }, + "scenePoint": { + "x": 261, + "y": 89 + }, + "pointer": { + "x": 261, + "y": 89 + }, + "absolutePointer": { + "x": 261, + "y": 89 + } + }, + "a" + ], + [ + "dragenter", + { + "subTargets": [], + "dragSource": "a", + "canDrop": false, + "e": { + "isTrusted": true + }, + "target": "b", + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "b" + ], + [ + "dragleave", + { + "subTargets": [], + "dragSource": "a", + "canDrop": false, + "e": { + "isTrusted": true + }, + "target": "b", + "viewportPoint": { + "x": 385, + "y": 76 + }, + "scenePoint": { + "x": 385, + "y": 76 + }, + "pointer": { + "x": 385, + "y": 76 + }, + "absolutePointer": { + "x": 385, + "y": 76 + } + }, + "b" + ], + [ + "dragenter", + { + "subTargets": [], + "dragSource": "a", + "canDrop": false, + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 250, + "y": 130 + }, + "scenePoint": { + "x": 250, + "y": 130 + }, + "pointer": { + "x": 250, + "y": 130 + }, + "absolutePointer": { + "x": 250, + "y": 130 + } + }, + "a" + ], + [ + "drop", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "viewportPoint": { + "x": 240, + "y": 140 + }, + "scenePoint": { + "x": 240, + "y": 140 + }, + "didDrop": false, + "pointer": { + "x": 240, + "y": 140 + }, + "absolutePointer": { + "x": 240, + "y": 140 + } + }, + "canvas" + ], + [ + "changed", + { + "index": 11, + "action": "drop" + }, + "a" + ], + [ + "text:changed", + { + "target": "a" + }, + "canvas" + ], + [ + "drop", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "viewportPoint": { + "x": 240, + "y": 140 + }, + "scenePoint": { + "x": 240, + "y": 140 + }, + "didDrop": true, + "dropTarget": "a", + "pointer": { + "x": 240, + "y": 140 + }, + "absolutePointer": { + "x": 240, + "y": 140 + } + }, + "a" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": true, + "dropTarget": "a" + }, + "canvas" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": true, + "dropTarget": "a" + }, + "a" + ] +] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/4-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/4-events.json new file mode 100644 index 00000000000..103c7bc437e --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/4-events.json @@ -0,0 +1,221 @@ +[ + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "canvas" + ], + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "a" + ], + [ + "dragenter", + { + "e": { + "isTrusted": true + }, + "target": "b", + "subTargets": [], + "dragSource": "a" + }, + "canvas" + ], + [ + "dragleave", + { + "subTargets": [], + "dragSource": "a", + "e": { + "isTrusted": true + }, + "target": "a", + "nextTarget": "b", + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "a" + ], + [ + "dragenter", + { + "subTargets": [], + "dragSource": "a", + "e": { + "isTrusted": true + }, + "target": "b", + "previousTarget": "a", + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "b" + ], + [ + "drop", + { + "e": { + "isTrusted": true + }, + "target": "b", + "subTargets": [], + "dragSource": "a", + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "didDrop": false, + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "canvas" + ], + [ + "selection:changed", + {}, + "b" + ], + [ + "text:selection:changed", + { + "target": "b" + }, + "canvas" + ], + [ + "changed", + { + "index": 3, + "action": "drop" + }, + "b" + ], + [ + "text:changed", + { + "target": "b" + }, + "canvas" + ], + [ + "drop", + { + "e": { + "isTrusted": true + }, + "target": "b", + "subTargets": [], + "dragSource": "a", + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "didDrop": true, + "dropTarget": "b", + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "b" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": true, + "dropTarget": "b" + }, + "canvas" + ], + [ + "changed", + { + "index": 11, + "action": "dragend" + }, + "a" + ], + [ + "text:changed", + { + "target": "a" + }, + "canvas" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": true, + "dropTarget": "b" + }, + "a" + ] +] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/5-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/5-events.json new file mode 100644 index 00000000000..4688bb9b7fc --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/5-events.json @@ -0,0 +1,221 @@ +[ + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "b" + }, + "canvas" + ], + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "b" + }, + "b" + ], + [ + "dragenter", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "b" + }, + "canvas" + ], + [ + "dragleave", + { + "subTargets": [], + "dragSource": "b", + "e": { + "isTrusted": true + }, + "target": "b", + "nextTarget": "a", + "viewportPoint": { + "x": 120, + "y": 55 + }, + "scenePoint": { + "x": 120, + "y": 55 + }, + "pointer": { + "x": 120, + "y": 55 + }, + "absolutePointer": { + "x": 120, + "y": 55 + } + }, + "b" + ], + [ + "dragenter", + { + "subTargets": [], + "dragSource": "b", + "e": { + "isTrusted": true + }, + "target": "a", + "previousTarget": "b", + "viewportPoint": { + "x": 120, + "y": 55 + }, + "scenePoint": { + "x": 120, + "y": 55 + }, + "pointer": { + "x": 120, + "y": 55 + }, + "absolutePointer": { + "x": 120, + "y": 55 + } + }, + "a" + ], + [ + "drop", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "b", + "viewportPoint": { + "x": 120, + "y": 55 + }, + "scenePoint": { + "x": 120, + "y": 55 + }, + "didDrop": false, + "pointer": { + "x": 120, + "y": 55 + }, + "absolutePointer": { + "x": 120, + "y": 55 + } + }, + "canvas" + ], + [ + "selection:changed", + {}, + "a" + ], + [ + "text:selection:changed", + { + "target": "a" + }, + "canvas" + ], + [ + "changed", + { + "index": 4, + "action": "drop" + }, + "a" + ], + [ + "text:changed", + { + "target": "a" + }, + "canvas" + ], + [ + "drop", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "b", + "viewportPoint": { + "x": 120, + "y": 55 + }, + "scenePoint": { + "x": 120, + "y": 55 + }, + "didDrop": true, + "dropTarget": "a", + "pointer": { + "x": 120, + "y": 55 + }, + "absolutePointer": { + "x": 120, + "y": 55 + } + }, + "a" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "b", + "subTargets": [], + "dragSource": "b", + "didDrop": true, + "dropTarget": "a" + }, + "canvas" + ], + [ + "changed", + { + "index": 9, + "action": "dragend" + }, + "b" + ], + [ + "text:changed", + { + "target": "b" + }, + "canvas" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "b", + "subTargets": [], + "dragSource": "b", + "didDrop": true, + "dropTarget": "a" + }, + "b" + ] +] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/6-drop-textarea.png b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/6-drop-textarea.png new file mode 100644 index 0000000000000000000000000000000000000000..d0c22431fabbccc4634eb024c7d9681d3999b221 GIT binary patch literal 45511 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 PG87K09Ev}vd+omg&K=zj literal 0 HcmV?d00001 diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/6-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/6-events.json new file mode 100644 index 00000000000..a32dee8a559 --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/6-events.json @@ -0,0 +1,50 @@ +[ + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "canvas" + ], + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "a" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": true, + "dropTarget": "a" + }, + "canvas" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": true, + "dropTarget": "a" + }, + "a" + ] +] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/7-drop-textarea-to-B-lor-dolor-fabrictur.png b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/7-drop-textarea-to-B-lor-dolor-fabrictur.png new file mode 100644 index 0000000000000000000000000000000000000000..2727eaa1eeedf77688251058cec1b40d4c9fb269 GIT binary patch literal 45869 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 literal 0 HcmV?d00001 diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/7-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/7-events.json new file mode 100644 index 00000000000..fd6d197556b --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/7-events.json @@ -0,0 +1,207 @@ +[ + [ + "dragenter", + { + "e": { + "isTrusted": true + }, + "subTargets": [] + }, + "canvas" + ], + [ + "dragleave", + { + "subTargets": [], + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 107, + "y": 451 + }, + "scenePoint": { + "x": 107, + "y": 451 + }, + "pointer": { + "x": 107, + "y": 451 + }, + "absolutePointer": { + "x": 107, + "y": 451 + } + }, + "a" + ], + [ + "dragenter", + { + "subTargets": [], + "canDrop": false, + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 189, + "y": 352 + }, + "scenePoint": { + "x": 189, + "y": 352 + }, + "pointer": { + "x": 189, + "y": 352 + }, + "absolutePointer": { + "x": 189, + "y": 352 + } + }, + "a" + ], + [ + "dragleave", + { + "subTargets": [], + "canDrop": false, + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 271, + "y": 253 + }, + "scenePoint": { + "x": 271, + "y": 253 + }, + "pointer": { + "x": 271, + "y": 253 + }, + "absolutePointer": { + "x": 271, + "y": 253 + } + }, + "a" + ], + [ + "dragenter", + { + "subTargets": [], + "canDrop": false, + "e": { + "isTrusted": true + }, + "target": "b", + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "b" + ], + [ + "drop", + { + "e": { + "isTrusted": true + }, + "target": "b", + "subTargets": [], + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "didDrop": false, + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "canvas" + ], + [ + "selection:changed", + {}, + "b" + ], + [ + "text:selection:changed", + { + "target": "b" + }, + "canvas" + ], + [ + "changed", + { + "index": 3, + "action": "drop" + }, + "b" + ], + [ + "text:changed", + { + "target": "b" + }, + "canvas" + ], + [ + "drop", + { + "e": { + "isTrusted": true + }, + "target": "b", + "subTargets": [], + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "didDrop": true, + "dropTarget": "b", + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "b" + ] +] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-draggableTextDelegate-isActive-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-draggableTextDelegate-isActive-events.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-draggableTextDelegate-isActive-events.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-draggableTextDelegate-start-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-draggableTextDelegate-start-events.json new file mode 100644 index 00000000000..94c5979446d --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-draggableTextDelegate-start-events.json @@ -0,0 +1,74 @@ +[ + [ + "selection:changed", + {}, + "a" + ], + [ + "text:selection:changed", + { + "target": "a" + }, + "canvas" + ], + [ + "selection:changed", + {}, + "a" + ], + [ + "text:selection:changed", + { + "target": "a" + }, + "canvas" + ], + [ + "selection:changed", + {}, + "a" + ], + [ + "text:selection:changed", + { + "target": "a" + }, + "canvas" + ], + [ + "selection:changed", + {}, + "a" + ], + [ + "text:selection:changed", + { + "target": "a" + }, + "canvas" + ], + [ + "selection:changed", + {}, + "a" + ], + [ + "text:selection:changed", + { + "target": "a" + }, + "canvas" + ], + [ + "selection:changed", + {}, + "a" + ], + [ + "text:selection:changed", + { + "target": "a" + }, + "canvas" + ] +] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-onDragStart-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-onDragStart-events.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drag-onDragStart-events.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drop-A-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drop-A-events.json new file mode 100644 index 00000000000..268b0f549d1 --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drop-A-events.json @@ -0,0 +1,128 @@ +[ + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "canvas" + ], + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "a" + ], + [ + "dragenter", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a" + }, + "canvas" + ], + [ + "dragenter", + { + "subTargets": [], + "dragSource": "a", + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 240, + "y": 140 + }, + "scenePoint": { + "x": 240, + "y": 140 + }, + "pointer": { + "x": 240, + "y": 140 + }, + "absolutePointer": { + "x": 240, + "y": 140 + } + }, + "a" + ], + [ + "dragleave", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a" + }, + "canvas" + ], + [ + "dragleave", + { + "subTargets": [], + "dragSource": "a", + "e": { + "isTrusted": true + }, + "target": "a", + "viewportPoint": { + "x": 240, + "y": 140 + }, + "scenePoint": { + "x": 240, + "y": 140 + }, + "pointer": { + "x": 240, + "y": 140 + }, + "absolutePointer": { + "x": 240, + "y": 140 + } + }, + "a" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": false + }, + "canvas" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": false + }, + "a" + ] +] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drop-B-events.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drop-B-events.json new file mode 100644 index 00000000000..7e274f07829 --- /dev/null +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/disabling-drop-B-events.json @@ -0,0 +1,128 @@ +[ + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "canvas" + ], + [ + "dragstart", + { + "e": { + "isTrusted": true + }, + "target": "a" + }, + "a" + ], + [ + "dragenter", + { + "e": { + "isTrusted": true + }, + "target": "b", + "subTargets": [], + "dragSource": "a" + }, + "canvas" + ], + [ + "dragenter", + { + "subTargets": [], + "dragSource": "a", + "e": { + "isTrusted": true + }, + "target": "b", + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "b" + ], + [ + "dragleave", + { + "e": { + "isTrusted": true + }, + "target": "b", + "subTargets": [], + "dragSource": "a" + }, + "canvas" + ], + [ + "dragleave", + { + "subTargets": [], + "dragSource": "a", + "e": { + "isTrusted": true + }, + "target": "b", + "viewportPoint": { + "x": 435, + "y": 55 + }, + "scenePoint": { + "x": 435, + "y": 55 + }, + "pointer": { + "x": 435, + "y": 55 + }, + "absolutePointer": { + "x": 435, + "y": 55 + } + }, + "b" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": false + }, + "canvas" + ], + [ + "dragend", + { + "e": { + "isTrusted": true + }, + "target": "a", + "subTargets": [], + "dragSource": "a", + "didDrop": false + }, + "a" + ] +] \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/drag-image-em---tge.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/drag-image-em---tge.json index c6bae5b5a0a..f0147530031 100644 --- a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/drag-image-em---tge.json +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/drag-image-em---tge.json @@ -1,4 +1,508 @@ { - "x": -400, - "y": -20 + "position": { + "x": -400, + "y": -20 + }, + "data": { + "text/plain": "em ipsum\ndolor\nsit Amet2\nconsectge", + "application/fabric": { + "value": "em ipsum\ndolor\nsit Amet2\nconsectge", + "styles": [ + { + "fontSize": 50, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "red", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 60, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "red", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "yellow" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": true, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "yellow", + "textDecoration": " line-through" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": true, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "yellow", + "textDecoration": " line-through" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "yellow" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "underline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "underline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "italic", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "green", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "underline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "italic", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "green", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "underline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "italic", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "green", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "underline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "bold", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "blue", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "bold", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "blue", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 63, + "fontWeight": "bold", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "blue", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": true, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": " underline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": true, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": " underline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": true, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": " overline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": true, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": " overline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": true, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": " overline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "#666", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "line-through" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "#666", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "line-through" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "#666", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "line-through" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "#666", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "line-through" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "#666", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": "line-through" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": true, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "", + "textDecoration": " underline" + }, + { + "fontSize": 40, + "fontWeight": "normal", + "fontFamily": "Arial", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": "#ff1e15", + "strokeWidth": 2, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + } + ] + } + } } \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/drag-image-fabric.json b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/drag-image-fabric.json index a6e1452e1b0..5ef7acfb8bb 100644 --- a/e2e/tests/text/drag&drop/index.spec.ts-snapshots/drag-image-fabric.json +++ b/e2e/tests/text/drag&drop/index.spec.ts-snapshots/drag-image-fabric.json @@ -1,4 +1,98 @@ { - "x": -44.5, - "y": -20 + "position": { + "x": -44.5, + "y": -20 + }, + "data": { + "text/plain": "fabric", + "application/fabric": { + "value": "fabric", + "styles": [ + { + "fontSize": 64, + "fontWeight": "bold", + "fontFamily": "Times New Roman", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 64, + "fontWeight": "bold", + "fontFamily": "Times New Roman", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 64, + "fontWeight": "bold", + "fontFamily": "Times New Roman", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 64, + "fontWeight": "bold", + "fontFamily": "Times New Roman", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 64, + "fontWeight": "bold", + "fontFamily": "Times New Roman", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + }, + { + "fontSize": 64, + "fontWeight": "bold", + "fontFamily": "Times New Roman", + "fontStyle": "normal", + "underline": false, + "overline": false, + "linethrough": false, + "stroke": null, + "strokeWidth": 1, + "fill": "rgb(0,0,0)", + "deltaY": 0, + "textBackgroundColor": "" + } + ] + } + } } \ No newline at end of file diff --git a/e2e/tests/text/drag&drop/index.ts b/e2e/tests/text/drag&drop/index.ts index 5bc16e0845b..2c9dc950f46 100644 --- a/e2e/tests/text/drag&drop/index.ts +++ b/e2e/tests/text/drag&drop/index.ts @@ -1,5 +1,5 @@ import * as fabric from 'fabric'; -import { beforeAll } from '../../test'; +import { before } from '../../test'; const styles = { 0: { @@ -77,41 +77,100 @@ const styles = { }, }; -beforeAll( - (canvas) => { - const textValue = 'fabric.js sandbox'; - const a = new fabric.Textbox(textValue, { - originX: 'center', - width: 210, - left: 150, - top: 20, - splitByGrapheme: true, - styles: fabric.util.stylesFromArray( - [ - { - style: { - fontWeight: 'bold', - fontSize: 64, - }, - start: 0, - end: 9, +const roundPoint = (point: fabric.Point) => ({ + x: Math.round(point.x), + y: Math.round(point.y), +}); + +const parseEvent = ( + type: string, + { pointer, absolutePointer, ...ev } = {}, + caller: fabric.Textbox | fabric.Canvas +) => + JSON.parse( + JSON.stringify([ + type, + { + ...ev, + ...(pointer ? { pointer: roundPoint(pointer) } : {}), + ...(absolutePointer + ? { absolutePointer: roundPoint(absolutePointer) } + : {}), + }, + caller, + ]) + ); + +class TestCanvas extends fabric.Canvas { + eventStream = []; + readEventStream() { + const data = this.eventStream; + this.eventStream = []; + return data; + } + toJSON() { + return 'canvas'; + } +} + +const commonEvents = [ + 'dragstart', + // 'dragover', + // 'drag', + 'dragenter', + 'dragleave', + 'drop', + 'dragend', +] as const; + +before('#canvas', (el) => { + const canvas = new TestCanvas(el, { width: 800, height: 500 }); + const textValue = 'fabric.js sandbox'; + const a = new fabric.Textbox(textValue, { + originX: 'center', + width: 210, + left: 150, + top: 20, + splitByGrapheme: true, + styles: fabric.util.stylesFromArray( + [ + { + style: { + fontWeight: 'bold', + fontSize: 64, }, - ], - textValue - ), + start: 0, + end: 9, + }, + ], + textValue + ), + }); + const b = new fabric.Textbox('lorem ipsum\ndolor\nsit Amet2\nconsectgetur', { + left: 400, + top: 20, + objectCaching: false, + fontFamily: 'Arial', + styles, + }); + + ( + [...commonEvents, 'text:selection:changed', 'text:changed'] as const + ).forEach((type) => { + canvas.on(type, (ev) => { + canvas.eventStream.push(parseEvent(type, ev, canvas)); }); - const b = new fabric.Textbox( - 'lorem ipsum\ndolor\nsit Amet2\nconsectgetur', - { - left: 400, - top: 20, - objectCaching: false, - fontFamily: 'Arial', - styles, - } - ); - canvas.add(a, b); - return { a, b }; - }, - { width: 800, height: 500 } -); + }); + ([...commonEvents, 'selection:changed', 'changed'] as const).forEach( + (type) => { + a.on(type, (ev) => canvas.eventStream.push(parseEvent(type, ev, a))); + b.on(type, (ev) => canvas.eventStream.push(parseEvent(type, ev, b))); + } + ); + Object.entries({ a, b }).forEach( + ([key, object]) => (object.toJSON = () => key) + ); + + canvas.add(a, b); + return { canvas, objects: { a, b } }; +}); diff --git a/e2e/utils/ObjectUtil.ts b/e2e/utils/ObjectUtil.ts index 99a32e905fd..0203cb93a31 100644 --- a/e2e/utils/ObjectUtil.ts +++ b/e2e/utils/ObjectUtil.ts @@ -44,7 +44,7 @@ export class ObjectUtil { ); } - async expectObjectToMatch>(expected: T) { + async expectObjectToMatch(expected: Partial) { const snapshot = await this.executeInBrowser((object) => object); expect(snapshot).toMatchObject(expected); } diff --git a/e2e/utils/TextUtil.ts b/e2e/utils/TextUtil.ts index 2f1f454b213..058e86a3bd4 100644 --- a/e2e/utils/TextUtil.ts +++ b/e2e/utils/TextUtil.ts @@ -19,4 +19,13 @@ export class TextUtil extends ObjectUtil { { index } ); } + + isCursorActive() { + return this.executeInBrowser((object) => { + return [ + object['_currentTickState'], + object['_currentTickCompleteState'], + ].some((cursorAnimation) => cursorAnimation && !cursorAnimation.isDone()); + }); + } } diff --git a/src/canvas/__tests__/__snapshots__/eventData.test.ts.snap b/src/canvas/__tests__/__snapshots__/eventData.test.ts.snap index cdf36d9eeea..f0e81fa324d 100644 --- a/src/canvas/__tests__/__snapshots__/eventData.test.ts.snap +++ b/src/canvas/__tests__/__snapshots__/eventData.test.ts.snap @@ -700,7 +700,7 @@ exports[`Canvas event data HTML event "wheel" should fire a corresponding canvas ] `; -exports[`should fire mouse over/out events on target 1`] = ` +exports[`Event targets should fire mouse over/out events on target 1`] = ` [ [ "mouseover", @@ -784,7 +784,7 @@ exports[`should fire mouse over/out events on target 1`] = ` ] `; -exports[`should fire mouse over/out events on target 2`] = ` +exports[`Event targets should fire mouse over/out events on target 2`] = ` [ [ "mouse:move:before", diff --git a/src/canvas/__tests__/eventData.test.ts b/src/canvas/__tests__/eventData.test.ts index 611549538d5..3d32723cdc6 100644 --- a/src/canvas/__tests__/eventData.test.ts +++ b/src/canvas/__tests__/eventData.test.ts @@ -1,15 +1,24 @@ /* eslint-disable no-restricted-globals */ import '../../../jest.extend'; -import type { TPointerEvent } from '../../EventTypeDefs'; import { Point } from '../../Point'; +import { ActiveSelection } from '../../shapes/ActiveSelection'; +import { Circle } from '../../shapes/Circle'; import { Group } from '../../shapes/Group'; import { IText } from '../../shapes/IText/IText'; import { FabricObject } from '../../shapes/Object/FabricObject'; +import { Rect } from '../../shapes/Rect'; +import { Triangle } from '../../shapes/Triangle'; import type { TMat2D } from '../../typedefs'; import { Canvas } from '../Canvas'; const genericVpt = [2.3, 0, 0, 2.3, 120, 80] as TMat2D; +const registerTestObjects = (objects: Record) => { + Object.entries(objects).forEach(([key, object]) => { + jest.spyOn(object, 'toJSON').mockReturnValue(key); + }); +}; + describe('Canvas event data', () => { let canvas: Canvas; let spy: jest.SpyInstance; @@ -131,38 +140,755 @@ describe('Canvas event data', () => { }); }); -it('A selected subtarget should not fire an event twice', () => { - const target = new FabricObject(); - const group = new Group([target], { - subTargetCheck: true, - interactive: true, +describe('Event targets', () => { + it('A selected subtarget should not fire an event twice', () => { + const target = new FabricObject(); + const group = new Group([target], { + subTargetCheck: true, + interactive: true, + }); + const canvas = new Canvas(); + canvas.add(group); + const targetSpy = jest.fn(); + target.on('mousedown', targetSpy); + jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); + canvas.getSelectionElement().dispatchEvent( + new MouseEvent('mousedown', { + clientX: 50, + clientY: 50, + }) + ); + expect(targetSpy).toHaveBeenCalledTimes(1); }); - const canvas = new Canvas(); - canvas.add(group); - const targetSpy = jest.fn(); - target.on('mousedown', targetSpy); - jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); - canvas.__onMouseDown({ - target: canvas.getSelectionElement(), - clientX: 0, - clientY: 0, - } as unknown as TPointerEvent); - expect(targetSpy).toHaveBeenCalledTimes(1); -}); -it('should fire mouse over/out events on target', () => { - const target = new FabricObject({ width: 10, height: 10 }); - const canvas = new Canvas(); - canvas.add(target); - - jest.spyOn(target, 'toJSON').mockReturnValue('target'); - - const targetSpy = jest.spyOn(target, 'fire'); - const canvasSpy = jest.spyOn(canvas, 'fire'); - const enter = new MouseEvent('mousemove', { clientX: 5, clientY: 5 }); - const exit = new MouseEvent('mousemove', { clientX: 20, clientY: 20 }); - canvas._onMouseMove(enter); - canvas._onMouseMove(exit); - expect(targetSpy.mock.calls).toMatchSnapshot(); - expect(canvasSpy.mock.calls).toMatchSnapshot(); + test('mouseover and mouseout with subTargetCheck', () => { + const rect1 = new FabricObject({ + width: 5, + height: 5, + left: 5, + top: 0, + strokeWidth: 0, + }); + const rect2 = new FabricObject({ + width: 5, + height: 5, + left: 5, + top: 5, + strokeWidth: 0, + }); + const rect3 = new FabricObject({ + width: 5, + height: 5, + left: 0, + top: 5, + strokeWidth: 0, + }); + const rect4 = new FabricObject({ + width: 5, + height: 5, + left: 0, + top: 0, + strokeWidth: 0, + }); + const rect5 = new FabricObject({ + width: 5, + height: 5, + left: 2.5, + top: 2.5, + strokeWidth: 0, + }); + const group1 = new Group([rect1, rect2], { + subTargetCheck: true, + }); + const group2 = new Group([rect3, rect4], { + subTargetCheck: true, + }); + // a group with 2 groups, with 2 rects each, one group left one group right + // each with 2 rects vertically aligned + const group = new Group([group1, group2], { + subTargetCheck: true, + }); + + const enter = jest.fn(); + const exit = jest.fn(); + + const getTargetsFromEventStream = (mock: jest.Mock) => + mock.mock.calls.map((args) => args[0].target); + + registerTestObjects({ + rect1, + rect2, + rect3, + rect4, + rect5, + group1, + group2, + group, + }); + + Object.values({ + rect1, + rect2, + rect3, + rect4, + rect5, + group1, + group2, + group, + }).forEach((object) => { + object.on('mouseover', enter); + object.on('mouseout', exit); + }); + + const canvas = new Canvas(); + canvas.add(group, rect5); + + const fire = (x: number, y: number) => { + enter.mockClear(); + exit.mockClear(); + canvas + .getSelectionElement() + .dispatchEvent(new MouseEvent('mousemove', { clientX: x, clientY: y })); + }; + + fire(1, 1); + expect(getTargetsFromEventStream(enter)).toEqual([group, rect4, group2]); + expect(getTargetsFromEventStream(exit)).toEqual([]); + + fire(5, 5); + expect(getTargetsFromEventStream(enter)).toEqual([rect5]); + expect(getTargetsFromEventStream(exit)).toEqual([group, rect4, group2]); + + fire(9, 9); + expect(getTargetsFromEventStream(enter)).toEqual([group, rect2, group1]); + expect(getTargetsFromEventStream(exit)).toEqual([rect5]); + + fire(9, 1); + expect(getTargetsFromEventStream(enter)).toEqual([rect1]); + expect(getTargetsFromEventStream(exit)).toEqual([rect2]); + }); + + describe('findTarget', () => { + const mockEvent = ({ + canvas, + ...init + }: MouseEventInit & { canvas: Canvas }) => { + const e = new MouseEvent('mousedown', { + ...init, + }); + jest + .spyOn(e, 'target', 'get') + .mockReturnValue(canvas.getSelectionElement()); + return e; + }; + + const findTarget = (canvas: Canvas, ev?: MouseEventInit) => { + const target = canvas.findTarget( + mockEvent({ canvas, clientX: 0, clientY: 0, ...ev }) + ); + const targets = canvas.targets; + canvas.targets = []; + return { target, targets }; + }; + + test.skip.each([true, false])( + 'findTargetsTraversal: search all is %s', + (searchAll) => { + const subTarget1 = new FabricObject(); + const target1 = new Group([subTarget1], { + subTargetCheck: true, + interactive: true, + }); + const subTarget2 = new FabricObject(); + const target2 = new Group([subTarget2], { + subTargetCheck: true, + }); + const parent = new Group([target1, target2], { + subTargetCheck: true, + interactive: true, + }); + registerTestObjects({ + subTarget1, + target1, + subTarget2, + target2, + parent, + }); + + const canvas = new Canvas(); + canvas.add(parent); + + jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); + const found = canvas['findTargetsTraversal']([parent], new Point(), { + searchStrategy: searchAll ? 'search-all' : 'first-hit', + }); + expect(found).toEqual( + searchAll + ? [subTarget2, target2, subTarget1, target1, parent] + : [subTarget2, target2, parent] + ); + } + ); + + test.failing('searchPossibleTargets', () => { + const subTarget = new FabricObject(); + const target = new Group([subTarget], { + subTargetCheck: true, + }); + const parent = new Group([target], { + subTargetCheck: true, + interactive: true, + }); + registerTestObjects({ subTarget, target, parent }); + + const canvas = new Canvas(); + canvas.add(parent); + + jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); + const found = canvas.searchPossibleTargets([parent], new Point()); + expect(found).toBe(target); + expect(canvas.targets).toEqual([subTarget, target, parent]); + }); + + test('searchPossibleTargets with selection', () => { + const subTarget = new FabricObject(); + const target = new Group([subTarget], { + subTargetCheck: true, + }); + const other = new FabricObject(); + const activeSelection = new ActiveSelection(); + registerTestObjects({ subTarget, target, other, activeSelection }); + + const canvas = new Canvas(undefined, { activeSelection }); + canvas.add(other, target); + activeSelection.add(target, other); + canvas.setActiveObject(activeSelection); + + jest.spyOn(canvas, '_checkTarget').mockReturnValue(true); + const found = canvas.searchPossibleTargets( + [activeSelection], + new Point() + ); + expect(found).toBe(activeSelection); + expect(canvas.targets).toEqual([]); + }); + + test('findTarget clears prev targets', () => { + const canvas = new Canvas(); + canvas.targets = [new FabricObject()]; + expect(findTarget(canvas, { clientX: 0, clientY: 0 })).toEqual({ + target: undefined, + targets: [], + }); + }); + + test('findTarget preserveObjectStacking false', () => { + const rect = new FabricObject({ + left: 0, + top: 0, + width: 10, + height: 10, + controls: {}, + }); + const rectOver = new FabricObject({ + left: 0, + top: 0, + width: 10, + height: 10, + controls: {}, + }); + registerTestObjects({ rect, rectOver }); + + const canvas = new Canvas(undefined, { preserveObjectStacking: false }); + canvas.add(rect, rectOver); + canvas.setActiveObject(rect); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: rect, + targets: [], + }); + }); + + test('findTarget preserveObjectStacking true', () => { + const rect = new FabricObject({ left: 0, top: 0, width: 30, height: 30 }); + const rectOver = new FabricObject({ + left: 0, + top: 0, + width: 30, + height: 30, + }); + registerTestObjects({ rect, rectOver }); + + const canvas = new Canvas(undefined, { preserveObjectStacking: true }); + canvas.add(rect, rectOver); + + const e = { + clientX: 15, + clientY: 15, + shiftKey: true, + }; + const e2 = { clientX: 4, clientY: 4 }; + + expect(findTarget(canvas, e)).toEqual( + { target: rectOver, targets: [] } + // 'Should return the rectOver, rect is not considered' + ); + + canvas.setActiveObject(rect); + expect(findTarget(canvas, e)).toEqual( + { target: rectOver, targets: [] } + // 'Should still return rectOver because is above active object' + ); + + expect(findTarget(canvas, e2)).toEqual( + { target: rect, targets: [] } + // 'Should rect because a corner of the activeObject has been hit' + ); + + canvas.altSelectionKey = 'shiftKey'; + expect(findTarget(canvas, e)).toEqual( + { target: rect, targets: [] } + // 'Should rect because active and altSelectionKey is pressed' + ); + }); + + test('findTarget with subTargetCheck', () => { + const canvas = new Canvas(); + const rect = new FabricObject({ left: 0, top: 0, width: 10, height: 10 }); + const rect2 = new FabricObject({ + left: 30, + top: 30, + width: 10, + height: 10, + }); + const group = new Group([rect, rect2]); + registerTestObjects({ rect, rect2, group }); + canvas.add(group); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: group, + targets: [], + }); + + expect(findTarget(canvas, { clientX: 35, clientY: 35 })).toEqual({ + target: group, + targets: [], + }); + + group.subTargetCheck = true; + group.setCoords(); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: group, + targets: [rect], + }); + + expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ + target: group, + targets: [], + }); + + expect(findTarget(canvas, { clientX: 35, clientY: 35 })).toEqual({ + target: group, + targets: [rect2], + }); + }); + + test('findTarget with subTargetCheck and canvas zoom', () => { + const nested1 = new FabricObject({ + width: 100, + height: 100, + fill: 'yellow', + }); + const nested2 = new FabricObject({ + width: 100, + height: 100, + left: 100, + top: 100, + fill: 'purple', + }); + const nestedGroup = new Group([nested1, nested2], { + scaleX: 0.5, + scaleY: 0.5, + top: 100, + left: 0, + subTargetCheck: true, + }); + const rect1 = new FabricObject({ + width: 100, + height: 100, + fill: 'red', + }); + const rect2 = new FabricObject({ + width: 100, + height: 100, + left: 100, + top: 100, + fill: 'blue', + }); + const group = new Group([rect1, rect2, nestedGroup], { + top: -150, + left: -50, + subTargetCheck: true, + }); + registerTestObjects({ + rect1, + rect2, + nested1, + nested2, + nestedGroup, + group, + }); + + const canvas = new Canvas(undefined, { + viewportTransform: [0.1, 0, 0, 0.1, 100, 200], + }); + canvas.add(group); + + expect(findTarget(canvas, { clientX: 96, clientY: 186 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 98, clientY: 188 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 100, clientY: 190 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 102, clientY: 192 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 104, clientY: 194 })).toEqual({ + target: group, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 106, clientY: 196 })).toEqual({ + target: group, + targets: [rect2], + }); + }); + + test.each([true, false])( + 'findTarget on activeObject with subTargetCheck and preserveObjectStacking %s', + (preserveObjectStacking) => { + const rect = new FabricObject({ + left: 0, + top: 0, + width: 10, + height: 10, + }); + const rect2 = new FabricObject({ + left: 30, + top: 30, + width: 10, + height: 10, + }); + const group = new Group([rect, rect2], { subTargetCheck: true }); + registerTestObjects({ rect, rect2, group }); + + const canvas = new Canvas(undefined, { preserveObjectStacking }); + canvas.add(group); + canvas.setActiveObject(group); + + expect(findTarget(canvas, { clientX: 9, clientY: 9 })).toEqual({ + target: group, + targets: [rect], + }); + } + ); + + test('findTarget with perPixelTargetFind', () => { + const triangle = new Triangle({ width: 30, height: 30 }); + registerTestObjects({ triangle }); + + const canvas = new Canvas(); + canvas.add(triangle); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: triangle, + targets: [], + }); + + canvas.perPixelTargetFind = true; + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: undefined, + targets: [], + }); + expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ + target: triangle, + targets: [], + }); + }); + + describe('findTarget with perPixelTargetFind in nested group', () => { + const prepareTest = () => { + const deepTriangle = new Triangle({ + left: 0, + top: 0, + width: 30, + height: 30, + fill: 'yellow', + }); + const triangle2 = new Triangle({ + left: 100, + top: 120, + width: 30, + height: 30, + angle: 100, + fill: 'pink', + }); + const deepCircle = new Circle({ + radius: 30, + top: 0, + left: 30, + fill: 'blue', + }); + const circle2 = new Circle({ + scaleX: 2, + scaleY: 2, + radius: 10, + top: 120, + left: -20, + fill: 'purple', + }); + const deepRect = new Rect({ + width: 50, + height: 30, + top: 10, + left: 110, + fill: 'red', + skewX: 40, + skewY: 20, + }); + const rect2 = new Rect({ + width: 100, + height: 80, + top: 50, + left: 60, + fill: 'green', + }); + const deepGroup = new Group([deepTriangle, deepCircle, deepRect], { + subTargetCheck: true, + }); + const group2 = new Group([deepGroup, circle2, rect2, triangle2], { + subTargetCheck: true, + }); + const group3 = new Group([group2], { subTargetCheck: true }); + + registerTestObjects({ + deepTriangle, + triangle2, + deepCircle, + circle2, + rect2, + deepRect, + deepGroup, + group2, + group3, + }); + + const canvas = new Canvas(undefined, { perPixelTargetFind: true }); + canvas.add(group3); + + return { + canvas, + deepTriangle, + triangle2, + deepCircle, + circle2, + rect2, + deepRect, + deepGroup, + group2, + group3, + }; + }; + + test.each([ + { x: 5, y: 5 }, + { x: 21, y: 9 }, + { x: 37, y: 7 }, + { x: 89, y: 47 }, + { x: 16, y: 122 }, + { x: 127, y: 37 }, + { x: 87, y: 139 }, + ])('transparent hit on %s', ({ x: clientX, y: clientY }) => { + const { canvas } = prepareTest(); + expect(findTarget(canvas, { clientX, clientY })).toEqual({ + target: undefined, + targets: [], + }); + }); + + test('findTarget with perPixelTargetFind in nested group', () => { + const { + canvas, + deepTriangle, + triangle2, + deepCircle, + circle2, + rect2, + deepRect, + deepGroup, + group2, + group3, + } = prepareTest(); + + expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ + target: group3, + targets: [deepTriangle, deepGroup, group2], + }); + + expect(findTarget(canvas, { clientX: 50, clientY: 20 })).toEqual({ + target: group3, + targets: [deepCircle, deepGroup, group2], + }); + + expect(findTarget(canvas, { clientX: 117, clientY: 16 })).toEqual({ + target: group3, + targets: [deepRect, deepGroup, group2], + }); + + expect(findTarget(canvas, { clientX: 100, clientY: 90 })).toEqual({ + target: group3, + targets: [rect2, group2], + }); + + expect(findTarget(canvas, { clientX: 9, clientY: 145 })).toEqual({ + target: group3, + targets: [circle2, group2], + }); + + expect(findTarget(canvas, { clientX: 66, clientY: 143 })).toEqual({ + target: group3, + targets: [triangle2, group2], + }); + }); + }); + + test('findTarget on active selection', () => { + const rect1 = new FabricObject({ + left: 0, + top: 0, + width: 10, + height: 10, + }); + const rect2 = new FabricObject({ + left: 20, + top: 20, + width: 10, + height: 10, + }); + const rect3 = new FabricObject({ + left: 20, + top: 0, + width: 10, + height: 10, + }); + const activeSelection = new ActiveSelection([rect1, rect2], { + subTargetCheck: true, + cornerSize: 2, + }); + registerTestObjects({ rect1, rect2, rect3, activeSelection }); + + const canvas = new Canvas(undefined, { activeSelection }); + canvas.add(rect1, rect2, rect3); + canvas.setActiveObject(activeSelection); + + expect(findTarget(canvas, { clientX: 5, clientY: 5 })).toEqual({ + target: activeSelection, + targets: [rect1], + }); + + expect(findTarget(canvas, { clientX: 40, clientY: 15 })).toEqual({ + target: undefined, + targets: [], + }); + expect(activeSelection.__corner).toBeUndefined(); + + expect(findTarget(canvas, { clientX: 0, clientY: 0 })).toEqual({ + target: activeSelection, + targets: [], + }); + expect(activeSelection.__corner).toBe('tl'); + + expect(findTarget(canvas, { clientX: 25, clientY: 5 })).toEqual( + { + target: activeSelection, + targets: [], + } + // 'Should not return the rect behind active selection' + ); + + canvas.discardActiveObject(); + expect(findTarget(canvas, { clientX: 25, clientY: 5 })).toEqual( + { + target: rect3, + targets: [], + } + // 'Should return the rect after clearing selection' + ); + }); + + test('findTarget on active selection with perPixelTargetFind', () => { + const rect1 = new Rect({ + left: 0, + top: 0, + width: 10, + height: 10, + }); + const rect2 = new Rect({ + left: 20, + top: 20, + width: 10, + height: 10, + }); + const activeSelection = new ActiveSelection([rect1, rect2]); + registerTestObjects({ rect1, rect2, activeSelection }); + + const canvas = new Canvas(undefined, { + activeSelection, + perPixelTargetFind: true, + preserveObjectStacking: true, + }); + canvas.add(rect1, rect2); + canvas.setActiveObject(activeSelection); + + expect(findTarget(canvas, { clientX: 8, clientY: 8 })).toEqual({ + target: activeSelection, + targets: [], + }); + + expect(findTarget(canvas, { clientX: 15, clientY: 15 })).toEqual({ + target: undefined, + targets: [], + }); + }); + }); + + it('should fire mouse over/out events on target', () => { + const target = new FabricObject({ width: 10, height: 10 }); + const canvas = new Canvas(); + canvas.add(target); + + jest.spyOn(target, 'toJSON').mockReturnValue('target'); + + const targetSpy = jest.spyOn(target, 'fire'); + const canvasSpy = jest.spyOn(canvas, 'fire'); + const enter = new MouseEvent('mousemove', { clientX: 5, clientY: 5 }); + const exit = new MouseEvent('mousemove', { clientX: 20, clientY: 20 }); + canvas._onMouseMove(enter); + canvas._onMouseMove(exit); + expect(targetSpy.mock.calls).toMatchSnapshot(); + expect(canvasSpy.mock.calls).toMatchSnapshot(); + }); }); diff --git a/test/unit/canvas.js b/test/unit/canvas.js index a5737d76b52..3ac67d4e7b3 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -771,392 +771,6 @@ canvas.remove(rect); }); - QUnit.test('findTarget preserveObjectStacking false', function(assert) { - canvas.preserveObjectStacking = false; - var rect = makeRect({ left: 0, top: 0 }), - rectOver = makeRect({ left: 0, top: 0 }), - target, - e = { clientX: 5, clientY: 5, target: canvas.upperCanvasEl }; - canvas.add(rect); - canvas.add(rectOver); - canvas.setActiveObject(rect); - canvas.renderAll(); - target = canvas.findTarget(e); - assert.equal(target, rect, 'Should return the rect'); - }); - - QUnit.test('findTarget preserveObjectStacking true', function(assert) { - assert.ok(typeof canvas.findTarget === 'function'); - canvas.preserveObjectStacking = true; - var rect = makeRect({ left: 0, top: 0, width: 30, height: 30 }), - rectOver = makeRect({ left: 0, top: 0, width: 30, height: 30 }), - target, - e1 = { clientX: 15, clientY: 15, shiftKey: true, target: canvas.upperCanvasEl }, - e2 = { clientX: 4, clientY: 4, target: canvas.upperCanvasEl }; - canvas.add(rect); - canvas.add(rectOver); - target = canvas.findTarget(e1); - assert.equal(target, rectOver, 'Should return the rectOver, rect is not considered'); - canvas.setActiveObject(rect); - target = canvas.findTarget(e1); - assert.equal(target, rectOver, 'Should still return rectOver because is above active object'); - target = canvas.findTarget(e2); - assert.equal(target, rect, 'Should rect because a corner of the activeObject has been hit'); - canvas.altSelectionKey = 'shiftKey'; - target = canvas.findTarget(e1); - assert.equal(target, rect, 'Should rect because active and altSelectionKey is pressed'); - canvas.preserveObjectStacking = false; - }); - - QUnit.test('findTarget with subTargetCheck', function(assert) { - var rect = makeRect({ left: 0, top: 0 }), - rect2 = makeRect({ left: 30, top: 30}), target, - group = new fabric.Group([rect, rect2]); - - canvas.add(group); - - target = canvas.findTarget({ - clientX: 5, clientY: 5, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the group'); - assert.equal(canvas.targets[0], undefined, 'no subtarget should return'); - - target = canvas.findTarget({ - clientX: 30, clientY: 30, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the group'); - group.subTargetCheck = true; - group.setCoords(); - target = canvas.findTarget({ - clientX: 5, clientY: 5, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the group'); - assert.equal(canvas.targets[0], rect, 'should return the rect'); - - target = canvas.findTarget({ - clientX: 15, clientY: 15, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the group'); - assert.equal(canvas.targets[0], undefined, 'no subtarget should return'); - - target = canvas.findTarget({ - clientX: 32, clientY: 32, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the group'); - assert.equal(canvas.targets[0], rect2, 'should return the rect2'); - canvas.remove(group); - }); - - QUnit.test('findTarget with subTargetCheck and canvas zoom', function(assert) { - var rect3 = new fabric.Rect({ - width: 100, - height: 100, - fill: 'yellow' - }); - var rect4 = new fabric.Rect({ - width: 100, - height: 100, - left: 100, - top: 100, - fill: 'purple' - }); - var group3 = new fabric.Group( - [rect3, rect4], - { scaleX: 0.5, scaleY: 0.5, top: 100, left: 0 }); - group3.subTargetCheck = true; - group3.setCoords(); - var rect1 = new fabric.Rect({ - width: 100, - height: 100, - fill: 'red' - }); - var rect2 = new fabric.Rect({ - width: 100, - height: 100, - left: 100, - top: 100, - fill: 'blue' - }); - var g = new fabric.Group([rect1, rect2, group3], { top: -150, left: -50 }); - g.subTargetCheck = true; - canvas.viewportTransform = [0.1, 0, 0, 0.1, 100, 200]; - canvas.add(g); - - var target = canvas.findTarget({ - clientX: 96, clientY: 186, target: canvas.upperCanvasEl - }); - assert.equal(target, g, 'Should return the group 96'); - assert.equal(canvas.targets[0], rect1, 'should find the target rect 96'); - canvas.targets = []; - - target = canvas.findTarget({ - clientX: 98, clientY: 188, target: canvas.upperCanvasEl - }); - assert.equal(target, g, 'Should return the group 98'); - assert.equal(canvas.targets[0], rect1, 'should find the target rect1 98'); - canvas.targets = []; - - target = canvas.findTarget({ - clientX: 100, clientY: 190, target: canvas.upperCanvasEl - }); - assert.equal(target, g, 'Should return the group 100'); - assert.equal(canvas.targets[0], rect1, 'should find the target rect1 100'); - canvas.targets = []; - - target = canvas.findTarget({ - clientX: 102, clientY: 192, target: canvas.upperCanvasEl - }); - assert.equal(target, g, 'Should return the group 102'); - assert.equal(canvas.targets[0], rect1, 'should find the target rect 102'); - canvas.targets = []; - - target = canvas.findTarget({ - clientX: 104, clientY: 194, target: canvas.upperCanvasEl - }); - assert.equal(target, g, 'Should return the group 104'); - assert.equal(canvas.targets[0], rect1, 'should find the target rect 104'); - canvas.targets = []; - - target = canvas.findTarget({ - clientX: 106, clientY: 196, target: canvas.upperCanvasEl - }); - assert.equal(target, g, 'Should return the group 106'); - assert.equal(canvas.targets[0], rect2, 'should find the target rect2 106'); - canvas.targets = []; - }); - - QUnit.test('findTarget with subTargetCheck on activeObject', function(assert) { - var rect = makeRect({ left: 0, top: 0 }), - rect2 = makeRect({ left: 30, top: 30}), target, - group = new fabric.Group([rect, rect2]); - - - group.subTargetCheck = true; - canvas.add(group); - canvas.setActiveObject(group); - target = canvas.findTarget({ - clientX: 9, clientY: 9, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the group'); - assert.equal(canvas.targets[0], rect, 'should return the rect'); - - target = canvas.findTarget({ - clientX: 9, clientY: 9, target: canvas.upperCanvasEl - }); - - target = canvas.findTarget({ - clientX: 9, clientY: 9, target: canvas.upperCanvasEl - }); - - target = canvas.findTarget({ - clientX: 9, clientY: 9, target: canvas.upperCanvasEl - }); - - assert.equal(canvas.targets.length, 1, 'multiple calls to subtarget should not add more to targets'); - - canvas.remove(group); - }); - - QUnit.test('findTarget with subTargetCheck on activeObject and preserveObjectStacking true', function(assert) { - var rect = makeRect({ left: 0, top: 0 }), - rect2 = makeRect({ left: 30, top: 30}), target, - group = new fabric.Group([rect, rect2]); - canvas.preserveObjectStacking = true; - group.subTargetCheck = true; - canvas.add(group); - canvas.setActiveObject(group); - target = canvas.findTarget({ - clientX: 9, clientY: 9, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the group'); - assert.equal(canvas.targets[0], rect, 'should return the rect'); - - target = canvas.findTarget({ - clientX: 9, clientY: 9, target: canvas.upperCanvasEl - }); - - target = canvas.findTarget({ - clientX: 9, clientY: 9, target: canvas.upperCanvasEl - }); - - target = canvas.findTarget({ - clientX: 9, clientY: 9, target: canvas.upperCanvasEl - }); - - assert.equal(canvas.targets.length, 1, 'multiple calls to subtarget should not add more to targets'); - - canvas.remove(group); - }); - - QUnit.test('findTarget with perPixelTargetFind', function(assert) { - assert.ok(typeof canvas.findTarget === 'function'); - var triangle = makeTriangle({ left: 0, top: 0 }), target; - canvas.add(triangle); - target = canvas.findTarget({ - clientX: 5, clientY: 5, target: canvas.upperCanvasEl - }); - assert.equal(target, triangle, 'Should return the triangle by bounding box'); - //TODO find out why this stops the tests - canvas.perPixelTargetFind = true; - target = canvas.findTarget({ - clientX: 5, clientY: 5, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should return null because of transparency checks'); - target = canvas.findTarget({ - clientX: 15, clientY: 15, target: canvas.upperCanvasEl - }); - assert.equal(target, triangle, 'Should return the triangle now'); - canvas.perPixelTargetFind = false; - canvas.remove(triangle); - }); - - QUnit.test('findTarget with perPixelTargetFind in nested group', function(assert) { - assert.ok(typeof canvas.findTarget === 'function'); - var triangle = makeTriangle({ left: 0, top: 0, width: 30, height: 30, fill: 'yellow' }), - triangle2 = makeTriangle({ left: 100, top: 120, width: 30, height: 30, angle: 100, fill: 'pink' }), - circle = new fabric.Circle({ radius: 30, top: 0, left: 30, fill: 'blue' }), - circle2 = new fabric.Circle({ scaleX: 2, scaleY: 2, radius: 10, top: 120, left: -20, fill: 'purple' }), - rect = new fabric.Rect({ width: 100, height: 80, top: 50, left: 60, fill: 'green' }), - rect2 = new fabric.Rect({ width: 50, height: 30, top: 10, left: 110, fill: 'red', skewX: 40, skewY: 20 }), - group1 = new fabric.Group([triangle, circle, rect2], { subTargetCheck: true }), - group2 = new fabric.Group([group1, circle2, rect, triangle2], { subTargetCheck: true }), - group3 = new fabric.Group([group2], { subTargetCheck: true }), - target; - - canvas.add(group3); - canvas.perPixelTargetFind = true; - target = canvas.findTarget({ - clientX: 5, clientY: 5, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should return null because of transparency checks case 1'); - target = canvas.findTarget({ - clientX: 21, clientY: 9, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should return null because of transparency checks case 2'); - target = canvas.findTarget({ - clientX: 37, clientY: 7, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should return null because of transparency checks case 3'); - target = canvas.findTarget({ - clientX: 89, clientY: 47, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should return null because of transparency checks case 4'); - target = canvas.findTarget({ - clientX: 16, clientY: 122, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should return null because of transparency checks case 5'); - target = canvas.findTarget({ - clientX: 127, clientY: 37, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should return null because of transparency checks case 6'); - target = canvas.findTarget({ - clientX: 87, clientY: 139, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should return null because of transparency checks case 7'); - target = canvas.findTarget({ - clientX: 15, clientY: 15, target: canvas.upperCanvasEl - }); - assert.equal(target, group3, 'Should return the group3 now'); - assert.equal(canvas.targets.length, 3, 'Subtargets length should be 3'); - assert.equal(canvas.targets[0], triangle, 'The deepest target should be triangle'); - target = canvas.findTarget({ - clientX: 50, clientY: 20, target: canvas.upperCanvasEl - }); - assert.equal(target, group3, 'Should return the group3 now'); - assert.equal(canvas.targets.length, 3, 'Subtargets length should be 3'); - assert.equal(canvas.targets[0], circle, 'The deepest target should be circle'); - target = canvas.findTarget({ - clientX: 117, clientY: 16, target: canvas.upperCanvasEl - }); - assert.equal(target, group3, 'Should return the group3 now'); - assert.equal(canvas.targets.length, 3, 'Subtargets length should be 2'); - assert.equal(canvas.targets[0], rect2, 'The deepest target should be rect2'); - target = canvas.findTarget({ - clientX: 100, clientY: 90, target: canvas.upperCanvasEl - }); - assert.equal(target, group3, 'Should return the group3 now'); - assert.equal(canvas.targets.length, 2, 'Subtargets length should be 2'); - assert.equal(canvas.targets[0], rect, 'The deepest target should be rect'); - target = canvas.findTarget({ - clientX: 9, clientY: 145, target: canvas.upperCanvasEl - }); - assert.equal(target, group3, 'Should return the group3 now'); - assert.equal(canvas.targets.length, 2, 'Subtargets length should be 2'); - assert.equal(canvas.targets[0], circle2, 'The deepest target should be circle2'); - target = canvas.findTarget({ - clientX: 66, clientY: 143, target: canvas.upperCanvasEl - }); - assert.equal(target, group3, 'Should return the group3 now'); - assert.equal(canvas.targets.length, 2, 'Subtargets length should be 2'); - assert.equal(canvas.targets[0], triangle2, 'The deepest target should be triangle2'); - canvas.remove(group3); - }); - - QUnit.test('findTarget on active selection', function(assert) { - var rect1 = makeRect({ left: 0, top: 0 }), target; - var rect2 = makeRect({ left: 20, top: 20 }); - var rect3 = makeRect({ left: 20, top: 0 }); - canvas.add(rect1); - canvas.add(rect2); - canvas.add(rect3); - const group = new fabric.ActiveSelection(); - group.subTargetCheck = true; - group.add(rect1, rect2); - group.cornerSize = 2; - group.setCoords(); - canvas.setActiveObject(group); - target = canvas.findTarget({ - clientX: 5, clientY: 5, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the activegroup'); - target = canvas.findTarget({ - clientX: 40, clientY: 15, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should miss the activegroup'); - assert.ok(!group.__corner, 'not over control'); - target = canvas.findTarget({ - clientX: 0, clientY: 0, target: canvas.upperCanvasEl - }); - assert.equal(group.__corner, 'tl', 'over control'); - assert.ok(target, group, 'should return active selection if over control'); - target = canvas.findTarget({ - clientX: 5, clientY: 5, target: canvas.upperCanvasEl - }); - assert.ok(target, group, 'should return active selection'); - assert.equal(canvas.targets[0], rect1, 'Should return the rect inside active selection'); - target = canvas.findTarget({ - clientX: 25, clientY: 5, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the active selection'); - assert.deepEqual(canvas.targets, [], 'Should not return the rect behind active selection'); - canvas.discardActiveObject(); - target = canvas.findTarget({ - clientX: 25, clientY: 5, target: canvas.upperCanvasEl - }); - assert.equal(target, rect3, 'Should return the rect3 now that active selection has been cleared'); - }); - - QUnit.test('findTarget on active selection with perPixelTargetFind', function(assert) { - var rect1 = makeRect({ left: 0, top: 0 }), target; - var rect2 = makeRect({ left: 20, top: 20 }); - canvas.perPixelTargetFind = true; - canvas.preserveObjectStacking = true; - canvas.add(rect1); - canvas.add(rect2); - const group = new fabric.ActiveSelection(); - group.add(rect1, rect2); - canvas.setActiveObject(group); - target = canvas.findTarget({ - clientX: 8, clientY: 8, target: canvas.upperCanvasEl - }); - assert.equal(target, group, 'Should return the activegroup'); - - target = canvas.findTarget({ - clientX: 15, clientY: 15, target: canvas.upperCanvasEl - }); - assert.equal(target, null, 'Should miss the activegroup'); - }); - QUnit.test('toDataURL', function(assert) { assert.ok(typeof canvas.toDataURL === 'function'); var dataURL = canvas.toDataURL(); diff --git a/test/unit/canvas_events.js b/test/unit/canvas_events.js index 48c14d79b34..c7b6d260868 100644 --- a/test/unit/canvas_events.js +++ b/test/unit/canvas_events.js @@ -630,70 +630,6 @@ assert.deepEqual(targetControl, [], 'no target should be referenced'); }); - QUnit.test('mouseover and mouseout with subtarget check', function(assert) { - var rect1 = new fabric.Rect({ width: 5, height: 5, left: 5, top: 0, strokeWidth: 0, name: 'rect1' }); - var rect2 = new fabric.Rect({ width: 5, height: 5, left: 5, top: 5, strokeWidth: 0, name: 'rect2' }); - var rect3 = new fabric.Rect({ width: 5, height: 5, left: 0, top: 5, strokeWidth: 0, name: 'rect3' }); - var rect4 = new fabric.Rect({ width: 5, height: 5, left: 0, top: 0, strokeWidth: 0, name: 'rect4' }); - var rect5 = new fabric.Rect({ width: 5, height: 5, left: 2.5, top: 2.5, strokeWidth: 0, name: 'rect5' }); - var group1 = new fabric.Group([rect1, rect2], { subTargetCheck: true, name: 'group1' }); - var group2 = new fabric.Group([rect3, rect4], { subTargetCheck: true, name: 'group2' }); - // a group with 2 groups, with 2 rects each, one group left one group right - // each with 2 rects vertically aligned - var group = new fabric.Group([group1, group2], { subTargetCheck: true, name: 'group' }); - var c = new fabric.Canvas(); - var targetArray = []; - var targetOutArray = []; - [rect1, rect2, rect3, rect4, rect5, group1, group2, group].forEach(function(t) { - t.on('mouseover', function(opt) { - targetArray.push(opt.target); - }); - t.on('mouseout', function(opt) { - targetOutArray.push(opt.target); - }); - }); - c.add(group, rect5); - simulateEvent(c.upperCanvasEl, 'mousemove', { - pointerX: 1, pointerY: 1 - }); - assert.equal(targetArray[0], group, 'first hit is group'); - assert.equal(targetArray[2], group2, 'then hit group2'); - assert.equal(targetArray[1], rect4, 'then hit rect4'); - assert.equal(targetOutArray.length, 0, 'no target out'); - - targetArray = []; - targetOutArray = []; - simulateEvent(c.upperCanvasEl, 'mousemove', { - pointerX: 5, pointerY: 5 - }); - assert.equal(targetArray[0], rect5, 'first hit is target5'); - assert.equal(targetArray.length, 1, 'only one target'); - assert.equal(targetOutArray[0], group, 'first targetOutArray is group'); - assert.equal(targetOutArray[2], group2, 'then targetOutArray group2'); - assert.equal(targetOutArray[1], rect4, 'then targetOutArray rect4'); - - targetArray = []; - targetOutArray = []; - simulateEvent(c.upperCanvasEl, 'mousemove', { - pointerX: 9, pointerY: 9 - }); - assert.equal(targetArray[0], group, 'first hit is group'); - assert.equal(targetArray[2], group1, 'then hit group1'); - assert.equal(targetArray[1], rect2, 'then hit rect2'); - assert.equal(targetOutArray.length, 1, 'only one target out when moving away from rect 5'); - assert.equal(targetOutArray[0], rect5, 'rect5 fires out'); - - targetArray = []; - targetOutArray = []; - simulateEvent(c.upperCanvasEl, 'mousemove', { - pointerX: 9, pointerY: 1 - }); - assert.equal(targetArray[0], rect1, 'the only target changing is rect1'); - assert.equal(targetArray.length, 1, 'only one target entering '); - assert.equal(targetOutArray.length, 1, 'one target out'); - assert.equal(targetOutArray[0], rect2, 'the only target out is rect2'); - }); - QUnit.test('Fabric mouseover, mouseout events fire for subTargets when subTargetCheck is enabled', function(assert){ var done = assert.async(); var counterOver = 0, counterOut = 0, canvas = new fabric.Canvas(); diff --git a/test/unit/draggable_text.js b/test/unit/draggable_text.js deleted file mode 100644 index 8f9cf427f7a..00000000000 --- a/test/unit/draggable_text.js +++ /dev/null @@ -1,613 +0,0 @@ -function assertDragEventStream(name, a, b) { - QUnit.assert.equal(a.length, b.length, `${name} event stream should be same in size`); - a.forEach(({ target, dragSource, dropTarget, ..._a }, index) => { - const { target: targetB, dragSource: dragSourceB, dropTarget: dropTargetB, ..._b } = b[index]; - QUnit.assert.equal(target, targetB, `target should match ${index}`); - QUnit.assert.equal(dragSource, dragSourceB, `dragSource should match ${index}`); - QUnit.assert.equal(dropTarget, dropTargetB, `dropTarget should match ${index}`); - QUnit.assert.deepEqual(_a, _b, `event ${index} should match`); - }); -} - -(isNode() ? QUnit.module.skip : QUnit.module)('draggable text', function (hooks) { - let canvas; - hooks.before(function () { - canvas = new fabric.Canvas(null, { - enableRetinaScaling: false - }); - }); - hooks.after(() => canvas.dispose()); - hooks.afterEach(function () { - canvas.clear(); - canvas.cancelRequestedRender(); - }); - - function assertCursorAnimation(assert, text, active = false) { - const cursorState = [text._currentTickState, text._currentTickCompleteState].some( - (cursorAnimation) => cursorAnimation && !cursorAnimation.isDone() - ); - assert.equal(cursorState, active, `cursor animation state should be ${active}`); - } - - function wait(ms = 32) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - [true, false].forEach(enableRetinaScaling => { - QUnit.module(`enableRetinaScaling = ${enableRetinaScaling}`, function (hooks) { - let canvas, eventData, iText, iText2, eventStream, renderEffects; - let count = 0, countCanvas = 0; - hooks.before(() => { - fabric.config.configure({ devicePixelRatio: 2 }); - }); - hooks.after(() => { - fabric.config.restoreDefaults(); - }); - hooks.beforeEach(() => { - canvas = new fabric.Canvas(null, { - enableRetinaScaling, - }); - eventData = { - which: 1, - target: canvas.upperCanvasEl, - preventDefault() { - this.defaultPrevented = true; - }, - stopPropagation() { - this.propagationStopped = true; - }, - dataTransfer: { - data: {}, - get types() { - return Object.keys(this.data); - }, - dropEffect: 'none', - getData(type) { - return this.data[type]; - }, - setData(type, value) { - this.data[type] = value; - }, - setDragImage(img, x, y) { - this.dragImageData = { img, x, y }; - }, - }, - ...(enableRetinaScaling ? { - clientX: 60, - clientY: 30 - } : { - clientX: 30, - clientY: 15 - }) - }; - iText = new fabric.IText('test test'); - iText2 = new fabric.IText('test2 test2', { left: 200 }); - canvas.add(iText, iText2); - canvas.setActiveObject(iText); - iText.enterEditing(); - iText.selectionStart = 0; - iText.selectionEnd = 4; - count = 0; - countCanvas = 0; - canvas.on('text:selection:changed', () => { - countCanvas++; - }); - iText.on('selection:changed', () => { - count++; - }); - eventStream = { - canvas: [], - source: [], - target: [], - }; - ['dragstart', 'dragover', 'drag', 'dragenter', 'dragleave', 'drop', 'dragend', 'changed', 'text:changed'].forEach(type => { - canvas.on(type, (ev) => { - eventStream.canvas.push({ ...ev, type }); - }); - iText.on(type, (ev) => { - eventStream.source.push({ ...ev, type }); - }); - iText2.on(type, (ev) => { - eventStream.target.push({ ...ev, type }); - }); - }); - renderEffects = []; - canvas._renderDragEffects = (e, source, target) => renderEffects.push({ e, source, target }); - }); - hooks.afterEach(() => canvas.dispose()); - - function startDragging(eventData) { - const e = { ...eventData }; - canvas._onMouseDown({ ...eventData }); - canvas._onDragStart(e); - return e; - } - - function createDragEvent(x = eventData.clientX, y = eventData.clientY, dataTransfer = {}) { - return { - ...eventData, - defaultPrevented: false, - clientX: x, - clientY: y, - dataTransfer: { - ...eventData.dataTransfer, - ...dataTransfer - } - }; - } - - QUnit.test('click sets cursor', async function (assert) { - assert.equal(count, 0, 'selection:changed fired'); - assert.equal(countCanvas, 0, 'text:selection:changed fired'); - let called = 0; - // sinon spy!! - // iText.setCursorByClick = () => called++; - canvas._onMouseDown(eventData); - assert.ok(iText.draggableTextDelegate.isActive(), 'flagged as dragging'); - assert.ok(iText.shouldStartDragging(), 'flagged as dragging'); - - await wait(); - assertCursorAnimation(assert, iText); - // assert.equal(called, 0, 'should not set cursor on mouse up'); - canvas._onMouseUp(eventData); - assert.ok(!iText.draggableTextDelegate.isActive(), 'unflagged as dragging'); - assert.ok(!iText.shouldStartDragging(), 'unflagged as dragging'); - // assert.equal(called, 1, 'should set cursor on mouse up'); - assert.equal(iText.selectionStart, 2, 'set the selectionStart'); - assert.equal(iText.selectionEnd, 2, 'set the selectionend'); - assertCursorAnimation(assert, iText, true); - assert.equal(count, 1, 'selection:changed fired'); - assert.equal(countCanvas, 1, 'text:selection:changed fired'); - }); - - QUnit.test('drag end over selection focuses hiddenTextarea', function (assert) { - startDragging(eventData); - iText.hiddenTextarea.blur(); - canvas._onDragEnd(createDragEvent()); - assert.equal(fabric.getFabricDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); - }); - - QUnit.test('drag start', function (assert) { - const e = startDragging(eventData); - const charStyle = { - "stroke": null, - "strokeWidth": 1, - "fill": "rgb(0,0,0)", - "fontFamily": "Times New Roman", - "fontSize": 40, - "fontWeight": "normal", - "fontStyle": "normal", - "underline": false, - "overline": false, - "linethrough": false, - "deltaY": 0, - "textBackgroundColor": "" - }; - assert.equal(e.dataTransfer.data['text/plain'], 'test', 'should set text/plain'); - assert.deepEqual(JSON.parse(e.dataTransfer.data['application/fabric']), { - value: 'test', - styles: [charStyle, charStyle, charStyle, charStyle] - }, 'should set application/fabric'); - assert.equal(e.dataTransfer.effectAllowed, 'copyMove', 'should set effectAllowed'); - assert.ok(e.dataTransfer.dragImageData.img instanceof HTMLCanvasElement, 'drag image was set'); - assert.equal(e.dataTransfer.dragImageData.x, 30, 'drag image position'); - assert.equal(e.dataTransfer.dragImageData.y, 15, 'drag image position'); - assert.deepEqual(renderEffects, [], 'not rendered effects yet'); - canvas._onDragEnd(eventData); - assert.deepEqual(eventStream.source, [ - { - e, - target: iText, - type: 'dragstart' - }, { - e: eventData, - target: iText, - type: 'dragend', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - didDrop: false - } - ], 'events should match'); - assert.deepEqual(eventStream.canvas, eventStream.source, 'events should match'); - }); - - QUnit.test('disable drag start: onDragStart', async function (assert) { - iText.onDragStart = () => false; - const e = startDragging(eventData); - assert.equal(iText.shouldStartDragging(), true, 'should flag dragging'); - assert.equal(iText.selectionStart, 0, 'selectionStart is kept'); - assert.equal(iText.selectionEnd, 4, 'selectionEnd is kept'); - assert.deepEqual(e.dataTransfer.data, {}, 'should not set dataTransfer'); - assert.equal(e.dataTransfer.effectAllowed, undefined, 'should not set effectAllowed'); - assert.deepEqual(e.dataTransfer.dragImageData, undefined, 'should not set dataTransfer'); - }); - - QUnit.test('disable drag start: start', async function (assert) { - iText.draggableTextDelegate.start = () => false; - const e = startDragging(eventData); - assert.equal(iText.shouldStartDragging(), false, 'should not flag dragging'); - assert.equal(iText.selectionStart, 2, 'selectionStart is set'); - assert.equal(iText.selectionEnd, 2, 'selectionEnd is set'); - assert.deepEqual(e.dataTransfer.data, {}, 'should not set dataTransfer'); - assert.equal(e.dataTransfer.effectAllowed, undefined, 'should not set effectAllowed'); - assert.deepEqual(e.dataTransfer.dragImageData, undefined, 'should not set dataTransfer'); - }); - - QUnit.test('disable drag start: isActive', async function (assert) { - iText.draggableTextDelegate.isActive = () => false; - const e = startDragging(eventData); - assert.equal(iText.shouldStartDragging(), false, 'should not flag dragging'); - assert.equal(iText.selectionStart, 0, 'selectionStart is kept'); - assert.equal(iText.selectionEnd, 4, 'selectionEnd is kept'); - assertCursorAnimation(assert, iText); - assert.deepEqual(e.dataTransfer.data, {}, 'should not set dataTransfer'); - assert.equal(e.dataTransfer.effectAllowed, undefined, 'should not set effectAllowed'); - assert.deepEqual(e.dataTransfer.dragImageData, undefined, 'should not set dataTransfer'); - }); - - QUnit.test('drag over: source', function (assert) { - const e = startDragging(eventData); - const dragEvents = []; - let index; - for (index = 0; index < 100; index++) { - const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); - canvas._onDragOver(dragOverEvent); - dragEvents.push(dragOverEvent); - } - const dragEnd = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); - canvas._onDragEnd(dragEnd); - assertDragEventStream('source', eventStream.source, [ - { e, target: iText, type: 'dragstart' }, - { - e: dragEvents[0], - target: iText, - type: 'dragenter', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false, - pointer: new fabric.Point(30, 15), - absolutePointer: new fabric.Point(30, 15), - viewportPoint: new fabric.Point(30, 15), - scenePoint: new fabric.Point(30, 15), - previousTarget: undefined - }, - ...dragEvents.slice(0, 32).map(e => ({ - e, - target: iText, - type: 'dragover', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false - })), - ...dragEvents.slice(32, 93).map(e => ({ - e, - target: iText, - type: 'dragover', - subTargets: [], - dragSource: iText, - dropTarget: iText, - canDrop: true - })), - { - e: dragEvents[93], - target: iText, - type: 'dragleave', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false, - pointer: new fabric.Point(123, 15), - absolutePointer: new fabric.Point(123, 15), - viewportPoint: new fabric.Point(123, 15), - scenePoint: new fabric.Point(123, 15), - nextTarget: undefined - }, - { - e: dragEnd, - target: iText, - type: 'dragend', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - didDrop: false, - } - ]); - assert.deepEqual(renderEffects, [ - ...dragEvents.slice(0, 32).map(e => ({ e, source: iText, target: undefined })), - ...dragEvents.slice(32, 93).map(e => ({ e, source: iText, target: iText })), - ...dragEvents.slice(93).map(e => ({ e, source: iText, target: undefined })), - ], 'render effects'); - assert.equal(fabric.getFabricDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); - }); - - QUnit.test('drag over: target', function (assert) { - const e = startDragging(eventData); - const dragEvents = []; - let index; - for (index = 180; index < 190; index = index + 5) { - const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); - canvas._onDragOver(dragOverEvent); - dragEvents.push(dragOverEvent); - } - for (index = 0; index <= 20; index = index + 5) { - const dragOverEvent = createDragEvent(eventData.clientX + 190 * canvas.getRetinaScaling(), eventData.clientY - index * canvas.getRetinaScaling()); - canvas._onDragOver(dragOverEvent); - dragEvents.push(dragOverEvent); - } - const dragEnd = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); - canvas._onDragEnd(dragEnd); - assertDragEventStream('source in target test', eventStream.source, [ - { e, target: iText, type: 'dragstart' }, - { - e: dragEnd, - target: iText, - type: 'dragend', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - didDrop: false, - } - ]); - assertDragEventStream('target', eventStream.target, [ - { - e: dragEvents[0], - target: iText2, - type: 'dragenter', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false, - pointer: new fabric.Point(210, 15), - absolutePointer: new fabric.Point(210, 15), - viewportPoint: new fabric.Point(210, 15), - scenePoint: new fabric.Point(210, 15), - previousTarget: undefined - }, - ...dragEvents.slice(0, 6).map(e => ({ - e, - target: iText2, - type: 'dragover', - subTargets: [], - dragSource: iText, - dropTarget: iText2, - canDrop: true - })), - { - e: dragEvents[6], - target: iText2, - type: 'dragleave', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false, - pointer: new fabric.Point(220, -5), - absolutePointer: new fabric.Point(220, -5), - viewportPoint: new fabric.Point(220, -5), - scenePoint: new fabric.Point(220, -5), - nextTarget: undefined - }, - ]); - assert.deepEqual(renderEffects, [ - ...dragEvents.slice(0, 6).map(e => ({ e, source: iText, target: iText2 })), - ...dragEvents.slice(6).map(e => ({ e, source: iText, target: undefined })), - ], 'render effects'); - assert.equal(fabric.getFabricDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); - }); - - QUnit.test('drag over: canvas', function (assert) { - const e = startDragging(eventData); - const dragEvents = []; - let index; - for (index = 0; index < 10; index++) { - const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); - canvas._onDragOver(dragOverEvent); - dragEvents.push(dragOverEvent); - } - // canvas._onDrop(dragEvent); - const dragEnd = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); - canvas._onDragEnd(dragEnd); - assertDragEventStream('canvas', eventStream.canvas, [ - { e, target: iText, type: 'dragstart' }, - ...dragEvents.map(e => ({ - e, - target: iText, - type: 'dragover', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false - })), - { - e: dragEnd, - target: iText, - type: 'dragend', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - didDrop: false, - } - ]); - assert.equal(fabric.getFabricDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); - }); - - QUnit.test('drop on drag source', function (assert) { - const e = startDragging(eventData); - const dragEvents = []; - let index; - for (index = 70; index < 80; index = index + 5) { - const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); - canvas._onDragOver(dragOverEvent); - dragEvents.push(dragOverEvent); - } - const drop = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling(), undefined, { dropEffect: 'move' }); - canvas._onDrop(drop); - canvas._onDragEnd(drop); - assert.equal(iText.text, ' testestt', 'text after drop'); - assert.equal(iText.selectionStart, 4, 'selection after drop'); - assert.equal(iText.selectionEnd, 8, 'selection after drop'); - assertDragEventStream('drop on drag source', eventStream.source, [ - { e, target: iText, type: 'dragstart' }, - { - e: dragEvents[0], - target: iText, - type: 'dragenter', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false, - pointer: new fabric.Point(100, 15), - absolutePointer: new fabric.Point(100, 15), - viewportPoint: new fabric.Point(100, 15), - scenePoint: new fabric.Point(100, 15), - previousTarget: undefined - }, - ...dragEvents.slice(0, 2).map(e => ({ - e, - target: iText, - type: 'dragover', - subTargets: [], - dragSource: iText, - dropTarget: iText, - canDrop: true - })), - { - action: 'drop', - index: 4, - type: 'changed' - }, - { - e: drop, - target: iText, - type: 'drop', - subTargets: [], - dragSource: iText, - dropTarget: iText, - didDrop: true, - pointer: new fabric.Point(110, 15), - absolutePointer: new fabric.Point(110, 15), - viewportPoint: new fabric.Point(110, 15), - scenePoint: new fabric.Point(110, 15), - }, - { - e: drop, - target: iText, - type: 'dragend', - subTargets: [], - dragSource: iText, - dropTarget: iText, - didDrop: true, - } - ]); - assert.equal(fabric.getFabricDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); - }); - - QUnit.test('drop', function (assert) { - const e = startDragging(eventData); - const dragEvents = []; - let index; - for (index = 200; index < 210; index = index + 5) { - const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); - canvas._onDragOver(dragOverEvent); - dragEvents.push(dragOverEvent); - } - const drop = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling(), undefined, { dropEffect: 'move' }); - canvas._onDrop(drop); - canvas._onDragEnd(drop); - assert.equal(iText2.text, 'testestt2 test2', 'text after drop'); - assert.equal(iText2.selectionStart, 3, 'selection after drop'); - assert.equal(iText2.selectionEnd, 7, 'selection after drop'); - assertDragEventStream('drop', eventStream.target, [ - { - e: dragEvents[0], - target: iText2, - type: 'dragenter', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false, - pointer: new fabric.Point(230, 15), - absolutePointer: new fabric.Point(230, 15), - viewportPoint: new fabric.Point(230, 15), - scenePoint: new fabric.Point(230, 15), - previousTarget: undefined - }, - ...dragEvents.slice(0, 2).map(e => ({ - e, - target: iText2, - type: 'dragover', - subTargets: [], - dragSource: iText, - dropTarget: iText2, - canDrop: true - })), - { - action: 'drop', - index: 3, - type: 'changed' - }, - { - e: drop, - target: iText2, - type: 'drop', - subTargets: [], - dragSource: iText, - dropTarget: iText2, - didDrop: true, - absolutePointer: new fabric.Point(240, 15), - pointer: new fabric.Point(240, 15), - viewportPoint: new fabric.Point(240, 15), - scenePoint: new fabric.Point(240, 15), - }, - ]); - assert.equal(fabric.getFabricDocument().activeElement, iText2.hiddenTextarea, 'should have focused hiddenTextarea'); - }); - - QUnit.test('disable drop', function (assert) { - iText2.canDrop = () => false; - const e = startDragging(eventData); - const dragEvents = []; - let index; - for (index = 200; index < 210; index = index + 5) { - const dragOverEvent = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling()); - canvas._onDragOver(dragOverEvent); - dragEvents.push(dragOverEvent); - } - const drop = createDragEvent(eventData.clientX + index * canvas.getRetinaScaling(), undefined, { dropEffect: 'none' }); - // the window will not invoke a drop event so we call drag end to simulate correctly - canvas._onDragEnd(drop); - assert.equal(iText2.text, 'test2 test2', 'text after drop'); - assert.equal(iText2.selectionStart, 0, 'selection after drop'); - assert.equal(iText2.selectionEnd, 0, 'selection after drop'); - assertDragEventStream('drop', eventStream.target, [ - { - e: dragEvents[0], - target: iText2, - type: 'dragenter', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false, - pointer: new fabric.Point(230, 15), - absolutePointer: new fabric.Point(230, 15), - viewportPoint: new fabric.Point(230, 15), - scenePoint: new fabric.Point(230, 15), - previousTarget: undefined - }, - ...dragEvents.slice(0, 2).map(e => ({ - e, - target: iText2, - type: 'dragover', - subTargets: [], - dragSource: iText, - dropTarget: undefined, - canDrop: false - })), - ]); - assert.equal(fabric.getFabricDocument().activeElement, iText.hiddenTextarea, 'should have focused hiddenTextarea'); - }); - }); - }); -}); \ No newline at end of file