import { Canvas } from './Canvas';
import { Rect } from '../shapes/Rect';
import { IText } from '../shapes/IText/IText';
import '../shapes/ActiveSelection';

import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest';
import { config } from '../config';
import type {
  FabricObject,
  MultiSelectionStacking,
  TPointerEvent,
} from '../../fabric';
import { createPointerEvent, makeRect } from '../../test/utils';
import {
  ActiveSelection,
  Circle,
  classRegistry,
  FabricText,
  getFabricDocument,
  Group,
  Path,
  version,
} from '../../fabric';
import TEST_IMAGE from '../../test/fixtures/test_image.gif';
import { isJSDOM } from '../../vitest.extend';
import {
  EMPTY_JSON,
  PATH_DATALESS_JSON,
  PATH_OBJ_JSON,
  PATH_WITHOUT_DEFAULTS_JSON,
  PATH_JSON,
  RECT_JSON,
  ERROR_IMAGE_JSON,
} from './Canvas.fixtures.ts';

describe('Canvas', () => {
  let canvas: Canvas;
  let upperCanvasEl: HTMLCanvasElement;
  let lowerCanvasEl: HTMLCanvasElement;

  beforeEach(() => {
    canvas = new Canvas(undefined, {
      enableRetinaScaling: false,
      width: 600,
      height: 600,
    });
    upperCanvasEl = canvas.upperCanvasEl;
    lowerCanvasEl = canvas.lowerCanvasEl;
  });

  afterEach(() => {
    config.restoreDefaults();
    classRegistry.setClass(ActiveSelection);
    return canvas.dispose();
  });

  describe('touchStart', () => {
    test('will prevent default to not allow dom scrolling on canvas touch drag', () => {
      const canvas = new Canvas(undefined, {
        allowTouchScrolling: false,
      });
      const touch = new Touch({
        clientX: 10,
        clientY: 0,
        identifier: 1,
        target: canvas.upperCanvasEl,
      });
      const evt = new TouchEvent('touchstart', {
        touches: [touch],
        changedTouches: [touch],
      });
      evt.preventDefault = vi.fn();
      canvas._onTouchStart(evt);
      expect(evt.preventDefault).toHaveBeenCalled();
    });
    test('will not prevent default when allowTouchScrolling is true and there is no action', () => {
      const canvas = new Canvas(undefined, {
        allowTouchScrolling: true,
      });
      const touch = new Touch({
        clientX: 10,
        clientY: 0,
        identifier: 1,
        target: canvas.upperCanvasEl,
      });
      const evt = new TouchEvent('touchstart', {
        touches: [touch],
        changedTouches: [touch],
      });
      evt.preventDefault = vi.fn();
      canvas._onTouchStart(evt);
      expect(evt.preventDefault).not.toHaveBeenCalled();
    });
    test('will prevent default when allowTouchScrolling is true but we are drawing', () => {
      const canvas = new Canvas(undefined, {
        allowTouchScrolling: true,
        isDrawingMode: true,
      });
      const touch = new Touch({
        clientX: 10,
        clientY: 0,
        identifier: 1,
        target: canvas.upperCanvasEl,
      });
      const evt = new TouchEvent('touchstart', {
        touches: [touch],
        changedTouches: [touch],
      });
      evt.preventDefault = vi.fn();
      canvas._onTouchStart(evt);
      expect(evt.preventDefault).toHaveBeenCalled();
    });
    test('will prevent default when allowTouchScrolling is true and we are dragging an object', () => {
      const canvas = new Canvas(undefined, {
        allowTouchScrolling: true,
      });
      const rect = new Rect({
        width: 2000,
        height: 2000,
        left: -500,
        top: -500,
      });
      canvas.add(rect);
      canvas.setActiveObject(rect);
      const touch = new Touch({
        clientX: 10,
        clientY: 0,
        identifier: 1,
        target: canvas.upperCanvasEl,
      });
      const evt = new TouchEvent('touchstart', {
        touches: [touch],
        changedTouches: [touch],
      });
      evt.preventDefault = vi.fn();
      canvas._onTouchStart(evt);
      expect(evt.preventDefault).toHaveBeenCalled();
    });
    test('will NOT prevent default when allowTouchScrolling is true and we just lost selection', () => {
      const canvas = new Canvas(undefined, {
        allowTouchScrolling: true,
      });
      const rect = new Rect({
        width: 200,
        height: 200,
        left: 1000,
        top: 1000,
      });
      canvas.add(rect);
      canvas.setActiveObject(rect);
      const touch = new Touch({
        clientX: 10,
        clientY: 0,
        identifier: 1,
        target: canvas.upperCanvasEl,
      });
      const evt = new TouchEvent('touchstart', {
        touches: [touch],
        changedTouches: [touch],
      });
      evt.preventDefault = vi.fn();
      canvas._onTouchStart(evt);
      expect(evt.preventDefault).not.toHaveBeenCalled();
    });
    test('dispose after _onTouchStart', () => {
      const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout');
      const canvas = new Canvas(undefined, {
        allowTouchScrolling: true,
        isDrawingMode: true,
      });
      const touch = new Touch({
        clientX: 10,
        clientY: 0,
        identifier: 1,
        target: canvas.upperCanvasEl,
      });
      const evtStart = new TouchEvent('touchstart', {
        touches: [touch],
        changedTouches: [touch],
      });
      canvas._onTouchStart(evtStart);
      const evtEnd = new TouchEvent('touchend', {
        touches: [],
        changedTouches: [touch],
      });
      canvas._onTouchEnd(evtEnd);
      // @ts-expect-error -- private method
      expect(+canvas._willAddMouseDown).toBeGreaterThan(0);
      canvas.dispose();
      // @ts-expect-error -- private method
      expect(clearTimeoutSpy).toHaveBeenCalledWith(canvas._willAddMouseDown);
    });
  });

  describe('handleMultiSelection', () => {
    const canvas = new Canvas();
    const rect = new Rect({ left: 100, width: 100, height: 100 });
    const iText = new IText('itext');
    canvas.add(rect, iText);
    test('Selecting shapes containing text does not trigger the exit event', () => {
      const exitMock = vi.fn();
      iText.on('editing:exited', exitMock);

      const firstClick = new MouseEvent('click', {
        clientX: 0,
        clientY: 0,
      });
      canvas._onMouseDown(firstClick);
      canvas._onMouseUp(firstClick);
      const secondClick = new MouseEvent('click', {
        shiftKey: true,
        clientX: 100,
        clientY: 0,
      });
      canvas._onMouseDown(secondClick);
      canvas._onMouseUp(secondClick);

      expect(exitMock).toHaveBeenCalledTimes(0);
    });
  });

  it('prevents multiple canvas initialization', () => {
    const newCanvas = new Canvas();
    expect(newCanvas.lowerCanvasEl).toBeTruthy();
    expect(() => new Canvas(newCanvas.lowerCanvasEl)).toThrow();
  });

  it('initializes with element existing in the dom', () => {
    const doc = getFabricDocument();
    const wrapper = doc.createElement('div');
    const canvasEl = doc.createElement('canvas');
    wrapper.appendChild(canvasEl);
    doc.body.appendChild(wrapper);
    const newCanvas = new Canvas(canvasEl);
    expect(wrapper.firstChild, 'replaced canvas el in dom').toBe(
      newCanvas.elements.container,
    );
    expect(
      newCanvas.elements.container.firstChild,
      'appended canvas el to container',
    ).toBe(newCanvas.elements.lower.el);
    expect(
      newCanvas.elements.container.lastChild,
      'appended upper canvas el to container',
    ).toBe(newCanvas.elements.upper.el);
  });

  it('has expected initial properties', () => {
    expect('backgroundColor' in canvas).toBeTruthy();
    expect(canvas.includeDefaultValues).toBe(true);
  });

  it('implements getObjects method', () => {
    expect(
      canvas.getObjects,
      'should respond to `getObjects` method',
    ).toBeTypeOf('function');
    expect(
      canvas.getObjects(),
      'should return empty array for `getObjects` when empty',
    ).toEqual([]);
    expect(
      canvas.getObjects().length,
      'should have a 0 length when empty',
    ).toBe(0);
  });

  it('implements getElement method', () => {
    expect(
      canvas.getElement,
      'should respond to `getElement` method',
    ).toBeTypeOf('function');
    expect(canvas.getElement(), 'should return a proper element').toBe(
      lowerCanvasEl,
    );
  });

  it('implements item method', () => {
    const rect = makeRect();

    expect(canvas.item, 'should respond to item').toBeTypeOf('function');
    canvas.add(rect);
    expect(canvas.item(0), 'should return proper item').toBe(rect);
  });

  it('preserveObjectStacking property', () => {
    expect(canvas.preserveObjectStacking).toBeTypeOf('boolean');
    expect(canvas.preserveObjectStacking, 'default is true').toBeTruthy();
  });

  it('uniformScaling property', () => {
    expect(canvas.uniformScaling).toBeTypeOf('boolean');
    expect(canvas.uniformScaling, 'default is true').toBeTruthy();
  });

  it('uniScaleKey property', () => {
    expect(canvas.uniScaleKey).toBeTypeOf('string');
    expect(canvas.uniScaleKey, 'default is shift').toBe('shiftKey');
  });

  it('centeredScaling property', () => {
    expect(canvas.centeredScaling).toBeTypeOf('boolean');
    expect(canvas.centeredScaling, 'default is false').toBeFalsy();
  });

  it('centeredRotation property', () => {
    expect(canvas.centeredRotation).toBeTypeOf('boolean');
    expect(canvas.centeredRotation, 'default is false').toBeFalsy();
  });

  it('centeredKey property', () => {
    expect(canvas.centeredKey).toBeTypeOf('string');
    expect(canvas.centeredKey, 'default is alt').toBe('altKey');
  });

  it('altActionKey property', () => {
    expect(canvas.altActionKey).toBeTypeOf('string');
    expect(canvas.altActionKey, 'default is shift').toBe('shiftKey');
  });

  it('selection property', () => {
    expect(canvas.selection).toBeTypeOf('boolean');
    expect(canvas.selection, 'default is true').toBeTruthy();
  });

  it('initializes DOM elements correctly', () => {
    expect(
      canvas.lowerCanvasEl.getAttribute('data-fabric'),
      'el should be marked by canvas init',
    ).toBe('main');
    expect(
      canvas.upperCanvasEl.getAttribute('data-fabric'),
      'el should be marked by canvas init',
    ).toBe('top');
    expect(
      canvas.wrapperEl.getAttribute('data-fabric'),
      'el should be marked by canvas init',
    ).toBe('wrapper');
  });

  it('implements renderTop method', () => {
    expect(canvas.renderTop).toBeTypeOf('function');
  });

  it('implements _chooseObjectsToRender method', () => {
    expect(canvas._chooseObjectsToRender).toBeTypeOf('function');
    canvas.preserveObjectStacking = false;
    const rect = makeRect(),
      rect2 = makeRect(),
      rect3 = makeRect();
    canvas.add(rect);
    canvas.add(rect2);
    canvas.add(rect3);

    let objs = canvas._chooseObjectsToRender();
    expect(objs[0]).toBe(rect);
    expect(objs[1]).toBe(rect2);
    expect(objs[2]).toBe(rect3);

    canvas.setActiveObject(rect);
    objs = canvas._chooseObjectsToRender();
    expect(objs[0]).toBe(rect2);
    expect(objs[1]).toBe(rect3);
    expect(objs[2]).toBe(rect);

    canvas.setActiveObject(rect2);
    canvas.preserveObjectStacking = true;
    objs = canvas._chooseObjectsToRender();
    expect(objs[0]).toBe(rect);
    expect(objs[1]).toBe(rect2);
    expect(objs[2]).toBe(rect3);
  });

  it('implements calcOffset method', () => {
    expect(canvas.calcOffset, 'should respond to `calcOffset`').toBeTypeOf(
      'function',
    );
    expect(canvas.calcOffset(), 'should return offset').toEqual({
      left: 0,
      top: 0,
    });
  });

  it('implements add method', () => {
    const rect1 = makeRect(),
      rect2 = makeRect(),
      rect3 = makeRect(),
      rect4 = makeRect();

    expect(canvas.add).toBeTypeOf('function');
    expect(
      canvas.add(rect1),
      'should return the new length of objects array',
    ).toBe(1);
    expect(canvas.item(0)).toBe(rect1);

    canvas.add(rect2, rect3, rect4);
    expect(
      canvas.getObjects().length,
      'should support multiple arguments',
    ).toBe(4);

    expect(canvas.item(1)).toBe(rect2);
    expect(canvas.item(2)).toBe(rect3);
    expect(canvas.item(3)).toBe(rect4);
  });

  it('implements insertAt method', () => {
    const rect1 = makeRect(),
      rect2 = makeRect();

    canvas.add(rect1, rect2);

    expect(canvas.insertAt, 'should respond to `insertAt` method').toBeTypeOf(
      'function',
    );

    const rect = makeRect();
    canvas.insertAt(1, rect);
    expect(canvas.item(1)).toBe(rect);
    canvas.insertAt(2, rect);
    expect(canvas.item(2)).toBe(rect);
  });

  it('implements remove method', () => {
    const rect1 = makeRect(),
      rect2 = makeRect(),
      rect3 = makeRect(),
      rect4 = makeRect();

    canvas.add(rect1, rect2, rect3, rect4);

    expect(canvas.remove).toBeTypeOf('function');
    expect(canvas.remove(rect1)[0], 'should return the object removed').toBe(
      rect1,
    );
    expect(canvas.item(0), 'should be second object').toBe(rect2);

    canvas.remove(rect2, rect3);
    expect(canvas.item(0)).toBe(rect4);

    canvas.remove(rect4);
    expect(canvas.isEmpty(), 'canvas should be empty').toBe(true);
  });

  it('clears hovered target when removed', () => {
    const rect1 = makeRect();
    canvas.add(rect1);
    canvas._hoveredTarget = rect1;
    canvas.remove(rect1);
    expect(
      canvas._hoveredTarget,
      'reference to hovered target should be removed',
    ).toBeUndefined();
  });

  it('fires before:selection:cleared only when removing active objects', () => {
    let isFired = false;
    canvas.on('before:selection:cleared', () => {
      isFired = true;
    });

    canvas.add(new Rect());
    canvas.remove(canvas.item(0));

    expect(
      isFired,
      'removing inactive object shouldnt fire "before:selection:cleared"',
    ).toBe(false);

    canvas.add(new Rect());
    canvas.setActiveObject(canvas.item(0));
    canvas.remove(canvas.item(0));

    expect(
      isFired,
      'removing active object should fire "before:selection:cleared"',
    ).toBe(true);
  });

  it('provides deselected objects in before:selection:cleared event', () => {
    let deselected: FabricObject[] = [];
    canvas.on('before:selection:cleared', (options) => {
      deselected = options.deselected;
    });

    const rect = new Rect();
    canvas.add(rect);
    canvas.setActiveObject(rect);
    canvas.discardActiveObject();

    expect(deselected.length, 'options.deselected was the removed object').toBe(
      1,
    );
    expect(deselected[0], 'options.deselected was the removed object').toBe(
      rect,
    );

    const rect1 = new Rect();
    const rect2 = new Rect();
    canvas.add(rect1, rect2);

    const activeSelection = new ActiveSelection();
    activeSelection.add(rect1, rect2);
    canvas.setActiveObject(activeSelection);
    canvas.discardActiveObject();

    expect(deselected.length, 'options.deselected was the removed object').toBe(
      1,
    );
    expect(
      deselected[0],
      'removing an activeSelection pass that as a target',
    ).toBe(activeSelection);
  });

  it('fires selection:cleared only when removing active objects', () => {
    let isFired = false;
    canvas.on('selection:cleared', () => {
      isFired = true;
    });

    canvas.add(new Rect());
    canvas.remove(canvas.item(0));

    expect(
      isFired,
      'removing inactive object shouldnt fire "selection:cleared"',
    ).toBe(false);

    canvas.add(new Rect());
    canvas.setActiveObject(canvas.item(0));
    canvas.remove(canvas.item(0));

    expect(
      isFired,
      'removing active object should fire "selection:cleared"',
    ).toBe(true);
    canvas.off('selection:cleared');
  });

  it('creating active selection fires selection:created event', () => {
    let isFired = false;
    const rect1 = new Rect();
    const rect2 = new Rect();
    canvas.add(rect1, rect2);
    canvas.on('selection:created', () => {
      isFired = true;
    });

    initActiveSelection(canvas, rect1, rect2, 'selection-order');

    expect(canvas._hoveredTarget, 'the created selection is also hovered').toBe(
      canvas.getActiveObject(),
    );
    expect(isFired, 'selection:created fired').toBe(true);

    canvas.off('selection:created');
    canvas.clear();
  });

  it('creating active selection fires selected event on new objects', () => {
    let isFired = false;
    const rect1 = new Rect();
    const rect2 = new Rect();
    canvas.add(rect1, rect2);
    rect2.on('selected', () => {
      isFired = true;
    });

    initActiveSelection(canvas, rect1, rect2, 'selection-order');

    const activeSelection = canvas.getActiveObjects();
    expect(isFired, 'selected fired on rect2').toBe(true);
    expect(activeSelection[0], 'first rec1').toBe(rect1);
    expect(activeSelection[1], 'then rect2').toBe(rect2);

    canvas.clear();
  });

  it('starts multiselection with correct order (default)', () => {
    const rect1 = new Rect();
    const rect2 = new Rect();
    canvas.add(rect1, rect2);

    initActiveSelection(canvas, rect2, rect1, 'selection-order');

    const activeSelection = canvas.getActiveObjects();
    expect(activeSelection[0], 'first rect2').toBe(rect2);
    expect(activeSelection[1], 'then rect1').toBe(rect1);
  });

  it('starts multiselection with canvas stacking order', () => {
    const rect1 = new Rect();
    const rect2 = new Rect();
    canvas.add(rect1, rect2);

    initActiveSelection(canvas, rect2, rect1, 'canvas-stacking');

    const activeSelection = canvas.getActiveObjects();
    expect(activeSelection[0], 'first rect1').toBe(rect1);
    expect(activeSelection[1], 'then rect2').toBe(rect2);
  });

  it('fires selection:updated when updating active selection', () => {
    let isFired = false;
    const rect1 = new Rect();
    const rect2 = new Rect();
    const rect3 = new Rect();
    canvas.add(rect1, rect2, rect3);
    canvas.on('selection:updated', () => {
      isFired = true;
    });

    updateActiveSelection(canvas, [rect1, rect2], rect3, 'selection-order');

    expect(isFired, 'selection:updated fired').toBe(true);
    expect(canvas._hoveredTarget, 'hovered target is updated').toBe(
      canvas.getActiveObject(),
    );
  });

  it('fires deselected event when removing object from active selection', () => {
    let isFired = false;
    const rect1 = new Rect({ width: 10, height: 10 });
    const rect2 = new Rect({ width: 10, height: 10 });
    canvas.add(rect1, rect2);
    rect2.on('deselected', () => {
      isFired = true;
    });

    updateActiveSelection(canvas, [rect1, rect2], rect2, 'selection-order');

    expect(isFired, 'deselected on rect2 fired').toBe(true);
  });

  it('fires selected event when adding object to active selection', () => {
    let isFired = false;
    const rect1 = new Rect();
    const rect2 = new Rect();
    const rect3 = new Rect();
    canvas.add(rect1, rect2, rect3);
    rect3.on('selected', () => {
      isFired = true;
    });

    updateActiveSelection(canvas, [rect1, rect2], rect3, 'selection-order');

    expect(isFired, 'selected on rect3 fired').toBe(true);
  });

  it('respects order of objects in continuing multiselection', () => {
    const rect1 = new Rect();
    const rect2 = new Rect();
    const rect3 = new Rect();
    canvas.add(rect1, rect2, rect3);

    function assertObjectsInOrder(init: FabricObject[], added: FabricObject) {
      updateActiveSelection(canvas, init, added, 'canvas-stacking');
      expect(
        canvas.getActiveObjects(),
        'updated selection while preserving canvas stacking order',
      ).toEqual([rect1, rect2, rect3]);
      canvas.discardActiveObject();

      updateActiveSelection(canvas, init, added, 'selection-order');
      expect(
        canvas.getActiveObjects(),
        'updated selection while preserving click order',
      ).toEqual([...init, added]);
      canvas.discardActiveObject();
    }

    function assertObjectsInOrderOnCanvas(
      init: FabricObject[],
      added: FabricObject,
    ) {
      expect(canvas.getObjects()).toEqual([rect1, rect2, rect3]);
      assertObjectsInOrder(init, added);
      expect(canvas.getObjects()).toEqual([rect1, rect2, rect3]);
    }

    assertObjectsInOrderOnCanvas([rect1, rect2], rect3);
    assertObjectsInOrderOnCanvas([rect1, rect3], rect2);
    assertObjectsInOrderOnCanvas([rect2, rect3], rect1);

    canvas.remove(rect2, rect3);
    const group = new Group([rect2, rect3], {
      subTargetCheck: true,
      interactive: true,
    });
    canvas.add(group);

    function assertNestedObjectsInOrder(
      init: FabricObject[],
      added: FabricObject,
    ) {
      expect(canvas.getObjects()).toEqual([rect1, group]);
      expect(group.getObjects()).toEqual([rect2, rect3]);
      assertObjectsInOrder(init, added);
      expect(canvas.getObjects()).toEqual([rect1, group]);
      expect(group.getObjects()).toEqual([rect2, rect3]);
    }

    assertNestedObjectsInOrder([rect1, rect2], rect3);
    assertNestedObjectsInOrder([rect1, rect3], rect2);
    assertNestedObjectsInOrder([rect2, rect3], rect1);

    canvas.remove(rect1);
    group.insertAt(0, rect1);
    group.remove(rect3);
    canvas.add(rect3);

    function assertNestedObjectsInOrder2(
      init: FabricObject[],
      added: FabricObject,
    ) {
      expect(canvas.getObjects()).toEqual([group, rect3]);
      expect(group.getObjects()).toEqual([rect1, rect2]);
      assertObjectsInOrder(init, added);
      expect(canvas.getObjects()).toEqual([group, rect3]);
      expect(group.getObjects()).toEqual([rect1, rect2]);
    }

    assertNestedObjectsInOrder2([rect1, rect2], rect3);
    assertNestedObjectsInOrder2([rect1, rect3], rect2);
    assertNestedObjectsInOrder2([rect2, rect3], rect1);
  });

  it('toggles selected objects in multiselection', () => {
    const rect1 = new Rect();
    const rect2 = new Rect();
    const rect3 = new Rect();
    let isFired = false;
    rect2.on('deselected', () => {
      isFired = true;
    });
    canvas.add(rect1, rect2, rect3);

    updateActiveSelection(
      canvas,
      [rect1, rect2, rect3],
      rect2,
      'selection-order',
    );

    expect(canvas.getActiveObjects(), 'rect2 was deselected').toEqual([
      rect1,
      rect3,
    ]);
    expect(isFired, 'fired deselected').toBeTruthy();
  });

  it('toggles nested target when clicking inside active selection', () => {
    const rect1 = new Rect({ width: 10, height: 10 });
    const rect2 = new Rect({ width: 10, height: 10 });
    const rect3 = new Rect({ width: 10, height: 10 });
    let isFired = false;
    rect3.on('deselected', () => {
      isFired = true;
    });
    canvas.add(rect1, rect2, rect3);

    updateActiveSelection(
      canvas,
      [rect1, rect2, rect3],
      null,
      'selection-order',
    );

    expect(canvas.getActiveObjects(), 'rect3 was deselected').toEqual([
      rect1,
      rect2,
    ]);
    expect(isFired, 'fired deselected').toBeTruthy();
  });

  it('does nothing when clicking active selection area', () => {
    const rect1 = new Rect({ left: 10, width: 10, height: 10 });
    const rect2 = new Rect({ left: -10, width: 5, height: 5 });
    const rect3 = new Rect({ top: 10, width: 10, height: 10 });
    canvas.add(rect1, rect2, rect3);

    updateActiveSelection(
      canvas,
      [rect1, rect2, rect3],
      null,
      'selection-order',
    );

    expect(canvas.getActiveObjects(), 'nothing happened').toEqual([
      rect1,
      rect2,
      rect3,
    ]);
    expect(
      canvas.getActiveObject() === canvas.getActiveObject(),
      'still selected',
    ).toBeTruthy();
  });

  it('selects target behind active selection when using selection key', () => {
    const rect1 = new Rect({ left: 15, top: 5, width: 10, height: 10 });
    const rect2 = new Rect({ width: 10, height: 10, left: 5, top: 5 });
    const rect3 = new Rect({ top: 15, left: 5, width: 10, height: 10 });
    canvas.add(rect1, rect2, rect3);

    initActiveSelection(canvas, rect1, rect3);

    expect(
      canvas.getActiveObject() === canvas.getActiveObject(),
      'selected',
    ).toBeTruthy();
    expect(canvas.getActiveObjects(), 'created').toEqual([rect1, rect3]);

    canvas._onMouseDown({
      clientX: 7,
      clientY: 7,
      [canvas.selectionKey as string]: true,
    } as unknown as TPointerEvent);

    expect(
      canvas.getActiveObjects(),
      'added from behind active selection',
    ).toEqual([rect1, rect2, rect3]);
    expect(
      canvas.getActiveObject() === canvas.getActiveObject(),
      'still selected',
    ).toBeTruthy();
  });

  it('fires deselected event when changing active object', () => {
    let isFired = false;
    const rect1 = new Rect();
    const rect2 = new Rect();
    rect1.on('deselected', () => {
      isFired = true;
    });

    canvas.setActiveObject(rect1);
    canvas.setActiveObject(rect2);

    expect(isFired, 'switching active group fires deselected').toBe(true);
  });

  it('fires selected event for each object when group selecting', () => {
    let fired = 0;
    const rect1 = new Rect({ width: 10, height: 10 });
    const rect2 = new Rect({ width: 10, height: 10 });
    const rect3 = new Rect({ width: 10, height: 10 });
    rect1.on('selected', () => {
      fired++;
    });
    rect2.on('selected', () => {
      fired++;
    });
    rect3.on('selected', () => {
      fired++;
    });
    canvas.add(rect1, rect2, rect3);

    setGroupSelector(canvas, {
      x: 1,
      y: 1,
      deltaX: 5,
      deltaY: 5,
    });
    canvas._onMouseUp(createPointerEvent({ target: canvas.upperCanvasEl }));

    expect(fired, 'event fired for each of 3 rects').toBe(3);
  });

  it('fires selection:created when multiple objects are selected', () => {
    let isFired = false;
    const rect1 = new Rect({ width: 10, height: 10 });
    const rect2 = new Rect({ width: 10, height: 10 });
    const rect3 = new Rect({ width: 10, height: 10 });
    canvas.on('selection:created', () => {
      isFired = true;
    });
    canvas.add(rect1, rect2, rect3);

    setGroupSelector(canvas, {
      x: 1,
      y: 1,
      deltaX: 5,
      deltaY: 5,
    });
    canvas._onMouseUp(createPointerEvent({ target: canvas.upperCanvasEl }));

    expect(isFired, 'selection created fired').toBe(true);
    expect(
      // @ts-expect-error -- constructor function has type
      canvas.getActiveObject()!.constructor.type,
      'an active selection is created',
    ).toBe('ActiveSelection');
    expect(canvas.getActiveObjects()[0], 'rect1 is first object').toBe(rect1);
    expect(canvas.getActiveObjects()[1], 'rect2 is second object').toBe(rect2);
    expect(canvas.getActiveObjects()[2], 'rect3 is third object').toBe(rect3);
    expect(canvas.getActiveObjects().length, 'contains exactly 3 objects').toBe(
      3,
    );
  });

  it('fires selection:created when a single object is selected', () => {
    let isFired = false;
    const rect1 = new Rect({ width: 10, height: 10 });
    canvas.on('selection:created', () => {
      isFired = true;
    });
    canvas.add(rect1);

    setGroupSelector(canvas, {
      x: 1,
      y: 1,
      deltaX: 5,
      deltaY: 5,
    });
    canvas._onMouseUp(createPointerEvent({ target: canvas.upperCanvasEl }));

    expect(isFired, 'selection:created fired').toBe(true);
    expect(canvas.getActiveObject(), 'rect1 is set as activeObject').toBe(
      rect1,
    );
  });

  it('collects topmost object when no dragging occurs', () => {
    const rect1 = new Rect({ width: 10, height: 10, top: 0, left: 0 });
    const rect2 = new Rect({ width: 10, height: 10, top: 0, left: 0 });
    const rect3 = new Rect({ width: 10, height: 10, top: 0, left: 0 });
    canvas.add(rect1, rect2, rect3);

    setGroupSelector(canvas, { x: 1, y: 1, deltaX: 0, deltaY: 0 });

    // @ts-expect-error -- protected method
    expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy();
    expect(
      canvas.getActiveObjects().length,
      'a rect that contains all objects collects them all',
    ).toBe(1);
    expect(canvas.getActiveObjects()[0], 'rect3 is collected').toBe(rect3);
  });

  it('does not collect objects with onSelect returning true', () => {
    const rect1 = new Rect({ width: 10, height: 10, top: 2, left: 2 });
    rect1.onSelect = () => {
      return true;
    };
    const rect2 = new Rect({ width: 10, height: 10, top: 2, left: 2 });
    canvas.add(rect1, rect2);

    setGroupSelector(canvas, { x: 1, y: 1, deltaX: 20, deltaY: 20 });

    // @ts-expect-error -- protected method
    expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy();
    expect(
      canvas.getActiveObjects().length,
      'objects are in the same position buy only one gets selected',
    ).toBe(1);
    expect(canvas.getActiveObjects()[0], 'contains rect2 but not rect 1').toBe(
      rect2,
    );
  });

  it('does not call onSelect on objects that are not intersected', () => {
    const rect1 = new Rect({ width: 10, height: 10, top: 5, left: 5 });
    const rect2 = new Rect({ width: 10, height: 10, top: 5, left: 15 });
    let onSelectRect1CallCount = 0;
    let onSelectRect2CallCount = 0;

    rect1.onSelect = () => {
      onSelectRect1CallCount++;
      return false;
    };
    rect2.onSelect = () => {
      onSelectRect2CallCount++;
      return false;
    };
    canvas.add(rect1, rect2);

    // Intersects none
    setGroupSelector(canvas, { x: 25, y: 25, deltaX: 1, deltaY: 1 });
    // @ts-expect-error -- protected method
    expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy();
    const onSelectCalls = onSelectRect1CallCount + onSelectRect2CallCount;
    expect(onSelectCalls, 'none of the onSelect methods was called').toBe(0);

    // Intersects one
    setGroupSelector(canvas, { x: 0, y: 0, deltaX: 5, deltaY: 5 });
    // @ts-expect-error -- protected method
    expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy();
    expect(canvas.getActiveObject(), 'rect1 was selected').toBe(rect1);
    expect(
      onSelectRect1CallCount,
      'rect1 onSelect was called while setting active object',
    ).toBe(1);
    expect(onSelectRect2CallCount, 'rect2 onSelect was not called').toBe(0);

    // Intersects both
    setGroupSelector(canvas, { x: 0, y: 0, deltaX: 15, deltaY: 5 });
    // @ts-expect-error -- protected method
    expect(canvas.handleSelection({}), 'selection occurred').toBeTruthy();
    expect(canvas.getActiveObjects(), 'rect1 selected').toEqual([rect1, rect2]);
    expect(
      onSelectRect1CallCount,
      'rect1 onSelect was called once when collectiong it and once when selecting it',
    ).toBe(2);
    expect(onSelectRect2CallCount, 'rect2 onSelect was called').toBe(1);
  });

  it('returns false from handleMultiSelection when onSelect returns true', () => {
    const rect = new Rect();
    const rect2 = new Rect();
    rect.onSelect = () => {
      return true;
    };
    canvas._activeObject = rect2;
    const selectionKey = canvas.selectionKey;
    const event = {};
    // @ts-expect-error -- typed as readonly but in test case we want to override
    event[selectionKey] = true;
    // @ts-expect-error -- protected method
    const returned = canvas.handleMultiSelection(event, rect);

    expect(returned, 'if onSelect returns true, shouldGroup return false').toBe(
      false,
    );
  });

  it('returns true from handleMultiSelection when onSelect returns false and selectionKey is true', () => {
    const rect = new Rect();
    const rect2 = new Rect();
    rect.onSelect = () => {
      return false;
    };
    canvas._activeObject = rect2;
    const selectionKey = canvas.selectionKey;
    const event = {};
    // @ts-expect-error -- typed as readonly but in test case we want to override
    event[selectionKey] = true;
    // @ts-expect-error -- protected method
    const returned = canvas.handleMultiSelection(event, rect);
    expect(returned, 'if onSelect returns false, shouldGroup return true').toBe(
      true,
    );
  });

  it('returns false from handleMultiSelection when selectionKey is false', () => {
    const rect = new Rect();
    const rect2 = new Rect();
    rect.onSelect = () => {
      return false;
    };
    canvas._activeObject = rect2;
    const selectionKey = canvas.selectionKey;
    const event = {};
    // @ts-expect-error -- typed as readonly but in test case we want to override
    event[selectionKey] = false;
    // @ts-expect-error -- protected method
    const returned = canvas.handleMultiSelection(event, rect);
    expect(returned, 'shouldGroup return false').toBe(false);
  });

  it('fires multiple events from _fireSelectionEvents', () => {
    let rect1Deselected = false;
    let rect3Selected = false;
    const rect1 = new Rect();
    const rect2 = new Rect();
    const rect3 = new Rect();
    const activeSelection = new ActiveSelection();
    activeSelection.add(rect1, rect2);
    canvas.setActiveObject(activeSelection);
    rect1.on('deselected', () => {
      rect1Deselected = true;
    });
    rect3.on('selected', () => {
      rect3Selected = true;
    });
    const currentObjects = canvas.getActiveObjects();
    activeSelection.remove(rect1);
    activeSelection.add(rect3);
    canvas._fireSelectionEvents(currentObjects, {} as TPointerEvent);
    expect(rect3Selected, 'rect 3 selected').toBeTruthy();
    expect(rect1Deselected, 'rect 1 deselected').toBeTruthy();
  });

  it('implements getContext method', () => {
    expect(canvas.getContext).toBeTypeOf('function');
  });

  it('implements clearContext method', () => {
    expect(canvas.clearContext).toBeTypeOf('function');
    canvas.clearContext(canvas.getContext());
  });

  it('implements clear method and empties the canvas', () => {
    expect(canvas.clear).toBeTypeOf('function');
    canvas.clear();
    expect(canvas.getObjects().length).toBe(0);
  });

  it('implements renderAll method', () => {
    expect(canvas.renderAll).toBeTypeOf('function');
  });

  it('implements _drawSelection method', () => {
    expect(canvas._drawSelection).toBeTypeOf('function');
  });

  it('finds target objects correctly with findTarget', () => {
    expect(canvas.findTarget).toBeTypeOf('function');
    const rect = makeRect({ left: 0, top: 0 });
    canvas.add(rect);

    const { target } = canvas.findTarget(
      createPointerEvent({
        clientX: 5,
        clientY: 5,
        target: canvas.upperCanvasEl,
      }),
    );
    expect(target, 'Should return the rect').toBe(rect);

    const { target: target2 } = canvas.findTarget(
      createPointerEvent({
        clientX: 30,
        clientY: 30,
        target: canvas.upperCanvasEl,
      }),
    );
    expect(target2, 'Should not find target').toBeUndefined();

    canvas.remove(rect);
  });

  it('implements toCanvasElement method that clears the contextTop', () => {
    const canvas = new Canvas();
    const mockSetCtx = vi.fn();
    class UpperMock {
      declare el: any;
      set ctx(value: any) {
        mockSetCtx(value);
      }
      get ctx() {
        return undefined;
      }
      constructor() {
        this.el = {
          getContext: vi.fn(),
        };
      }
    }
    canvas.elements.upper = new UpperMock();

    canvas.toCanvasElement();
    expect(mockSetCtx).toHaveBeenCalledWith(undefined);
    expect(mockSetCtx).toHaveBeenCalledTimes(2);
  });

  it('implements toDataURL method that returns valid data URL', () => {
    expect(canvas.toDataURL).toBeTypeOf('function');
    const dataURL = canvas.toDataURL();
    // don't compare actual data url, as it is often browser-dependent
    expect(typeof dataURL, 'is a string').toBe('string');
    expect(dataURL.substring(0, 21), 'starts with correct prefix').toBe(
      'data:image/png;base64',
    );
  });

  it('implements getCenterPoint method that returns canvas center as Point', () => {
    expect(canvas.getCenterPoint).toBeTypeOf('function');
    const center = canvas.getCenterPoint();
    expect(center.x, 'center x is half width').toBe(upperCanvasEl.width / 2);
    expect(center.y, 'center y is half height').toBe(upperCanvasEl.height / 2);
  });

  it('centers objects horizontally with centerObjectH', () => {
    expect(canvas.centerObjectH).toBeTypeOf('function');
    const rect = makeRect({ left: 102, top: 202 });
    canvas.add(rect);
    canvas.centerObjectH(rect);
    expect(
      rect.getCenterPoint().x,
      'object\'s "left" property should correspond to canvas element\'s center',
    ).toBe(upperCanvasEl.width / 2);
  });

  it('centers objects vertically with centerObjectV', () => {
    expect(canvas.centerObjectV).toBeTypeOf('function');
    const rect = makeRect({ left: 102, top: 202 });
    canvas.add(rect);
    canvas.centerObjectV(rect);
    expect(
      rect.getCenterPoint().y,
      'object\'s "top" property should correspond to canvas element\'s center',
    ).toBe(upperCanvasEl.height / 2);
  });

  it('centers objects with centerObject', () => {
    expect(canvas.centerObject).toBeTypeOf('function');
    const rect = makeRect({ left: 102, top: 202 });
    canvas.add(rect);
    canvas.centerObject(rect);

    expect(
      rect.getCenterPoint().y,
      'object\'s "top" property should correspond to canvas element\'s center',
    ).toBe(upperCanvasEl.height / 2);
    expect(
      rect.getCenterPoint().x,
      'object\'s "left" property should correspond to canvas element\'s center',
    ).toBe(upperCanvasEl.width / 2);
  });

  it('serializes to JSON with toJSON', () => {
    expect(canvas.toJSON).toBeTypeOf('function');
    expect(JSON.stringify(canvas.toJSON())).toBe(JSON.stringify(EMPTY_JSON));

    canvas.backgroundColor = '#ff5555';
    canvas.overlayColor = 'rgba(0,0,0,0.2)';
    expect(
      canvas.toJSON(),
      '`background` and `overlayColor` value should be reflected in json',
    ).toEqual({
      version: version,
      objects: [],
      background: '#ff5555',
      overlay: 'rgba(0,0,0,0.2)',
    });

    canvas.add(makeRect());
    expect(canvas.toJSON()).toEqual(RECT_JSON);
  });

  it('serializes to JSON with active selection', () => {
    const rect = new Rect({ width: 50, height: 50, left: 100, top: 100 });
    const circle = new Circle({ radius: 50, left: 50, top: 50 });
    canvas.add(rect, circle);
    const json = JSON.stringify(canvas);

    const activeSelection = new ActiveSelection();
    activeSelection.add(rect, circle);
    canvas.setActiveObject(activeSelection);
    const jsonWithActiveGroup = JSON.stringify(canvas);

    expect(json).toBe(jsonWithActiveGroup);
  });

  it('serializes to dataless JSON with toDatalessJSON', () => {
    const path = new Path('M 100 100 L 300 100 L 200 300 z', {
      sourcePath: 'http://example.com/',
    });
    canvas.add(path);
    expect(canvas.toDatalessJSON()).toEqual(PATH_DATALESS_JSON);
  });

  it('converts to object with toObject', () => {
    expect(canvas.toObject).toBeTypeOf('function');
    const expectedObject = {
      version: version,
      objects: canvas.getObjects(),
    };
    expect(canvas.toObject()).toEqual(expectedObject);

    const rect = makeRect();
    canvas.add(rect);

    // @ts-expect-error -- constructor function has type
    expect(canvas.toObject().objects[0].type).toBe(rect.constructor.type);
  });

  it('includes clipPath in toObject when present', () => {
    const clipPath = makeRect();
    const canvasWithClipPath = new Canvas(undefined, { clipPath: clipPath });
    const expectedObject = {
      version: version,
      objects: canvasWithClipPath.getObjects(),
      clipPath: {
        type: 'Rect',
        version: version,
        originX: 'center',
        originY: 'center',
        left: 0,
        top: 0,
        width: 10,
        height: 10,
        fill: 'rgb(0,0,0)',
        stroke: null,
        strokeWidth: 1,
        strokeDashArray: null,
        strokeLineCap: 'butt',
        strokeDashOffset: 0,
        strokeLineJoin: 'miter',
        strokeMiterLimit: 4,
        scaleX: 1,
        scaleY: 1,
        angle: 0,
        flipX: false,
        flipY: false,
        opacity: 1,
        shadow: null,
        visible: true,
        backgroundColor: '',
        fillRule: 'nonzero',
        paintFirst: 'fill',
        globalCompositeOperation: 'source-over',
        skewX: 0,
        skewY: 0,
        rx: 0,
        ry: 0,
        strokeUniform: false,
      },
    };

    expect(canvasWithClipPath.toObject).toBeTypeOf('function');
    expect(canvasWithClipPath.toObject()).toEqual(expectedObject);

    const rect = makeRect();
    canvasWithClipPath.add(rect);

    expect(canvasWithClipPath.toObject().objects[0].type).toBe(
      // @ts-expect-error -- constructor function has type
      rect.constructor.type,
    );
  });

  it('converts to dataless object with toDatalessObject', () => {
    expect(canvas.toDatalessObject).toBeTypeOf('function');
    const expectedObject = {
      version: version,
      objects: canvas.getObjects(),
    };

    expect(canvas.toDatalessObject()).toEqual(expectedObject);

    const rect = makeRect();
    canvas.add(rect);

    // @ts-expect-error -- constructor function has type
    expect(canvas.toObject().objects[0].type).toBe(rect.constructor.type);
    // TODO (kangax): need to test this method with fabric.Path to ensure that path is not populated
  });

  it('checks if canvas is empty with isEmpty', () => {
    expect(canvas.isEmpty).toBeTypeOf('function');
    expect(canvas.isEmpty()).toBeTruthy();
    canvas.add(makeRect());
    expect(canvas.isEmpty()).toBeFalsy();
  });

  it('loads from JSON string with loadFromJSON', async () => {
    expect(canvas.loadFromJSON).toBeTypeOf('function');
    await canvas.loadFromJSON(PATH_JSON);

    const obj = canvas.item(0);

    expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy();
    // @ts-expect-error -- constructor function has type
    expect(obj.constructor.type, 'first object is a path object').toBe('Path');
    expect(
      canvas.backgroundColor,
      'backgroundColor is populated properly',
    ).toBe('#ff5555');
    expect(canvas.overlayColor, 'overlayColor is populated properly').toBe(
      'rgba(0,0,0,0.2)',
    );

    expect(obj.get('left')).toBe(268);
    expect(obj.get('top')).toBe(266);
    expect(obj.get('width')).toBe(49.803999999999995);
    expect(obj.get('height')).toBe(48.027);
    expect(obj.get('fill')).toBe('rgb(0,0,0)');
    expect(obj.get('stroke')).toBe(null);
    expect(obj.get('strokeWidth')).toBe(1);
    expect(obj.get('scaleX')).toBe(1);
    expect(obj.get('scaleY')).toBe(1);
    expect(obj.get('angle')).toBe(0);
    expect(obj.get('flipX')).toBe(false);
    expect(obj.get('flipY')).toBe(false);
    expect(obj.get('opacity')).toBe(1);
    expect(obj.get('path').length > 0).toBeTruthy();
  });

  it('loads from JSON object with loadFromJSON', async () => {
    await canvas.loadFromJSON(PATH_JSON);

    const obj = canvas.item(0);

    expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy();
    // @ts-expect-error -- constructor function has type
    expect(obj.constructor.type, 'first object is a path object').toBe('Path');
    expect(
      canvas.backgroundColor,
      'backgroundColor is populated properly',
    ).toBe('#ff5555');
    expect(canvas.overlayColor, 'overlayColor is populated properly').toBe(
      'rgba(0,0,0,0.2)',
    );

    expect(obj.get('left')).toBe(268);
    expect(obj.get('top')).toBe(266);
    expect(obj.get('width')).toBe(49.803999999999995);
    expect(obj.get('height')).toBe(48.027);
    expect(obj.get('fill')).toBe('rgb(0,0,0)');
    expect(obj.get('stroke')).toBe(null);
    expect(obj.get('strokeWidth')).toBe(1);
    expect(obj.get('scaleX')).toBe(1);
    expect(obj.get('scaleY')).toBe(1);
    expect(obj.get('angle')).toBe(0);
    expect(obj.get('flipX')).toBe(false);
    expect(obj.get('flipY')).toBe(false);
    expect(obj.get('opacity')).toBe(1);
    expect(obj.get('path').length > 0).toBeTruthy();
  });

  it('loads from JSON string with loadFromJSON with images not existing', async () => {
    expect(canvas.loadFromJSON).toBeTypeOf('function');
    await canvas.loadFromJSON(ERROR_IMAGE_JSON);

    const obj = canvas.item(0);

    expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy();
    expect(canvas.getObjects().length).toBe(1);
    // @ts-expect-error -- constructor function has type
    expect(obj.constructor.type, 'first object is a Image object').toBe('Text');
  });

  it('loads from JSON string with loadFromJSON with images not existing passing reviver', async () => {
    expect(canvas.loadFromJSON).toBeTypeOf('function');
    await canvas.loadFromJSON(
      ERROR_IMAGE_JSON,
      async (serializedObject, instance, error) => {
        if (error) {
          return new FabricText('text-placeholder');
        }
      },
    );

    const obj = canvas.item(0);

    expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy();
    expect(canvas.getObjects().length).toBe(2);
    // @ts-expect-error -- constructor function has type
    expect(obj.constructor.type, 'first object is a Image object').toBe('Text');
    expect(obj).toBeInstanceOf(FabricText);
    expect((obj as FabricText).text).toBe('text-placeholder');
  });

  it('loads from JSON object without default values', async () => {
    await canvas.loadFromJSON(PATH_WITHOUT_DEFAULTS_JSON);
    const obj = canvas.item(0);

    expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy();
    // @ts-expect-error -- constructor function has type
    expect(obj.constructor.type, 'first object is a path object').toBe('Path');
    expect(
      canvas.backgroundColor,
      'backgroundColor is populated properly',
    ).toBe('#ff5555');
    expect(canvas.overlayColor, 'overlayColor is populated properly').toBe(
      'rgba(0,0,0,0.2)',
    );

    expect(obj.get('originX')).toBe('center');
    expect(obj.get('originY')).toBe('center');
    expect(obj.get('left')).toBe(268);
    expect(obj.get('top')).toBe(266);
    expect(obj.get('width')).toBe(49.803999999999995);
    expect(obj.get('height')).toBe(48.027);
    expect(obj.get('fill')).toBe('rgb(0,0,0)');
    expect(obj.get('stroke')).toBe(null);
    expect(obj.get('strokeWidth')).toBe(1);
    expect(obj.get('scaleX')).toBe(1);
    expect(obj.get('scaleY')).toBe(1);
    expect(obj.get('angle')).toBe(0);
    expect(obj.get('flipX')).toBe(false);
    expect(obj.get('flipY')).toBe(false);
    expect(obj.get('opacity')).toBe(1);
    expect(obj.get('path').length > 0).toBeTruthy();
  });

  it('loads from JSON with reviver function', async () => {
    await canvas.loadFromJSON(PATH_JSON, function (obj, instance) {
      expect(obj).toEqual(PATH_OBJ_JSON);
      // @ts-expect-error -- constructor function has type
      if (instance.constructor.type === 'Path') {
        // @ts-expect-error -- custom prop
        instance.customID = 'fabric_1';
      }
    });
    const obj = canvas.item(0);

    expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy();
    // @ts-expect-error -- constructor function has type
    expect(obj.constructor.type, 'first object is a path object').toBe('Path');
    expect(
      canvas.backgroundColor,
      'backgroundColor is populated properly',
    ).toBe('#ff5555');
    expect(canvas.overlayColor, 'overlayColor is populated properly').toBe(
      'rgba(0,0,0,0.2)',
    );

    expect(obj.get('left')).toBe(268);
    expect(obj.get('top')).toBe(266);
    expect(obj.get('width')).toBe(49.803999999999995);
    expect(obj.get('height')).toBe(48.027);
    expect(obj.get('fill')).toBe('rgb(0,0,0)');
    expect(obj.get('stroke')).toBe(null);
    expect(obj.get('strokeWidth')).toBe(1);
    expect(obj.get('scaleX')).toBe(1);
    expect(obj.get('scaleY')).toBe(1);
    expect(obj.get('angle')).toBe(0);
    expect(obj.get('flipX')).toBe(false);
    expect(obj.get('flipY')).toBe(false);
    expect(obj.get('opacity')).toBe(1);
    expect(obj.get('customID')).toBe('fabric_1');
    expect(obj.get('path').length > 0).toBeTruthy();
  });

  it('loads from JSON with no objects', async () => {
    const canvas1 = getFabricDocument().createElement('canvas');
    const canvas2 = getFabricDocument().createElement('canvas');
    const c1 = new Canvas(canvas1, {
      backgroundColor: 'green',
      overlayColor: 'yellow',
    });
    const c2 = new Canvas(canvas2, {
      backgroundColor: 'red',
      overlayColor: 'orange',
    });

    const json = c1.toJSON();
    let fired = false;

    await c2.loadFromJSON(json).then(() => {
      fired = true;
    });

    expect(fired, 'Callback should be fired even if no objects').toBeTruthy();
    expect(c2.backgroundColor, 'Color should be set properly').toBe('green');
    expect(c2.overlayColor, 'Color should be set properly').toBe('yellow');
  });

  it('loads from JSON without "objects" property', async () => {
    const canvas1 = getFabricDocument().createElement('canvas');
    const canvas2 = getFabricDocument().createElement('canvas');
    const c1 = new Canvas(canvas1, {
      backgroundColor: 'green',
      overlayColor: 'yellow',
    });
    const c2 = new Canvas(canvas2, {
      backgroundColor: 'red',
      overlayColor: 'orange',
    });

    const json = c1.toJSON();
    let fired = false;

    delete json.objects;

    await c2.loadFromJSON(json).then(() => {
      fired = true;
    });

    expect(
      fired,
      'Callback should be fired even if no "objects" property exists',
    ).toBeTruthy();
    expect(c2.backgroundColor, 'Color should be set properly').toBe('green');
    expect(c2.overlayColor, 'Color should be set properly').toBe('yellow');
  });

  it('loads from JSON with empty Group', async () => {
    const canvas1 = getFabricDocument().createElement('canvas');
    const canvas2 = getFabricDocument().createElement('canvas');
    const c1 = new Canvas(canvas1);
    const c2 = new Canvas(canvas2);
    const group = new Group();

    c1.add(group);
    expect(c1.isEmpty(), 'canvas is not empty').toBeFalsy();

    const json = c1.toJSON();
    let fired = false;

    await c2.loadFromJSON(json).then(() => {
      fired = true;
    });

    expect(
      fired,
      'Callback should be fired even if empty fabric.Group exists',
    ).toBeTruthy();
  });

  it('loads from JSON with async content', async () => {
    const group = new Group([
      new Rect({ width: 10, height: 20 }),
      new Circle({ radius: 10 }),
    ]);
    const rect = new Rect({ width: 20, height: 10 });
    const circle = new Circle({ radius: 25 });

    canvas.add(group, rect, circle);
    const json = JSON.stringify(canvas);
    canvas.clear();

    expect(canvas.getObjects().length).toBe(0);

    await canvas.loadFromJSON(json);
    expect(canvas.getObjects().length).toBe(3);
  });

  it('loads custom properties on Canvas with no async objects', async () => {
    const serialized = JSON.parse(JSON.stringify(PATH_JSON));
    serialized.controlsAboveOverlay = true;
    serialized.preserveObjectStacking = true;

    expect(canvas.controlsAboveOverlay).toBe(
      Canvas.getDefaults().controlsAboveOverlay,
    );
    expect(canvas.preserveObjectStacking).toBe(
      Canvas.getDefaults().preserveObjectStacking,
    );

    await canvas.loadFromJSON(serialized);

    expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy();
    expect(canvas.controlsAboveOverlay).toBe(true);
    expect(canvas.preserveObjectStacking).toBe(true);
  });

  it('loads custom properties on Canvas with image', async () => {
    const serialized = {
      objects: [
        {
          type: 'image',
          originX: 'left',
          originY: 'top',
          left: 13.6,
          top: -1.4,
          width: 3000,
          height: 3351,
          fill: 'rgb(0,0,0)',
          stroke: null,
          strokeWidth: 0,
          strokeDashArray: null,
          strokeLineCap: 'butt',
          strokeDashOffset: 0,
          strokeLineJoin: 'miter',
          strokeMiterLimit: 4,
          scaleX: 0.05,
          scaleY: 0.05,
          angle: 0,
          flipX: false,
          flipY: false,
          opacity: 1,
          shadow: null,
          visible: true,
          backgroundColor: '',
          fillRule: 'nonzero',
          globalCompositeOperation: 'source-over',
          skewX: 0,
          skewY: 0,
          src: isJSDOM() ? 'test_image.gif' : TEST_IMAGE,
          filters: [],
          crossOrigin: '',
        },
      ],
      background: 'green',
    };

    // @ts-expect-error -- custom prop
    serialized.controlsAboveOverlay = true;
    // @ts-expect-error -- custom prop
    serialized.preserveObjectStacking = false;

    expect(canvas.controlsAboveOverlay).toBe(
      Canvas.getDefaults().controlsAboveOverlay,
    );
    expect(canvas.preserveObjectStacking).toBe(
      Canvas.getDefaults().preserveObjectStacking,
    );

    // before callback the properties are still false.
    expect(canvas.controlsAboveOverlay).toBe(false);
    expect(canvas.preserveObjectStacking).toBe(true);

    await canvas.loadFromJSON(serialized);

    expect(canvas.isEmpty(), 'canvas is not empty').toBeFalsy();
    expect(canvas.controlsAboveOverlay).toBe(true);
    expect(canvas.preserveObjectStacking).toBe(false);
  });

  // TODO: does this test makes sense in vitest?
  // QUnit.test('loadFromJSON with backgroundImage', function(assert) {
  //   var done = assert.async();
  //   canvas.setBackgroundImage('../../assets/pug.jpg');
  //   var anotherCanvas = new fabric.Canvas();

  //   setTimeout(function() {

  //     var json = JSON.stringify(canvas);
  //     anotherCanvas.loadFromJSON(json);

  //     setTimeout(function() {

  //       assert.equal(JSON.stringify(anotherCanvas), json, 'backgrondImage and properties are initialized correctly');
  //       done();

  //     }, 1000);
  //   }, 1000);
  // });

  it('sends objects to the back of the stack', () => {
    expect(canvas.sendObjectToBack).toBeTypeOf('function');

    const rect1 = makeRect();
    const rect2 = makeRect();
    const rect3 = makeRect();

    canvas.add(rect1, rect2, rect3);

    canvas.sendObjectToBack(rect3);
    expect(canvas.item(0), 'third should now be the first one').toBe(rect3);

    canvas.sendObjectToBack(rect2);
    expect(canvas.item(0), 'second should now be the first one').toBe(rect2);

    canvas.sendObjectToBack(rect2);
    expect(canvas.item(0), 'second should *still* be the first one').toBe(
      rect2,
    );
  });

  it('brings objects to the front of the stack', () => {
    expect(canvas.bringObjectToFront).toBeTypeOf('function');

    const rect1 = makeRect();
    const rect2 = makeRect();
    const rect3 = makeRect();

    canvas.add(rect1, rect2, rect3);

    canvas.bringObjectToFront(rect1);
    expect(canvas.item(2), 'first should now be the last one').toBe(rect1);

    canvas.bringObjectToFront(rect2);
    expect(canvas.item(2), 'second should now be the last one').toBe(rect2);

    canvas.bringObjectToFront(rect2);
    expect(canvas.item(2), 'second should *still* be the last one').toBe(rect2);
  });

  it('sends objects backwards in the stack', () => {
    expect(canvas.sendObjectBackwards).toBeTypeOf('function');

    const rect1 = makeRect();
    const rect2 = makeRect();
    const rect3 = makeRect();

    canvas.add(rect1, rect2, rect3);

    // [ 1, 2, 3 ]
    expect(canvas.item(0)).toBe(rect1);
    expect(canvas.item(1)).toBe(rect2);
    expect(canvas.item(2)).toBe(rect3);

    canvas.sendObjectBackwards(rect3);

    // moved 3 one level back — [1, 3, 2]
    expect(canvas.item(0)).toBe(rect1);
    expect(canvas.item(2)).toBe(rect2);
    expect(canvas.item(1)).toBe(rect3);

    canvas.sendObjectBackwards(rect3);

    // moved 3 one level back — [3, 1, 2]
    expect(canvas.item(1)).toBe(rect1);
    expect(canvas.item(2)).toBe(rect2);
    expect(canvas.item(0)).toBe(rect3);

    canvas.sendObjectBackwards(rect3);

    // 3 stays at the same position — [3, 1, 2]
    expect(canvas.item(1)).toBe(rect1);
    expect(canvas.item(2)).toBe(rect2);
    expect(canvas.item(0)).toBe(rect3);

    canvas.sendObjectBackwards(rect2);

    expect(canvas.item(2)).toBe(rect1);
    expect(canvas.item(1)).toBe(rect2);
    expect(canvas.item(0)).toBe(rect3);

    canvas.sendObjectBackwards(rect2);

    expect(canvas.item(2)).toBe(rect1);
    expect(canvas.item(0)).toBe(rect2);
    expect(canvas.item(1)).toBe(rect3);
  });

  it('brings objects forward in the stack', () => {
    expect(canvas.bringObjectForward).toBeTypeOf('function');

    const rect1 = makeRect();
    const rect2 = makeRect();
    const rect3 = makeRect();

    canvas.add(rect1, rect2, rect3);

    // initial position — [ 1, 2, 3 ]
    expect(canvas.item(0)).toBe(rect1);
    expect(canvas.item(1)).toBe(rect2);
    expect(canvas.item(2)).toBe(rect3);

    canvas.bringObjectForward(rect1);

    // 1 moves one way up — [ 2, 1, 3 ]
    expect(canvas.item(1)).toBe(rect1);
    expect(canvas.item(0)).toBe(rect2);
    expect(canvas.item(2)).toBe(rect3);

    canvas.bringObjectForward(rect1);

    // 1 moves one way up again — [ 2, 3, 1 ]
    expect(canvas.item(2)).toBe(rect1);
    expect(canvas.item(0)).toBe(rect2);
    expect(canvas.item(1)).toBe(rect3);

    canvas.bringObjectForward(rect1);

    // 1 is already all the way on top and so doesn't change position — [ 2, 3, 1 ]
    expect(canvas.item(2)).toBe(rect1);
    expect(canvas.item(0)).toBe(rect2);
    expect(canvas.item(1)).toBe(rect3);

    canvas.bringObjectForward(rect3);

    // 3 moves one way up — [ 2, 1, 3 ]
    expect(canvas.item(1)).toBe(rect1);
    expect(canvas.item(0)).toBe(rect2);
    expect(canvas.item(2)).toBe(rect3);
  });

  it('sets active object and tracks it', () => {
    expect(canvas.setActiveObject).toBeTypeOf('function');

    const rect1 = makeRect();
    const rect2 = makeRect();

    canvas.add(rect1, rect2);

    expect(canvas.setActiveObject(rect1), 'selected').toBeTruthy();
    expect(rect1 === canvas._activeObject).toBeTruthy();

    expect(canvas.setActiveObject(rect2), 'selected').toBeTruthy();
    expect(canvas.setActiveObject(rect2), 'no effect').toBeFalsy();
    expect(rect2 === canvas._activeObject).toBeTruthy();
  });

  it('gets active object', () => {
    expect(canvas.getActiveObject).toBeTypeOf('function');
    expect(
      canvas.getActiveObject(),
      'should initially be undefined',
    ).toBeUndefined();

    const rect1 = makeRect();
    const rect2 = makeRect();

    canvas.add(rect1, rect2);

    canvas.setActiveObject(rect1);
    expect(canvas.getActiveObject()).toBe(rect1);

    canvas.setActiveObject(rect2);
    expect(canvas.getActiveObject()).toBe(rect2);
  });

  it('sets and gets active object with groups', () => {
    expect(
      canvas.getActiveObject(),
      'should initially be undefined',
    ).toBeUndefined();

    const group = new Group([
      makeRect({ left: 10, top: 10 }),
      makeRect({ left: 20, top: 20 }),
    ]);

    canvas.setActiveObject(group);
    expect(canvas.getActiveObject()).toBe(group);
  });

  it('retrieves objects by index with item method', () => {
    expect(canvas.item).toBeTypeOf('function');

    const rect1 = makeRect();
    const rect2 = makeRect();

    canvas.add(rect1, rect2);

    expect(canvas.item(0)).toBe(rect1);
    expect(canvas.item(1)).toBe(rect2);

    canvas.remove(canvas.item(0));

    expect(canvas.item(0)).toBe(rect2);
  });

  it('discards active object on ActiveSelection', () => {
    const group = new ActiveSelection([makeRect(), makeRect()], { canvas });
    canvas.setActiveObject(group);
    canvas.discardActiveObject();
    expect(
      canvas.getActiveObject(),
      'removing active group sets it to undefined',
    ).toBeUndefined();
  });

  it('discards active object with internal method', () => {
    canvas.add(makeRect());
    canvas.setActiveObject(canvas.item(0));

    expect(canvas._discardActiveObject(), 'discarded').toBeTruthy();
    expect(canvas._discardActiveObject(), 'no effect').toBeFalsy();
    expect(canvas.getActiveObject()).toBeUndefined();
  });

  it('cleans up transform when discarding active object', () => {
    const e = createPointerEvent({
      clientX: 5,
      clientY: 5,
      target: canvas.upperCanvasEl,
    });
    const target = makeRect();
    canvas.add(target);
    canvas.setActiveObject(target);
    canvas._setupCurrentTransform(e, target, true);
    expect(canvas._currentTransform, 'transform should be set').toBeTruthy();

    target.isMoving = true;
    canvas._discardActiveObject();

    expect(canvas._currentTransform, 'transform should be cleared').toBeFalsy();
    expect(target.isMoving, 'moving flag should have been negated').toBeFalsy();
    expect(canvas.getActiveObject()).toBeUndefined();
  });

  it('discards active object and fires events', () => {
    expect(canvas.discardActiveObject).toBeTypeOf('function');

    canvas.add(makeRect());
    canvas.setActiveObject(canvas.item(0));

    const group = new Group([
      makeRect({ left: 10, top: 10 }),
      makeRect({ left: 20, top: 20 }),
    ]);

    canvas.setActiveObject(group);

    const eventsFired: Record<string, unknown> = {
      selectionCleared: false,
    };

    canvas.on('selection:cleared', () => {
      eventsFired.selectionCleared = true;
    });

    expect(canvas.discardActiveObject(), 'deselected').toBeTruthy();
    expect(canvas.getActiveObject(), 'no active object').toBeUndefined();
    expect(canvas.discardActiveObject(), 'no effect').toBeFalsy();
    expect(canvas.getActiveObject()).toBeUndefined();

    for (const prop in eventsFired) {
      expect(eventsFired[prop]).toBeTruthy();
    }
  });

  it('refuses to discard active object when onDeselect returns true', () => {
    const rect = makeRect();
    rect.onDeselect = () => true;
    canvas.setActiveObject(rect);

    expect(canvas.discardActiveObject(), 'no effect').toBeFalsy();
    expect(canvas.getActiveObject() === rect, 'active object').toBeTruthy();

    canvas.clear();
    expect(canvas.getActiveObject(), 'cleared the stubborn ref').toBeFalsy();
  });

  it('calculates complexity based on number of objects', () => {
    expect(canvas.complexity).toBeTypeOf('function');
    expect(canvas.complexity()).toBe(0);

    canvas.add(makeRect());
    expect(canvas.complexity()).toBe(1);

    canvas.add(makeRect(), makeRect());
    expect(canvas.complexity()).toBe(3);
  });

  it('converts to string representation', () => {
    expect(canvas.toString).toBeTypeOf('function');

    expect(canvas.toString()).toBe('#<Canvas (0): { objects: 0 }>');

    canvas.add(makeRect());
    expect(canvas.toString()).toBe('#<Canvas (1): { objects: 1 }>');
  });

  it('produces same SVG with or without active selection', () => {
    const rect = new Rect({ width: 50, height: 50, left: 100, top: 100 });
    const circle = new Circle({ radius: 50, left: 50, top: 50 });
    canvas.add(rect, circle);
    const svg = canvas.toSVG();

    const activeSelection = new ActiveSelection();
    activeSelection.add(rect, circle);
    canvas.setActiveObject(activeSelection);
    const svgWithActiveGroup = canvas.toSVG();

    expect(svg).toBe(svgWithActiveGroup);
  });

  describe.each([true, false])(
    'set dimensions with enableRetinaScaling=%s',
    (enableRetinaScaling) => {
      it('sets and restores dimensions correctly', async () => {
        const el = getFabricDocument().createElement('canvas');
        const parentEl = getFabricDocument().createElement('div');
        el.width = 200;
        el.height = 200;
        parentEl.className = 'rootNode';
        parentEl.appendChild(el);

        const dpr = 1.25;
        config.configure({ devicePixelRatio: dpr });

        expect(
          parentEl.firstChild,
          'canvas should be appended at partentEl',
        ).toBe(el);
        expect(parentEl.childNodes.length, 'parentEl has 1 child only').toBe(1);

        el.style.position = 'relative';
        const elStyle = el.style.cssText;
        expect(elStyle, 'el style should not be empty').toBe(
          'position: relative;',
        );

        const canvasObj = new Canvas(el, {
          enableRetinaScaling,
          renderOnAddRemove: false,
        });

        canvasObj.setDimensions({ width: 500, height: 500 });
        expect(
          // @ts-expect-error -- private prop
          canvasObj.elements._originalCanvasStyle,
          'saved original canvas style for disposal',
        ).toBe(elStyle);
        expect(el.style.cssText, 'canvas el style has been changed').not.toBe(
          // @ts-expect-error -- private prop
          canvasObj.elements._originalCanvasStyle,
        );
        expect(el.width, 'expected width').toBe(
          500 * (enableRetinaScaling ? dpr : 1),
        );
        expect(el.height, 'expected height').toBe(
          500 * (enableRetinaScaling ? dpr : 1),
        );
        expect(canvasObj.upperCanvasEl.width, 'expected width').toBe(
          500 * (enableRetinaScaling ? dpr : 1),
        );
        expect(canvasObj.upperCanvasEl.height, 'expected height').toBe(
          500 * (enableRetinaScaling ? dpr : 1),
        );

        await canvasObj.dispose();
        expect(
          // @ts-expect-error -- private prop
          canvasObj.elements._originalCanvasStyle,
          'removed original canvas style',
        ).toBe(undefined);
        expect(el.style.cssText, 'restored original canvas style').toBe(
          elStyle,
        );
        expect(el.width, 'restored width').toBe(500);
        expect(el.height, 'restored height').toBe(500);
      });
    },
  );

  it('clones the canvas and its contents', async () => {
    expect(canvas.clone).toBeTypeOf('function');

    canvas.add(
      new Rect({
        width: 100,
        height: 110,
        top: 120,
        left: 130,
        fill: 'rgba(0,1,2,0.3)',
      }),
    );
    const canvasData = JSON.stringify(canvas);

    const clone = await canvas.clone([]);
    expect(clone).toBeInstanceOf(Canvas);

    expect(
      JSON.stringify(clone),
      'data on cloned canvas should be identical',
    ).toBe(canvasData);

    expect(clone.getWidth()).toBe(canvas.getWidth());
    expect(clone.getHeight()).toBe(canvas.getHeight());
    clone.renderAll();
  });

  it('clones the canvas without data', () => {
    expect(canvas.cloneWithoutData).toBeTypeOf('function');

    canvas.add(
      new Rect({
        width: 100,
        height: 110,
        top: 120,
        left: 130,
        fill: 'rgba(0,1,2,0.3)',
      }),
    );

    const clone = canvas.cloneWithoutData();

    expect(clone).toBeInstanceOf(Canvas);
    expect(clone.toJSON(), 'data on cloned canvas should be empty').toEqual(
      EMPTY_JSON,
    );

    expect(clone.getWidth()).toBe(canvas.getWidth());
    expect(clone.getHeight()).toBe(canvas.getHeight());
    clone.renderAll();
  });

  it('gets and sets width', () => {
    expect(canvas.getWidth).toBeTypeOf('function');
    expect(canvas.getWidth()).toBe(600);

    canvas.setDimensions({ width: 444 });
    expect(canvas.getWidth()).toBe(444);
    expect(canvas.lowerCanvasEl.style.width).toBe('444px');
  });

  it('gets and sets height', () => {
    expect(canvas.getHeight).toBeTypeOf('function');
    expect(canvas.getHeight()).toBe(600);

    canvas.setDimensions({ height: 765 });
    expect(canvas.getHeight()).toBe(765);
    expect(canvas.lowerCanvasEl.style.height).toBe('765px');
  });

  it('sets width with cssOnly option', () => {
    canvas.setDimensions({ width: 123 });
    canvas.setDimensions({ width: '100%' }, { cssOnly: true });

    expect(
      canvas.lowerCanvasEl.style.width,
      'Should be as the css only value',
    ).toBe('100%');
    expect(
      canvas.upperCanvasEl.style.width,
      'Should be as the css only value',
    ).toBe('100%');
    expect(
      canvas.wrapperEl.style.width,
      'Should be as the css only value',
    ).toBe('100%');
    expect(canvas.getWidth(), 'Should be as the none css only value').toBe(123);
  });

  it('sets height with cssOnly option', () => {
    canvas.setDimensions({ height: 123 });
    canvas.setDimensions({ height: '100%' }, { cssOnly: true });

    expect(
      canvas.lowerCanvasEl.style.height,
      'Should be as the css only value',
    ).toBe('100%');
    expect(
      canvas.upperCanvasEl.style.height,
      'Should be as the css only value',
    ).toBe('100%');
    expect(
      canvas.wrapperEl.style.height,
      'Should be as the css only value',
    ).toBe('100%');
    expect(canvas.getHeight(), 'Should be as the none css only value').toBe(
      123,
    );
  });

  it('sets width with backstoreOnly option', () => {
    canvas.setDimensions({ width: 123 });
    canvas.setDimensions({ width: 500 }, { backstoreOnly: true });

    expect(
      canvas.lowerCanvasEl.style.width,
      'Should be as none backstore only value + "px"',
    ).toBe('123px');
    expect(
      canvas.upperCanvasEl.style.width,
      'Should be as none backstore only value + "px"',
    ).toBe('123px');
    expect(
      canvas.wrapperEl.style.width,
      'Should be as none backstore only value + "px"',
    ).toBe('123px');
    expect(canvas.getWidth(), 'Should be as the backstore only value').toBe(
      500,
    );
  });

  it('sets height with backstoreOnly option', () => {
    canvas.setDimensions({ height: 123 });
    canvas.setDimensions({ height: 500 }, { backstoreOnly: true });

    expect(
      canvas.lowerCanvasEl.style.height,
      'Should be as none backstore only value + "px"',
    ).toBe('123px');
    expect(
      canvas.upperCanvasEl.style.height,
      'Should be as none backstore only value + "px"',
    ).toBe('123px');
    expect(
      canvas.wrapperEl.style.height,
      'Should be as none backstore only value + "px"',
    ).toBe('123px');
    expect(canvas.getHeight(), 'Should be as the backstore only value').toBe(
      500,
    );
  });

  it('sets up current transform based on interaction point', () => {
    expect(canvas._setupCurrentTransform).toBeTypeOf('function');

    const rect = new Rect({ left: 100, top: 100, width: 50, height: 50 });
    canvas.add(rect);
    const canvasOffset = canvas.calcOffset();
    let eventStub = createPointerEvent({
      clientX: canvasOffset.left + 100,
      clientY: canvasOffset.top + 100,
      target: canvas.upperCanvasEl,
    });
    canvas.setActiveObject(rect);
    const targetCorner = rect.findControl(canvas.getViewportPoint(eventStub));
    rect.__corner = targetCorner ? targetCorner.key : undefined;
    canvas._setupCurrentTransform(eventStub, rect, false);
    let t = canvas._currentTransform!;
    expect(t.target, 'should have rect as a target').toBe(rect);
    expect(t.action, 'should target inside rect and setup drag').toBe('drag');
    expect(t.corner, 'no corner selected').toBe('');
    expect(t.originX, 'no origin change for drag').toBe(rect.originX);
    expect(t.originY, 'no origin change for drag').toBe(rect.originY);

    eventStub = createPointerEvent({
      clientX: canvasOffset.left + rect.oCoords.tl.corner.tl.x + 1,
      clientY: canvasOffset.top + rect.oCoords.tl.corner.tl.y + 1,
      target: canvas.upperCanvasEl,
    });
    rect.__corner = rect.findControl(canvas.getViewportPoint(eventStub))!.key;
    canvas._setupCurrentTransform(eventStub, rect, false);
    t = canvas._currentTransform!;
    expect(t.target, 'should have rect as a target').toBe(rect);
    expect(
      t.action,
      'should setup drag since the object was not selected',
    ).toBe('drag');
    expect(t.corner, 'tl selected').toBe('tl');
    expect(t.shiftKey, 'shift was not pressed').toBe(undefined);

    const alreadySelected = true;
    rect.__corner = rect.findControl(canvas.getViewportPoint(eventStub))!.key;
    canvas._setupCurrentTransform(eventStub, rect, alreadySelected);
    t = canvas._currentTransform!;
    expect(t.target, 'should have rect as a target').toBe(rect);
    expect(t.action, 'should target a corner and setup scale').toBe('scale');
    expect(t.corner, 'tl selected').toBe('tl');
    expect(t.originX, 'origin in opposite direction').toBe('right');
    expect(t.originY, 'origin in opposite direction').toBe('bottom');
    expect(t.shiftKey, 'shift was not pressed').toBe(undefined);

    eventStub = createPointerEvent({
      clientX: canvasOffset.left + rect.left - 2 - rect.width / 2,
      clientY: canvasOffset.top + rect.top,
      target: canvas.upperCanvasEl,
      shiftKey: true,
    });
    rect.__corner = rect.findControl(canvas.getViewportPoint(eventStub))!.key;
    canvas._setupCurrentTransform(eventStub, rect, alreadySelected);
    t = canvas._currentTransform!;
    expect(t.target, 'should have rect as a target').toBe(rect);
    expect(t.action, 'should target a corner and setup skew').toBe('skewY');
    expect(t.shiftKey, 'shift was pressed').toBe(true);
    expect(t.corner, 'ml selected').toBe('ml');
    expect(t.originX, 'origin in opposite direction').toBe('right');

    // to be replaced with new api test
    // eventStub = {
    //   clientX: canvasOffset.left + rect.oCoords.mtr.x,
    //   clientY: canvasOffset.top + rect.oCoords.mtr.y,
    //   target: canvas.upperCanvasEl,
    // };
    // canvas._setupCurrentTransform(eventStub, rect, alreadySelected);
    // t = canvas._currentTransform;
    // expect(t.target, 'should have rect as a target').toBe(rect);
    // expect(t.action, 'should target a corner and setup rotate').toBe('mtr');
    // expect(t.corner, 'mtr selected').toBe('mtr');
    // expect(t.originX, 'origin in center').toBe('center');
    // expect(t.originY, 'origin in center').toBe('center');
    // canvas._currentTransform = false;
  });

  // TODO: do these tests make sense in vitest?
  // QUnit.test('_rotateObject', function(assert) {
  //   assert.ok(typeof canvas._rotateObject === 'function');
  //   var rect = new fabric.Rect({ left: 75, top: 75, width: 50, height: 50 });
  //   canvas.add(rect);
  //   var canvasEl = canvas.getElement(),
  //       canvasOffset = fabric.util.getElementOffset(canvasEl);
  //   var eventStub = {
  //     clientX: canvasOffset.left + rect.oCoords.mtr.x,
  //     clientY: canvasOffset.top + rect.oCoords.mtr.y,
  //     target: canvas.upperCanvasEl,
  //   };
  //   canvas._setupCurrentTransform(eventStub, rect);
  //   var rotated = canvas._rotateObject(30, 30, 'equally');
  //   assert.equal(rotated, true, 'return true if a rotation happened');
  //   rotated = canvas._rotateObject(30, 30);
  //   assert.equal(rotated, false, 'return true if no rotation happened');
  // });
  //
  // QUnit.test('_rotateObject do not change origins', function(assert) {
  //   assert.ok(typeof canvas._rotateObject === 'function');
  //   var rect = new fabric.Rect({ left: 75, top: 75, width: 50, height: 50, originX: 'right', originY: 'bottom' });
  //   canvas.add(rect);
  //   var canvasEl = canvas.getElement(),
  //       canvasOffset = fabric.util.getElementOffset(canvasEl);
  //   var eventStub = {
  //     clientX: canvasOffset.left + rect.oCoords.mtr.x,
  //     clientY: canvasOffset.top + rect.oCoords.mtr.y,
  //     target: canvas.upperCanvasEl,
  //   };
  //   canvas._setupCurrentTransform(eventStub, rect);
  //   assert.equal(rect.originX, 'right');
  //   assert.equal(rect.originY, 'bottom');
  // });
  //
  // QUnit.test('backgroundImage', function(assert) {
  //   var done = assert.async();
  //   assert.deepEqual('', canvas.backgroundImage);
  //   canvas.setBackgroundImage('../../assets/pug.jpg');

  //   setTimeout(function() {

  //     assert.ok(typeof canvas.backgroundImage == 'object');
  //     assert.ok(/pug\.jpg$/.test(canvas.backgroundImage.src));

  //     assert.deepEqual(canvas.toJSON(), {
  //       "objects": [],
  //       "background": "rgba(0, 0, 0, 0)",
  //       "backgroundImage": (fabric.getFabricDocument().location.protocol +
  //                           '//' +
  //                           fabric.getFabricDocument().location.hostname +
  //                           ((fabric.getFabricDocument().location.port === '' || parseInt(fabric.getFabricDocument().location.port, 10) === 80)
  //                               ? ''
  //                               : (':' + fabric.getFabricDocument().location.port)) +
  //                           '/assets/pug.jpg'),
  //       "backgroundImageOpacity": 1,
  //       "backgroundImageStretch": true
  //     });

  //     done();
  //   }, 1000);
  // });
  //
  // QUnit.skip('fxRemove', function(assert) {
  //   var done = assert.async();
  //   assert.ok(typeof canvas.fxRemove === 'function');
  //
  //   var rect = new fabric.Rect();
  //   canvas.add(rect);
  //
  //   var callbackFired = false;
  //   function onComplete() {
  //     callbackFired = true;
  //   }
  //
  //   assert.equal(canvas.item(0), rect);
  //   assert.ok(typeof canvas.fxRemove(rect, { onComplete: onComplete }).abort === 'function', 'should return animation abort function');
  //
  //   setTimeout(function() {
  //     assert.equal(canvas.item(0), undefined);
  //     assert.ok(callbackFired);
  //     done();
  //   }, 1000);
  // });

  describe.each([true, false])(
    'isTargetTransparent with objectCaching = %s',
    (objectCaching) => {
      function testPixelDetection(
        canvas: Canvas,
        target: FabricObject,
        expectedHits: {
          start: number;
          end: number;
          message: string;
          transparent: boolean;
        }[],
      ) {
        function execute(context = '') {
          expectedHits.forEach(({ start, end, message, transparent }) => {
            // make less sensitive by skipping edges for firefox 110
            const round = 0;
            for (let index = start + round; index < end - round; index++) {
              expect(
                canvas.isTargetTransparent(target, index, index),
                `checking transparency of (${index}, ${index}), expected to be ${transparent}, ${message}, ${context}`,
              ).toBe(transparent);
            }
          });
        }

        execute();
        canvas.setActiveObject(target);
        execute('target is selected');
      }

      it('detects transparent regions correctly', () => {
        const rect = new Rect({
          width: 10,
          height: 10,
          strokeWidth: 4,
          stroke: 'red',
          fill: '',
          top: 7,
          left: 7,
          objectCaching,
        });
        canvas.add(rect);
        testPixelDetection(canvas, rect, [
          { start: -5, end: 0, message: 'outside', transparent: true },
          { start: 0, end: 4, message: 'stroke', transparent: false },
          { start: 4, end: 10, message: 'fill', transparent: true },
          { start: 10, end: 14, message: 'stroke', transparent: false },
          { start: 14, end: 20, message: 'outside', transparent: true },
        ]);
      });

      it('detects transparent regions correctly with viewport transform', () => {
        const rect = new Rect({
          width: 10,
          height: 10,
          strokeWidth: 4,
          stroke: 'red',
          fill: '',
          top: 7,
          left: 7,
          objectCaching,
        });
        canvas.add(rect);
        canvas.setViewportTransform([2, 0, 0, 2, 0, 0]);
        testPixelDetection(canvas, rect, [
          { start: -5, end: 0, message: 'outside', transparent: true },
          { start: 0, end: 8, message: 'stroke', transparent: false },
          { start: 8, end: 20, message: 'fill', transparent: true },
          { start: 20, end: 28, message: 'stroke', transparent: false },
          { start: 28, end: 40, message: 'outside', transparent: true },
        ]);
      });

      it('detects transparent regions correctly with viewport transform and tolerance', () => {
        const rect = new Rect({
          width: 10,
          height: 10,
          strokeWidth: 4,
          stroke: 'red',
          fill: '',
          top: 7,
          left: 7,
          objectCaching,
        });
        canvas.add(rect);
        canvas.setTargetFindTolerance(5);
        canvas.setViewportTransform([2, 0, 0, 2, 0, 0]);
        testPixelDetection(canvas, rect, [
          { start: -10, end: -5, message: 'outside', transparent: true },
          {
            start: -5,
            end: 0,
            message: 'stroke tolerance not affected by vpt',
            transparent: false,
          },
          { start: 0, end: 8, message: 'stroke', transparent: false },
          {
            start: 8,
            end: 13,
            message: 'stroke tolerance not affected by vpt',
            transparent: false,
          },
          { start: 13, end: 15, message: 'fill', transparent: true },
          {
            start: 15,
            end: 20,
            message: 'stroke tolerance not affected by vpt',
            transparent: false,
          },
          { start: 20, end: 28, message: 'stroke', transparent: false },
          {
            start: 28,
            end: 33,
            message: 'stroke tolerance not affected by vpt',
            transparent: false,
          },
          { start: 33, end: 40, message: 'outside', transparent: true },
        ]);
      });
    },
  );

  it('provides access to top context with getTopContext', () => {
    expect(canvas.getTopContext).toBeTypeOf('function');
    expect(canvas.getTopContext(), 'it just returns contextTop').toBe(
      canvas.contextTop,
    );
  });

  it('determines when transformations should be centered', () => {
    expect(
      // @ts-expect-error -- private method
      canvas._shouldCenterTransform({}, 'someAction', false),
      'a non standard action does not center scale',
    ).toBe(false);

    expect(
      // @ts-expect-error -- private method
      canvas._shouldCenterTransform({}, 'someAction', true),
      'a non standard action will center scale if altKey is true',
    ).toBe(true);

    canvas.centeredScaling = true;

    ['scale', 'scaleX', 'scaleY', 'resizing'].forEach((action) => {
      expect(
        // @ts-expect-error -- private method
        canvas._shouldCenterTransform({}, action, false),
        action +
          ' standard action will center scale if canvas.centeredScaling is true and no centeredKey pressed',
      ).toBe(true);
    });

    ['scale', 'scaleX', 'scaleY', 'resizing'].forEach((action) => {
      expect(
        // @ts-expect-error -- private method
        canvas._shouldCenterTransform({}, action, true),
        action +
          ' standard action will NOT center scale if canvas.centeredScaling is true and centeredKey is pressed',
      ).toBe(false);
    });

    expect(
      // @ts-expect-error -- private method
      canvas._shouldCenterTransform({}, 'rotate', false),
      'rotate standard action will NOT center scale if canvas.centeredScaling is true',
    ).toBe(false);

    canvas.centeredRotation = true;

    expect(
      // @ts-expect-error -- private method
      canvas._shouldCenterTransform({}, 'rotate', false),
      'rotate standard action will center scale if canvas.centeredRotation is true',
    ).toBe(true);
  });
});

