mirror of
https://github.com/penpot/penpot.git
synced 2026-07-19 20:48:10 +00:00
272 lines
14 KiB
Clojure
272 lines
14 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.plugins.page-active-validation-test
|
|
"Tests for the guard that prevents plugins from modifying shapes/properties that
|
|
live on a page which is not the currently active one (see
|
|
`app.plugins.utils/page-active?`).
|
|
|
|
Strategy: build a rich page2 via the public API *while page2 is active*, then
|
|
enable `throwValidationErrors` so rejections surface as exceptions.
|
|
|
|
- Negative test: with page1 active, every guarded property/method invoked on a
|
|
page2-bound proxy must throw.
|
|
- Positive test: with page2 active, every setter (with a valid value) must NOT
|
|
throw. This proves the values used are valid, so the negative throws can only
|
|
come from the page guard."
|
|
(:require
|
|
[app.common.test-helpers.files :as cthf]
|
|
[app.main.store :as st]
|
|
[app.plugins.api :as api]
|
|
[app.util.object :as obj]
|
|
[cljs.test :as t :include-macros true]
|
|
[frontend-tests.helpers.state :as ths]
|
|
[frontend-tests.helpers.wasm :as thw]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
|
|
|
(defn- activate-page!
|
|
[store page-id]
|
|
(ptk/emit! store #(assoc % :current-page-id page-id)))
|
|
|
|
(defn- throws?
|
|
[thunk]
|
|
(try (thunk) false (catch :default _ true)))
|
|
|
|
(defn- setup
|
|
"Creates a file with two pages and builds, *on the non-active page2*, a set of
|
|
proxies covering every guarded proxy type. Leaves page1 active with the
|
|
`throwValidationErrors` flag enabled. Returns the proxies + store + page ids."
|
|
[]
|
|
(let [file (-> (cthf/sample-file :file1 :page-label :page1)
|
|
(cthf/add-sample-page :page2))
|
|
store (ths/setup-store file)
|
|
_ (set! st/state store)
|
|
_ (set! st/stream (ptk/input-stream store))
|
|
^js context (api/create-context plugin-id)
|
|
pages (.. context -currentFile -pages)
|
|
page1-id (obj/get (aget pages 0) "$id")
|
|
^js page2 (aget pages 1)
|
|
page2-id (obj/get page2 "$id")
|
|
|
|
;; Build everything with page2 active so the constructive API succeeds.
|
|
_ (activate-page! store page2-id)
|
|
|
|
^js rect (.createRectangle context)
|
|
^js rect2 (.createRectangle context)
|
|
^js board (.createBoard context)
|
|
^js child (.createRectangle context)
|
|
_ (.appendChild board child)
|
|
|
|
^js flex-board (.createBoard context)
|
|
^js flex (.addFlexLayout flex-board)
|
|
^js flex-child (.createRectangle context)
|
|
_ (.appendChild flex-board flex-child)
|
|
^js layout-child (.-layoutChild flex-child)
|
|
|
|
^js grid-board (.createBoard context)
|
|
^js grid (.addGridLayout grid-board)
|
|
|
|
^js text (.createText context "hello")
|
|
^js range (.getRange text 0 1)
|
|
|
|
^js guide (.addRulerGuide page2 "vertical" 10 board)
|
|
|
|
;; Back to page1: page2 proxies must now be read-only.
|
|
_ (activate-page! store page1-id)
|
|
_ (ptk/emit! store #(assoc-in % [:plugins :flags plugin-id :throw-validation-errors] true))]
|
|
|
|
{:store store :page1-id page1-id :page2-id page2-id :context context :page2 page2
|
|
:rect rect :rect2 rect2 :board board :child child
|
|
:flex flex :flex-board flex-board :layout-child layout-child :flex-child flex-child
|
|
:grid grid :grid-board grid-board
|
|
:text text :range range :guide guide}))
|
|
|
|
(defn- setter-specs
|
|
"Property setters (value mutations). Used by both the positive and negative
|
|
tests, so they must be idempotent / non-destructive."
|
|
[{:keys [^js rect ^js child ^js flex ^js grid ^js layout-child
|
|
^js range ^js guide ^js board ^js text]}]
|
|
[;; ---- ShapeProxy ----
|
|
["ShapeProxy.name" #(set! (.-name rect) "X")]
|
|
["ShapeProxy.x" #(set! (.-x rect) 10)]
|
|
["ShapeProxy.y" #(set! (.-y rect) 10)]
|
|
["ShapeProxy.blocked" #(set! (.-blocked rect) true)]
|
|
["ShapeProxy.hidden" #(set! (.-hidden rect) true)]
|
|
["ShapeProxy.visible" #(set! (.-visible rect) false)]
|
|
["ShapeProxy.proportionLock" #(set! (.-proportionLock rect) true)]
|
|
["ShapeProxy.constraintsHorizontal" #(set! (.-constraintsHorizontal rect) "right")]
|
|
["ShapeProxy.constraintsVertical" #(set! (.-constraintsVertical rect) "bottom")]
|
|
["ShapeProxy.borderRadius" #(set! (.-borderRadius rect) 5)]
|
|
["ShapeProxy.borderRadiusTopLeft" #(set! (.-borderRadiusTopLeft rect) 5)]
|
|
["ShapeProxy.borderRadiusTopRight" #(set! (.-borderRadiusTopRight rect) 5)]
|
|
["ShapeProxy.borderRadiusBottomRight" #(set! (.-borderRadiusBottomRight rect) 5)]
|
|
["ShapeProxy.borderRadiusBottomLeft" #(set! (.-borderRadiusBottomLeft rect) 5)]
|
|
["ShapeProxy.opacity" #(set! (.-opacity rect) 0.5)]
|
|
["ShapeProxy.blendMode" #(set! (.-blendMode rect) "multiply")]
|
|
["ShapeProxy.shadows" #(set! (.-shadows rect) #js [#js {:style "drop-shadow" :color #js {:color "#000000" :opacity 1}}])]
|
|
["ShapeProxy.blur" #(set! (.-blur rect) #js {:value 10})]
|
|
["ShapeProxy.exports" #(set! (.-exports rect) #js [#js {:type "png" :scale 1 :suffix ""}])]
|
|
["ShapeProxy.flipX" #(set! (.-flipX rect) true)]
|
|
["ShapeProxy.flipY" #(set! (.-flipY rect) true)]
|
|
["ShapeProxy.rotation" #(set! (.-rotation rect) 45)]
|
|
["ShapeProxy.fills" #(set! (.-fills rect) #js [#js {:fillColor "#fabada" :fillOpacity 1}])]
|
|
["ShapeProxy.strokes" #(set! (.-strokes rect) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 2}])]
|
|
;; relative geometry (shape inside a board)
|
|
["ShapeProxy.boardX" #(set! (.-boardX child) 10)]
|
|
["ShapeProxy.boardY" #(set! (.-boardY child) 10)]
|
|
["ShapeProxy.parentX" #(set! (.-parentX child) 10)]
|
|
["ShapeProxy.parentY" #(set! (.-parentY child) 10)]
|
|
;; layout-item sizing (frame is a layout item)
|
|
["ShapeProxy.horizontalSizing" #(set! (.-horizontalSizing board) "fix")]
|
|
["ShapeProxy.verticalSizing" #(set! (.-verticalSizing board) "fix")]
|
|
;; text shape props (added via add-text-props)
|
|
["ShapeProxy.growType" #(set! (.-growType text) "fixed")]
|
|
["ShapeProxy.verticalAlign" #(set! (.-verticalAlign text) "center")]
|
|
|
|
;; ---- FlexLayoutProxy ----
|
|
["FlexLayoutProxy.dir" #(set! (.-dir flex) "row")]
|
|
["FlexLayoutProxy.wrap" #(set! (.-wrap flex) "wrap")]
|
|
["FlexLayoutProxy.alignItems" #(set! (.-alignItems flex) "center")]
|
|
["FlexLayoutProxy.alignContent" #(set! (.-alignContent flex) "center")]
|
|
["FlexLayoutProxy.justifyItems" #(set! (.-justifyItems flex) "start")]
|
|
["FlexLayoutProxy.justifyContent" #(set! (.-justifyContent flex) "center")]
|
|
["FlexLayoutProxy.rowGap" #(set! (.-rowGap flex) 5)]
|
|
["FlexLayoutProxy.columnGap" #(set! (.-columnGap flex) 5)]
|
|
["FlexLayoutProxy.verticalPadding" #(set! (.-verticalPadding flex) 5)]
|
|
["FlexLayoutProxy.horizontalPadding" #(set! (.-horizontalPadding flex) 5)]
|
|
["FlexLayoutProxy.topPadding" #(set! (.-topPadding flex) 5)]
|
|
["FlexLayoutProxy.rightPadding" #(set! (.-rightPadding flex) 5)]
|
|
["FlexLayoutProxy.bottomPadding" #(set! (.-bottomPadding flex) 5)]
|
|
["FlexLayoutProxy.leftPadding" #(set! (.-leftPadding flex) 5)]
|
|
|
|
;; ---- LayoutChildProxy ----
|
|
["LayoutChildProxy.absolute" #(set! (.-absolute layout-child) true)]
|
|
["LayoutChildProxy.zIndex" #(set! (.-zIndex layout-child) 1)]
|
|
["LayoutChildProxy.horizontalSizing" #(set! (.-horizontalSizing layout-child) "fix")]
|
|
["LayoutChildProxy.verticalSizing" #(set! (.-verticalSizing layout-child) "fix")]
|
|
["LayoutChildProxy.alignSelf" #(set! (.-alignSelf layout-child) "center")]
|
|
["LayoutChildProxy.horizontalMargin" #(set! (.-horizontalMargin layout-child) 5)]
|
|
["LayoutChildProxy.verticalMargin" #(set! (.-verticalMargin layout-child) 5)]
|
|
["LayoutChildProxy.topMargin" #(set! (.-topMargin layout-child) 5)]
|
|
["LayoutChildProxy.rightMargin" #(set! (.-rightMargin layout-child) 5)]
|
|
["LayoutChildProxy.bottomMargin" #(set! (.-bottomMargin layout-child) 5)]
|
|
["LayoutChildProxy.leftMargin" #(set! (.-leftMargin layout-child) 5)]
|
|
["LayoutChildProxy.maxWidth" #(set! (.-maxWidth layout-child) 100)]
|
|
["LayoutChildProxy.maxHeight" #(set! (.-maxHeight layout-child) 100)]
|
|
["LayoutChildProxy.minWidth" #(set! (.-minWidth layout-child) 0)]
|
|
["LayoutChildProxy.minHeight" #(set! (.-minHeight layout-child) 0)]
|
|
|
|
;; ---- GridLayoutProxy ----
|
|
["GridLayoutProxy.dir" #(set! (.-dir grid) "row")]
|
|
["GridLayoutProxy.alignItems" #(set! (.-alignItems grid) "center")]
|
|
["GridLayoutProxy.alignContent" #(set! (.-alignContent grid) "center")]
|
|
["GridLayoutProxy.justifyItems" #(set! (.-justifyItems grid) "start")]
|
|
["GridLayoutProxy.justifyContent" #(set! (.-justifyContent grid) "center")]
|
|
["GridLayoutProxy.rowGap" #(set! (.-rowGap grid) 5)]
|
|
["GridLayoutProxy.columnGap" #(set! (.-columnGap grid) 5)]
|
|
["GridLayoutProxy.verticalPadding" #(set! (.-verticalPadding grid) 5)]
|
|
["GridLayoutProxy.horizontalPadding" #(set! (.-horizontalPadding grid) 5)]
|
|
["GridLayoutProxy.topPadding" #(set! (.-topPadding grid) 5)]
|
|
["GridLayoutProxy.rightPadding" #(set! (.-rightPadding grid) 5)]
|
|
["GridLayoutProxy.bottomPadding" #(set! (.-bottomPadding grid) 5)]
|
|
["GridLayoutProxy.leftPadding" #(set! (.-leftPadding grid) 5)]
|
|
|
|
;; NOTE: GridCellProxy setters (row/column/rowSpan/columnSpan/areaName/position/
|
|
;; justifySelf/alignSelf) share the identical guard but require a child placed in
|
|
;; a grid cell, which cannot be fixtured in this headless test env
|
|
;; (move-shapes-to-frame is WASM-bound). They are verified by source review.
|
|
|
|
;; ---- TextRangeProxy ----
|
|
["TextRangeProxy.align" #(set! (.-align range) "center")]
|
|
["TextRangeProxy.direction" #(set! (.-direction range) "ltr")]
|
|
["TextRangeProxy.textTransform" #(set! (.-textTransform range) "uppercase")]
|
|
["TextRangeProxy.textDecoration" #(set! (.-textDecoration range) "underline")]
|
|
["TextRangeProxy.fontSize" #(set! (.-fontSize range) "16")]
|
|
["TextRangeProxy.lineHeight" #(set! (.-lineHeight range) "1.2")]
|
|
["TextRangeProxy.letterSpacing" #(set! (.-letterSpacing range) "1")]
|
|
["TextRangeProxy.fills" #(set! (.-fills range) #js [#js {:fillColor "#fabada" :fillOpacity 1}])]
|
|
|
|
;; ---- RulerGuideProxy ----
|
|
["RulerGuideProxy.board" #(set! (.-board guide) board)]
|
|
["RulerGuideProxy.color" #(set! (.-color guide) "#fabada")]
|
|
["RulerGuideProxy.position" #(set! (.-position guide) 20)]])
|
|
|
|
(defn- method-specs
|
|
"Structural / destructive methods. Tested in the negative direction only (they
|
|
are rejected before performing any mutation), with trivially valid arguments."
|
|
[{:keys [^js context ^js page2 ^js rect ^js rect2 ^js board ^js child
|
|
^js flex ^js grid ^js guide]}]
|
|
[;; ---- ShapeProxy ----
|
|
["ShapeProxy.resize" #(.resize rect 10 10)]
|
|
["ShapeProxy.rotate" #(.rotate rect 45)]
|
|
["ShapeProxy.clone" #(.clone rect)]
|
|
["ShapeProxy.remove" #(.remove rect)]
|
|
["ShapeProxy.setParentIndex" #(.setParentIndex child 0)]
|
|
["ShapeProxy.setPluginData" #(.setPluginData rect "k" "v")]
|
|
["ShapeProxy.setSharedPluginData" #(.setSharedPluginData rect "ns" "k" "v")]
|
|
["ShapeProxy.appendChild" #(.appendChild board rect)]
|
|
["ShapeProxy.insertChild" #(.insertChild board 0 rect)]
|
|
["ShapeProxy.addFlexLayout" #(.addFlexLayout board)]
|
|
["ShapeProxy.addGridLayout" #(.addGridLayout board)]
|
|
|
|
;; ---- FlexLayoutProxy ----
|
|
["FlexLayoutProxy.appendChild" #(.appendChild flex rect)]
|
|
|
|
;; ---- GridLayoutProxy ----
|
|
["GridLayoutProxy.appendChild" #(.appendChild grid rect 1 1)]
|
|
["GridLayoutProxy.addRow" #(.addRow grid "flex" 1)]
|
|
["GridLayoutProxy.addColumn" #(.addColumn grid "flex" 1)]
|
|
["GridLayoutProxy.addRowAtIndex" #(.addRowAtIndex grid 0 "flex" 1)]
|
|
["GridLayoutProxy.addColumnAtIndex" #(.addColumnAtIndex grid 0 "flex" 1)]
|
|
["GridLayoutProxy.setRow" #(.setRow grid 1 "flex" 1)]
|
|
["GridLayoutProxy.setColumn" #(.setColumn grid 1 "flex" 1)]
|
|
["GridLayoutProxy.removeRow" #(.removeRow grid 0)]
|
|
["GridLayoutProxy.removeColumn" #(.removeColumn grid 0)]
|
|
["GridLayoutProxy.remove" #(.remove grid)]
|
|
|
|
;; ---- GridCellProxy methods ----
|
|
;; (none beyond setters)
|
|
|
|
;; ---- RulerGuideProxy ----
|
|
["RulerGuideProxy.remove" #(.remove guide)]
|
|
|
|
;; ---- Context ----
|
|
["context.group" #(.group context #js [rect rect2])]
|
|
["context.ungroup" #(.ungroup context rect)]
|
|
["context.createBoolean" #(.createBoolean context "union" #js [rect rect2])]
|
|
|
|
;; ---- PageProxy ----
|
|
["page.addRulerGuide" #(.addRulerGuide page2 "vertical" 20 board)]
|
|
["page.removeRulerGuide" #(.removeRulerGuide page2 guide)]])
|
|
|
|
(t/deftest test-all-setters-rejected-on-non-active-page
|
|
(thw/with-wasm-mocks*
|
|
(fn []
|
|
(let [m (setup)]
|
|
(doseq [[label thunk] (setter-specs m)]
|
|
(t/is (throws? thunk) (str label " must be rejected on a non-active page")))))))
|
|
|
|
(t/deftest test-all-methods-rejected-on-non-active-page
|
|
(thw/with-wasm-mocks*
|
|
(fn []
|
|
(let [m (setup)]
|
|
(doseq [[label thunk] (method-specs m)]
|
|
(t/is (throws? thunk) (str label " must be rejected on a non-active page")))))))
|
|
|
|
(t/deftest test-all-setters-allowed-on-active-page
|
|
;; Sanity / value-validation anchor: every setter uses a valid value, so when
|
|
;; page2 is active none of them must throw. This guarantees the negative test's
|
|
;; throws come from the page guard and not from value validation.
|
|
(thw/with-wasm-mocks*
|
|
(fn []
|
|
(let [{:keys [store page2-id] :as m} (setup)]
|
|
(activate-page! store page2-id)
|
|
(doseq [[label thunk] (setter-specs m)]
|
|
(t/is (not (throws? thunk)) (str label " must be allowed on the active page")))))))
|
|
|