fix: add history capture for paste and drop of images and embeds (#9605)
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
COLOR_PALETTE,
|
||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
|
||||
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
|
||||
reseed,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import "@excalidraw/utils/test-utils";
|
||||
@@ -35,6 +36,7 @@ import type {
|
||||
ExcalidrawGenericElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElement,
|
||||
FileId,
|
||||
FixedPointBinding,
|
||||
FractionalIndex,
|
||||
SceneElementsMap,
|
||||
@@ -49,12 +51,16 @@ import {
|
||||
} from "../actions";
|
||||
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||
import * as StaticScene from "../renderer/staticScene";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { Excalidraw } from "../index";
|
||||
import * as StaticScene from "../renderer/staticScene";
|
||||
import { createPasteEvent } from "../clipboard";
|
||||
|
||||
import * as blobModule from "../data/blob";
|
||||
|
||||
import { API } from "./helpers/api";
|
||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
||||
import { mockHTMLImageElement } from "./helpers/mocks";
|
||||
import {
|
||||
GlobalTestState,
|
||||
act,
|
||||
@@ -63,6 +69,7 @@ import {
|
||||
togglePopover,
|
||||
getCloneByOrigId,
|
||||
checkpointHistory,
|
||||
unmountComponent,
|
||||
} from "./test-utils";
|
||||
|
||||
import type { AppState } from "../types";
|
||||
@@ -106,7 +113,22 @@ const violet = COLOR_PALETTE.violet[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX];
|
||||
|
||||
describe("history", () => {
|
||||
beforeEach(() => {
|
||||
unmountComponent();
|
||||
renderStaticScene.mockClear();
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
|
||||
reseed(7);
|
||||
|
||||
const generateIdSpy = vi.spyOn(blobModule, "generateIdFromFile");
|
||||
const resizeFileSpy = vi.spyOn(blobModule, "resizeImageFile");
|
||||
|
||||
generateIdSpy.mockImplementation(() => Promise.resolve("fileId" as FileId));
|
||||
resizeFileSpy.mockImplementation((file: File) => Promise.resolve(file));
|
||||
|
||||
Object.assign(document, {
|
||||
elementFromPoint: () => GlobalTestState.canvas,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -559,6 +581,227 @@ describe("history", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create new history entry on image drag&drop", async () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
|
||||
// it's necessary to specify the height in order to calculate natural dimensions of the image
|
||||
h.state.height = 1000;
|
||||
|
||||
const deerImageDimensions = {
|
||||
width: 318,
|
||||
height: 335,
|
||||
};
|
||||
|
||||
mockHTMLImageElement(
|
||||
deerImageDimensions.width,
|
||||
deerImageDimensions.height,
|
||||
);
|
||||
|
||||
await API.drop(await API.loadFile("./fixtures/deer.png"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "image",
|
||||
fileId: expect.any(String),
|
||||
x: expect.toBeNonNaNNumber(),
|
||||
y: expect.toBeNonNaNNumber(),
|
||||
...deerImageDimensions,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "image",
|
||||
fileId: expect.any(String),
|
||||
x: expect.toBeNonNaNNumber(),
|
||||
y: expect.toBeNonNaNNumber(),
|
||||
isDeleted: true,
|
||||
...deerImageDimensions,
|
||||
}),
|
||||
]);
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "image",
|
||||
fileId: expect.any(String),
|
||||
x: expect.toBeNonNaNNumber(),
|
||||
y: expect.toBeNonNaNNumber(),
|
||||
isDeleted: false,
|
||||
...deerImageDimensions,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create new history entry on embeddable link drag&drop", async () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
|
||||
const link = "https://www.youtube.com/watch?v=gkGMXY0wekg";
|
||||
await API.drop(
|
||||
new Blob([link], {
|
||||
type: MIME_TYPES.text,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "embeddable",
|
||||
link,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "embeddable",
|
||||
link,
|
||||
isDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "embeddable",
|
||||
link,
|
||||
isDeleted: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create new history entry on image paste", async () => {
|
||||
await render(
|
||||
<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />,
|
||||
);
|
||||
|
||||
// it's necessary to specify the height in order to calculate natural dimensions of the image
|
||||
h.state.height = 1000;
|
||||
|
||||
const smileyImageDimensions = {
|
||||
width: 56,
|
||||
height: 77,
|
||||
};
|
||||
|
||||
mockHTMLImageElement(
|
||||
smileyImageDimensions.width,
|
||||
smileyImageDimensions.height,
|
||||
);
|
||||
|
||||
document.dispatchEvent(
|
||||
createPasteEvent({
|
||||
files: [await API.loadFile("./fixtures/smiley_embedded_v2.png")],
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "image",
|
||||
fileId: expect.any(String),
|
||||
x: expect.toBeNonNaNNumber(),
|
||||
y: expect.toBeNonNaNNumber(),
|
||||
...smileyImageDimensions,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "image",
|
||||
fileId: expect.any(String),
|
||||
x: expect.toBeNonNaNNumber(),
|
||||
y: expect.toBeNonNaNNumber(),
|
||||
isDeleted: true,
|
||||
...smileyImageDimensions,
|
||||
}),
|
||||
]);
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "image",
|
||||
fileId: expect.any(String),
|
||||
x: expect.toBeNonNaNNumber(),
|
||||
y: expect.toBeNonNaNNumber(),
|
||||
isDeleted: false,
|
||||
...smileyImageDimensions,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create new history entry on embeddable link paste", async () => {
|
||||
await render(
|
||||
<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />,
|
||||
);
|
||||
|
||||
const link = "https://www.youtube.com/watch?v=gkGMXY0wekg";
|
||||
|
||||
document.dispatchEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/plain": link,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "embeddable",
|
||||
link,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
Keyboard.undo();
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "embeddable",
|
||||
link,
|
||||
isDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
Keyboard.redo();
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "embeddable",
|
||||
link,
|
||||
isDeleted: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should support appstate name or viewBackgroundColor change", async () => {
|
||||
await render(
|
||||
<Excalidraw
|
||||
|
||||
Reference in New Issue
Block a user