Compare commits

..

8 Commits

Author SHA1 Message Date
Mark Tolmacs
2ed78d4895 fix: Improve rendering stability 2025-09-05 12:58:22 +02:00
Mark Tolmacs
5ba2798306 Iterating on arrows 2025-08-26 14:55:40 +02:00
Mark Tolmacs
b8aae34e32 Fixes and handling 2025-08-25 14:46:07 +02:00
Mark Tolmacs
6317ac16ee Working screenshot diff 2025-08-25 11:20:38 +02:00
Mark Tolmacs
9ad75b8375 Direct function generation 2025-08-25 10:42:47 +02:00
Mark Tolmacs
4c9ad1a22f Implementation of one test 2025-08-20 17:53:20 +02:00
Mark Tolmacs
023895b49b Can generate and play back 2025-08-20 15:17:49 +02:00
Mark Tolmacs
eb37be953a Install playwright 2025-08-20 15:17:35 +02:00
19 changed files with 1261 additions and 543 deletions

27
.github/workflows/playwright.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm install -g yarn && yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Run Playwright tests
run: yarn playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

6
.gitignore vendored
View File

@@ -27,3 +27,9 @@ dev-dist
html
meta*.json
.claude
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -1,101 +0,0 @@
## Excalidraw Hierarchical Model Plan
### Background & Goals
Introduce a fully in-memory hierarchical (tree) model on top of the existing flat `elements[]` storage for more efficient complex operations (queries, selection, collision), while keeping flat arrays as the persistence/collab projection. Gradually move to tree-first edits with flat projection.
### Capabilities To Preserve
- z-index via fractional indices
- add/remove to frame
- group/ungroup
- bound texts (containerId)
- arrow bindings (start/endBinding)
- history (undo/redo) and collab (delta broadcast)
- load/save the flat array
### Existing Reusable Capabilities
- Deltas & History: `element/src/delta.ts` (ElementsDelta/AppStateDelta/StoreDelta), `excalidraw/history.ts` (HistoryDelta), auto rebind, text bbox redraw, z-index normalization.
- Store & Snapshot: `element/src/store.ts` provides commit levels, batching, and delta emission.
- Scene & Relationships: `element/src/Scene.ts`, `element/src/frame.ts`, `element/src/groups.ts` for frames and groups logic.
- Rendering: `excalidraw/renderer/staticScene.ts` with order by fractional index.
- Restore/Import: `excalidraw/data/restore.ts`.
### Data Model & Invariants
- Node types: `ElementNode`, logical `GroupNode` (id=groupId), `FrameNode` (bound to frame element). `Table*Node` reserved.
- Parent priority: container > group (deep→shallow) > frame > root; single parent per node.
- Groups must not span multiple frames.
- Drawing order remains by fractional index; the tree offers structural and sibling-order views only.
### Flat→Tree Build (`buildFromFlat`)
- Input: `elements[]`/`elementsMap` (optionally including deleted).
- Output: `{ nodesById, roots, orderHints, diagnostics }`.
- Rules:
- Bound text attaches to its container; groups form deep→shallow parent chains from `groupIds`; frame parent from `frameId`; otherwise root.
- Sibling order: ascending by the minimum `index` across the nodes represented elements.
- Diagnostics: cross-frame groups, invalid container, cycles, missing refs (error/warn).
### Tree → Flat Projection (`flattenToArray`)
- Input: tree, optional "apply recommended reorder".
- Output: `{ nextFieldsByElementId, reorderIntent? }`.
- Rules:
- `frameId` from nearest frame ancestor; `groupIds` nearest→farthest; `containerId` from nearest container.
- Do not change draw order by default; any reordering is applied by the caller via `Scene.insert*` and `syncMovedIndices`.
### Operations Mapping (Tree edits → Flat deltas)
- z-index: sibling reordering → index deltas; normalized with `syncMovedIndices`.
- Frame membership: reparent to `FrameNode`/root → `frameId` updates; cross-frame groups disallowed.
- Group/ungroup: modify `GroupNode` structure → update `groupIds` chains.
- Bound text: reparent to container → update `containerId`/`boundElements`; text bbox redraw handled by `ElementsDelta`.
- Arrow binding: does not change parentage; only update start/endBinding; `ElementsDelta` handles rebind/unbind.
### History & Collab
- Transactional edits on the tree via `HierarchyManager.begin/commit/rollback`; commit projects to a minimal flat diff, wrapped as `StoreDelta`, and submitted via `Store.scheduleMicroAction` (IMMEDIATELY).
- Undo/redo uses `HistoryDelta`; replay re-emits flat deltas for sync.
- Collab remains flat-delta based; peers rebuild the tree deterministically from flats.
### Rendering Strategy
- Add a tree-backed rendering adapter beside `renderStaticScene` behind a feature flag, preserving draw-order semantics (fractional index). In the short term, use the tree for selection/collision pruning (frame → group → element).
### Challenges & Risks
- Cross-frame group handling (block or guided fix).
- Reorder consistency (tree sibling order vs fractional index).
- Collab conflicts (use `ElementsDelta.applyLatestChanges`).
- Performance (build O(n), queries O(1)/O(k)); cache/incremental via `sceneNonce`.
- Test coverage (round-trip, collab equivalence, history replay, deep groups/large frames/binding chains).
### Phased Plan
- Phase 0 Rules & Contracts
- Lock invariants and priorities; define diagnostics (error/warn).
- Phase 1 Pure functions & Validation
- Implement `buildFromFlat`, `flattenToArray`, `validateIntegrity`; cache by `sceneNonce`; add round-trip tests.
- Phase 2 Read-only integration
- Tree-backed selection and collision pruning; measure wins.
- Phase 3 Parallel render adapter
- Tree render adapter (flag) with preserved order semantics.
- Phase 4 Projection & Transactions
- `HierarchyManager.begin/commit/rollback`; commit→`StoreDelta`→Store.
- Phase 5 Migrate operations
- Frame membership and group/ungroup → tree+projection; then bound text; optional z-index reorder intent.
- Phase 6 Extensions & Tables
- Introduce `Table*Node` (in-memory first, then projection), with validation and UI.
### Success Criteria
- Correctness: same flat → same tree; unchanged structure round-trip no-ops; existing operations equivalent.
- History/Collab: still record and broadcast minimal flat deltas; deterministic tree on peers.
- Performance: selection/collision candidate reduction on large scenes; build/query latency targets met.
- Rollback: feature flag to fall back to legacy path at any time.
### Next Steps
- Finalize invariants and IO contracts; implement `buildFromFlat`/`flattenToArray` and `validateIntegrity`; add roundtrip and failure-case tests; prototype read-only integration and render adapter.

View File

