snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Has been cancelled
Some checks failed
Tests / test (push) Has been cancelled
This commit is contained in:
59
excalidraw-app/tests/LanguageList.test.tsx
Normal file
59
excalidraw-app/tests/LanguageList.test.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Excalidraw, MainMenu } from "@excalidraw/excalidraw";
|
||||
import { defaultLang, languages } from "@excalidraw/excalidraw/i18n";
|
||||
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||
import {
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
render,
|
||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||
|
||||
import { LanguageList } from "../app-language/LanguageList";
|
||||
import { useAppLangCode } from "../app-language/language-state";
|
||||
import { Provider } from "../app-jotai";
|
||||
|
||||
const TEST_LANG_CODE = "fr-FR";
|
||||
|
||||
const TestApp = () => {
|
||||
const [langCode] = useAppLangCode();
|
||||
|
||||
return (
|
||||
<Excalidraw langCode={langCode}>
|
||||
<MainMenu>
|
||||
<MainMenu.ItemCustom>
|
||||
<LanguageList />
|
||||
</MainMenu.ItemCustom>
|
||||
</MainMenu>
|
||||
</Excalidraw>
|
||||
);
|
||||
};
|
||||
|
||||
describe("Test LanguageList", () => {
|
||||
it("rerenders UI on language change", async () => {
|
||||
expect(languages.some((lang) => lang.code === TEST_LANG_CODE)).toBe(true);
|
||||
|
||||
await render(
|
||||
<Provider>
|
||||
<TestApp />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
// select rectangle tool to show properties menu
|
||||
UI.clickTool("rectangle");
|
||||
// english lang should display `thin` label
|
||||
expect(screen.queryByTitle(/thin/i)).not.toBeNull();
|
||||
fireEvent.click(document.querySelector(".dropdown-menu-button")!);
|
||||
|
||||
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
|
||||
target: { value: TEST_LANG_CODE },
|
||||
});
|
||||
// switching to French, `thin` label should no longer exist
|
||||
await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
|
||||
// reset language
|
||||
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
|
||||
target: { value: defaultLang.code },
|
||||
});
|
||||
// switching back to English
|
||||
await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
|
||||
});
|
||||
});
|
||||
39
excalidraw-app/tests/MobileMenu.test.tsx
Normal file
39
excalidraw-app/tests/MobileMenu.test.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Excalidraw, WelcomeScreen } from "@excalidraw/excalidraw";
|
||||
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||
import {
|
||||
mockBoundingClientRect,
|
||||
render,
|
||||
restoreOriginalGetBoundingClientRect,
|
||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||
|
||||
describe("Test MobileMenu", () => {
|
||||
const { h } = window;
|
||||
const dimensions = { height: 400, width: 800 };
|
||||
|
||||
beforeAll(() => {
|
||||
mockBoundingClientRect(dimensions);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await render(
|
||||
<Excalidraw>
|
||||
<WelcomeScreen />
|
||||
</Excalidraw>,
|
||||
);
|
||||
h.app.refreshEditorInterface();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
restoreOriginalGetBoundingClientRect();
|
||||
});
|
||||
|
||||
it("should set editor interface correctly", () => {
|
||||
expect(h.app.editorInterface.formFactor).toBe("phone");
|
||||
});
|
||||
|
||||
it("should initialize with welcome screen and hide once user interacts", async () => {
|
||||
expect(document.querySelector(".welcome-screen-center")).toMatchSnapshot();
|
||||
UI.clickTool("rectangle");
|
||||
expect(document.querySelector(".welcome-screen-center")).toBeNull();
|
||||
});
|
||||
});
|
||||
150
excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap
Normal file
150
excalidraw-app/tests/__snapshots__/MobileMenu.test.tsx.snap
Normal file
File diff suppressed because one or more lines are too long
252
excalidraw-app/tests/collab.test.tsx
Normal file
252
excalidraw-app/tests/collab.test.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
|
||||
import {
|
||||
createRedoAction,
|
||||
createUndoAction,
|
||||
} from "@excalidraw/excalidraw/actions/actionHistory";
|
||||
import { syncInvalidIndices } from "@excalidraw/element";
|
||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { StoreIncrement } from "@excalidraw/element";
|
||||
|
||||
import type { DurableIncrement, EphemeralIncrement } from "@excalidraw/element";
|
||||
|
||||
import ExcalidrawApp from "../App";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: {
|
||||
getRandomValues: (arr: number[]) =>
|
||||
arr.forEach((v, i) => (arr[i] = Math.floor(Math.random() * 256))),
|
||||
subtle: {
|
||||
generateKey: () => {},
|
||||
exportKey: () => ({ k: "sTdLvMC_M3V8_vGa3UVRDg" }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mock("../../excalidraw-app/data/firebase.ts", () => {
|
||||
const loadFromFirebase = async () => null;
|
||||
const saveToFirebase = () => {};
|
||||
const isSavedToFirebase = () => true;
|
||||
const loadFilesFromFirebase = async () => ({
|
||||
loadedFiles: [],
|
||||
erroredFiles: [],
|
||||
});
|
||||
const saveFilesToFirebase = async () => ({
|
||||
savedFiles: new Map(),
|
||||
erroredFiles: new Map(),
|
||||
});
|
||||
|
||||
return {
|
||||
loadFromFirebase,
|
||||
saveToFirebase,
|
||||
isSavedToFirebase,
|
||||
loadFilesFromFirebase,
|
||||
saveFilesToFirebase,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("socket.io-client", () => {
|
||||
return {
|
||||
default: () => {
|
||||
return {
|
||||
close: () => {},
|
||||
on: () => {},
|
||||
once: () => {},
|
||||
off: () => {},
|
||||
emit: () => {},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* These test would deserve to be extended by testing collab with (at least) two clients simultanouesly,
|
||||
* while having access to both scenes, appstates stores, histories and etc.
|
||||
* i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously.
|
||||
*/
|
||||
describe("collaboration", () => {
|
||||
it("should emit two ephemeral increments even though updates get batched", async () => {
|
||||
const durableIncrements: DurableIncrement[] = [];
|
||||
const ephemeralIncrements: EphemeralIncrement[] = [];
|
||||
|
||||
await render(<ExcalidrawApp />);
|
||||
|
||||
h.store.onStoreIncrementEmitter.on((increment) => {
|
||||
if (StoreIncrement.isDurable(increment)) {
|
||||
durableIncrements.push(increment);
|
||||
} else {
|
||||
ephemeralIncrements.push(increment);
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(h.store["scheduledMicroActions"].length).toBe(0);
|
||||
expect(durableIncrements.length).toBe(0);
|
||||
expect(ephemeralIncrements.length).toBe(0);
|
||||
|
||||
const rectProps = {
|
||||
type: "rectangle",
|
||||
id: "A",
|
||||
height: 200,
|
||||
width: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
} as const;
|
||||
|
||||
const rect = API.createElement({ ...rectProps });
|
||||
|
||||
API.updateScene({
|
||||
elements: [rect],
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// expect(commitSpy).toHaveBeenCalledTimes(1);
|
||||
expect(durableIncrements.length).toBe(1);
|
||||
});
|
||||
|
||||
// simulate two batched remote updates
|
||||
act(() => {
|
||||
h.app.updateScene({
|
||||
elements: [newElementWith(h.elements[0], { x: 100 })],
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
h.app.updateScene({
|
||||
elements: [newElementWith(h.elements[0], { x: 200 })],
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
|
||||
// we scheduled two micro actions,
|
||||
// which confirms they are going to be executed as part of one batched component update
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(h.store["scheduledMicroActions"].length).toBe(2);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// altough the updates get batched,
|
||||
// we expect two ephemeral increments for each update,
|
||||
// and each such update should have the expected change
|
||||
expect(ephemeralIncrements.length).toBe(2);
|
||||
expect(ephemeralIncrements[0].change.elements.A).toEqual(
|
||||
expect.objectContaining({ x: 100 }),
|
||||
);
|
||||
expect(ephemeralIncrements[1].change.elements.A).toEqual(
|
||||
expect.objectContaining({ x: 200 }),
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(h.store["scheduledMicroActions"].length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow to undo / redo even on force-deleted elements", async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
const rect1Props = {
|
||||
type: "rectangle",
|
||||
id: "A",
|
||||
height: 200,
|
||||
width: 100,
|
||||
} as const;
|
||||
|
||||
const rect2Props = {
|
||||
type: "rectangle",
|
||||
id: "B",
|
||||
width: 100,
|
||||
height: 200,
|
||||
} as const;
|
||||
|
||||
const rect1 = API.createElement({ ...rect1Props });
|
||||
const rect2 = API.createElement({ ...rect2Props });
|
||||
|
||||
API.updateScene({
|
||||
elements: syncInvalidIndices([rect1, rect2]),
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
|
||||
API.updateScene({
|
||||
elements: syncInvalidIndices([
|
||||
rect1,
|
||||
newElementWith(h.elements[1], { isDeleted: true }),
|
||||
]),
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
expect(API.getSnapshot()).toEqual([
|
||||
expect.objectContaining(rect1Props),
|
||||
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||
]);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining(rect1Props),
|
||||
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
// one form of force deletion happens when starting the collab, not to sync potentially sensitive data into the server
|
||||
window.collab.startCollaboration(null);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
// we never delete from the local snapshot as it is used for correct diff calculation
|
||||
expect(API.getSnapshot()).toEqual([
|
||||
expect.objectContaining(rect1Props),
|
||||
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||
]);
|
||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||
});
|
||||
|
||||
const undoAction = createUndoAction(h.history);
|
||||
act(() => h.app.actionManager.executeAction(undoAction));
|
||||
|
||||
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(API.getSnapshot()).toEqual([
|
||||
expect.objectContaining(rect1Props),
|
||||
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
||||
]);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining(rect1Props),
|
||||
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
||||
]);
|
||||
});
|
||||
|
||||
// simulate force deleting the element remotely
|
||||
API.updateScene({
|
||||
elements: syncInvalidIndices([rect1]),
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
expect(API.getRedoStack().length).toBe(1);
|
||||
expect(API.getSnapshot()).toEqual([
|
||||
expect.objectContaining(rect1Props),
|
||||
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||
]);
|
||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||
});
|
||||
|
||||
const redoAction = createRedoAction(h.history);
|
||||
act(() => h.app.actionManager.executeAction(redoAction));
|
||||
|
||||
// with explicit redo (as removal) we again restore the element from the snapshot!
|
||||
await waitFor(() => {
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(API.getSnapshot()).toEqual([
|
||||
expect.objectContaining(rect1Props),
|
||||
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||
]);
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining(rect1Props),
|
||||
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user