mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 10:56:20 +00:00
🐛 Add plugin-api-test-suite tests and fix some synchronization problems
This commit is contained in:
parent
3fddf2c5bc
commit
91d3c3db39
@ -99,26 +99,33 @@
|
|||||||
;; Never call this directly but through the data-event `:layout/update`
|
;; Never call this directly but through the data-event `:layout/update`
|
||||||
;; Otherwise a lot of cycle dependencies could be generated
|
;; Otherwise a lot of cycle dependencies could be generated
|
||||||
(defn- update-layout-positions
|
(defn- update-layout-positions
|
||||||
[{:keys [page-id ids undo-group]}]
|
[{:keys [page-id ids undo-group drain-ids]}]
|
||||||
(ptk/reify ::update-layout-positions
|
(ptk/reify ::update-layout-positions
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [page-id (or page-id (:current-page-id state))
|
(let [page-id (or page-id (:current-page-id state))
|
||||||
objects (dsh/lookup-page-objects state page-id)
|
objects (dsh/lookup-page-objects state page-id)
|
||||||
ids (->> ids (remove uuid/zero?) (filter #(contains? objects %)))]
|
ids (->> ids (remove uuid/zero?) (filter #(contains? objects %)))
|
||||||
(if (d/not-empty? ids)
|
|
||||||
(let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))]
|
update-positions-stream
|
||||||
(if (features/active-feature? state "render-wasm/v1")
|
(if (d/not-empty? ids)
|
||||||
(rx/of (dwm/apply-wasm-modifiers modif-tree
|
(let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))]
|
||||||
|
(if (features/active-feature? state "render-wasm/v1")
|
||||||
|
(rx/of (dwm/apply-wasm-modifiers modif-tree
|
||||||
|
:stack-undo? true
|
||||||
|
:undo-group undo-group
|
||||||
|
:ignore-touched true))
|
||||||
|
(rx/of (dwm/apply-modifiers {:page-id page-id
|
||||||
|
:modifiers modif-tree
|
||||||
:stack-undo? true
|
:stack-undo? true
|
||||||
:undo-group undo-group
|
:ignore-touched true
|
||||||
:ignore-touched true))
|
:undo-group undo-group}))))
|
||||||
(rx/of (dwm/apply-modifiers {:page-id page-id
|
(rx/empty))]
|
||||||
:modifiers modif-tree
|
|
||||||
:stack-undo? true
|
(cond->> update-positions-stream
|
||||||
:ignore-touched true
|
;; Drain the pending-reflow marks only after the apply events above are processed
|
||||||
:undo-group undo-group}))))
|
(d/not-empty? drain-ids)
|
||||||
(rx/empty))))))
|
(rx/finalize #(wrf/mark-done! :layout drain-ids)))))))
|
||||||
|
|
||||||
(defn initialize-shape-layout
|
(defn initialize-shape-layout
|
||||||
[]
|
[]
|
||||||
@ -138,15 +145,15 @@
|
|||||||
;; they are process together. It will get a better performance.
|
;; they are process together. It will get a better performance.
|
||||||
(rx/buffer-time 100)
|
(rx/buffer-time 100)
|
||||||
(rx/filter #(d/not-empty? %))
|
(rx/filter #(d/not-empty? %))
|
||||||
;; Balance the per-event marks: decrement once per buffered event
|
|
||||||
;; (multiplicity preserved via mapcat, no deduping).
|
|
||||||
(rx/tap #(wrf/mark-done! :layout (mapcat :ids %)))
|
|
||||||
(rx/mapcat
|
(rx/mapcat
|
||||||
(fn [data]
|
(fn [data]
|
||||||
(->> (group-by :page-id data)
|
(->> (group-by :page-id data)
|
||||||
(map (fn [[page-id items]]
|
(map (fn [[page-id items]]
|
||||||
(let [ids (reduce #(into %1 (:ids %2)) #{} items)]
|
(let [ids (reduce #(into %1 (:ids %2)) #{} items)
|
||||||
(update-layout-positions {:page-id page-id :ids ids})))))))
|
drain-ids (mapcat :ids items)]
|
||||||
|
(update-layout-positions {:page-id page-id
|
||||||
|
:ids ids
|
||||||
|
:drain-ids drain-ids})))))))
|
||||||
(rx/take-until stopper)
|
(rx/take-until stopper)
|
||||||
;; On workspace teardown clear everything still pending.
|
;; On workspace teardown clear everything still pending.
|
||||||
(rx/finalize wrf/reset-pending!))))))
|
(rx/finalize wrf/reset-pending!))))))
|
||||||
|
|||||||
@ -420,7 +420,8 @@
|
|||||||
(wrf/mark-pending! :font [id])
|
(wrf/mark-pending! :font [id])
|
||||||
(let [stopper (rx/filter (ptk/type? :app.main.data.workspace/finalize) stream)]
|
(let [stopper (rx/filter (ptk/type? :app.main.data.workspace/finalize) stream)]
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::commit-position-data))
|
(rx/filter (ptk/type? ::position-data-committed))
|
||||||
|
(rx/filter #(contains? (:ids (deref %)) id))
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
;; Timeout so the shape cannot stay pending forever
|
;; Timeout so the shape cannot stay pending forever
|
||||||
(rx/timeout stuck-timeout (rx/of :timeout))
|
(rx/timeout stuck-timeout (rx/of :timeout))
|
||||||
@ -793,6 +794,8 @@
|
|||||||
(-> shape
|
(-> shape
|
||||||
(assoc :position-data (get position-data (:id shape)))))
|
(assoc :position-data (get position-data (:id shape)))))
|
||||||
{:stack-undo? true :reg-objects? false}))
|
{:stack-undo? true :reg-objects? false}))
|
||||||
|
(rx/of (ptk/data-event ::position-data-committed
|
||||||
|
{:ids (into #{} (keys position-data))}))
|
||||||
(rx/of (fn [state]
|
(rx/of (fn [state]
|
||||||
(dissoc state ::update-position-data-debounce ::update-position-data))))))))
|
(dissoc state ::update-position-data-debounce ::update-position-data))))))))
|
||||||
|
|
||||||
@ -897,7 +900,8 @@
|
|||||||
(wrf/mark-pending! :font [id])
|
(wrf/mark-pending! :font [id])
|
||||||
(let [stopper (rx/filter (ptk/type? :app.main.data.workspace/finalize) stream)]
|
(let [stopper (rx/filter (ptk/type? :app.main.data.workspace/finalize) stream)]
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::commit-position-data))
|
(rx/filter (ptk/type? ::position-data-committed))
|
||||||
|
(rx/filter #(contains? (:ids (deref %)) id))
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
;; Timeout so the shape cannot stay pending forever
|
;; Timeout so the shape cannot stay pending forever
|
||||||
(rx/timeout stuck-timeout (rx/of :timeout))
|
(rx/timeout stuck-timeout (rx/of :timeout))
|
||||||
|
|||||||
@ -201,18 +201,24 @@
|
|||||||
(every?
|
(every?
|
||||||
(fn [font]
|
(fn [font]
|
||||||
(let [font-data (wasm.fonts/make-font-data font)]
|
(let [font-data (wasm.fonts/make-font-data font)]
|
||||||
(wasm.fonts/font-stored? font-data (:emoji? font-data))))))]
|
(wasm.fonts/font-stored? font-data (:emoji? font-data))))))
|
||||||
|
|
||||||
(if fonts-loaded?
|
|
||||||
(let [pass-opts (when (or (some? undo-group) (some? undo-id))
|
|
||||||
(cond-> {}
|
|
||||||
(some? undo-group) (assoc :undo-group undo-group)
|
|
||||||
(some? undo-id) (assoc :undo-id undo-id)))]
|
|
||||||
(rx/of (resize-wasm-text-debounce-inner id pass-opts)))
|
|
||||||
|
|
||||||
;; Fonts not loaded; retry after 20 msecs
|
resize-wasm-stream
|
||||||
(->> (rx/of (resize-wasm-text-debounce id opts))
|
(if fonts-loaded?
|
||||||
(rx/delay 20))))))))
|
(let [pass-opts (when (or (some? undo-group) (some? undo-id))
|
||||||
|
(cond-> {}
|
||||||
|
(some? undo-group) (assoc :undo-group undo-group)
|
||||||
|
(some? undo-id) (assoc :undo-id undo-id)))]
|
||||||
|
(rx/of (resize-wasm-text-debounce-inner id pass-opts)))
|
||||||
|
|
||||||
|
;; Fonts not loaded; retry after 20 msecs
|
||||||
|
(->> (rx/of (resize-wasm-text-debounce id opts))
|
||||||
|
(rx/delay 20)))]
|
||||||
|
|
||||||
|
(wrf/mark-pending! :text-resize [id])
|
||||||
|
(->> resize-wasm-stream
|
||||||
|
(rx/finalize #(wrf/mark-done! :text-resize [id]))))))))
|
||||||
|
|
||||||
(defn resize-wasm-text-all
|
(defn resize-wasm-text-all
|
||||||
"Resize all text shapes (auto-width/auto-height) from a collection of ids."
|
"Resize all text shapes (auto-width/auto-height) from a collection of ids."
|
||||||
|
|||||||
@ -161,12 +161,26 @@
|
|||||||
"Stores the original WASM function values so they can be restored."
|
"Stores the original WASM function values so they can be restored."
|
||||||
(atom {}))
|
(atom {}))
|
||||||
|
|
||||||
|
;; Nesting depth of setup/teardown pairs. Only the outermost setup saves the
|
||||||
|
;; originals and installs the mocks, and only the outermost teardown restores
|
||||||
|
;; them, so a `with-wasm-mocks*` inside a fixture that already installed the
|
||||||
|
;; mocks does not capture mocks as "originals" and leak them.
|
||||||
|
(def ^:private install-depth (atom 0))
|
||||||
|
|
||||||
|
(declare install-wasm-mocks!)
|
||||||
|
|
||||||
(defn setup-wasm-mocks!
|
(defn setup-wasm-mocks!
|
||||||
"Install WASM mocks via `set!` that persist across async boundaries.
|
"Install WASM mocks via `set!` that persist across async boundaries.
|
||||||
Also resets call counts. Call `teardown-wasm-mocks!` to restore."
|
Also resets call counts. Call `teardown-wasm-mocks!` to restore.
|
||||||
|
Nested calls are no-ops apart from resetting call counts."
|
||||||
[]
|
[]
|
||||||
;; Reset call tracking
|
;; Reset call tracking
|
||||||
(reset-call-counts!)
|
(reset-call-counts!)
|
||||||
|
(when (= 1 (swap! install-depth inc))
|
||||||
|
(install-wasm-mocks!)))
|
||||||
|
|
||||||
|
(defn- install-wasm-mocks!
|
||||||
|
[]
|
||||||
;; Save originals
|
;; Save originals
|
||||||
(reset! originals
|
(reset! originals
|
||||||
{:initialized? wasm.api/initialized?
|
{:initialized? wasm.api/initialized?
|
||||||
@ -201,12 +215,16 @@
|
|||||||
(set! wasm.fonts/make-font-data mock-make-font-data)
|
(set! wasm.fonts/make-font-data mock-make-font-data)
|
||||||
(set! wasm.fonts/get-content-fonts mock-get-content-fonts))
|
(set! wasm.fonts/get-content-fonts mock-get-content-fonts))
|
||||||
|
|
||||||
|
(declare restore-wasm-mocks!)
|
||||||
|
|
||||||
(defn teardown-wasm-mocks!
|
(defn teardown-wasm-mocks!
|
||||||
"Restore the original WASM functions saved by `setup-wasm-mocks!`.
|
"Restore the original WASM functions saved by `setup-wasm-mocks!`.
|
||||||
No-op when there is nothing to restore (`originals` empty): restoring from an
|
Nested calls are no-ops; only the outermost teardown restores."
|
||||||
empty snapshot would `set!` every WASM function to nil, breaking any later
|
[]
|
||||||
code that calls them (e.g. a leaked debounced event firing during a
|
(when (zero? (swap! install-depth #(max 0 (dec %))))
|
||||||
subsequent test namespace)."
|
(restore-wasm-mocks!)))
|
||||||
|
|
||||||
|
(defn- restore-wasm-mocks!
|
||||||
[]
|
[]
|
||||||
(when-let [orig (not-empty @originals)]
|
(when-let [orig (not-empty @originals)]
|
||||||
(set! wasm.api/initialized? (:initialized? orig))
|
(set! wasm.api/initialized? (:initialized? orig))
|
||||||
|
|||||||
@ -420,8 +420,15 @@
|
|||||||
(def ^:private zero-id "00000000-0000-0000-0000-000000000000")
|
(def ^:private zero-id "00000000-0000-0000-0000-000000000000")
|
||||||
|
|
||||||
(t/use-fixtures :each
|
(t/use-fixtures :each
|
||||||
{:before wrf/reset-pending!
|
{:before (fn []
|
||||||
|
;; Keep the WASM mocks installed across the async test bodies:
|
||||||
|
;; debounced text-resize timers leaked by earlier test namespaces
|
||||||
|
;; can fire inside this namespace's async waits, and in Node they
|
||||||
|
;; would otherwise reach the real (unavailable) WASM API.
|
||||||
|
(thw/setup-wasm-mocks!)
|
||||||
|
(wrf/reset-pending!))
|
||||||
:after (fn []
|
:after (fn []
|
||||||
|
(thw/teardown-wasm-mocks!)
|
||||||
(wrf/reset-pending!)
|
(wrf/reset-pending!)
|
||||||
(set! st/state original-st-state)
|
(set! st/state original-st-state)
|
||||||
(set! st/stream original-st-stream))})
|
(set! st/stream original-st-stream))})
|
||||||
|
|||||||
@ -262,7 +262,7 @@
|
|||||||
(fn [id theme]
|
(fn [id theme]
|
||||||
(swap! captured conj {:id id :theme theme})
|
(swap! captured conj {:id id :theme theme})
|
||||||
:update-token-theme)
|
:update-token-theme)
|
||||||
st/emit! identity]
|
st/emit! mock/noop]
|
||||||
(.addSet theme set)
|
(.addSet theme set)
|
||||||
(.removeSet theme set)
|
(.removeSet theme set)
|
||||||
(t/is (= [theme-id theme-id] (mapv :id @captured)))
|
(t/is (= [theme-id theme-id] (mapv :id @captured)))
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"interfaces": {
|
"interfaces": {
|
||||||
"ActiveUser": ["position", "zoom"],
|
"ActiveUser": [
|
||||||
"Blur": ["hidden", "id", "value"],
|
"position",
|
||||||
|
"zoom"
|
||||||
|
],
|
||||||
|
"Blur": [
|
||||||
|
"hidden",
|
||||||
|
"id",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
"Board": [
|
"Board": [
|
||||||
"addFlexLayout",
|
"addFlexLayout",
|
||||||
"addGridLayout",
|
"addGridLayout",
|
||||||
@ -31,8 +38,17 @@
|
|||||||
"insertChild",
|
"insertChild",
|
||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"Bounds": ["height", "width", "x", "y"],
|
"Bounds": [
|
||||||
"CloseOverlay": ["animation", "destination", "type"],
|
"height",
|
||||||
|
"width",
|
||||||
|
"x",
|
||||||
|
"y"
|
||||||
|
],
|
||||||
|
"CloseOverlay": [
|
||||||
|
"animation",
|
||||||
|
"destination",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"Color": [
|
"Color": [
|
||||||
"color",
|
"color",
|
||||||
"fileId",
|
"fileId",
|
||||||
@ -43,9 +59,20 @@
|
|||||||
"opacity",
|
"opacity",
|
||||||
"path"
|
"path"
|
||||||
],
|
],
|
||||||
"ColorShapeInfo": ["shapesInfo"],
|
"ColorShapeInfo": [
|
||||||
"ColorShapeInfoEntry": ["index", "property", "shapeId"],
|
"shapesInfo"
|
||||||
"Comment": ["content", "date", "remove", "user"],
|
],
|
||||||
|
"ColorShapeInfoEntry": [
|
||||||
|
"index",
|
||||||
|
"property",
|
||||||
|
"shapeId"
|
||||||
|
],
|
||||||
|
"Comment": [
|
||||||
|
"content",
|
||||||
|
"date",
|
||||||
|
"remove",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
"CommentThread": [
|
"CommentThread": [
|
||||||
"board",
|
"board",
|
||||||
"findComments",
|
"findComments",
|
||||||
@ -114,9 +141,12 @@
|
|||||||
"uploadMediaData",
|
"uploadMediaData",
|
||||||
"uploadMediaUrl",
|
"uploadMediaUrl",
|
||||||
"version",
|
"version",
|
||||||
"viewport"
|
"viewport",
|
||||||
|
"waitForLayoutUpdate"
|
||||||
|
],
|
||||||
|
"ContextGeometryUtils": [
|
||||||
|
"center"
|
||||||
],
|
],
|
||||||
"ContextGeometryUtils": ["center"],
|
|
||||||
"ContextTypesUtils": [
|
"ContextTypesUtils": [
|
||||||
"isBoard",
|
"isBoard",
|
||||||
"isBool",
|
"isBool",
|
||||||
@ -130,9 +160,19 @@
|
|||||||
"isVariantComponent",
|
"isVariantComponent",
|
||||||
"isVariantContainer"
|
"isVariantContainer"
|
||||||
],
|
],
|
||||||
"ContextUtils": ["geometry", "types"],
|
"ContextUtils": [
|
||||||
"Dissolve": ["duration", "easing", "type"],
|
"geometry",
|
||||||
"Ellipse": ["fills", "type"],
|
"types"
|
||||||
|
],
|
||||||
|
"Dissolve": [
|
||||||
|
"duration",
|
||||||
|
"easing",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"Ellipse": [
|
||||||
|
"fills",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"EventsMap": [
|
"EventsMap": [
|
||||||
"contentsave",
|
"contentsave",
|
||||||
"filechange",
|
"filechange",
|
||||||
@ -142,7 +182,12 @@
|
|||||||
"shapechange",
|
"shapechange",
|
||||||
"themechange"
|
"themechange"
|
||||||
],
|
],
|
||||||
"Export": ["scale", "skipChildren", "suffix", "type"],
|
"Export": [
|
||||||
|
"scale",
|
||||||
|
"skipChildren",
|
||||||
|
"suffix",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"File": [
|
"File": [
|
||||||
"export",
|
"export",
|
||||||
"findVersions",
|
"findVersions",
|
||||||
@ -171,9 +216,21 @@
|
|||||||
"fillImage",
|
"fillImage",
|
||||||
"fillOpacity"
|
"fillOpacity"
|
||||||
],
|
],
|
||||||
"Flags": ["naturalChildOrdering", "throwValidationErrors"],
|
"Flags": [
|
||||||
"FlexLayout": ["appendChild", "dir", "wrap"],
|
"naturalChildOrdering",
|
||||||
"Flow": ["name", "page", "remove", "startingBoard"],
|
"throwValidationErrors"
|
||||||
|
],
|
||||||
|
"FlexLayout": [
|
||||||
|
"appendChild",
|
||||||
|
"dir",
|
||||||
|
"wrap"
|
||||||
|
],
|
||||||
|
"Flow": [
|
||||||
|
"name",
|
||||||
|
"page",
|
||||||
|
"remove",
|
||||||
|
"startingBoard"
|
||||||
|
],
|
||||||
"Font": [
|
"Font": [
|
||||||
"applyToRange",
|
"applyToRange",
|
||||||
"applyToText",
|
"applyToText",
|
||||||
@ -192,8 +249,21 @@
|
|||||||
"findById",
|
"findById",
|
||||||
"findByName"
|
"findByName"
|
||||||
],
|
],
|
||||||
"FontVariant": ["fontStyle", "fontVariantId", "fontWeight", "name"],
|
"FontVariant": [
|
||||||
"Gradient": ["endX", "endY", "startX", "startY", "stops", "type", "width"],
|
"fontStyle",
|
||||||
|
"fontVariantId",
|
||||||
|
"fontWeight",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"Gradient": [
|
||||||
|
"endX",
|
||||||
|
"endY",
|
||||||
|
"startX",
|
||||||
|
"startY",
|
||||||
|
"stops",
|
||||||
|
"type",
|
||||||
|
"width"
|
||||||
|
],
|
||||||
"GridLayout": [
|
"GridLayout": [
|
||||||
"addColumn",
|
"addColumn",
|
||||||
"addColumnAtIndex",
|
"addColumnAtIndex",
|
||||||
@ -217,7 +287,11 @@
|
|||||||
"removeMask",
|
"removeMask",
|
||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"GuideColumn": ["display", "params", "type"],
|
"GuideColumn": [
|
||||||
|
"display",
|
||||||
|
"params",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"GuideColumnParams": [
|
"GuideColumnParams": [
|
||||||
"color",
|
"color",
|
||||||
"gutter",
|
"gutter",
|
||||||
@ -226,10 +300,24 @@
|
|||||||
"size",
|
"size",
|
||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"GuideRow": ["display", "params", "type"],
|
"GuideRow": [
|
||||||
"GuideSquare": ["display", "params", "type"],
|
"display",
|
||||||
"GuideSquareParams": ["color", "size"],
|
"params",
|
||||||
"HistoryContext": ["undoBlockBegin", "undoBlockFinish"],
|
"type"
|
||||||
|
],
|
||||||
|
"GuideSquare": [
|
||||||
|
"display",
|
||||||
|
"params",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"GuideSquareParams": [
|
||||||
|
"color",
|
||||||
|
"size"
|
||||||
|
],
|
||||||
|
"HistoryContext": [
|
||||||
|
"undoBlockBegin",
|
||||||
|
"undoBlockFinish"
|
||||||
|
],
|
||||||
"ImageData": [
|
"ImageData": [
|
||||||
"data",
|
"data",
|
||||||
"height",
|
"height",
|
||||||
@ -239,7 +327,13 @@
|
|||||||
"name",
|
"name",
|
||||||
"width"
|
"width"
|
||||||
],
|
],
|
||||||
"Interaction": ["action", "delay", "remove", "shape", "trigger"],
|
"Interaction": [
|
||||||
|
"action",
|
||||||
|
"delay",
|
||||||
|
"remove",
|
||||||
|
"shape",
|
||||||
|
"trigger"
|
||||||
|
],
|
||||||
"LayoutCellProperties": [
|
"LayoutCellProperties": [
|
||||||
"areaName",
|
"areaName",
|
||||||
"column",
|
"column",
|
||||||
@ -297,7 +391,12 @@
|
|||||||
"connected",
|
"connected",
|
||||||
"local"
|
"local"
|
||||||
],
|
],
|
||||||
"LibraryElement": ["id", "libraryId", "name", "path"],
|
"LibraryElement": [
|
||||||
|
"id",
|
||||||
|
"libraryId",
|
||||||
|
"name",
|
||||||
|
"path"
|
||||||
|
],
|
||||||
"LibrarySummary": [
|
"LibrarySummary": [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
@ -326,15 +425,25 @@
|
|||||||
"variantProps",
|
"variantProps",
|
||||||
"variants"
|
"variants"
|
||||||
],
|
],
|
||||||
"LocalStorage": ["getItem", "getKeys", "removeItem", "setItem"],
|
"LocalStorage": [
|
||||||
|
"getItem",
|
||||||
|
"getKeys",
|
||||||
|
"removeItem",
|
||||||
|
"setItem"
|
||||||
|
],
|
||||||
"NavigateTo": [
|
"NavigateTo": [
|
||||||
"animation",
|
"animation",
|
||||||
"destination",
|
"destination",
|
||||||
"preserveScrollPosition",
|
"preserveScrollPosition",
|
||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"OpenOverlay": ["type"],
|
"OpenOverlay": [
|
||||||
"OpenUrl": ["type", "url"],
|
"type"
|
||||||
|
],
|
||||||
|
"OpenUrl": [
|
||||||
|
"type",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
"OverlayAction": [
|
"OverlayAction": [
|
||||||
"addBackgroundOverlay",
|
"addBackgroundOverlay",
|
||||||
"animation",
|
"animation",
|
||||||
@ -361,9 +470,23 @@
|
|||||||
"root",
|
"root",
|
||||||
"rulerGuides"
|
"rulerGuides"
|
||||||
],
|
],
|
||||||
"Path": ["commands", "d", "fills", "type"],
|
"Path": [
|
||||||
"PathCommand": ["command", "params"],
|
"commands",
|
||||||
"Penpot": ["closePlugin", "off", "on", "ui", "utils"],
|
"d",
|
||||||
|
"fills",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"PathCommand": [
|
||||||
|
"command",
|
||||||
|
"params"
|
||||||
|
],
|
||||||
|
"Penpot": [
|
||||||
|
"closePlugin",
|
||||||
|
"off",
|
||||||
|
"on",
|
||||||
|
"ui",
|
||||||
|
"utils"
|
||||||
|
],
|
||||||
"PluginData": [
|
"PluginData": [
|
||||||
"getPluginData",
|
"getPluginData",
|
||||||
"getPluginDataKeys",
|
"getPluginDataKeys",
|
||||||
@ -372,11 +495,28 @@
|
|||||||
"setPluginData",
|
"setPluginData",
|
||||||
"setSharedPluginData"
|
"setSharedPluginData"
|
||||||
],
|
],
|
||||||
"Point": ["x", "y"],
|
"Point": [
|
||||||
"PreviousScreen": ["type"],
|
"x",
|
||||||
"Push": ["direction", "duration", "easing", "type"],
|
"y"
|
||||||
"Rectangle": ["fills", "type"],
|
],
|
||||||
"RulerGuide": ["board", "orientation", "position", "remove"],
|
"PreviousScreen": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"Push": [
|
||||||
|
"direction",
|
||||||
|
"duration",
|
||||||
|
"easing",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"Rectangle": [
|
||||||
|
"fills",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"RulerGuide": [
|
||||||
|
"board",
|
||||||
|
"orientation",
|
||||||
|
"position"
|
||||||
|
],
|
||||||
"Shadow": [
|
"Shadow": [
|
||||||
"blur",
|
"blur",
|
||||||
"color",
|
"color",
|
||||||
@ -454,11 +594,19 @@
|
|||||||
"switchVariant",
|
"switchVariant",
|
||||||
"tokens",
|
"tokens",
|
||||||
"visible",
|
"visible",
|
||||||
|
"waitForLayoutUpdate",
|
||||||
"width",
|
"width",
|
||||||
"x",
|
"x",
|
||||||
"y"
|
"y"
|
||||||
],
|
],
|
||||||
"Slide": ["direction", "duration", "easing", "offsetEffect", "type", "way"],
|
"Slide": [
|
||||||
|
"direction",
|
||||||
|
"duration",
|
||||||
|
"easing",
|
||||||
|
"offsetEffect",
|
||||||
|
"type",
|
||||||
|
"way"
|
||||||
|
],
|
||||||
"Stroke": [
|
"Stroke": [
|
||||||
"strokeAlignment",
|
"strokeAlignment",
|
||||||
"strokeCapEnd",
|
"strokeCapEnd",
|
||||||
@ -472,7 +620,9 @@
|
|||||||
"strokeStyle",
|
"strokeStyle",
|
||||||
"strokeWidth"
|
"strokeWidth"
|
||||||
],
|
],
|
||||||
"SvgRaw": ["type"],
|
"SvgRaw": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"Text": [
|
"Text": [
|
||||||
"align",
|
"align",
|
||||||
"applyTypography",
|
"applyTypography",
|
||||||
@ -513,7 +663,9 @@
|
|||||||
"textTransform",
|
"textTransform",
|
||||||
"verticalAlign"
|
"verticalAlign"
|
||||||
],
|
],
|
||||||
"ToggleOverlay": ["type"],
|
"ToggleOverlay": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
"TokenBase": [
|
"TokenBase": [
|
||||||
"applyToSelected",
|
"applyToSelected",
|
||||||
"applyToShapes",
|
"applyToShapes",
|
||||||
@ -524,8 +676,16 @@
|
|||||||
"remove",
|
"remove",
|
||||||
"resolvedValueString"
|
"resolvedValueString"
|
||||||
],
|
],
|
||||||
"TokenBorderRadius": ["resolvedValue", "type", "value"],
|
"TokenBorderRadius": [
|
||||||
"TokenBorderWidth": ["resolvedValue", "type", "value"],
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"TokenBorderWidth": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
"TokenCatalog": [
|
"TokenCatalog": [
|
||||||
"addSet",
|
"addSet",
|
||||||
"addTheme",
|
"addTheme",
|
||||||
@ -534,15 +694,51 @@
|
|||||||
"sets",
|
"sets",
|
||||||
"themes"
|
"themes"
|
||||||
],
|
],
|
||||||
"TokenColor": ["resolvedValue", "type", "value"],
|
"TokenColor": [
|
||||||
"TokenDimension": ["resolvedValue", "type", "value"],
|
"resolvedValue",
|
||||||
"TokenFontFamilies": ["resolvedValue", "type", "value"],
|
"type",
|
||||||
"TokenFontSizes": ["resolvedValue", "type", "value"],
|
"value"
|
||||||
"TokenFontWeights": ["resolvedValue", "type", "value"],
|
],
|
||||||
"TokenLetterSpacing": ["resolvedValue", "type", "value"],
|
"TokenDimension": [
|
||||||
"TokenNumber": ["resolvedValue", "type", "value"],
|
"resolvedValue",
|
||||||
"TokenOpacity": ["resolvedValue", "type", "value"],
|
"type",
|
||||||
"TokenRotation": ["resolvedValue", "type", "value"],
|
"value"
|
||||||
|
],
|
||||||
|
"TokenFontFamilies": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"TokenFontSizes": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"TokenFontWeights": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"TokenLetterSpacing": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"TokenNumber": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"TokenOpacity": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"TokenRotation": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
"TokenSet": [
|
"TokenSet": [
|
||||||
"active",
|
"active",
|
||||||
"addToken",
|
"addToken",
|
||||||
@ -555,7 +751,11 @@
|
|||||||
"tokens",
|
"tokens",
|
||||||
"tokensByType"
|
"tokensByType"
|
||||||
],
|
],
|
||||||
"TokenShadow": ["resolvedValue", "type", "value"],
|
"TokenShadow": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
"TokenShadowValue": [
|
"TokenShadowValue": [
|
||||||
"blur",
|
"blur",
|
||||||
"color",
|
"color",
|
||||||
@ -572,10 +772,26 @@
|
|||||||
"offsetY",
|
"offsetY",
|
||||||
"spread"
|
"spread"
|
||||||
],
|
],
|
||||||
"TokenSizing": ["resolvedValue", "type", "value"],
|
"TokenSizing": [
|
||||||
"TokenSpacing": ["resolvedValue", "type", "value"],
|
"resolvedValue",
|
||||||
"TokenTextCase": ["resolvedValue", "type", "value"],
|
"type",
|
||||||
"TokenTextDecoration": ["resolvedValue", "type", "value"],
|
"value"
|
||||||
|
],
|
||||||
|
"TokenSpacing": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"TokenTextCase": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"TokenTextDecoration": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
"TokenTheme": [
|
"TokenTheme": [
|
||||||
"active",
|
"active",
|
||||||
"activeSets",
|
"activeSets",
|
||||||
@ -589,7 +805,11 @@
|
|||||||
"removeSet",
|
"removeSet",
|
||||||
"toggleActive"
|
"toggleActive"
|
||||||
],
|
],
|
||||||
"TokenTypography": ["resolvedValue", "type", "value"],
|
"TokenTypography": [
|
||||||
|
"resolvedValue",
|
||||||
|
"type",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
"TokenTypographyValue": [
|
"TokenTypographyValue": [
|
||||||
"fontFamilies",
|
"fontFamilies",
|
||||||
"fontSizes",
|
"fontSizes",
|
||||||
@ -608,9 +828,20 @@
|
|||||||
"textCase",
|
"textCase",
|
||||||
"textDecoration"
|
"textDecoration"
|
||||||
],
|
],
|
||||||
"Track": ["type", "value"],
|
"Track": [
|
||||||
"User": ["avatarUrl", "color", "id", "name", "sessionId"],
|
"type",
|
||||||
"VariantContainer": ["variants"],
|
"value"
|
||||||
|
],
|
||||||
|
"User": [
|
||||||
|
"avatarUrl",
|
||||||
|
"color",
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"sessionId"
|
||||||
|
],
|
||||||
|
"VariantContainer": [
|
||||||
|
"variants"
|
||||||
|
],
|
||||||
"Variants": [
|
"Variants": [
|
||||||
"addProperty",
|
"addProperty",
|
||||||
"addVariant",
|
"addVariant",
|
||||||
@ -1213,6 +1444,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -1713,6 +1950,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -2312,6 +2555,12 @@
|
|||||||
"kind": "method",
|
"kind": "method",
|
||||||
"type": "VariantContainer",
|
"type": "VariantContainer",
|
||||||
"array": false
|
"array": false
|
||||||
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "Context",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ContextGeometryUtils": {
|
"ContextGeometryUtils": {
|
||||||
@ -2845,6 +3094,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -4087,6 +4342,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -4671,6 +4932,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -6299,6 +6566,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -6620,6 +6893,12 @@
|
|||||||
"kind": "method",
|
"kind": "method",
|
||||||
"type": "VariantContainer",
|
"type": "VariantContainer",
|
||||||
"array": false
|
"array": false
|
||||||
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "Context",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PluginData": {
|
"PluginData": {
|
||||||
@ -7129,6 +7408,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -7657,6 +7942,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -8221,6 +8512,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -8787,6 +9084,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -11085,6 +11388,12 @@
|
|||||||
"type": null,
|
"type": null,
|
||||||
"array": false
|
"array": false
|
||||||
},
|
},
|
||||||
|
"waitForLayoutUpdate": {
|
||||||
|
"decl": "ShapeBase",
|
||||||
|
"kind": "method",
|
||||||
|
"type": null,
|
||||||
|
"array": false
|
||||||
|
},
|
||||||
"getPluginData": {
|
"getPluginData": {
|
||||||
"decl": "PluginData",
|
"decl": "PluginData",
|
||||||
"kind": "method",
|
"kind": "method",
|
||||||
@ -11240,7 +11549,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Animation": {
|
"Animation": {
|
||||||
"variants": ["Dissolve", "Slide", "Push"],
|
"variants": [
|
||||||
|
"Dissolve",
|
||||||
|
"Slide",
|
||||||
|
"Push"
|
||||||
|
],
|
||||||
"discriminant": {
|
"discriminant": {
|
||||||
"field": "type",
|
"field": "type",
|
||||||
"map": {
|
"map": {
|
||||||
@ -11251,7 +11564,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Guide": {
|
"Guide": {
|
||||||
"variants": ["GuideColumn", "GuideRow", "GuideSquare"],
|
"variants": [
|
||||||
|
"GuideColumn",
|
||||||
|
"GuideRow",
|
||||||
|
"GuideSquare"
|
||||||
|
],
|
||||||
"discriminant": {
|
"discriminant": {
|
||||||
"field": "type",
|
"field": "type",
|
||||||
"map": {
|
"map": {
|
||||||
@ -11332,7 +11649,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"TokenValueString": {
|
"TokenValueString": {
|
||||||
"variants": ["TokenShadowValueString", "TokenTypographyValueString"],
|
"variants": [
|
||||||
|
"TokenShadowValueString",
|
||||||
|
"TokenTypographyValueString"
|
||||||
|
],
|
||||||
"discriminant": null
|
"discriminant": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
399
plugins/apps/plugin-api-test-suite/src/tests/wait-layout.test.ts
Normal file
399
plugins/apps/plugin-api-test-suite/src/tests/wait-layout.test.ts
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
import { expect, expectReject } from '../framework/expect';
|
||||||
|
import { describe, test } from '../framework/registry';
|
||||||
|
import type { Board, Font, Shape, Text } from '@penpot/plugin-types';
|
||||||
|
import type { TestContext } from '../framework/types';
|
||||||
|
|
||||||
|
// waitForLayoutUpdate (context-level and per-shape).
|
||||||
|
// The promise must resolve only once the pending reflow work has been applied,
|
||||||
|
// so every test asserts the *observable geometry* after awaiting: if the
|
||||||
|
// promise settles too early the read-back is stale and the test goes red.
|
||||||
|
// Reflow work is scheduled by three pipelines: flex/grid layout updates
|
||||||
|
// (buffered ~100ms), wasm text resizes (debounced), and font-change
|
||||||
|
// re-measurement. The probes below cover each one, including cascaded reflows
|
||||||
|
// (a child resize that must also re-hug its ancestors) and waits racing the
|
||||||
|
// font-loading retry loop.
|
||||||
|
|
||||||
|
function flexBoard(ctx: TestContext): Board {
|
||||||
|
const b = ctx.penpot.createBoard();
|
||||||
|
ctx.board.appendChild(b);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a horizontal flex board with two 50x50 rects and a 10px gap. */
|
||||||
|
function flexRow(ctx: TestContext): {
|
||||||
|
board: Board;
|
||||||
|
rects: [Shape, Shape];
|
||||||
|
} {
|
||||||
|
const b = flexBoard(ctx);
|
||||||
|
const flex = b.addFlexLayout();
|
||||||
|
flex.dir = 'row';
|
||||||
|
flex.columnGap = 10;
|
||||||
|
const r1 = ctx.penpot.createRectangle();
|
||||||
|
r1.resize(50, 50);
|
||||||
|
flex.appendChild(r1);
|
||||||
|
const r2 = ctx.penpot.createRectangle();
|
||||||
|
r2.resize(50, 50);
|
||||||
|
flex.appendChild(r2);
|
||||||
|
return { board: b, rects: [r1, r2] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Orders the two rects of a flex row by their current x position. */
|
||||||
|
function byX(rects: [Shape, Shape]): { left: Shape; right: Shape } {
|
||||||
|
const [a, b] = rects;
|
||||||
|
return a.x <= b.x ? { left: a, right: b } : { left: b, right: a };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks a font from the end of the catalog that differs from the text's
|
||||||
|
* current font, so assigning it triggers a real (not-yet-loaded) font fetch.
|
||||||
|
* `offset` lets consecutive tests pick distinct fonts: fonts loaded by an
|
||||||
|
* earlier test stay cached for the whole run and would drain the pending mark
|
||||||
|
* near-instantly if reused.
|
||||||
|
*/
|
||||||
|
function unloadedFont(ctx: TestContext, t: Text, offset: number): Font {
|
||||||
|
const all = ctx.penpot.fonts.all;
|
||||||
|
for (let i = all.length - 1 - offset; i >= 0; i--) {
|
||||||
|
const f = all[i];
|
||||||
|
if (f.fontId !== t.fontId && f.variants.length > 0) return f;
|
||||||
|
}
|
||||||
|
throw new Error('no alternative font available');
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoWidthText(ctx: TestContext, value: string): Text {
|
||||||
|
const t = ctx.penpot.createText(value);
|
||||||
|
if (!t) throw new Error('createText returned null');
|
||||||
|
ctx.board.appendChild(t);
|
||||||
|
t.growType = 'auto-width';
|
||||||
|
t.fontSize = '36';
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('WaitForLayoutUpdate', () => {
|
||||||
|
describe('Basics', () => {
|
||||||
|
test('context method returns a promise that resolves', async (ctx) => {
|
||||||
|
const p = ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(typeof p.then).toBe('function');
|
||||||
|
const value = await p;
|
||||||
|
expect(value).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// For the next two, resolving (not rejecting, not hitting the runner
|
||||||
|
// timeout) is itself the behaviour under test, so they have no expects.
|
||||||
|
|
||||||
|
test('a timeout wait resolves via the fast path when nothing is pending', async (ctx) => {
|
||||||
|
// Drain any residual work first; once the untimed wait resolves the
|
||||||
|
// pending map is empty, so the settle signal replays synchronously and
|
||||||
|
// must win the race against the deadline: even a 1ms timeout has to
|
||||||
|
// resolve, never reject.
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
await ctx.penpot.waitForLayoutUpdate(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shape method resolves for a shape with no pending work', async (ctx) => {
|
||||||
|
const r = ctx.penpot.createRectangle();
|
||||||
|
ctx.board.appendChild(r);
|
||||||
|
await r.waitForLayoutUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('consecutive waits each track their own pending work', async (ctx) => {
|
||||||
|
// Each call opens a fresh subscription to the pending map, so a wait must
|
||||||
|
// settle on the work scheduled right before it and never carry state from
|
||||||
|
// an earlier, already-resolved wait. Two successive resizes to different
|
||||||
|
// sizes prove it: the second wait has to reflect the second reflow.
|
||||||
|
const { rects } = flexRow(ctx);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
|
||||||
|
const { left, right } = byX(rects);
|
||||||
|
left.resize(120, 50);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(right.x - left.x).toBeCloseTo(130, 0);
|
||||||
|
|
||||||
|
left.resize(70, 50);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(right.x - left.x).toBeCloseTo(80, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Flex reflow', () => {
|
||||||
|
test('sibling is repositioned after awaiting a child resize', async (ctx) => {
|
||||||
|
const { rects } = flexRow(ctx);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
|
||||||
|
const { left, right } = byX(rects);
|
||||||
|
left.resize(120, 50);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(right.x - left.x).toBeCloseTo(130, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('board-level wait settles the reflow of its layout', async (ctx) => {
|
||||||
|
const { board: b, rects } = flexRow(ctx);
|
||||||
|
await b.waitForLayoutUpdate();
|
||||||
|
|
||||||
|
const { left, right } = byX(rects);
|
||||||
|
left.resize(120, 50);
|
||||||
|
// The layout pipeline marks the laid-out board (the parent) as pending,
|
||||||
|
// so the board proxy is the shape-level handle for this reflow.
|
||||||
|
await b.waitForLayoutUpdate();
|
||||||
|
expect(right.x - left.x).toBeCloseTo(130, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('back-to-back resizes settle to the last value', async (ctx) => {
|
||||||
|
// Overlapping operations on the same shapes: the wait must not resolve
|
||||||
|
// after the first one drains while the second is still in flight.
|
||||||
|
const { rects } = flexRow(ctx);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
|
||||||
|
const { left, right } = byX(rects);
|
||||||
|
left.resize(80, 50);
|
||||||
|
left.resize(120, 50);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(right.x - left.x).toBeCloseTo(130, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('grid layout reflow settles child positions', async (ctx) => {
|
||||||
|
const b = flexBoard(ctx);
|
||||||
|
const grid = b.addGridLayout();
|
||||||
|
grid.addRow('fixed', 60);
|
||||||
|
grid.addColumn('fixed', 60);
|
||||||
|
grid.addColumn('fixed', 60);
|
||||||
|
const r1 = ctx.penpot.createRectangle();
|
||||||
|
r1.resize(50, 50);
|
||||||
|
grid.appendChild(r1, 1, 1);
|
||||||
|
const r2 = ctx.penpot.createRectangle();
|
||||||
|
r2.resize(50, 50);
|
||||||
|
grid.appendChild(r2, 1, 2);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
// Cells are 60px wide, so the second child sits one track further right.
|
||||||
|
expect(Math.abs(r2.x - r1.x)).toBeCloseTo(60, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Hug sizing', () => {
|
||||||
|
test('hug board fits its content after awaiting the initial layout', async (ctx) => {
|
||||||
|
const { board: b } = flexRow(ctx);
|
||||||
|
const flex = b.flex;
|
||||||
|
if (!flex) throw new Error('expected a flex layout on the board');
|
||||||
|
flex.horizontalSizing = 'auto';
|
||||||
|
flex.verticalSizing = 'auto';
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
// 50 + 10 gap + 50, no padding.
|
||||||
|
expect(b.width).toBeCloseTo(110, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hug board grows after awaiting a child resize', async (ctx) => {
|
||||||
|
const { board: b, rects } = flexRow(ctx);
|
||||||
|
const flex = b.flex;
|
||||||
|
if (!flex) throw new Error('expected a flex layout on the board');
|
||||||
|
flex.horizontalSizing = 'auto';
|
||||||
|
flex.verticalSizing = 'auto';
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
|
||||||
|
const { left } = byX(rects);
|
||||||
|
left.resize(120, 50);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(b.width).toBeCloseTo(180, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nested hug boards both grow after awaiting a child resize', async (ctx) => {
|
||||||
|
// Cascade probe: the child resize reflows the inner board, whose new
|
||||||
|
// size must then reflow the outer board. The wait has to cover the whole
|
||||||
|
// chain, not just the first reflow round.
|
||||||
|
const outer = flexBoard(ctx);
|
||||||
|
const outerFlex = outer.addFlexLayout();
|
||||||
|
outerFlex.dir = 'row';
|
||||||
|
outerFlex.horizontalSizing = 'auto';
|
||||||
|
outerFlex.verticalSizing = 'auto';
|
||||||
|
|
||||||
|
const inner = ctx.penpot.createBoard();
|
||||||
|
outerFlex.appendChild(inner);
|
||||||
|
const innerFlex = inner.addFlexLayout();
|
||||||
|
innerFlex.dir = 'row';
|
||||||
|
innerFlex.horizontalSizing = 'auto';
|
||||||
|
innerFlex.verticalSizing = 'auto';
|
||||||
|
|
||||||
|
const rect = ctx.penpot.createRectangle();
|
||||||
|
rect.resize(50, 50);
|
||||||
|
innerFlex.appendChild(rect);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(inner.width).toBeCloseTo(50, 0);
|
||||||
|
expect(outer.width).toBeCloseTo(50, 0);
|
||||||
|
|
||||||
|
rect.resize(200, 50);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(inner.width).toBeCloseTo(200, 0);
|
||||||
|
expect(outer.width).toBeCloseTo(200, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hug board hugs an auto-width text after a characters change', async (ctx) => {
|
||||||
|
// Cascade probe across pipelines: the text resize is scheduled by the
|
||||||
|
// text pipeline and only then does the board reflow to hug it.
|
||||||
|
const b = flexBoard(ctx);
|
||||||
|
const flex = b.addFlexLayout();
|
||||||
|
flex.horizontalSizing = 'auto';
|
||||||
|
flex.verticalSizing = 'auto';
|
||||||
|
const t = ctx.penpot.createText('hi');
|
||||||
|
if (!t) throw new Error('createText returned null');
|
||||||
|
flex.appendChild(t);
|
||||||
|
t.growType = 'auto-width';
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
const w0 = b.width;
|
||||||
|
|
||||||
|
t.characters = 'a much much much longer text content than before';
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(t.width).toBeGreaterThan(w0);
|
||||||
|
expect(b.width).toBeCloseTo(t.width, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Text', () => {
|
||||||
|
test('auto-width text grows after awaiting a fontSize bump', async (ctx) => {
|
||||||
|
const t = autoWidthText(ctx, 'The quick brown fox');
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
const w0 = t.width;
|
||||||
|
|
||||||
|
t.fontSize = '72';
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(t.width).toBeGreaterThan(w0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auto-width text grows after awaiting a characters change', async (ctx) => {
|
||||||
|
const t = autoWidthText(ctx, 'short');
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
const w0 = t.width;
|
||||||
|
|
||||||
|
t.characters = 'short plus a considerably longer tail 0123456789';
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(t.width).toBeGreaterThan(w0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shape-level wait covers the text own resize', async (ctx) => {
|
||||||
|
const t = autoWidthText(ctx, 'The quick brown fox');
|
||||||
|
await t.waitForLayoutUpdate();
|
||||||
|
const w0 = t.width;
|
||||||
|
|
||||||
|
t.fontSize = '72';
|
||||||
|
await t.waitForLayoutUpdate();
|
||||||
|
expect(t.width).toBeGreaterThan(w0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auto-width text is re-measured after awaiting a font change', async (ctx) => {
|
||||||
|
// Probe for the font-loading window: right after a font change the new
|
||||||
|
// font is typically not loaded yet, and the wait must cover the load +
|
||||||
|
// re-measure, not resolve while the loader is still retrying.
|
||||||
|
const t = autoWidthText(
|
||||||
|
ctx,
|
||||||
|
'The quick brown fox jumps over the lazy dog 0123456789',
|
||||||
|
);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
const w0 = t.width;
|
||||||
|
|
||||||
|
const font = ctx.penpot.fonts.all.find(
|
||||||
|
(f) => f.fontId !== t.fontId && f.variants.length > 0,
|
||||||
|
);
|
||||||
|
if (!font) throw new Error('no alternative font available');
|
||||||
|
t.fontId = font.fontId;
|
||||||
|
t.fontFamily = font.fontFamily;
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
// A different family practically never yields the same measured width
|
||||||
|
// for a 54-char string at 36px.
|
||||||
|
expect(Math.abs(t.width - w0)).toBeGreaterThan(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('range font change is re-measured after awaiting', async (ctx) => {
|
||||||
|
// Same probe through the text-range pipeline instead of the shape-level
|
||||||
|
// font setter.
|
||||||
|
const t = autoWidthText(
|
||||||
|
ctx,
|
||||||
|
'The quick brown fox jumps over the lazy dog 0123456789',
|
||||||
|
);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
const w0 = t.width;
|
||||||
|
|
||||||
|
const font = ctx.penpot.fonts.all.find(
|
||||||
|
(f) => f.fontId !== t.fontId && f.variants.length > 0,
|
||||||
|
);
|
||||||
|
if (!font) throw new Error('no alternative font available');
|
||||||
|
const range = t.getRange(0, t.characters.length);
|
||||||
|
range.fontFamily = font.fontFamily;
|
||||||
|
range.fontId = font.fontId;
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
expect(Math.abs(t.width - w0)).toBeGreaterThan(0.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Timeout', () => {
|
||||||
|
// Pure geometry mutations (resize & co.) are settled synchronously by the
|
||||||
|
// renderer, so right after them nothing is pending and a short-timeout
|
||||||
|
// wait legitimately resolves. The deterministic pending source is a font
|
||||||
|
// change: it marks the text pending until the font is fetched and the
|
||||||
|
// shape re-measured, which can never complete within 1ms. These tests hit
|
||||||
|
// the real backend for the font asset, hence skipIfMocked.
|
||||||
|
|
||||||
|
test.skipIfMocked(
|
||||||
|
'rejects with a timeout error while a font is loading',
|
||||||
|
async (ctx) => {
|
||||||
|
const t = autoWidthText(ctx, 'The quick brown fox');
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
|
||||||
|
const font = unloadedFont(ctx, t, 0);
|
||||||
|
t.fontId = font.fontId;
|
||||||
|
t.fontFamily = font.fontFamily;
|
||||||
|
await expectReject(ctx.penpot.waitForLayoutUpdate(1), /timeout/i);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.skipIfMocked('shape-level wait also rejects on timeout', async (ctx) => {
|
||||||
|
const t = autoWidthText(ctx, 'The quick brown fox');
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
|
||||||
|
const font = unloadedFont(ctx, t, 1);
|
||||||
|
t.fontId = font.fontId;
|
||||||
|
t.fontFamily = font.fontFamily;
|
||||||
|
await expectReject(t.waitForLayoutUpdate(1), /timeout/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves normally when work settles before the timeout', async (ctx) => {
|
||||||
|
const { rects } = flexRow(ctx);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
|
||||||
|
const { left, right } = byX(rects);
|
||||||
|
left.resize(120, 50);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate(5000);
|
||||||
|
expect(right.x - left.x).toBeCloseTo(130, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skipIfMocked('a rejected wait leaves later waits working', async (ctx) => {
|
||||||
|
const t = autoWidthText(ctx, 'The quick brown fox');
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
const w0 = t.width;
|
||||||
|
|
||||||
|
const font = unloadedFont(ctx, t, 2);
|
||||||
|
t.fontId = font.fontId;
|
||||||
|
t.fontFamily = font.fontFamily;
|
||||||
|
await expectReject(ctx.penpot.waitForLayoutUpdate(1), /timeout/i);
|
||||||
|
// A second wait with a generous timeout still tracks the same pending
|
||||||
|
// work and resolves once the font lands and the text is re-measured.
|
||||||
|
await ctx.penpot.waitForLayoutUpdate(12000);
|
||||||
|
expect(Math.abs(t.width - w0)).toBeGreaterThan(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skipIfMocked(
|
||||||
|
'per-shape wait is not blocked by another shape pending work',
|
||||||
|
async (ctx) => {
|
||||||
|
const t = autoWidthText(ctx, 'The quick brown fox');
|
||||||
|
const bystander = ctx.penpot.createRectangle();
|
||||||
|
ctx.board.appendChild(bystander);
|
||||||
|
await ctx.penpot.waitForLayoutUpdate();
|
||||||
|
|
||||||
|
const font = unloadedFont(ctx, t, 3);
|
||||||
|
t.fontId = font.fontId;
|
||||||
|
t.fontFamily = font.fontFamily;
|
||||||
|
// The text has pending font work but the bystander does not; its wait
|
||||||
|
// must resolve on the fast path instead of waiting for the font.
|
||||||
|
const started = Date.now();
|
||||||
|
await bystander.waitForLayoutUpdate();
|
||||||
|
expect(Date.now() - started).toBeLessThan(90);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user