@@ -20,6 +20,7 @@ import {
APP_NAME,
EVENT,
THEME,
TITLE_TIMEOUT,
VERSION_TIMEOUT,
debounce,
getVersion,
@@ -73,6 +74,7 @@ import type {
import type { ResolutionType } from "@excalidraw/common/utility-types";
import type { ResolvablePromise } from "@excalidraw/common/utils";
import "./record";
import CustomStats from "./CustomStats";
import {
Provider,
@@ -498,6 +500,11 @@ const ExcalidrawWrapper = () => {
}
};
const titleTimeout = setTimeout(
() => (document.title = APP_NAME),
TITLE_TIMEOUT,
);
const syncData = debounce(() => {
if (isTestEnv()) {
return;
@@ -588,6 +595,7 @@ const ExcalidrawWrapper = () => {
visibilityChange,
false,
);
clearTimeout(titleTimeout);
};
}, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);

View File

@@ -2,7 +2,9 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Excalidraw Whiteboard</title>
<title>
Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw
</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"

View File

@@ -46,6 +46,7 @@
"build:version": "node ../scripts/build-version.js",
"build": "yarn build:app && yarn build:version",
"start": "yarn && vite",
"start:test": "yarn && vite --mode test",
"start:production": "yarn build && yarn serve",
"serve": "npx http-server build -a localhost -p 5001 -o",
"build:preview": "yarn build && vite preview --port 5000"

270
excalidraw-app/record.ts Normal file
View File

@@ -0,0 +1,270 @@
import { isDevEnv } from "@excalidraw/common";
declare global {
interface Window {
record: typeof Record;
}
}
export class Record {
private static recording: boolean = false;
private static events: string = "";
private static timestamp: number = 0;
public static get isRecording() {
return Record.recording;
}
private static header() {
Record.events += " await page.addInitScript(() => {\n";
Record.events += " Math.random = () => 0.42;\n\n";
// Capture LocalStorage, which is essential to re-establish state
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key != null) {
const value = JSON.stringify(localStorage.getItem(key));
Record.events += ` localStorage.getItem("${key}");\n`;
Record.events += ` localStorage.setItem("${key}", ${value});\n`;
}
}
Record.events += " });\n";
Record.events += ` await page.setViewportSize({ width: ${window.innerWidth}, height: ${window.innerHeight} });\n`;
Record.events += ` await page.goto("http://localhost:3000");\n`;
Record.events += ` await page.waitForLoadState("load");\n`;
}
public static restart() {
if (!Record.recording) {
Record.start();
return;
}
Record.events += `});\n\n`;
Record.events += `test("${
Date.now() + Math.floor(Math.random() * Date.now()).toString(36)
}", async ({ page }) => {\n`;
Record.header();
}
public static start() {
Record.recording = true;
// Record header
this.header();
// Set up the events
Record.timestamp = performance.now();
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mouseup", this.onMouseUp);
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
public static stop() {
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mouseup", this.onMouseUp);
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
Record.recording = false;
}
/// Displays a window as an absolutely positioned DIV with the generated
/// events within <pre> tags as formatted JSON, so it can be copied easily.
public static showGeneratedEvents() {
if (Record.recording) {
Record.stop();
}
const div = document.createElement("div");
div.style.position = "absolute";
div.style.top = "10px";
div.style.right = "10px";
div.style.left = "10px";
div.style.height = "60vh";
div.style.backgroundColor = "gray";
div.style.padding = "10px";
div.style.zIndex = "10000";
const pre = document.createElement("pre");
let textContent = `import { expect, test } from "@playwright/test";\n\n`;
textContent += `test("${
Date.now() + Math.floor(Math.random() * Date.now()).toString(36)
}", async ({ page }) => {\n`;
textContent += Record.events;
textContent += `});\n`;
pre.textContent = textContent;
//pre.textContent = Record.events;
pre.style.marginTop = "18px";
pre.style.maxHeight = "60vh";
pre.style.overflow = "auto";
div.appendChild(pre);
const copyBtn = document.createElement("button");
copyBtn.textContent = "Copy";
copyBtn.title = "Copy generated events to clipboard";
copyBtn.setAttribute("aria-label", "Copy generated events to clipboard");
copyBtn.style.position = "absolute";
copyBtn.style.top = "4px";
copyBtn.style.left = "4px";
copyBtn.style.border = "none";
copyBtn.style.background = "transparent";
copyBtn.style.fontSize = "12px";
copyBtn.style.lineHeight = "1";
copyBtn.style.cursor = "pointer";
copyBtn.style.padding = "4px 8px";
copyBtn.addEventListener("click", async () => {
const text = pre.textContent ?? "";
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
}
const orig = copyBtn.textContent;
copyBtn.textContent = "Copied";
setTimeout(() => (copyBtn.textContent = orig), 1000);
} catch {}
});
div.appendChild(copyBtn);
const closeBtn = document.createElement("button");
closeBtn.textContent = "×";
closeBtn.title = "Close";
closeBtn.style.position = "absolute";
closeBtn.style.top = "4px";
closeBtn.style.right = "4px";
closeBtn.style.border = "none";
closeBtn.style.background = "transparent";
closeBtn.style.fontSize = "18px";
closeBtn.style.lineHeight = "1";
closeBtn.style.cursor = "pointer";
closeBtn.addEventListener("click", () => {
// remove the dialog from DOM
if (div.parentNode) {
div.parentNode.removeChild(div);
}
});
div.appendChild(closeBtn);
document.body.appendChild(div);
}
private static onMouseMove(event: MouseEvent) {
if (
event.clientX < 0 ||
event.clientX > window.innerWidth ||
event.clientY < 0 ||
event.clientY > window.innerHeight
) {
return;
}
const now = event.timeStamp || performance.now();
const delay = now - Record.timestamp;
Record.timestamp = now;
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
Record.events += ` await page.mouse.move(${event.clientX}, ${event.clientY});\n`;
}
private static onMouseDown(event: MouseEvent) {
const now = event.timeStamp || performance.now();
const delay = now - Record.timestamp;
Record.timestamp = now;
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
const button =
event.button === 0 ? "left" : event.button === 1 ? "middle" : "right";
Record.events += ` await page.mouse.down({ button: "${button}" });\n`;
}
private static onMouseUp(event: MouseEvent) {
const now = event.timeStamp || performance.now();
const delay = now - Record.timestamp;
Record.timestamp = now;
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
const button =
event.button === 0 ? "left" : event.button === 1 ? "middle" : "right";
Record.events += ` await page.mouse.up({ button: "${button}" });\n`;
Record.events += " await expect(page).toHaveScreenshot({\n";
Record.events += " maxDiffPixels: 100,\n";
Record.events += " maxDiffPixelRatio: 0.01,\n";
Record.events += " });\n";
}
private static onKeyDown(event: KeyboardEvent) {
// Only record if the recording key is not pressed
if (event.key !== "F2") {
const now = event.timeStamp || performance.now();
const delay = now - Record.timestamp;
Record.timestamp = now;
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
Record.events += ` await page.keyboard.down("${event.key}");\n`;
}
}
private static onKeyUp(event: KeyboardEvent) {
// Only record if the recording key is not pressed
if (event.key !== "F2") {
const now = event.timeStamp || performance.now();
const delay = now - Record.timestamp;
Record.timestamp = now;
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
Record.events += ` await page.keyboard.up("${event.key}");\n`;
Record.events += " await expect(page).toHaveScreenshot({\n";
Record.events += " maxDiffPixels: 100,\n";
Record.events += " maxDiffPixelRatio: 0.01,\n";
Record.events += " });\n";
}
}
}
if (isDevEnv()) {
window.record = Record;
window.addEventListener("keyup", (event) => {
if (event.key === "F2") {
if (Record.isRecording) {
if (event.ctrlKey) {
console.info("Stopping Playwright recording");
Record.stop();
} else {
Record.restart();
}
} else {
console.info("Starting Playwright recording");
Record.start();
}
} else if (event.key === "Enter" && event.ctrlKey) {
Record.showGeneratedEvents();
}
});
}

View File

@@ -0,0 +1,716 @@
import { expect, test } from "@playwright/test";
test("17562123239901g67cqde", async ({ page }) => {
await page.addInitScript(() => {
Math.random = () => 0.42;
localStorage.getItem("i18nextLng");
localStorage.setItem("i18nextLng", "en");
localStorage.getItem("excalidraw-collab");
localStorage.setItem("excalidraw-collab", '{"username":""}');
localStorage.getItem("excalidraw-debug");
localStorage.setItem("excalidraw-debug", '{"enabled":true}');
localStorage.getItem("excalidraw-theme");
localStorage.setItem("excalidraw-theme", "dark");
localStorage.getItem("version-files");
localStorage.setItem("version-files", "1756212319038");
localStorage.getItem("version-dataState");
localStorage.setItem("version-dataState", "1756212319038");
localStorage.getItem("excalidraw-state");
localStorage.setItem(
"excalidraw-state",
'{"showWelcomeScreen":true,"theme":"dark","currentChartType":"bar","currentItemBackgroundColor":"#a5d8ff","currentItemEndArrowhead":"arrow","currentItemFillStyle":"solid","currentItemFontFamily":5,"currentItemFontSize":20,"currentItemOpacity":100,"currentItemRoughness":2,"currentItemStartArrowhead":null,"currentItemStrokeColor":"#1e1e1e","currentItemRoundness":"round","currentItemArrowType":"round","currentItemStrokeStyle":"solid","currentItemStrokeWidth":2,"currentItemTextAlign":"left","cursorButton":"up","editingGroupId":null,"activeTool":{"type":"arrow","customType":null,"locked":false,"fromSelection":false,"lastActiveTool":null},"penMode":false,"penDetected":false,"exportBackground":true,"exportScale":1,"exportEmbedScene":false,"exportWithDarkMode":false,"gridSize":20,"gridStep":5,"gridModeEnabled":false,"defaultSidebarDockedPreference":false,"lastPointerDownWith":"mouse","name":"Untitled-2025-07-28-1603","openMenu":null,"openSidebar":null,"previousSelectedElementIds":{},"scrolledOutside":false,"scrollX":688.1079394457738,"scrollY":349.585883261872,"selectedElementIds":{},"selectedGroupIds":{},"shouldCacheIgnoreZoom":false,"stats":{"open":true,"panels":3},"viewBackgroundColor":"#ffffff","zenModeEnabled":false,"zoom":{"value":1.331666},"selectedLinearElement":null,"objectsSnapModeEnabled":false,"lockedMultiSelections":{}}',
);
localStorage.getItem("excalidraw");
localStorage.setItem(
"excalidraw",
'[{"id":"hJuTw4QcwTsFtadNNnkLj","type":"rectangle","x":-100,"y":-100,"width":200,"height":200,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#a5d8ff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"index":"a1","roundness":{"type":3},"seed":43277494,"version":839,"versionNonce":1923298088,"isDeleted":false,"boundElements":[{"id":"0xSZCPMN8RzKiJOpvGaKB","type":"arrow"}],"updated":1756212298830,"link":null,"locked":false},{"id":"qXw5KqKvAjHRr5uwPi9B-","type":"rectangle","x":-523.1841597523046,"y":-129.52451989693097,"width":200,"height":200,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#a5d8ff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"index":"a2","roundness":{"type":3},"seed":392633439,"version":963,"versionNonce":210394408,"isDeleted":false,"boundElements":[{"id":"0xSZCPMN8RzKiJOpvGaKB","type":"arrow"}],"updated":1756212298830,"link":null,"locked":false}]',
);
});
await page.setViewportSize({ width: 1280, height: 1001 });
await page.goto("http://localhost:3000");
await page.waitForLoadState("load");
await page.waitForTimeout(3.599999999627471);
await page.mouse.move(425, 390);
await page.waitForTimeout(9.900000000372529);
await page.mouse.move(424, 390);
await page.waitForTimeout(51.09999999962747);
await page.mouse.move(423, 390);
await page.waitForTimeout(1);
await page.mouse.move(423, 392);
await page.waitForTimeout(1);
await page.mouse.move(422, 392);
await page.waitForTimeout(2);
await page.mouse.move(420, 393);
await page.waitForTimeout(1);
await page.mouse.move(420, 395);
await page.waitForTimeout(1);
await page.mouse.move(418, 395);
await page.waitForTimeout(6);
await page.mouse.move(417, 396);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(416, 396);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(416, 397);
await page.waitForTimeout(1);
await page.mouse.move(414, 397);
await page.waitForTimeout(1);
await page.mouse.move(414, 399);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(413, 399);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(413, 401);
await page.waitForTimeout(1);
await page.mouse.move(413, 402);
await page.waitForTimeout(3);
await page.mouse.move(411, 402);
await page.waitForTimeout(3);
await page.mouse.move(411, 403);
await page.waitForTimeout(32);
await page.mouse.move(412, 403);
await page.waitForTimeout(3);
await page.mouse.move(412, 403);
await page.waitForTimeout(4);
await page.mouse.move(412, 402);
await page.waitForTimeout(1);
await page.mouse.move(413, 402);
await page.waitForTimeout(0.900000000372529);
await page.mouse.move(413, 400);
await page.waitForTimeout(1.099999999627471);
await page.mouse.move(415, 400);
await page.waitForTimeout(7);
await page.mouse.move(416, 400);
await page.waitForTimeout(71);
await page.mouse.move(417, 400);
await page.waitForTimeout(450);
await page.mouse.move(417, 399);
await page.waitForTimeout(6);
await page.mouse.move(418, 399);
await page.waitForTimeout(6.1000000005587935);
await page.mouse.move(419, 399);
await page.mouse.down({ button: "left" });
await page.waitForTimeout(11.899999999441206);
await page.mouse.move(420, 399);
await page.waitForTimeout(69);
await page.mouse.move(421, 399);
await page.waitForTimeout(13);
await page.mouse.move(422, 399);
await page.waitForTimeout(7.1000000005587935);
await page.mouse.move(423, 399);
await page.waitForTimeout(5.3999999994412065);
await page.mouse.move(425, 399);
await page.waitForTimeout(0.5);
await page.mouse.move(427, 401);
await page.waitForTimeout(4);
await page.mouse.move(428, 401);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(428, 403);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(430, 403);
await page.waitForTimeout(1);
await page.mouse.move(432, 403);
await page.waitForTimeout(1);
await page.mouse.move(432, 404);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(433, 404);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(434, 404);
await page.waitForTimeout(1);
await page.mouse.move(436, 406);
await page.waitForTimeout(3);
await page.mouse.move(437, 406);
await page.waitForTimeout(2);
await page.mouse.move(438, 407);
await page.waitForTimeout(1);
await page.mouse.move(440, 407);
await page.waitForTimeout(1);
await page.mouse.move(442, 407);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(443, 409);
await page.waitForTimeout(1);
await page.mouse.move(445, 409);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(447, 411);
await page.waitForTimeout(1);
await page.mouse.move(451, 411);
await page.waitForTimeout(1);
await page.mouse.move(455, 411);
await page.waitForTimeout(1);
await page.mouse.move(457, 413);
await page.waitForTimeout(1);
await page.mouse.move(459, 413);
await page.waitForTimeout(1);
await page.mouse.move(461, 415);
await page.waitForTimeout(1);
await page.mouse.move(463, 417);
await page.waitForTimeout(1);
await page.mouse.move(467, 419);
await page.waitForTimeout(1);
await page.mouse.move(469, 419);
await page.waitForTimeout(1);
await page.mouse.move(471, 420);
await page.waitForTimeout(1);
await page.mouse.move(473, 424);
await page.waitForTimeout(1);
await page.mouse.move(477, 424);
await page.waitForTimeout(1);
await page.mouse.move(477, 426);
await page.waitForTimeout(1);
await page.mouse.move(478, 426);
await page.waitForTimeout(1);
await page.mouse.move(482, 428);
await page.waitForTimeout(1);
await page.mouse.move(484, 430);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(486, 430);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(488, 430);
await page.waitForTimeout(1);
await page.mouse.move(488, 432);
await page.waitForTimeout(1);
await page.mouse.move(489, 432);
await page.waitForTimeout(1);
await page.mouse.move(491, 432);
await page.waitForTimeout(1);
await page.mouse.move(493, 434);
await page.waitForTimeout(1);
await page.mouse.move(495, 434);
await page.waitForTimeout(1);
await page.mouse.move(499, 436);
await page.waitForTimeout(1);
await page.mouse.move(501, 436);
await page.waitForTimeout(1);
await page.mouse.move(502, 436);
await page.waitForTimeout(1);
await page.mouse.move(504, 438);
await page.waitForTimeout(1);
await page.mouse.move(506, 440);
await page.waitForTimeout(1);
await page.mouse.move(508, 440);
await page.waitForTimeout(1);
await page.mouse.move(510, 442);
await page.waitForTimeout(1);
await page.mouse.move(512, 444);
await page.waitForTimeout(1);
await page.mouse.move(514, 444);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(514, 445);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(515, 445);
await page.waitForTimeout(1);
await page.mouse.move(517, 445);
await page.waitForTimeout(1);
await page.mouse.move(517, 447);
await page.waitForTimeout(1);
await page.mouse.move(519, 448);
await page.waitForTimeout(1);
await page.mouse.move(521, 448);
await page.waitForTimeout(1);
await page.mouse.move(522, 450);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(524, 450);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(525, 450);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(527, 450);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(529, 450);
await page.waitForTimeout(1);
await page.mouse.move(530, 450);
await page.waitForTimeout(3);
await page.mouse.move(532, 450);
await page.waitForTimeout(2);
await page.mouse.move(533, 451);
await page.waitForTimeout(6);
await page.mouse.move(534, 451);
await page.waitForTimeout(3.1000000005587935);
await page.mouse.move(535, 451);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(535, 453);
await page.waitForTimeout(1);
await page.mouse.move(537, 453);
await page.waitForTimeout(1);
await page.mouse.move(538, 453);
await page.waitForTimeout(3);
await page.mouse.move(540, 453);
await page.waitForTimeout(1);
await page.mouse.move(540, 454);
await page.waitForTimeout(2);
await page.mouse.move(541, 454);
await page.waitForTimeout(2);
await page.mouse.move(542, 454);
await page.waitForTimeout(2);
await page.mouse.move(543, 454);
await page.waitForTimeout(3);
await page.mouse.move(544, 454);
await page.waitForTimeout(1);
await page.mouse.move(544, 455);
await page.waitForTimeout(1);
await page.mouse.move(546, 455);
await page.waitForTimeout(2);
await page.mouse.move(547, 455);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(548, 455);
await page.waitForTimeout(2);
await page.mouse.move(549, 455);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(551, 455);
await page.waitForTimeout(1);
await page.mouse.move(552, 455);
await page.waitForTimeout(1);
await page.mouse.move(554, 455);
await page.waitForTimeout(1);
await page.mouse.move(555, 455);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(557, 455);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(559, 457);
await page.waitForTimeout(2);
await page.mouse.move(560, 457);
await page.waitForTimeout(1);
await page.mouse.move(562, 459);
await page.waitForTimeout(1);
await page.mouse.move(564, 459);
await page.waitForTimeout(1);
await page.mouse.move(566, 459);
await page.waitForTimeout(1);
await page.mouse.move(567, 459);
await page.waitForTimeout(1);
await page.mouse.move(569, 459);
await page.waitForTimeout(1);
await page.mouse.move(570, 459);
await page.waitForTimeout(3);
await page.mouse.move(574, 460);
await page.waitForTimeout(3);
await page.mouse.move(575, 460);
await page.waitForTimeout(1);
await page.mouse.move(575, 462);
await page.waitForTimeout(1);
await page.mouse.move(577, 464);
await page.waitForTimeout(1);
await page.mouse.move(578, 464);
await page.waitForTimeout(1);
await page.mouse.move(580, 464);
await page.waitForTimeout(1);
await page.mouse.move(582, 465);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(584, 465);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(585, 465);
await page.waitForTimeout(3);
await page.mouse.move(586, 465);
await page.waitForTimeout(1);
await page.mouse.move(588, 467);
await page.waitForTimeout(1);
await page.mouse.move(590, 467);
await page.waitForTimeout(2);
await page.mouse.move(591, 467);
await page.waitForTimeout(3);
await page.mouse.move(591, 468);
await page.waitForTimeout(2);
await page.mouse.move(594, 468);
await page.waitForTimeout(1);
await page.mouse.move(595, 468);
await page.waitForTimeout(1);
await page.mouse.move(597, 468);
await page.waitForTimeout(2.7000000001862645);
await page.mouse.move(601, 470);
await page.waitForTimeout(1.400000000372529);
await page.mouse.move(602, 470);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(603, 470);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(605, 470);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(607, 470);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(608, 470);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(610, 470);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(612, 470);
await page.waitForTimeout(2.8999999994412065);
await page.mouse.move(613, 470);
await page.waitForTimeout(1.2000000001862645);
await page.mouse.move(613, 471);
await page.waitForTimeout(1.7999999998137355);
await page.mouse.move(614, 471);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(616, 473);
await page.waitForTimeout(1);
await page.mouse.move(618, 473);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(621, 473);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(623, 473);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(625, 473);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(626, 473);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(628, 473);
await page.waitForTimeout(2);
await page.mouse.move(629, 473);
await page.waitForTimeout(1);
await page.mouse.move(631, 473);
await page.waitForTimeout(1);
await page.mouse.move(634, 475);
await page.waitForTimeout(2);
await page.mouse.move(636, 475);
await page.waitForTimeout(1);
await page.mouse.move(638, 475);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(639, 475);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(640, 475);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(642, 475);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(644, 475);
await page.waitForTimeout(1);
await page.mouse.move(647, 475);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(649, 475);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(650, 475);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(652, 475);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(653, 475);
await page.waitForTimeout(1);
await page.mouse.move(655, 475);
await page.waitForTimeout(2);
await page.mouse.move(656, 476);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(658, 476);
await page.waitForTimeout(2);
await page.mouse.move(659, 476);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(660, 476);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(662, 476);
await page.waitForTimeout(2);
await page.mouse.move(663, 476);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(664, 476);
await page.waitForTimeout(1);
await page.mouse.move(666, 474);
await page.waitForTimeout(1);
await page.mouse.move(668, 474);
await page.waitForTimeout(1);
await page.mouse.move(670, 474);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(671, 474);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(673, 476);
await page.waitForTimeout(3);
await page.mouse.move(675, 476);
await page.waitForTimeout(1);
await page.mouse.move(678, 476);
await page.waitForTimeout(1);
await page.mouse.move(680, 476);
await page.waitForTimeout(1);
await page.mouse.move(682, 476);
await page.waitForTimeout(4);
await page.mouse.move(683, 476);
await page.waitForTimeout(1);
await page.mouse.move(684, 476);
await page.waitForTimeout(1);
await page.mouse.move(686, 476);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(690, 476);
await page.waitForTimeout(1);
await page.mouse.move(694, 478);
await page.waitForTimeout(1);
await page.mouse.move(696, 478);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(698, 478);
await page.waitForTimeout(1);
await page.mouse.move(699, 478);
await page.waitForTimeout(1);
await page.mouse.move(703, 478);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(705, 478);
await page.waitForTimeout(1);
await page.mouse.move(707, 478);
await page.waitForTimeout(2);
await page.mouse.move(708, 478);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(711, 478);
await page.waitForTimeout(1);
await page.mouse.move(713, 478);
await page.waitForTimeout(1);
await page.mouse.move(714, 478);
await page.waitForTimeout(1);
await page.mouse.move(716, 478);
await page.waitForTimeout(2);
await page.mouse.move(717, 478);
await page.waitForTimeout(1);
await page.mouse.move(719, 478);
await page.waitForTimeout(2);
await page.mouse.move(722, 478);
await page.waitForTimeout(1);
await page.mouse.move(724, 478);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(725, 478);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(726, 478);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(727, 478);
await page.waitForTimeout(3);
await page.mouse.move(733, 480);
await page.waitForTimeout(2.8999999994412065);
await page.mouse.move(734, 480);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(735, 480);
await page.waitForTimeout(1);
await page.mouse.move(737, 480);
await page.waitForTimeout(1);
await page.mouse.move(739, 480);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(740, 480);
await page.waitForTimeout(1);
await page.mouse.move(742, 480);
await page.waitForTimeout(1);
await page.mouse.move(744, 480);
await page.waitForTimeout(1);
await page.mouse.move(744, 482);
await page.waitForTimeout(1);
await page.mouse.move(745, 482);
await page.waitForTimeout(2);
await page.mouse.move(747, 482);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(748, 482);
await page.waitForTimeout(4);
await page.mouse.move(749, 482);
await page.waitForTimeout(3);
await page.mouse.move(751, 482);
await page.waitForTimeout(1);
await page.mouse.move(753, 482);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(755, 482);
await page.waitForTimeout(1);
await page.mouse.move(758, 482);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(760, 482);
await page.waitForTimeout(6);
await page.mouse.move(761, 483);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(763, 484);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(765, 484);
await page.waitForTimeout(2.8999999994412065);
await page.mouse.move(766, 485);
await page.waitForTimeout(2);
await page.mouse.move(767, 485);
await page.waitForTimeout(11);
await page.mouse.move(768, 485);
await page.waitForTimeout(12.100000000558794);
await page.mouse.move(769, 485);
await page.waitForTimeout(51.89999999944121);
await page.mouse.move(770, 485);
await page.waitForTimeout(9);
await page.mouse.move(771, 485);
await page.waitForTimeout(3.1000000005587935);
await page.mouse.move(772, 485);
await page.waitForTimeout(5);
await page.mouse.move(773, 485);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(774, 485);
await page.waitForTimeout(2);
await page.mouse.move(776, 487);
await page.waitForTimeout(4);
await page.mouse.move(777, 487);
await page.waitForTimeout(4.1000000005587935);
await page.mouse.move(778, 487);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(779, 487);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(780, 488);
await page.waitForTimeout(2.8999999994412065);
await page.mouse.move(781, 488);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(782, 488);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(784, 488);
await page.waitForTimeout(3);
await page.mouse.move(785, 488);
await page.waitForTimeout(1);
await page.mouse.move(786, 488);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(788, 488);
await page.waitForTimeout(8.899999999441206);
await page.mouse.move(789, 488);
await page.waitForTimeout(3);
await page.mouse.move(790, 488);
await page.waitForTimeout(5);
await page.mouse.move(791, 488);
await page.waitForTimeout(4);
await page.mouse.move(792, 488);
await page.waitForTimeout(6.1000000005587935);
await page.mouse.move(793, 488);
await page.waitForTimeout(3.8999999994412065);
await page.mouse.move(793, 489);
await page.waitForTimeout(6);
await page.mouse.move(794, 489);
await page.waitForTimeout(4.1000000005587935);
await page.mouse.move(795, 489);
await page.waitForTimeout(7.599999999627471);
await page.mouse.move(796, 489);
await page.waitForTimeout(3.400000000372529);
await page.mouse.move(797, 489);
await page.waitForTimeout(3);
await page.mouse.move(798, 489);
await page.waitForTimeout(3);
await page.mouse.move(799, 489);
await page.waitForTimeout(2);
await page.mouse.move(800, 489);
await page.waitForTimeout(3);
await page.mouse.move(801, 489);
await page.waitForTimeout(2);
await page.mouse.move(802, 489);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(803, 489);
await page.waitForTimeout(3);
await page.mouse.move(804, 489);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(805, 489);
await page.waitForTimeout(5.8999999994412065);
await page.mouse.move(806, 490);
await page.waitForTimeout(3);
await page.mouse.move(807, 490);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(809, 490);
await page.waitForTimeout(1);
await page.mouse.move(810, 490);
await page.waitForTimeout(1);
await page.mouse.move(812, 490);
await page.waitForTimeout(1.8999999994412065);
await page.mouse.move(813, 490);
await page.waitForTimeout(1);
await page.mouse.move(815, 490);
await page.waitForTimeout(2);
await page.mouse.move(816, 492);
await page.waitForTimeout(1);
await page.mouse.move(818, 492);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(819, 492);
await page.waitForTimeout(1);
await page.mouse.move(820, 492);
await page.waitForTimeout(1);
await page.mouse.move(822, 492);
await page.waitForTimeout(2);
await page.mouse.move(823, 492);
await page.waitForTimeout(3);
await page.mouse.move(825, 492);
await page.waitForTimeout(2.8999999994412065);
await page.mouse.move(826, 492);
await page.waitForTimeout(1.1000000005587935);
await page.mouse.move(827, 492);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(828, 492);
await page.waitForTimeout(3);
await page.mouse.move(830, 492);
await page.waitForTimeout(3);
await page.mouse.move(831, 492);
await page.waitForTimeout(2.1000000005587935);
await page.mouse.move(832, 492);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(833, 493);
await page.waitForTimeout(6.1000000005587935);
await page.mouse.move(835, 493);
await page.waitForTimeout(1);
await page.mouse.move(836, 493);
await page.waitForTimeout(1);
await page.mouse.move(838, 493);
await page.waitForTimeout(0.8999999994412065);
await page.mouse.move(839, 493);
await page.waitForTimeout(4.1000000005587935);
await page.mouse.move(841, 493);
await page.waitForTimeout(2);
await page.mouse.move(842, 493);
await page.waitForTimeout(3.8999999994412065);
await page.mouse.move(843, 493);
await page.waitForTimeout(2);
await page.mouse.move(843, 492);
await page.waitForTimeout(4.1000000005587935);
await page.mouse.move(844, 492);
await page.waitForTimeout(4);
await page.mouse.move(844, 493);
await page.waitForTimeout(1);
await page.mouse.move(845, 493);
await page.waitForTimeout(3);
await page.mouse.move(846, 493);
await page.waitForTimeout(4);
await page.mouse.move(847, 494);
await page.waitForTimeout(24.899999999441206);
await page.mouse.move(847, 495);
await page.waitForTimeout(29.100000000558794);
await page.mouse.move(848, 495);
await page.waitForTimeout(297.8999999994412);
await page.mouse.move(848, 495);
await page.waitForTimeout(5.7000000001862645);
await page.mouse.up({ button: "left" });
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
maxDiffPixelRatio: 0.01,
});
await page.waitForTimeout(11.299999999813735);
await page.mouse.move(847, 495);
await page.waitForTimeout(199.1000000005588);
await page.mouse.move(847, 495);
await page.waitForTimeout(249.29999999981374);
await page.mouse.move(846, 495);
await page.waitForTimeout(5.7000000001862645);
await page.mouse.move(846, 496);
await page.waitForTimeout(6);
await page.mouse.move(845, 496);
await page.waitForTimeout(1);
await page.mouse.move(845, 497);
await page.waitForTimeout(9);
await page.mouse.move(845, 498);
await page.waitForTimeout(3.8999999994412065);
await page.mouse.move(844, 498);
await page.waitForTimeout(1);
await page.mouse.move(844, 500);
await page.waitForTimeout(8);
await page.mouse.move(844, 501);
await page.waitForTimeout(3.1000000005587935);
await page.mouse.move(843, 501);
await page.waitForTimeout(137);
await page.mouse.move(844, 501);
await page.waitForTimeout(68.09999999962747);
await page.mouse.move(845, 501);
await page.waitForTimeout(7.7999999998137355);
await page.mouse.move(845, 500);
await page.waitForTimeout(1);
await page.mouse.move(846, 500);
await page.waitForTimeout(4);
await page.mouse.move(847, 500);
await page.waitForTimeout(2);
await page.mouse.move(848, 500);
await page.waitForTimeout(3.1000000005587935);
await page.mouse.move(849, 500);
await page.waitForTimeout(3);
await page.mouse.move(850, 500);
await page.waitForTimeout(5);
await page.mouse.move(851, 500);
await page.waitForTimeout(2);
await page.mouse.move(852, 499);
await page.waitForTimeout(3);
await page.mouse.move(854, 499);
await page.waitForTimeout(5);
await page.mouse.move(855, 499);
await page.waitForTimeout(3);
await page.mouse.move(856, 499);
await page.waitForTimeout(6);
await page.mouse.move(857, 499);
await page.waitForTimeout(12);
await page.mouse.move(858, 499);
await page.waitForTimeout(24);
await page.mouse.move(858, 498);
await page.waitForTimeout(4);
await page.mouse.move(858, 498);
await page.waitForTimeout(59.200000000186265);
await page.keyboard.down("Control");
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -11,9 +11,11 @@
"@babel/preset-env": "7.26.9",
"@excalidraw/eslint-config": "1.0.3",
"@excalidraw/prettier-config": "1.0.2",
"@playwright/test": "1.55.0",
"@types/chai": "4.3.0",
"@types/jest": "27.4.0",
"@types/lodash.throttle": "4.1.7",
"@types/node": "24.3.0",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/socket.io-client": "3.0.0",
@@ -62,6 +64,7 @@
"build:preview": "yarn --cwd ./excalidraw-app build:preview",
"start": "yarn --cwd ./excalidraw-app start",
"start:production": "yarn --cwd ./excalidraw-app start:production",
"start:test": "yarn --cwd ./excalidraw-app start:test",
"start:example": "yarn build:packages && yarn --cwd ./examples/with-script-in-browser start",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
"test:app": "vitest",

View File

@@ -1111,16 +1111,16 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
inserted,
}: Delta<ElementPartial>) =>
!!(
deleted.version &&
inserted.version &&
// versions are required integers
(
Number.isInteger(deleted.version) &&
Number.isInteger(inserted.version) &&
// versions should be positive, zero included
deleted.version! >= 0 &&
inserted.version! >= 0 &&
deleted.version >= 0 &&
inserted.version >= 0 &&
// versions should never be the same
deleted.version !== inserted.version
)
);
private static satisfiesUniqueInvariants = (
@@ -1191,10 +1191,9 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
ElementsDelta.stripIrrelevantProps,
);
// ignore updates which would "delete" already deleted element
if (!prevElement.isDeleted) {
removed[prevElement.id] = delta;
} else {
updated[prevElement.id] = delta;
}
}
}
@@ -1222,8 +1221,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
// ignore updates which would "delete" already deleted element
if (!nextElement.isDeleted) {
added[nextElement.id] = delta;
} else {
updated[nextElement.id] = delta;
}
continue;
@@ -1253,9 +1250,17 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
continue;
}
const strippedDeleted = ElementsDelta.stripVersionProps(delta.deleted);
const strippedInserted = ElementsDelta.stripVersionProps(
delta.inserted,
);
// making sure there are at least some changes and only changed version & versionNonce does not count!
if (Delta.isInnerDifferent(strippedDeleted, strippedInserted, true)) {
updated[nextElement.id] = delta;
}
}
}
return ElementsDelta.create(added, removed, updated);
}
@@ -1367,8 +1372,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
latestDelta = delta;
}
const strippedDeleted = ElementsDelta.stripVersionProps(
latestDelta.deleted,
);
const strippedInserted = ElementsDelta.stripVersionProps(
latestDelta.inserted,
);
// it might happen that after applying latest changes the delta itself does not contain any changes
if (Delta.isInnerDifferent(latestDelta.deleted, latestDelta.inserted)) {
if (Delta.isInnerDifferent(strippedDeleted, strippedInserted)) {
modifiedDeltas[id] = latestDelta;
}
}
@@ -2063,4 +2075,12 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
return strippedPartial;
}
private static stripVersionProps(
partial: Partial<OrderedExcalidrawElement>,
): ElementPartial {
const { version, versionNonce, ...strippedPartial } = partial;
return strippedPartial;
}
}

View File

@@ -21,6 +21,7 @@ import {
assertNever,
COLOR_PALETTE,
LINE_POLYGON_POINT_MERGE_DISTANCE,
isTestEnv,
} from "@excalidraw/common";
import { RoughGenerator } from "roughjs/bin/generator";
@@ -182,7 +183,7 @@ export const generateRoughOptions = (
continuousPath = false,
): Options => {
const options: Options = {
seed: element.seed,
seed: isTestEnv() ? 1 : element.seed,
strokeLineDash:
element.strokeStyle === "dashed"
? getDashArrayDashed(element.strokeWidth)

View File

@@ -8,7 +8,7 @@ import { AppStateDelta, Delta, ElementsDelta } from "../src/delta";
describe("ElementsDelta", () => {
describe("elements delta calculation", () => {
it("should not throw when element gets removed but was already deleted", () => {
it("should not create removed delta when element gets removed but was already deleted", () => {
const element = API.createElement({
type: "rectangle",
x: 100,
@@ -19,12 +19,12 @@ describe("ElementsDelta", () => {
const prevElements = new Map([[element.id, element]]);
const nextElements = new Map();
expect(() =>
ElementsDelta.calculate(prevElements, nextElements),
).not.toThrow();
const delta = ElementsDelta.calculate(prevElements, nextElements);
expect(delta.isEmpty()).toBeTruthy();
});
it("should not throw when adding element as already deleted", () => {
it("should not create added delta when adding element as already deleted", () => {
const element = API.createElement({
type: "rectangle",
x: 100,
@@ -35,12 +35,12 @@ describe("ElementsDelta", () => {
const prevElements = new Map();
const nextElements = new Map([[element.id, element]]);
expect(() =>
ElementsDelta.calculate(prevElements, nextElements),
).not.toThrow();
const delta = ElementsDelta.calculate(prevElements, nextElements);
expect(delta.isEmpty()).toBeTruthy();
});
it("should create updated delta even when there is only version and versionNonce change", () => {
it("should not create updated delta when there is only version and versionNonce change", () => {
const baseElement = API.createElement({
type: "rectangle",
x: 100,
@@ -65,24 +65,7 @@ describe("ElementsDelta", () => {
nextElements as SceneElementsMap,
);
expect(delta).toEqual(
ElementsDelta.create(
{},
{},
{
[baseElement.id]: Delta.create(
{
version: baseElement.version,
versionNonce: baseElement.versionNonce,
},
{
version: baseElement.version + 1,
versionNonce: baseElement.versionNonce + 1,
},
),
},
),
);
expect(delta.isEmpty()).toBeTruthy();
});
});

View File

@@ -83,7 +83,7 @@
"@excalidraw/element": "0.18.0",
"@excalidraw/math": "0.18.0",
"@excalidraw/laser-pointer": "1.3.1",
"@excalidraw/mermaid-to-excalidraw": "1.1.3",
"@excalidraw/mermaid-to-excalidraw": "1.1.2",
"@excalidraw/random-username": "1.1.0",
"@radix-ui/react-popover": "1.1.6",
"@radix-ui/react-tabs": "1.1.3",

View File

@@ -282,14 +282,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"added": {},
"removed": {},
"updated": {
"id0": {
"deleted": {
"version": 12,
},
"inserted": {
"version": 11,
},
},
"id1": {
"deleted": {
"boundElements": [],
@@ -404,14 +396,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"version": 12,
},
},
"id15": {
"deleted": {
"version": 10,
},
"inserted": {
"version": 9,
},
},
"id4": {
"deleted": {
"height": "99.19972",
@@ -853,14 +837,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"added": {},
"removed": {},
"updated": {
"id0": {
"deleted": {
"version": 13,
},
"inserted": {
"version": 12,
},
},
"id1": {
"deleted": {
"boundElements": [],
@@ -2656,7 +2632,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"height": 100,
"id": "id0",
"index": "a0",
"isDeleted": false,
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
@@ -2705,7 +2681,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"textAlign": "left",
"type": "text",
"updated": 1,
"version": 8,
"version": 6,
"verticalAlign": "top",
"width": 100,
"x": 15,
@@ -2719,7 +2695,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id0",
"containerId": null,
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 5,
@@ -2766,12 +2742,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
},
},
"elements": {
"added": {},
"removed": {},
"updated": {
"added": {
"id0": {
"deleted": {
"isDeleted": false,
"isDeleted": true,
"version": 9,
},
"inserted": {
@@ -2800,21 +2774,16 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"y": 10,
},
},
"id1": {
"deleted": {
"containerId": null,
"version": 8,
},
"inserted": {
"containerId": null,
"version": 7,
},
},
"removed": {},
"updated": {
"id5": {
"deleted": {
"containerId": null,
"version": 7,
},
"inserted": {
"containerId": "id0",
"version": 6,
},
},
@@ -3127,14 +3096,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"version": 8,
},
},
"id5": {
"deleted": {
"version": 7,
},
"inserted": {
"version": 6,
},
},
},
},
"id": "id9",
@@ -4684,15 +4645,15 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"id1": {
"deleted": {
"angle": 0,
"version": 8,
"version": 4,
"x": 15,
"y": 15,
},
"inserted": {
"angle": 0,
"version": 7,
"x": 15,
"y": 15,
"angle": 90,
"version": 3,
"x": 205,
"y": 205,
},
},
},
@@ -5671,12 +5632,12 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"updated": {
"id1": {
"deleted": {
"frameId": null,
"version": 9,
"frameId": "id0",
"version": 5,
},
"inserted": {
"frameId": null,
"version": 8,
"version": 6,
},
},
},
@@ -5823,7 +5784,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 6,
"version": 5,
"width": 100,
"x": 0,
"y": 0,
@@ -5855,7 +5816,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 5,
"version": 4,
"width": 100,
"x": 100,
"y": 100,
@@ -5891,74 +5852,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {
"id0": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
"A",
],
"height": 100,
"index": "a0",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 5,
"width": 100,
"x": 0,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 4,
},
},
"id1": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [
"A",
],
"height": 100,
"index": "a1",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 5,
"width": 100,
"x": 100,
"y": 100,
},
"inserted": {
"isDeleted": true,
"version": 4,
},
},
},
"updated": {},
},
"id": "id13",
},
@@ -6178,7 +6072,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 9,
"version": 8,
"width": 10,
"x": 20,
"y": 0,
@@ -6208,7 +6102,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 9,
"version": 8,
"width": 10,
"x": 50,
"y": 50,
@@ -6293,39 +6187,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {
"id3": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"index": "a1",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 8,
"width": 10,
"x": 20,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 7,
},
},
},
"updated": {},
},
"id": "id18",
},
@@ -6343,11 +6205,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"id3": {
"deleted": {
"backgroundColor": "#ffc9c9",
"version": 9,
"version": 8,
},
"inserted": {
"backgroundColor": "transparent",
"version": 8,
"version": 7,
},
},
},
@@ -6372,39 +6234,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {
"id8": {
"deleted": {
"angle": 0,
"backgroundColor": "#ffc9c9",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"index": "a2",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 8,
"width": 10,
"x": 30,
"y": 30,
},
"inserted": {
"isDeleted": true,
"version": 7,
},
},
},
"updated": {},
},
"id": "id20",
},
@@ -6421,12 +6251,12 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"updated": {
"id8": {
"deleted": {
"version": 9,
"version": 8,
"x": 50,
"y": 50,
},
"inserted": {
"version": 8,
"version": 7,
"x": 30,
"y": 30,
},
@@ -7274,7 +7104,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "arrow",
"updated": 1,
"version": 9,
"version": 8,
"width": 10,
"x": 0,
"y": 0,
@@ -7305,60 +7135,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {
"id0": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"index": "a0",
"isDeleted": true,
"lastCommittedPoint": [
10,
10,
],
"link": null,
"locked": false,
"opacity": 100,
"points": [
[
0,
0,
],
[
10,
10,
],
],
"roughness": 1,
"roundness": {
"type": 2,
},
"startArrowhead": null,
"startBinding": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "arrow",
"version": 9,
"width": 10,
"x": 0,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 8,
},
},
},
"updated": {},
},
"id": "id13",
},
@@ -7567,7 +7344,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 9,
"version": 8,
"width": 10,
"x": 10,
"y": 0,
@@ -7598,39 +7375,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"elements": {
"added": {},
"removed": {},
"updated": {
"id0": {
"deleted": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 10,
"index": "a0",
"isDeleted": true,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": null,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 8,
"width": 10,
"x": 10,
"y": 0,
},
"inserted": {
"isDeleted": true,
"version": 7,
},
},
},
"updated": {},
},
"id": "id7",
},
@@ -7648,11 +7393,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"id0": {
"deleted": {
"backgroundColor": "#ffec99",
"version": 9,
"version": 8,
},
"inserted": {
"backgroundColor": "transparent",
"version": 8,
"version": 7,
},
},
},
@@ -10581,7 +10326,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 9,
"version": 8,
"width": 10,
"x": 10,
"y": 0,
@@ -10664,18 +10409,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"elements": {
"added": {},
"removed": {},
"updated": {
"id0": {
"deleted": {
"isDeleted": false,
"version": 9,
},
"inserted": {
"isDeleted": false,
"version": 8,
},
},
},
"updated": {},
},
"id": "id8",
},
@@ -16041,14 +15775,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 5,
},
},
"id1": {
"deleted": {
"version": 5,
},
"inserted": {
"version": 4,
},
},
"id2": {
"deleted": {
"boundElements": [
@@ -17010,14 +16736,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 5,
},
},
"id1": {
"deleted": {
"version": 6,
},
"inserted": {
"version": 5,
},
},
"id2": {
"deleted": {
"boundElements": [
@@ -17643,14 +17361,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 9,
},
},
"id1": {
"deleted": {
"version": 10,
},
"inserted": {
"version": 9,
},
},
"id2": {
"deleted": {
"boundElements": [
@@ -18012,14 +17722,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"version": 7,
},
},
"id2": {
"deleted": {
"version": 4,
},
"inserted": {
"version": 3,
},
},
},
},
"id": "id21",

View File

@@ -2216,16 +2216,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] undo
},
},
},
"updated": {
"id0": {
"deleted": {
"version": 5,
},
"inserted": {
"version": 3,
},
},
},
"updated": {},
},
"id": "id6",
},
@@ -10901,32 +10892,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s
},
},
},
"updated": {
"id0": {
"deleted": {
"version": 6,
},
"inserted": {
"version": 4,
},
},
"id3": {
"deleted": {
"version": 6,
},
"inserted": {
"version": 4,
},
},
"id6": {
"deleted": {
"version": 6,
},
"inserted": {
"version": 4,
},
},
},
"updated": {},
},
"id": "id21",
},

View File

@@ -4055,7 +4055,7 @@ describe("history", () => {
expect.objectContaining({
id: container.id,
boundElements: [{ id: remoteText.id, type: "text" }],
isDeleted: false,
isDeleted: true,
}),
expect.objectContaining({
id: text.id,
@@ -4064,7 +4064,8 @@ describe("history", () => {
}),
expect.objectContaining({
id: remoteText.id,
containerId: container.id,
// unbound
containerId: null,
isDeleted: false,
}),
]);

89
playwright.config.ts Normal file
View File

@@ -0,0 +1,89 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./excalidraw-app/tests/regression",
snapshotPathTemplate:
"{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 1,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
headless: true,
},
timeout: 1200000,
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
launchOptions: {
args: ["--disable-font-subpixel-positioning", "--disable-gpu"],
},
},
},
// {
// name: "firefox",
// use: { ...devices["Desktop Firefox"] },
// },
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "yarn start --no-open",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});

View File

@@ -1452,15 +1452,14 @@
resolved "https://registry.yarnpkg.com/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"
integrity sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==
"@excalidraw/mermaid-to-excalidraw@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-1.1.3.tgz#3204642c99f3d49c2ad41108217a5d493ef7fd09"
integrity sha512-/50GUWlGotc+FCMX7nM1P1kWm9vNd3fuq38v7upBp9IHqlw6Zmfyj79eG/0vz1heifuYrSW9yzzv0q9jVALzxg==
"@excalidraw/mermaid-to-excalidraw@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-1.1.2.tgz#74d9507971976a7d3d960a1b2e8fb49a9f1f0d22"
integrity sha512-hAFv/TTIsOdoy0dL5v+oBd297SQ+Z88gZ5u99fCIFuEMHfQuPgLhU/ztKhFSTs7fISwVo6fizny/5oQRR3d4tQ==
dependencies:
"@excalidraw/markdown-to-text" "0.1.2"
mermaid "10.9.4"
mermaid "10.9.3"
nanoid "4.0.2"
react-split "^2.0.14"
"@excalidraw/prettier-config@1.0.2":
version "1.0.2"
@@ -2163,6 +2162,13 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@playwright/test@1.55.0":
version "1.55.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.55.0.tgz#080fa6d9ee6d749ff523b1c18259572d0268b963"
integrity sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==
dependencies:
playwright "1.55.0"
"@polka/url@^1.0.0-next.24":
version "1.0.0-next.28"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73"
@@ -2946,6 +2952,13 @@
dependencies:
undici-types "~6.20.0"
"@types/node@24.3.0":
version "24.3.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec"
integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
dependencies:
undici-types "~7.10.0"
"@types/node@>=12.12.47", "@types/node@>=13.7.0":
version "22.13.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.8.tgz#57e2450295b33a6518d6fd4f65f47236d3e41d8d"
@@ -5827,6 +5840,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@@ -7058,10 +7076,10 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
mermaid@10.9.4:
version "10.9.4"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.9.4.tgz#985fd4b6d73ae795b87f0b32f620a56d3d6bf1f8"
integrity sha512-VIG2B0R9ydvkS+wShA8sXqkzfpYglM2Qwj7VyUeqzNVqSGPoP/tcaUr3ub4ESykv8eqQJn3p99bHNvYdg3gCHQ==
mermaid@10.9.3:
version "10.9.3"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.9.3.tgz#90bc6f15c33dbe5d9507fed31592cc0d88fee9f7"
integrity sha512-V80X1isSEvAewIL3xhmz/rVmc27CVljcsbWxkxlWJWY/1kQa4XOABqpDl2qQLGKzpKm6WbTfUEKImBlUfFYArw==
dependencies:
"@braintree/sanitize-url" "^6.0.1"
"@types/d3-scale" "^4.0.3"
@@ -7839,6 +7857,20 @@ pkg-dir@4.2.0:
dependencies:
find-up "^4.0.0"
playwright-core@1.55.0:
version "1.55.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.55.0.tgz#ec8a9f8ef118afb3e86e0f46f1393e3bea32adf4"
integrity sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==
playwright@1.55.0:
version "1.55.0"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.55.0.tgz#7aca7ac3ffd9e083a8ad8b2514d6f9ba401cc78b"
integrity sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==
dependencies:
playwright-core "1.55.0"
optionalDependencies:
fsevents "2.3.2"
png-chunk-text@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/png-chunk-text/-/png-chunk-text-1.0.0.tgz#1c6006d8e34ba471d38e1c9c54b3f53e1085e18f"
@@ -7964,7 +7996,7 @@ progress@2.0.3, progress@^2.0.0:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
prop-types@^15.5.7, prop-types@^15.8.1:
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -8109,14 +8141,6 @@ react-remove-scroll@^2.6.3:
use-callback-ref "^1.3.3"
use-sidecar "^1.1.3"
react-split@^2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/react-split/-/react-split-2.0.14.tgz#ef198259bf43264d605f792fb3384f15f5b34432"
integrity sha512-bKWydgMgaKTg/2JGQnaJPg51T6dmumTWZppFgEbbY0Fbme0F5TuatAScCLaqommbGQQf/ZT1zaejuPDriscISA==
dependencies:
prop-types "^15.5.7"
split.js "^1.6.0"
react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
@@ -8756,11 +8780,6 @@ sourcemap-codec@^1.4.8:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
split.js@^1.6.0:
version "1.6.5"
resolved "https://registry.yarnpkg.com/split.js/-/split.js-1.6.5.tgz#f7f61da1044c9984cb42947df4de4fadb5a3f300"
integrity sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -9342,6 +9361,11 @@ undici-types@~6.20.0:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
undici-types@~7.10.0:
version "7.10.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350"
integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2"