Files
upstream-excalidraw/packages/excalidraw/tests/created.test.tsx
2026-09-16 05:31:04 +05:00

168 lines
4.8 KiB
TypeScript

import { randomInteger } from "@excalidraw/common";
import { CaptureUpdateAction, newElementWith } from "@excalidraw/element";
import type { FractionalIndex } from "@excalidraw/element/types";
import { reconcileElements } from "../data/reconcile";
import { Excalidraw } from "../index";
import { API } from "./helpers/api";
import { Keyboard } from "./helpers/ui";
import { act, render } from "./test-utils";
import type { RemoteExcalidrawElement } from "../data/reconcile";
const { h } = window;
describe("element creation timestamps", () => {
beforeEach(async () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
});
it.each([
{ type: "rectangle", created: 123 },
{ type: "rectangle", created: null },
{ type: "stickynote", created: 123 },
{ type: "stickynote", created: null },
] as const)(
"preserves $type created=$created through capture, edits, and creation undo/redo",
({ type, created }) => {
const element = API.createElement({
type,
x: 10,
created,
index: "a0" as FractionalIndex,
});
API.updateScene({
elements: [element],
captureUpdate: CaptureUpdateAction.EVENTUALLY,
});
expect(API.getUndoStack()).toHaveLength(0);
act(() => {
h.app.scheduleCapture();
h.setState({});
});
expect(API.getUndoStack()).toHaveLength(1);
expect(h.elements[0]).toMatchObject({
created,
version: element.version,
versionNonce: element.versionNonce,
});
const edited = newElementWith(h.elements[0], { x: 50 });
API.updateScene({
elements: [edited],
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
});
expect(API.getUndoStack()).toHaveLength(2);
expect(h.elements[0]).toMatchObject({
created,
x: 50,
version: edited.version,
versionNonce: edited.versionNonce,
});
expect(h.store.snapshot.elements.get(element.id)?.created).toBe(created);
Keyboard.undo();
expect(h.elements[0]).toMatchObject({
created,
x: 10,
isDeleted: false,
});
expect(h.elements[0].version).toBeGreaterThan(edited.version);
Keyboard.undo();
expect(h.elements[0]).toMatchObject({ created, isDeleted: true });
expect(API.getUndoStack()).toHaveLength(0);
Keyboard.redo();
expect(h.elements[0]).toMatchObject({
created,
x: 10,
isDeleted: false,
});
Keyboard.redo();
expect(h.elements[0]).toMatchObject({
created,
x: 50,
isDeleted: false,
});
expect(API.getUndoStack()).toHaveLength(2);
expect(API.getRedoStack()).toHaveLength(0);
},
);
it("continues to a visible undo change after replaying creation metadata", () => {
const element = API.createElement({
type: "rectangle",
x: 10,
created: null,
});
API.updateScene({
elements: [element],
captureUpdate: CaptureUpdateAction.NEVER,
});
API.updateScene({
elements: [newElementWith(h.elements[0], { x: 50 })],
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
});
// An explicit metadata repair must round-trip without consuming a
// separate undo keypress before the user's visible edit. `created` is not
// an element update, so a repair constructs a new element revision.
API.updateScene({
elements: [
{
...h.elements[0],
created: 123,
version: h.elements[0].version + 1,
versionNonce: randomInteger(),
},
],
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
});
expect(API.getUndoStack()).toHaveLength(2);
Keyboard.undo();
expect(h.elements[0]).toMatchObject({ x: 10, created: null });
expect(API.getUndoStack()).toHaveLength(0);
expect(API.getRedoStack()).toHaveLength(2);
});
it("preserves the remote creation timestamp and revision during reconciliation", () => {
const element = API.createElement({ type: "rectangle", created: 123 });
API.updateScene({
elements: [element],
captureUpdate: CaptureUpdateAction.NEVER,
});
const remoteElement = newElementWith(h.elements[0], { x: 50 });
const reconciled = reconcileElements(
h.app.scene.getElementsIncludingDeleted(),
[remoteElement as RemoteExcalidrawElement],
h.state,
);
API.updateScene({
elements: reconciled,
captureUpdate: CaptureUpdateAction.NEVER,
});
expect(h.elements[0]).toMatchObject({
x: 50,
created: 123,
version: remoteElement.version,
versionNonce: remoteElement.versionNonce,
});
expect(h.store.snapshot.elements.get(element.id)).toMatchObject({
created: 123,
version: remoteElement.version,
versionNonce: remoteElement.versionNonce,
});
expect(API.getUndoStack()).toHaveLength(0);
});
});