function initActiveSelection(
  canvas: Canvas,
  activeObject: FabricObject,
  target: FabricObject,
  multiSelectionStacking?: MultiSelectionStacking,
) {
  classRegistry.setClass(
    class TextActiveSelection extends ActiveSelection {
      static ownDefaults = {
        multiSelectionStacking,
      };

      constructor(
        objects: FabricObject[],
        options: Record<PropertyKey, unknown>,
      ) {
        super(objects, { ...TextActiveSelection.ownDefaults, ...options });
      }
    },
  );
  canvas.setActiveObject(activeObject);
  // @ts-expect-error -- protected method
  canvas.handleMultiSelection(
    {
      clientX: 0,
      clientY: 0,
      [canvas.selectionKey as string]: true,
    } as unknown as TPointerEvent,
    target,
  );
}

function updateActiveSelection(
  canvas: Canvas,
  existing: FabricObject[],
  target: FabricObject | null,
  multiSelectionStacking?: MultiSelectionStacking,
) {
  const activeSelection = new ActiveSelection([], { canvas });
  if (multiSelectionStacking) {
    activeSelection.multiSelectionStacking = multiSelectionStacking;
  }
  activeSelection.add(...existing);
  canvas.setActiveObject(activeSelection);
  // @ts-expect-error -- protected method
  canvas.handleMultiSelection(
    {
      clientX: 1,
      clientY: 1,
      [canvas.selectionKey as string]: true,
      target: canvas.upperCanvasEl,
    } as unknown as TPointerEvent,
    target || activeSelection,
  );
}

function setGroupSelector(
  canvas: Canvas,
  { x = 0, y = 0, deltaX = 0, deltaY = 0 } = {},
) {
  // @ts-expect-error -- protected member
  canvas._groupSelector = {
    x,
    y,
    deltaX,
    deltaY,
  };
}
