mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-03 20:34:40 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			1513 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1513 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { reseed } from "@excalidraw/common";
 | 
						|
 | 
						|
import {
 | 
						|
  actionSendBackward,
 | 
						|
  actionBringForward,
 | 
						|
  actionBringToFront,
 | 
						|
  actionSendToBack,
 | 
						|
  actionDuplicateSelection,
 | 
						|
} from "@excalidraw/excalidraw/actions";
 | 
						|
 | 
						|
import { Excalidraw } from "@excalidraw/excalidraw";
 | 
						|
 | 
						|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
 | 
						|
import {
 | 
						|
  act,
 | 
						|
  getCloneByOrigId,
 | 
						|
  render,
 | 
						|
  unmountComponent,
 | 
						|
} from "@excalidraw/excalidraw/tests/test-utils";
 | 
						|
 | 
						|
import type { AppState } from "@excalidraw/excalidraw/types";
 | 
						|
 | 
						|
import { selectGroupsForSelectedElements } from "../src/groups";
 | 
						|
 | 
						|
import type {
 | 
						|
  ExcalidrawElement,
 | 
						|
  ExcalidrawFrameElement,
 | 
						|
  ExcalidrawSelectionElement,
 | 
						|
} from "../src/types";
 | 
						|
 | 
						|
unmountComponent();
 | 
						|
 | 
						|
beforeEach(() => {
 | 
						|
  localStorage.clear();
 | 
						|
  reseed(7);
 | 
						|
});
 | 
						|
 | 
						|
const { h } = window;
 | 
						|
 | 
						|
type ExcalidrawElementType = Exclude<
 | 
						|
  ExcalidrawElement,
 | 
						|
  ExcalidrawSelectionElement
 | 
						|
>["type"];
 | 
						|
 | 
						|
const populateElements = (
 | 
						|
  elements: {
 | 
						|
    id: string;
 | 
						|
    type?: ExcalidrawElementType;
 | 
						|
    isDeleted?: boolean;
 | 
						|
    isSelected?: boolean;
 | 
						|
    groupIds?: string[];
 | 
						|
    y?: number;
 | 
						|
    x?: number;
 | 
						|
    width?: number;
 | 
						|
    height?: number;
 | 
						|
    containerId?: string;
 | 
						|
    frameId?: ExcalidrawFrameElement["id"];
 | 
						|
    index?: ExcalidrawElement["index"];
 | 
						|
  }[],
 | 
						|
  appState?: Partial<AppState>,
 | 
						|
) => {
 | 
						|
  const selectedElementIds: any = {};
 | 
						|
 | 
						|
  const newElements = elements.map(
 | 
						|
    ({
 | 
						|
      id,
 | 
						|
      isDeleted = false,
 | 
						|
      isSelected = false,
 | 
						|
      groupIds = [],
 | 
						|
      y = 100,
 | 
						|
      x = 100,
 | 
						|
      width = 100,
 | 
						|
      height = 100,
 | 
						|
      containerId = null,
 | 
						|
      frameId = null,
 | 
						|
      type,
 | 
						|
    }) => {
 | 
						|
      const element = API.createElement({
 | 
						|
        type: type ?? (containerId ? "text" : "rectangle"),
 | 
						|
        id,
 | 
						|
        isDeleted,
 | 
						|
        x,
 | 
						|
        y,
 | 
						|
        width,
 | 
						|
        height,
 | 
						|
        groupIds,
 | 
						|
        containerId,
 | 
						|
        frameId: frameId || null,
 | 
						|
      });
 | 
						|
      if (isSelected) {
 | 
						|
        selectedElementIds[element.id] = true;
 | 
						|
      }
 | 
						|
      return element;
 | 
						|
    },
 | 
						|
  );
 | 
						|
 | 
						|
  // initialize `boundElements` on containers, if applicable
 | 
						|
  API.setElements(
 | 
						|
    newElements.map((element, index, elements) => {
 | 
						|
      const nextElement = elements[index + 1];
 | 
						|
      if (
 | 
						|
        nextElement &&
 | 
						|
        "containerId" in nextElement &&
 | 
						|
        element.id === nextElement.containerId
 | 
						|
      ) {
 | 
						|
        return {
 | 
						|
          ...element,
 | 
						|
          boundElements: [{ type: "text", id: nextElement.id }],
 | 
						|
        };
 | 
						|
      }
 | 
						|
      return element;
 | 
						|
    }),
 | 
						|
  );
 | 
						|
 | 
						|
  act(() => {
 | 
						|
    h.setState({
 | 
						|
      ...selectGroupsForSelectedElements(
 | 
						|
        { ...h.state, ...appState, selectedElementIds },
 | 
						|
        h.elements,
 | 
						|
        h.state,
 | 
						|
        null,
 | 
						|
      ),
 | 
						|
      ...appState,
 | 
						|
      selectedElementIds,
 | 
						|
    } as AppState);
 | 
						|
  });
 | 
						|
 | 
						|
  return selectedElementIds;
 | 
						|
};
 | 
						|
 | 
						|
type Actions =
 | 
						|
  | typeof actionBringForward
 | 
						|
  | typeof actionSendBackward
 | 
						|
  | typeof actionBringToFront
 | 
						|
  | typeof actionSendToBack;
 | 
						|
 | 
						|
const assertZindex = ({
 | 
						|
  elements,
 | 
						|
  appState,
 | 
						|
  operations,
 | 
						|
}: {
 | 
						|
  elements: {
 | 
						|
    id: string;
 | 
						|
    isDeleted?: true;
 | 
						|
    isSelected?: true;
 | 
						|
    groupIds?: string[];
 | 
						|
    containerId?: string;
 | 
						|
    frameId?: ExcalidrawFrameElement["id"];
 | 
						|
    type?: ExcalidrawElementType;
 | 
						|
  }[];
 | 
						|
  appState?: Partial<AppState>;
 | 
						|
  operations: [Actions, string[]][];
 | 
						|
}) => {
 | 
						|
  const selectedElementIds = populateElements(elements, appState);
 | 
						|
  operations.forEach(([action, expected]) => {
 | 
						|
    API.executeAction(action);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual(expected);
 | 
						|
    expect(h.state.selectedElementIds).toEqual(selectedElementIds);
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
describe("z-index manipulation", () => {
 | 
						|
  beforeEach(async () => {
 | 
						|
    await render(<Excalidraw />);
 | 
						|
  });
 | 
						|
 | 
						|
  it("send back", () => {
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", isDeleted: true },
 | 
						|
        { id: "C", isDeleted: true },
 | 
						|
        { id: "D", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["D", "A", "B", "C"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["D", "A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isDeleted: true },
 | 
						|
        { id: "B" },
 | 
						|
        { id: "C", isDeleted: true },
 | 
						|
        { id: "D", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [[actionSendBackward, ["A", "D", "B", "C"]]],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", isDeleted: true },
 | 
						|
        { id: "C", isDeleted: true },
 | 
						|
        { id: "D", isSelected: true },
 | 
						|
        { id: "E", isSelected: true },
 | 
						|
        { id: "F" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["D", "E", "A", "B", "C", "F"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["D", "E", "A", "B", "C", "F"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B" },
 | 
						|
        { id: "C", isDeleted: true },
 | 
						|
        { id: "D", isDeleted: true },
 | 
						|
        { id: "E", isSelected: true },
 | 
						|
        { id: "F" },
 | 
						|
        { id: "G", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "E", "B", "C", "D", "G", "F"]],
 | 
						|
        [actionSendBackward, ["E", "A", "G", "B", "C", "D", "F"]],
 | 
						|
        [actionSendBackward, ["E", "G", "A", "B", "C", "D", "F"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["E", "G", "A", "B", "C", "D", "F"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B" },
 | 
						|
        { id: "C", isDeleted: true },
 | 
						|
        { id: "D", isSelected: true },
 | 
						|
        { id: "E", isDeleted: true },
 | 
						|
        { id: "F", isSelected: true },
 | 
						|
        { id: "G" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "D", "E", "F", "B", "C", "G"]],
 | 
						|
        [actionSendBackward, ["D", "E", "F", "A", "B", "C", "G"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["D", "E", "F", "A", "B", "C", "G"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // elements should not duplicate
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", containerId: "C" },
 | 
						|
        { id: "B" },
 | 
						|
        { id: "C", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "C", "B"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["A", "C", "B"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // grouped elements should be atomic
 | 
						|
    // -------------------------------------------------------------------------
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g1"] },
 | 
						|
        { id: "C", groupIds: ["g1"] },
 | 
						|
        { id: "D", isDeleted: true },
 | 
						|
        { id: "E", isDeleted: true },
 | 
						|
        { id: "F", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "F", "B", "C", "D", "E"]],
 | 
						|
        [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D", groupIds: ["g1"] },
 | 
						|
        { id: "E", isDeleted: true },
 | 
						|
        { id: "F", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "F", "B", "C", "D", "E"]],
 | 
						|
        [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g1"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "E", isDeleted: true },
 | 
						|
        { id: "F", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "F", "B", "C", "D", "E"]],
 | 
						|
        [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["F", "A", "B", "C", "D", "E"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B1", groupIds: ["g1"] },
 | 
						|
        { id: "C1", groupIds: ["g1"] },
 | 
						|
        { id: "D2", groupIds: ["g2"], isSelected: true },
 | 
						|
        { id: "E2", groupIds: ["g2"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: null,
 | 
						|
      },
 | 
						|
      operations: [[actionSendBackward, ["A", "D2", "E2", "B1", "C1"]]],
 | 
						|
    });
 | 
						|
 | 
						|
    // in-group siblings
 | 
						|
    // -------------------------------------------------------------------------
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g1"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D", groupIds: ["g2", "g1"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "B", "D", "C"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionSendBackward, ["A", "B", "D", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D", groupIds: ["g1"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "D", "B", "C"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionSendBackward, ["A", "D", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g1"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"], isSelected: true },
 | 
						|
        { id: "D", groupIds: ["g2", "g1"], isDeleted: true },
 | 
						|
        { id: "E", groupIds: ["g2", "g1"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "C", "D", "E", "B"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionSendBackward, ["A", "C", "D", "E", "B"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g1"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "E", groupIds: ["g3", "g1"], isSelected: true },
 | 
						|
        { id: "F", groupIds: ["g3", "g1"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "B", "E", "F", "C", "D"]],
 | 
						|
        [actionSendBackward, ["A", "E", "F", "B", "C", "D"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionSendBackward, ["A", "E", "F", "B", "C", "D"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // invalid z-indexes across groups (legacy) → allow to sort to next sibling
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g1"] },
 | 
						|
        { id: "B", groupIds: ["g2"] },
 | 
						|
        { id: "C", groupIds: ["g1"] },
 | 
						|
        { id: "D", groupIds: ["g2"], isSelected: true },
 | 
						|
        { id: "E", groupIds: ["g2"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "D", "E", "B", "C"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["A", "D", "E", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // invalid z-indexes across groups (legacy) → allow to sort to next sibling
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g1"] },
 | 
						|
        { id: "B", groupIds: ["g2"] },
 | 
						|
        { id: "C", groupIds: ["g1"] },
 | 
						|
        { id: "D", groupIds: ["g2"], isSelected: true },
 | 
						|
        { id: "F" },
 | 
						|
        { id: "G", groupIds: ["g2"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["A", "D", "G", "B", "C", "F"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["A", "D", "G", "B", "C", "F"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  it("bring forward", () => {
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", isSelected: true },
 | 
						|
        { id: "D", isDeleted: true },
 | 
						|
        { id: "E" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["A", "D", "E", "B", "C"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["A", "D", "E", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B", isDeleted: true },
 | 
						|
        { id: "C", isDeleted: true },
 | 
						|
        { id: "D" },
 | 
						|
        { id: "E", isSelected: true },
 | 
						|
        { id: "F", isDeleted: true },
 | 
						|
        { id: "G" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["B", "C", "D", "A", "F", "G", "E"]],
 | 
						|
        [actionBringForward, ["B", "C", "D", "F", "G", "A", "E"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["B", "C", "D", "F", "G", "A", "E"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // grouped elements should be atomic
 | 
						|
    // -------------------------------------------------------------------------
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B", isDeleted: true },
 | 
						|
        { id: "C", isDeleted: true },
 | 
						|
        { id: "D", groupIds: ["g1"] },
 | 
						|
        { id: "E", groupIds: ["g1"] },
 | 
						|
        { id: "F" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["B", "C", "D", "E", "A", "F"]],
 | 
						|
        [actionBringForward, ["B", "C", "D", "E", "F", "A"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["B", "C", "D", "E", "F", "A"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "E", groupIds: ["g1"] },
 | 
						|
        { id: "F" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["A", "C", "D", "E", "B", "F"]],
 | 
						|
        [actionBringForward, ["A", "C", "D", "E", "F", "B"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["A", "C", "D", "E", "F", "B"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g1"] },
 | 
						|
        { id: "D", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "E", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "F" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["A", "C", "D", "E", "B", "F"]],
 | 
						|
        [actionBringForward, ["A", "C", "D", "E", "F", "B"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["A", "C", "D", "E", "F", "B"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // in-group siblings
 | 
						|
    // -------------------------------------------------------------------------
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g2", "g1"], isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D", groupIds: ["g1"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["A", "C", "B", "D"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionBringForward, ["A", "C", "B", "D"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g1"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D" },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["B", "C", "A", "D"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionBringForward, ["B", "C", "A", "D"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g2", "g1"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g2", "g1"], isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g1"] },
 | 
						|
        { id: "D" },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["C", "A", "B", "D"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionBringForward, ["C", "A", "B", "D"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // invalid z-indexes across groups (legacy) → allow to sort to next sibling
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g2"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g2"], isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g1"] },
 | 
						|
        { id: "D", groupIds: ["g2"] },
 | 
						|
        { id: "E", groupIds: ["g1"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["C", "D", "A", "B", "E"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["C", "D", "A", "B", "E"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // invalid z-indexes across groups (legacy) → allow to sort to next sibling
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g2"], isSelected: true },
 | 
						|
        { id: "B" },
 | 
						|
        { id: "C", groupIds: ["g2"], isSelected: true },
 | 
						|
        { id: "D", groupIds: ["g1"] },
 | 
						|
        { id: "E", groupIds: ["g2"] },
 | 
						|
        { id: "F", groupIds: ["g1"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["B", "D", "E", "A", "C", "F"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["B", "D", "E", "A", "C", "F"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  it("bring to front", () => {
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "0" },
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B", isDeleted: true },
 | 
						|
        { id: "C", isDeleted: true },
 | 
						|
        { id: "D" },
 | 
						|
        { id: "E", isSelected: true },
 | 
						|
        { id: "F", isDeleted: true },
 | 
						|
        { id: "G" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionBringToFront, ["0", "B", "C", "D", "F", "G", "A", "E"]],
 | 
						|
        // noop
 | 
						|
        [actionBringToFront, ["0", "B", "C", "D", "F", "G", "A", "E"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // noop
 | 
						|
        [actionBringToFront, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // noop
 | 
						|
        [actionBringToFront, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionBringToFront, ["C", "A", "B"]],
 | 
						|
        // noop
 | 
						|
        [actionBringToFront, ["C", "A", "B"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // in-group sorting
 | 
						|
    // -------------------------------------------------------------------------
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g1"] },
 | 
						|
        { id: "C", groupIds: ["g1"], isSelected: true },
 | 
						|
        { id: "D", groupIds: ["g1"] },
 | 
						|
        { id: "E", groupIds: ["g1"], isSelected: true },
 | 
						|
        { id: "F", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "G", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "H", groupIds: ["g3", "g1"] },
 | 
						|
        { id: "I", groupIds: ["g3", "g1"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringToFront, ["A", "B", "D", "F", "G", "H", "I", "C", "E"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionBringToFront, ["A", "B", "D", "F", "G", "H", "I", "C", "E"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g2", "g1"], isSelected: true },
 | 
						|
        { id: "D", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "C", groupIds: ["g1"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringToFront, ["A", "D", "B", "C"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionBringToFront, ["A", "D", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // invalid z-indexes across groups (legacy) → allow to sort to next sibling
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g2", "g3"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g1", "g3"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g3"] },
 | 
						|
        { id: "D", groupIds: ["g1", "g3"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringToFront, ["B", "C", "A", "D"]],
 | 
						|
        // noop
 | 
						|
        [actionBringToFront, ["B", "C", "A", "D"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // invalid z-indexes across groups (legacy) → allow to sort to next sibling
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g2"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g1"] },
 | 
						|
        { id: "C", groupIds: ["g2"] },
 | 
						|
        { id: "D", groupIds: ["g1"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringToFront, ["B", "C", "A", "D"]],
 | 
						|
        // noop
 | 
						|
        [actionBringToFront, ["B", "C", "A", "D"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  it("send to back", () => {
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", isDeleted: true },
 | 
						|
        { id: "C" },
 | 
						|
        { id: "D", isDeleted: true },
 | 
						|
        { id: "E", isSelected: true },
 | 
						|
        { id: "F", isDeleted: true },
 | 
						|
        { id: "G" },
 | 
						|
        { id: "H", isSelected: true },
 | 
						|
        { id: "I" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendToBack, ["E", "H", "A", "B", "C", "D", "F", "G", "I"]],
 | 
						|
        // noop
 | 
						|
        [actionSendToBack, ["E", "H", "A", "B", "C", "D", "F", "G", "I"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // noop
 | 
						|
        [actionSendToBack, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // noop
 | 
						|
        [actionSendToBack, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", isSelected: true },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendToBack, ["B", "C", "A"]],
 | 
						|
        // noop
 | 
						|
        [actionSendToBack, ["B", "C", "A"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // in-group sorting
 | 
						|
    // -------------------------------------------------------------------------
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D", groupIds: ["g3", "g1"] },
 | 
						|
        { id: "E", groupIds: ["g3", "g1"] },
 | 
						|
        { id: "F", groupIds: ["g1"], isSelected: true },
 | 
						|
        { id: "G", groupIds: ["g1"] },
 | 
						|
        { id: "H", groupIds: ["g1"], isSelected: true },
 | 
						|
        { id: "I", groupIds: ["g1"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendToBack, ["A", "F", "H", "B", "C", "D", "E", "G", "I"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionSendToBack, ["A", "F", "H", "B", "C", "D", "E", "G", "I"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", groupIds: ["g1"] },
 | 
						|
        { id: "C", groupIds: ["g2", "g1"] },
 | 
						|
        { id: "D", groupIds: ["g2", "g1"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendToBack, ["A", "B", "D", "C"]],
 | 
						|
        // noop (prevented)
 | 
						|
        [actionSendToBack, ["A", "B", "D", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // invalid z-indexes across groups (legacy) → allow to sort to next sibling
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g1", "g3"] },
 | 
						|
        { id: "B", groupIds: ["g2", "g3"] },
 | 
						|
        { id: "C", groupIds: ["g1", "g3"] },
 | 
						|
        { id: "D", groupIds: ["g2", "g3"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendToBack, ["A", "D", "B", "C"]],
 | 
						|
        // noop
 | 
						|
        [actionSendToBack, ["A", "D", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // invalid z-indexes across groups (legacy) → allow to sort to next sibling
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g1"] },
 | 
						|
        { id: "B", groupIds: ["g2"] },
 | 
						|
        { id: "C", groupIds: ["g1"] },
 | 
						|
        { id: "D", groupIds: ["g2"], isSelected: true },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g2",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendToBack, ["A", "D", "B", "C"]],
 | 
						|
        // noop
 | 
						|
        [actionSendToBack, ["A", "D", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  it("duplicating elements should retain zindex integrity", () => {
 | 
						|
    populateElements([
 | 
						|
      { id: "A", isSelected: true },
 | 
						|
      { id: "B", isSelected: true },
 | 
						|
    ]);
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements).toMatchObject([
 | 
						|
      { id: "A" },
 | 
						|
      { id: getCloneByOrigId("A").id },
 | 
						|
      { id: "B" },
 | 
						|
      { id: getCloneByOrigId("B").id },
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements([
 | 
						|
      { id: "A", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "B", groupIds: ["g1"], isSelected: true },
 | 
						|
    ]);
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements).toMatchObject([
 | 
						|
      { id: "A" },
 | 
						|
      { id: "B" },
 | 
						|
      {
 | 
						|
        id: getCloneByOrigId("A").id,
 | 
						|
 | 
						|
        groupIds: [expect.stringMatching(/.{3,}/)],
 | 
						|
      },
 | 
						|
      {
 | 
						|
        id: getCloneByOrigId("B").id,
 | 
						|
 | 
						|
        groupIds: [expect.stringMatching(/.{3,}/)],
 | 
						|
      },
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements([
 | 
						|
      { id: "A", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "B", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "C" },
 | 
						|
    ]);
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements).toMatchObject([
 | 
						|
      { id: "A" },
 | 
						|
      { id: "B" },
 | 
						|
      {
 | 
						|
        id: getCloneByOrigId("A").id,
 | 
						|
 | 
						|
        groupIds: [expect.stringMatching(/.{3,}/)],
 | 
						|
      },
 | 
						|
      {
 | 
						|
        id: getCloneByOrigId("B").id,
 | 
						|
 | 
						|
        groupIds: [expect.stringMatching(/.{3,}/)],
 | 
						|
      },
 | 
						|
      { id: "C" },
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements([
 | 
						|
      { id: "A", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "B", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "C", isSelected: true },
 | 
						|
    ]);
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      "B",
 | 
						|
      getCloneByOrigId("A").id,
 | 
						|
      getCloneByOrigId("B").id,
 | 
						|
      "C",
 | 
						|
      getCloneByOrigId("C").id,
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements([
 | 
						|
      { id: "A", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "B", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "C", groupIds: ["g2"], isSelected: true },
 | 
						|
      { id: "D", groupIds: ["g2"], isSelected: true },
 | 
						|
    ]);
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      "B",
 | 
						|
      getCloneByOrigId("A").id,
 | 
						|
      getCloneByOrigId("B").id,
 | 
						|
      "C",
 | 
						|
      "D",
 | 
						|
      getCloneByOrigId("C").id,
 | 
						|
      getCloneByOrigId("D").id,
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements(
 | 
						|
      [
 | 
						|
        { id: "A", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g2"], isSelected: true },
 | 
						|
      ],
 | 
						|
      {
 | 
						|
        selectedGroupIds: { g1: true },
 | 
						|
      },
 | 
						|
    );
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      "B",
 | 
						|
      getCloneByOrigId("A").id,
 | 
						|
      getCloneByOrigId("B").id,
 | 
						|
      "C",
 | 
						|
      getCloneByOrigId("C").id,
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements(
 | 
						|
      [
 | 
						|
        { id: "A", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g2"], isSelected: true },
 | 
						|
      ],
 | 
						|
      {
 | 
						|
        selectedGroupIds: { g2: true },
 | 
						|
      },
 | 
						|
    );
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      "B",
 | 
						|
      "C",
 | 
						|
      getCloneByOrigId("A").id,
 | 
						|
      getCloneByOrigId("B").id,
 | 
						|
      getCloneByOrigId("C").id,
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements(
 | 
						|
      [
 | 
						|
        { id: "A", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g2"], isSelected: true },
 | 
						|
        { id: "D", groupIds: ["g3", "g4"], isSelected: true },
 | 
						|
        { id: "E", groupIds: ["g3", "g4"], isSelected: true },
 | 
						|
        { id: "F", groupIds: ["g4"], isSelected: true },
 | 
						|
      ],
 | 
						|
      {
 | 
						|
        selectedGroupIds: { g2: true, g4: true },
 | 
						|
      },
 | 
						|
    );
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      "B",
 | 
						|
      "C",
 | 
						|
      getCloneByOrigId("A").id,
 | 
						|
      getCloneByOrigId("B").id,
 | 
						|
      getCloneByOrigId("C").id,
 | 
						|
      "D",
 | 
						|
      "E",
 | 
						|
      "F",
 | 
						|
      getCloneByOrigId("D").id,
 | 
						|
      getCloneByOrigId("E").id,
 | 
						|
      getCloneByOrigId("F").id,
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements(
 | 
						|
      [
 | 
						|
        { id: "A", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g1", "g2"] },
 | 
						|
        { id: "C", groupIds: ["g2"] },
 | 
						|
      ],
 | 
						|
      { editingGroupId: "g1" },
 | 
						|
    );
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      getCloneByOrigId("A").id,
 | 
						|
      "B",
 | 
						|
      "C",
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements(
 | 
						|
      [
 | 
						|
        { id: "A", groupIds: ["g1", "g2"] },
 | 
						|
        { id: "B", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g2"] },
 | 
						|
      ],
 | 
						|
      { editingGroupId: "g1" },
 | 
						|
    );
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      "B",
 | 
						|
      getCloneByOrigId("B").id,
 | 
						|
      "C",
 | 
						|
    ]);
 | 
						|
 | 
						|
    populateElements(
 | 
						|
      [
 | 
						|
        { id: "A", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "B", groupIds: ["g1", "g2"], isSelected: true },
 | 
						|
        { id: "C", groupIds: ["g2"] },
 | 
						|
      ],
 | 
						|
      { editingGroupId: "g1" },
 | 
						|
    );
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      getCloneByOrigId("A").id,
 | 
						|
      "B",
 | 
						|
      getCloneByOrigId("B").id,
 | 
						|
      "C",
 | 
						|
    ]);
 | 
						|
  });
 | 
						|
 | 
						|
  it("duplicating incorrectly interleaved elements (group elements should be together) should still produce reasonable result", () => {
 | 
						|
    populateElements([
 | 
						|
      { id: "A", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "B" },
 | 
						|
      { id: "C", groupIds: ["g1"], isSelected: true },
 | 
						|
    ]);
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      "C",
 | 
						|
      getCloneByOrigId("A").id,
 | 
						|
      getCloneByOrigId("C").id,
 | 
						|
      "B",
 | 
						|
    ]);
 | 
						|
  });
 | 
						|
 | 
						|
  it("group-selected duplication should includes deleted elements that weren't selected on account of being deleted", () => {
 | 
						|
    populateElements([
 | 
						|
      { id: "A", groupIds: ["g1"], isDeleted: true },
 | 
						|
      { id: "B", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "C", groupIds: ["g1"], isSelected: true },
 | 
						|
      { id: "D" },
 | 
						|
    ]);
 | 
						|
    expect(h.state.selectedGroupIds).toEqual({ g1: true });
 | 
						|
    API.executeAction(actionDuplicateSelection);
 | 
						|
    expect(h.elements.map((element) => element.id)).toEqual([
 | 
						|
      "A",
 | 
						|
      "B",
 | 
						|
      "C",
 | 
						|
      getCloneByOrigId("A").id,
 | 
						|
      getCloneByOrigId("B").id,
 | 
						|
      getCloneByOrigId("C").id,
 | 
						|
      "D",
 | 
						|
    ]);
 | 
						|
  });
 | 
						|
 | 
						|
  it("text-container binding should be atomic", () => {
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true },
 | 
						|
        { id: "B" },
 | 
						|
        { id: "C", containerId: "B" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["B", "C", "A"]],
 | 
						|
        [actionSendBackward, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A" },
 | 
						|
        { id: "B", isSelected: true },
 | 
						|
        { id: "C", containerId: "B" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["B", "C", "A"]],
 | 
						|
        [actionBringForward, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", isSelected: true, groupIds: ["g1"] },
 | 
						|
        { id: "B", groupIds: ["g1"] },
 | 
						|
        { id: "C", containerId: "B", groupIds: ["g1"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionBringForward, ["B", "C", "A"]],
 | 
						|
        [actionSendBackward, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g1"] },
 | 
						|
        { id: "B", groupIds: ["g1"], isSelected: true },
 | 
						|
        { id: "C", containerId: "B", groupIds: ["g1"] },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [
 | 
						|
        [actionSendBackward, ["B", "C", "A"]],
 | 
						|
        [actionBringForward, ["A", "B", "C"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "A", groupIds: ["g1"] },
 | 
						|
        { id: "B", isSelected: true, groupIds: ["g1"] },
 | 
						|
        { id: "C" },
 | 
						|
        { id: "D", containerId: "C" },
 | 
						|
      ],
 | 
						|
      appState: {
 | 
						|
        editingGroupId: "g1",
 | 
						|
      },
 | 
						|
      operations: [[actionBringForward, ["A", "B", "C", "D"]]],
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 | 
						|
 | 
						|
describe("z-indexing with frames", () => {
 | 
						|
  beforeEach(async () => {
 | 
						|
    await render(<Excalidraw />);
 | 
						|
  });
 | 
						|
 | 
						|
  // naming scheme:
 | 
						|
  // F#   ... frame element
 | 
						|
  // F#_# ... frame child of F# (rectangle)
 | 
						|
  // R#   ... unrelated element (rectangle)
 | 
						|
 | 
						|
  it("moving whole frame by one (normalized)", () => {
 | 
						|
    // normalized frame order
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1" },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "F1", type: "frame", isSelected: true },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "R2" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +1
 | 
						|
        [actionBringForward, ["R1", "F1_1", "F1_2", "F1", "R2"]],
 | 
						|
        // +1
 | 
						|
        [actionBringForward, ["R1", "R2", "F1_1", "F1_2", "F1"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["R1", "R2", "F1_1", "F1_2", "F1"]],
 | 
						|
        // -1
 | 
						|
        [actionSendBackward, ["R1", "F1_1", "F1_2", "F1", "R2"]],
 | 
						|
        // -1
 | 
						|
        [actionSendBackward, ["F1_1", "F1_2", "F1", "R1", "R2"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["F1_1", "F1_2", "F1", "R1", "R2"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  it("moving whole frame by one (DENORMALIZED)", () => {
 | 
						|
    // DENORMALIZED FRAME ORDER
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1" },
 | 
						|
        { id: "F1", type: "frame", isSelected: true },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "R2" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +1
 | 
						|
        [actionBringForward, ["R1", "F1_1", "F1", "F1_2", "R2"]],
 | 
						|
        // +1
 | 
						|
        [actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // DENORMALIZED FRAME ORDER
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1" },
 | 
						|
        { id: "F1", type: "frame", isSelected: true },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "R2" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +1
 | 
						|
        [actionBringForward, ["R1", "F1_1", "F1", "R2", "F1_2"]],
 | 
						|
        // +1
 | 
						|
        [actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // DENORMALIZED FRAME ORDER
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1" },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "F1", type: "frame", isSelected: true },
 | 
						|
        { id: "R2" },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "R3" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +1
 | 
						|
        [actionBringForward, ["R1", "F1_1", "R2", "F1", "R3", "F1_2"]],
 | 
						|
        // +1
 | 
						|
        // FIXME incorrect, should put F1_1 after R3
 | 
						|
        [actionBringForward, ["R1", "R2", "F1_1", "R3", "F1", "F1_2"]],
 | 
						|
        // +1
 | 
						|
        // FIXME should be noop from previous step after it's fixed
 | 
						|
        [actionBringForward, ["R1", "R2", "R3", "F1_1", "F1", "F1_2"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // DENORMALIZED FRAME ORDER
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1" },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "F1", type: "frame", isSelected: true },
 | 
						|
        { id: "R2" },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "R3" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // -1
 | 
						|
        [actionSendBackward, ["F1_1", "F1", "R1", "F1_2", "R2", "R3"]],
 | 
						|
        // -1
 | 
						|
        [actionSendBackward, ["F1_1", "F1", "F1_2", "R1", "R2", "R3"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  it("moving selected frame children by one (normalized)", () => {
 | 
						|
    // normalized frame order
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1", isSelected: true },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "F1", type: "frame" },
 | 
						|
        { id: "R1" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +1
 | 
						|
        [actionBringForward, ["F1_2", "F1_1", "F1", "R1"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["F1_2", "F1_1", "F1", "R1"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // normalized frame order, multiple frames
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1", isSelected: true },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "F1", type: "frame" },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "F2_1", frameId: "F2", isSelected: true },
 | 
						|
        { id: "F2_2", frameId: "F2" },
 | 
						|
        { id: "F2", type: "frame" },
 | 
						|
        { id: "R2" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +1
 | 
						|
        [
 | 
						|
          actionBringForward,
 | 
						|
          ["F1_2", "F1_1", "F1", "R1", "F2_2", "F2_1", "F2", "R2"],
 | 
						|
        ],
 | 
						|
        // noop
 | 
						|
        [
 | 
						|
          actionBringForward,
 | 
						|
          ["F1_2", "F1_1", "F1", "R1", "F2_2", "F2_1", "F2", "R2"],
 | 
						|
        ],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  it("moving selected frame children by one (DENORMALIZED)", () => {
 | 
						|
    // DENORMALIZED FRAME ORDER
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1", isSelected: true },
 | 
						|
        { id: "F1", type: "frame" },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "R1" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +1
 | 
						|
        // NOTE not sure what we wanna do here
 | 
						|
        [actionBringForward, ["F1", "F1_2", "F1_1", "R1"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["F1", "F1_2", "F1_1", "R1"]],
 | 
						|
        // -1
 | 
						|
        [actionSendBackward, ["F1", "F1_1", "F1_2", "R1"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["F1", "F1_1", "F1_2", "R1"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // DENORMALIZED FRAME ORDER
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1", isSelected: true },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "F1", type: "frame" },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "R2" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +1
 | 
						|
        // NOTE not sure what we wanna do here
 | 
						|
        [actionBringForward, ["R1", "F1", "F1_2", "F1_1", "R2"]],
 | 
						|
        // noop
 | 
						|
        [actionBringForward, ["R1", "F1", "F1_2", "F1_1", "R2"]],
 | 
						|
        // -1
 | 
						|
        [actionSendBackward, ["R1", "F1", "F1_1", "F1_2", "R2"]],
 | 
						|
        // noop
 | 
						|
        [actionSendBackward, ["R1", "F1", "F1_1", "F1_2", "R2"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  it("moving whole frame to front/end", () => {
 | 
						|
    // normalized frame order
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1" },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "F1", type: "frame", isSelected: true },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "R2" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +∞
 | 
						|
        [actionBringToFront, ["R1", "R2", "F1_1", "F1_2", "F1"]],
 | 
						|
        // noop
 | 
						|
        [actionBringToFront, ["R1", "R2", "F1_1", "F1_2", "F1"]],
 | 
						|
        // -∞
 | 
						|
        [actionSendToBack, ["F1_1", "F1_2", "F1", "R1", "R2"]],
 | 
						|
        // noop
 | 
						|
        [actionSendToBack, ["F1_1", "F1_2", "F1", "R1", "R2"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // DENORMALIZED FRAME ORDER
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1" },
 | 
						|
        { id: "F1", type: "frame", isSelected: true },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "R2" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +∞
 | 
						|
        [actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]],
 | 
						|
        // noop
 | 
						|
        [actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]],
 | 
						|
        // -∞
 | 
						|
        [actionSendToBack, ["F1_1", "F1", "F1_2", "R1", "R2"]],
 | 
						|
        // noop
 | 
						|
        [actionSendToBack, ["F1_1", "F1", "F1_2", "R1", "R2"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // DENORMALIZED FRAME ORDER
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1" },
 | 
						|
        { id: "F1", type: "frame", isSelected: true },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "R2" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +∞
 | 
						|
        [actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
 | 
						|
    // DENORMALIZED FRAME ORDER
 | 
						|
    assertZindex({
 | 
						|
      elements: [
 | 
						|
        { id: "F1_1", frameId: "F1" },
 | 
						|
        { id: "R1" },
 | 
						|
        { id: "F1", type: "frame", isSelected: true },
 | 
						|
        { id: "R2" },
 | 
						|
        { id: "F1_2", frameId: "F1" },
 | 
						|
        { id: "R3" },
 | 
						|
      ],
 | 
						|
      operations: [
 | 
						|
        // +1
 | 
						|
        [actionBringToFront, ["R1", "R2", "R3", "F1_1", "F1", "F1_2"]],
 | 
						|
      ],
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |