mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: multiplayer undo / redo (#7348)
This commit is contained in:
parent
5211b003b8
commit
530617be90
71 changed files with 34885 additions and 14877 deletions
File diff suppressed because it is too large
Load diff
18005
packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
Normal file
18005
packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
Normal file
File diff suppressed because it is too large
Load diff
|
@ -173,7 +173,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
|
|||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"versionNonce": 745419401,
|
||||
"versionNonce": 1984422985,
|
||||
"width": 300,
|
||||
"x": 201,
|
||||
"y": 2,
|
||||
|
@ -186,7 +186,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"customData": undefined,
|
||||
"endArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": {
|
||||
"elementId": "id1",
|
||||
"focus": -0.46666666666666673,
|
||||
|
@ -227,10 +227,10 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "line",
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 12,
|
||||
"versionNonce": 1984422985,
|
||||
"version": 14,
|
||||
"versionNonce": 2066753033,
|
||||
"width": 81,
|
||||
"x": 110,
|
||||
"y": 49.981789081137734,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -27,7 +27,7 @@ const checkpoint = (name: string) => {
|
|||
`[${name}] number of renders`,
|
||||
);
|
||||
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||
expect(h.history.getSnapshotForTest()).toMatchSnapshot(`[${name}] history`);
|
||||
expect(h.history).toMatchSnapshot(`[${name}] history`);
|
||||
expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
|
||||
h.elements.forEach((element, i) =>
|
||||
expect(element).toMatchSnapshot(`[${name}] element ${i}`),
|
||||
|
|
|
@ -69,9 +69,18 @@ export class API {
|
|||
return selectedElements[0];
|
||||
};
|
||||
|
||||
static getStateHistory = () => {
|
||||
static getUndoStack = () => {
|
||||
// @ts-ignore
|
||||
return h.history.stateHistory;
|
||||
return h.history.undoStack;
|
||||
};
|
||||
|
||||
static getRedoStack = () => {
|
||||
// @ts-ignore
|
||||
return h.history.redoStack;
|
||||
};
|
||||
|
||||
static getSnapshot = () => {
|
||||
return Array.from(h.store.snapshot.elements.values());
|
||||
};
|
||||
|
||||
static clearSelection = () => {
|
||||
|
|
|
@ -113,6 +113,18 @@ export class Keyboard {
|
|||
Keyboard.codeDown(code);
|
||||
Keyboard.codeUp(code);
|
||||
};
|
||||
|
||||
static undo = () => {
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
Keyboard.keyPress("z");
|
||||
});
|
||||
};
|
||||
|
||||
static redo = () => {
|
||||
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
||||
Keyboard.keyPress("z");
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const getElementPointForSelection = (element: ExcalidrawElement): Point => {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -77,30 +77,30 @@ describe("move element", () => {
|
|||
// create elements
|
||||
const rectA = UI.createElement("rectangle", { size: 100 });
|
||||
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
|
||||
const line = UI.createElement("line", { x: 110, y: 50, size: 80 });
|
||||
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
|
||||
const elementsMap = h.app.scene.getNonDeletedElementsMap();
|
||||
// bind line to two rectangles
|
||||
bindOrUnbindLinearElement(
|
||||
line.get() as NonDeleted<ExcalidrawLinearElement>,
|
||||
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
|
||||
rectA.get() as ExcalidrawRectangleElement,
|
||||
rectB.get() as ExcalidrawRectangleElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// select the second rectangles
|
||||
// select the second rectangle
|
||||
new Pointer("mouse").clickOn(rectB);
|
||||
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`21`,
|
||||
`20`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`19`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`17`);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(3);
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
expect([rectB.x, rectB.y]).toEqual([200, 0]);
|
||||
expect([line.x, line.y]).toEqual([110, 50]);
|
||||
expect([line.width, line.height]).toEqual([80, 80]);
|
||||
expect([arrow.x, arrow.y]).toEqual([110, 50]);
|
||||
expect([arrow.width, arrow.height]).toEqual([80, 80]);
|
||||
|
||||
renderInteractiveScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
|
@ -118,8 +118,10 @@ describe("move element", () => {
|
|||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
||||
expect([Math.round(line.x), Math.round(line.y)]).toEqual([110, 50]);
|
||||
expect([Math.round(line.width), Math.round(line.height)]).toEqual([81, 81]);
|
||||
expect([Math.round(arrow.x), Math.round(arrow.y)]).toEqual([110, 50]);
|
||||
expect([Math.round(arrow.width), Math.round(arrow.height)]).toEqual([
|
||||
81, 81,
|
||||
]);
|
||||
|
||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||
});
|
||||
|
|
|
@ -35,7 +35,7 @@ const checkpoint = (name: string) => {
|
|||
`[${name}] number of renders`,
|
||||
);
|
||||
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||
expect(h.history.getSnapshotForTest()).toMatchSnapshot(`[${name}] history`);
|
||||
expect(h.history).toMatchSnapshot(`[${name}] history`);
|
||||
expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
|
||||
h.elements.forEach((element, i) =>
|
||||
expect(element).toMatchSnapshot(`[${name}] element ${i}`),
|
||||
|
@ -359,6 +359,7 @@ describe("regression tests", () => {
|
|||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
Keyboard.keyPress(KEYS.Z);
|
||||
Keyboard.keyPress(KEYS.Z);
|
||||
Keyboard.keyPress(KEYS.Z);
|
||||
});
|
||||
expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
|
@ -372,7 +373,7 @@ describe("regression tests", () => {
|
|||
});
|
||||
|
||||
it("noop interaction after undo shouldn't create history entry", () => {
|
||||
expect(API.getStateHistory().length).toBe(1);
|
||||
expect(API.getUndoStack().length).toBe(0);
|
||||
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
|
@ -386,35 +387,35 @@ describe("regression tests", () => {
|
|||
|
||||
const secondElementEndPoint = mouse.getPosition();
|
||||
|
||||
expect(API.getStateHistory().length).toBe(3);
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
Keyboard.keyPress(KEYS.Z);
|
||||
});
|
||||
|
||||
expect(API.getStateHistory().length).toBe(2);
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
|
||||
// clicking an element shouldn't add to history
|
||||
mouse.restorePosition(...firstElementEndPoint);
|
||||
mouse.click();
|
||||
expect(API.getStateHistory().length).toBe(2);
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
|
||||
Keyboard.withModifierKeys({ shift: true, ctrl: true }, () => {
|
||||
Keyboard.keyPress(KEYS.Z);
|
||||
});
|
||||
|
||||
expect(API.getStateHistory().length).toBe(3);
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
|
||||
// clicking an element shouldn't add to history
|
||||
// clicking an element should add to history
|
||||
mouse.click();
|
||||
expect(API.getStateHistory().length).toBe(3);
|
||||
expect(API.getUndoStack().length).toBe(3);
|
||||
|
||||
const firstSelectedElementId = API.getSelectedElement().id;
|
||||
|
||||
// same for clicking the element just redo-ed
|
||||
mouse.restorePosition(...secondElementEndPoint);
|
||||
mouse.click();
|
||||
expect(API.getStateHistory().length).toBe(3);
|
||||
expect(API.getUndoStack().length).toBe(4);
|
||||
|
||||
expect(API.getSelectedElement().id).not.toEqual(firstSelectedElementId);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue