penpot/frontend/test/frontend_tests/logic/update_position_test.cljs
Andrey Antukh 531e276a69 🐛 Fix workspace crash on rapid sidebar measures input changes (#10793)
The sidebar measures panel numeric inputs (X, Y, width, height,
rotation) emitted one full apply-modifiers commit per DOM event with
no throttle: every arrow key-repeat, wheel tick and scrub pointermove
became update-positions / update-dimensions / increase-rotation. A
sustained gesture starved the React renderer and crashed the
workspace with error #185 (Maximum update depth exceeded).

Coalesce those bursts at the data layer (potok), following the
update-position-data debounce pattern in texts.cljs:

- update-positions is now burst-coalesced in place (its only caller
  is the measures panel); new update-dimensions-coalesced and
  increase-rotation-coalesced variants are used by the measures
  panel, while the immediate events keep serving plugins, variants
  and token application (including the delta? rotation path).
- The first event of a burst commits immediately (leading edge, so
  single edits stay synchronous); further ticks commit at most once
  per 50 ms (throttle); a trailing debounced flush guarantees the
  exact final value lands. All payloads are absolute values, so
  keeping the latest queued value per shape/attribute is lossless.
- Pending payloads are drained atomically and stale shape ids
  (deleted mid-burst) are skipped. The drain stream lives until the
  workspace is finalized, so bursts reuse a single subscription.
- Fewer commits per burst also means fewer undo entries; scrub drags
  still produce a single entry via the input's outer transaction.

Tests: new frontend-tests.logic.sidebar-transform-coalescing-test (8
tests, legacy SVG and WASM renderer branches) guards the invariant
that a 20-event burst commits the exact final value in a handful of
commits. The previously unregistered update-position-test is wired
into the runner with WASM mock fixtures (it fails in full-suite
context without them due to a pre-existing global mock-state issue).

AI-assisted-by: kimi-k3
2026-08-03 17:50:09 +02:00

43 lines
1.7 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns frontend-tests.logic.update-position-test
(:require
[app.common.geom.rect :as grc]
[app.common.test-helpers.compositions :as ctho]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.shapes :as cths]
[app.main.data.workspace :as dw]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]
[frontend-tests.helpers.wasm :as thw]))
(t/use-fixtures :each
{:before (fn [] (thw/setup-wasm-mocks!))
:after (fn [] (thw/teardown-wasm-mocks!))})
(t/deftest test-update-positions-multiple-ids
(t/async
done
(let [file (-> (cthf/sample-file :file1)
(ctho/add-rect :rect1 :x 10 :y 20 :width 10 :height 10)
(ctho/add-rect :rect2 :x 30 :y 40 :width 10 :height 10))
store (ths/setup-store file)
rect1 (cths/get-shape file :rect1)
rect2 (cths/get-shape file :rect2)
ids [(:id rect1) (:id rect2)]
events [(dw/update-positions ids {:x 123.45})]]
(ths/run-store
store done events
(fn [new-state]
(let [file' (ths/get-file-from-state new-state)
rect1' (cths/get-shape file' :rect1)
rect2' (cths/get-shape file' :rect2)
x1 (-> rect1' :points grc/points->rect :x)
x2 (-> rect2' :points grc/points->rect :x)]
(t/is (= 123.45 x1))
(t/is (= 123.45 x2))))))))