mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
🚧 WIP Step 2, common scene code, validation
This commit is contained in:
parent
50c72a1ad9
commit
01f6eb2a7a
@ -36,7 +36,7 @@
|
||||
"^(?:backend|frontend|exporter|common)/deps.edn"
|
||||
"^(?:backend|frontend|exporter|common)/scripts/"
|
||||
"^(?:backend|frontend|exporter|common)/dev/"
|
||||
"^(?:backend|frontend|exporter|common)/test/(?!benches/)"]
|
||||
"^(?:backend|frontend|exporter|common)/test/(?!benches/|frontend_tests/benches/)"]
|
||||
|
||||
:linter-name true}
|
||||
|
||||
|
||||
@ -37,9 +37,9 @@
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"fmt:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
|
||||
"fmt:scss": "prettier -c resources/styles -c src/**/*.scss -w",
|
||||
"lint:clj": "clj-kondo --fail-level error --config-dir ../.clj-kondo --lint ../common/src src/ test/benches/",
|
||||
"lint:clj": "clj-kondo --fail-level error --config-dir ../.clj-kondo --lint ../common/src src/ test/benches/ test/frontend_tests/benches/",
|
||||
"lint:js": "exit 0",
|
||||
"lint:renderer-benchmarks": "clj-kondo --config-dir ../.clj-kondo --lint test/benches/",
|
||||
"lint:renderer-benchmarks": "clj-kondo --config-dir ../.clj-kondo --lint test/benches/ test/frontend_tests/benches/",
|
||||
"lint:scss": "pnpm exec stylelint '{src,resources}/**/*.scss'",
|
||||
"build:test": "pnpm run build:wasm && clojure -M:dev:shadow-cljs compile test",
|
||||
"test": "pnpm run build:test && node target/tests/test.js",
|
||||
|
||||
198
frontend/test/benches/render_wasm/scenes/common.cljs
Normal file
198
frontend/test/benches/render_wasm/scenes/common.cljs
Normal file
@ -0,0 +1,198 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
|
||||
(ns benches.render-wasm.scenes.common
|
||||
"Utilities shared by generated and saved-page fixtures.
|
||||
|
||||
A fixture instance is
|
||||
`{:objects {uuid shape ...} :refs {label uuid ...}}`.
|
||||
|
||||
- `:objects` is a Penpot page object map.
|
||||
- `:refs` is an instance-local lookup from custom labels to object uuids.
|
||||
|
||||
A fixture instance is just static data (a 'snapshot'). It is independent of
|
||||
camera, operations, WASM buffers and renderer calls, and it never mutates after
|
||||
upload.
|
||||
|
||||
Validation uses penpot's facilities, so that benchmark fixtures follow editor
|
||||
semantics. We do check reachability from the root here because neither the
|
||||
editor validator nor file loading guarantees it.
|
||||
|
||||
Authoring and scoped construction are ticket17; containers ticket18/19;
|
||||
case declarations ticket20; saved-page loading ticket21.
|
||||
|
||||
Which shape types may own children is container semantics owned by
|
||||
ticket18/19; this contract only checks hierarchy consistency."
|
||||
(:require
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.validate :as cfv]
|
||||
[app.common.geom.shapes.tree-seq :as gts]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
(def schema:instance
|
||||
"Envelope schema for a fixture snapshot."
|
||||
[:map {:title "RendererBenchmarkFixture"}
|
||||
[:objects ctp/schema:objects]
|
||||
[:refs [:map-of :any ::sm/uuid]]])
|
||||
|
||||
(defn- fail!
|
||||
[hint data]
|
||||
(throw (ex-info (str "Invalid fixture instance: " hint)
|
||||
(merge {:type ::invalid-fixture
|
||||
:hint hint}
|
||||
data))))
|
||||
|
||||
(defn- check-schema!
|
||||
[instance]
|
||||
(when-not (sm/validate schema:instance instance)
|
||||
(fail! "instance does not match the fixture schema"
|
||||
{::sm/explain (sm/explain schema:instance instance)})))
|
||||
|
||||
(defn- check-root!
|
||||
[instance]
|
||||
(let [root (get-in instance [:objects uuid/zero])]
|
||||
(when-not (and (some? root) (cfh/root? root))
|
||||
(fail! "root object must be a frame at uuid/zero"
|
||||
{:id uuid/zero}))
|
||||
(when-not (= uuid/zero (:parent-id root))
|
||||
(fail! "root object :parent-id must be uuid/zero"
|
||||
{:id uuid/zero
|
||||
:parent-id (:parent-id root)}))
|
||||
(when-not (= uuid/zero (:frame-id root))
|
||||
(fail! "root object :frame-id must be uuid/zero"
|
||||
{:id uuid/zero
|
||||
:frame-id (:frame-id root)}))))
|
||||
|
||||
(defn- check-object-ids!
|
||||
[objects]
|
||||
(doseq [[id shape] objects]
|
||||
(when-not (= id (:id shape))
|
||||
(fail! "object key does not match the shape :id"
|
||||
{:key id
|
||||
:shape-id (:id shape)}))))
|
||||
|
||||
(defn- check-child-links!
|
||||
"Guard the reachability walk against malformed child links.
|
||||
|
||||
`:shapes` must be a vector when present, and every listed child that
|
||||
exists must point back at the listing shape. This keeps each child with a
|
||||
single parent, so the descendant walk visits a forest instead of fanning
|
||||
out over shared children or looping on a cycle. Production validation
|
||||
repeats these checks for reachable shapes; this one only protects our own
|
||||
traversal order."
|
||||
[objects]
|
||||
(doseq [[id shape] objects
|
||||
:let [children (:shapes shape)]]
|
||||
(when (some? children)
|
||||
(when-not (vector? children)
|
||||
(fail! "shape :shapes must be a vector"
|
||||
{:id id}))
|
||||
(doseq [child-id children
|
||||
:let [child (get objects child-id)]
|
||||
:when (some? child)]
|
||||
(when-not (= id (:parent-id child))
|
||||
(fail! "shape :shapes lists a child with a different :parent-id"
|
||||
{:id id
|
||||
:child child-id}))))))
|
||||
|
||||
(defn- check-reachability!
|
||||
"Check the root graph through the cycle-safe descendant walk.
|
||||
|
||||
Runs `check-child-links!` first, which keeps the walk linear: one parent
|
||||
per child and vector `:shapes` mean the walk cannot fan out over shared
|
||||
children or loop on a cycle.
|
||||
|
||||
Detects missing child ids, objects reachable more than once (shared
|
||||
children or cycles), detached subtrees and a root listed as its own
|
||||
descendant. Runs before the production validator, whose walk follows
|
||||
`:shapes` links and would loop on a reachable cycle."
|
||||
[objects]
|
||||
(check-child-links! objects)
|
||||
(let [ids (cfh/get-children-ids objects uuid/zero)
|
||||
seen (set ids)]
|
||||
|
||||
(when (contains? seen uuid/zero)
|
||||
(fail! "root must not be a descendant of itself"
|
||||
{:id uuid/zero}))
|
||||
|
||||
(doseq [id ids]
|
||||
(when-not (contains? objects id)
|
||||
(fail! "shape :shapes references a missing object"
|
||||
{:child id})))
|
||||
|
||||
(when-let [dup (first (for [[id n] (frequencies ids) :when (> n 1)] id))]
|
||||
(fail! "an object is reachable more than once"
|
||||
{:id dup}))
|
||||
|
||||
(doseq [id (keys objects)
|
||||
:when (not= id uuid/zero)]
|
||||
(when-not (contains? seen id)
|
||||
(fail! "object is not reachable from the root"
|
||||
{:id id})))))
|
||||
|
||||
(defn- check-referential-integrity!
|
||||
"Run the production referential validation over the reachable graph.
|
||||
|
||||
`cfv/validate-shape` checks parent/child links, frame ids and geometry
|
||||
with editor semantics. The file and page only carry the uuids the
|
||||
validation error schema requires; fixtures contain no component shapes.
|
||||
Unreachable objects never reach this walk; `check-reachability!` owns
|
||||
them."
|
||||
[instance]
|
||||
(let [file {:id uuid/zero :data {}}
|
||||
page {:id uuid/zero :objects (:objects instance)}
|
||||
errors (cfv/validate-shape uuid/zero file page {})]
|
||||
(when-let [{:keys [hint] :as error} (first errors)]
|
||||
(fail! hint (select-keys error [:code :shape-id :args])))))
|
||||
|
||||
(defn- check-refs!
|
||||
[instance]
|
||||
(let [objects (:objects instance)]
|
||||
(doseq [[label target] (:refs instance)]
|
||||
(when-not (contains? objects target)
|
||||
(fail! "ref target is not present in :objects"
|
||||
{:label label
|
||||
:target target})))))
|
||||
|
||||
(defn validate!
|
||||
"Validates a fixture instance and returns it unchanged.
|
||||
|
||||
Throws `ex-info` with `{:type ::invalid-fixture :hint ...}` when the
|
||||
snapshot is invalid. Schema failures also carry `::sm/explain`.
|
||||
|
||||
Reachability runs before the production validator, because that walk
|
||||
follows `:shapes` links and would not terminate on a reachable cycle.
|
||||
|
||||
Explicit `throw`s are used instead of assertions so the checks survive
|
||||
elided assertions."
|
||||
[instance]
|
||||
(check-schema! instance)
|
||||
(let [objects (:objects instance)]
|
||||
(check-root! instance)
|
||||
(check-object-ids! objects)
|
||||
(check-reachability! objects)
|
||||
(check-referential-integrity! instance)
|
||||
(check-refs! instance))
|
||||
instance)
|
||||
|
||||
(defn upload-order
|
||||
"Returns the instance objects as a vector in parent-before-child order.
|
||||
|
||||
The root comes first and each shape's `:shapes` order is followed, using
|
||||
the shared tree traversal. That traversal reverses children of flex
|
||||
containers marked reverse; upload order carries no renderer semantics
|
||||
because a shape's child list is uploaded from its own `:shapes` vector.
|
||||
|
||||
Assumes a validated instance: every child id exists."
|
||||
[instance]
|
||||
(vec (gts/get-children-seq uuid/zero (:objects instance))))
|
||||
|
||||
(defn ref-id
|
||||
"Resolves an author label to the referenced object uuid, or nil."
|
||||
[instance label]
|
||||
(get-in instance [:refs label]))
|
||||
317
frontend/test/frontend_tests/benches/fixture_test.cljs
Normal file
317
frontend/test/frontend_tests/benches/fixture_test.cljs
Normal file
@ -0,0 +1,317 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.benches.fixture-test
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[benches.render-wasm.scenes.common :as fixture]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(defn- sample-instance
|
||||
"Simple tiny fixture: root frame with one rectangle."
|
||||
[]
|
||||
(let [rect-id (uuid/custom 1 1)
|
||||
rect (cts/setup-shape {:id rect-id :type :rect :name "Rect"
|
||||
:x 32 :y 48 :width 200 :height 120})
|
||||
root (cts/setup-shape {:id uuid/zero :type :frame :name "Root Frame"
|
||||
:x 0 :y 0 :width 1920 :height 1080
|
||||
:shapes [rect-id]})]
|
||||
{:objects {uuid/zero root, rect-id rect}
|
||||
:refs {:rect rect-id}}))
|
||||
|
||||
(defn- nested-instance
|
||||
"Root with two frames; frame A with two rectangles, frame B with one."
|
||||
[]
|
||||
(let [a-id (uuid/custom 2 1)
|
||||
b-id (uuid/custom 2 2)
|
||||
a1-id (uuid/custom 2 3)
|
||||
a2-id (uuid/custom 2 4)
|
||||
b1-id (uuid/custom 2 5)
|
||||
frame-a (cts/setup-shape {:id a-id :type :frame :name "A"
|
||||
:x 0 :y 0 :width 100 :height 100
|
||||
:parent-id uuid/zero :frame-id uuid/zero
|
||||
:shapes [a1-id a2-id]})
|
||||
frame-b (cts/setup-shape {:id b-id :type :frame :name "B"
|
||||
:x 200 :y 0 :width 100 :height 100
|
||||
:parent-id uuid/zero :frame-id uuid/zero
|
||||
:shapes [b1-id]})
|
||||
rect-a1 (cts/setup-shape {:id a1-id :type :rect :name "A1"
|
||||
:x 10 :y 10 :width 10 :height 10
|
||||
:parent-id a-id :frame-id a-id})
|
||||
rect-a2 (cts/setup-shape {:id a2-id :type :rect :name "A2"
|
||||
:x 30 :y 10 :width 10 :height 10
|
||||
:parent-id a-id :frame-id a-id})
|
||||
rect-b1 (cts/setup-shape {:id b1-id :type :rect :name "B1"
|
||||
:x 210 :y 10 :width 10 :height 10
|
||||
:parent-id b-id :frame-id b-id})
|
||||
root (cts/setup-shape {:id uuid/zero :type :frame :name "Root Frame"
|
||||
:x 0 :y 0 :width 1920 :height 1080
|
||||
:parent-id uuid/zero :frame-id uuid/zero
|
||||
:shapes [a-id b-id]})]
|
||||
{:objects {uuid/zero root
|
||||
a-id frame-a
|
||||
b-id frame-b
|
||||
a1-id rect-a1
|
||||
a2-id rect-a2
|
||||
b1-id rect-b1}
|
||||
:refs {:a a-id
|
||||
:a1 a1-id
|
||||
:b b-id}}))
|
||||
|
||||
(defn- validation-error
|
||||
"Returns the ex-data of the validation failure, or nil when it passes."
|
||||
[instance]
|
||||
(try
|
||||
(fixture/validate! instance)
|
||||
nil
|
||||
(catch :default cause
|
||||
(ex-data cause))))
|
||||
|
||||
(t/deftest canonical-example-is-valid
|
||||
(t/testing "validate! returns the same instance"
|
||||
(let [instance (sample-instance)]
|
||||
(t/is (identical? instance (fixture/validate! instance)))))
|
||||
|
||||
(t/testing "the envelope schema accepts the example"
|
||||
(t/is (true? (sm/validate fixture/schema:instance (sample-instance))))))
|
||||
|
||||
(t/deftest nested-hierarchy-is-valid
|
||||
(let [instance (nested-instance)]
|
||||
(t/is (identical? instance (fixture/validate! instance)))))
|
||||
|
||||
(t/deftest root-only-instance-is-valid
|
||||
(let [root (cts/setup-shape {:id uuid/zero :type :frame :name "Root Frame"
|
||||
:x 0 :y 0 :width 100 :height 100
|
||||
:parent-id uuid/zero :frame-id uuid/zero
|
||||
:shapes []})
|
||||
instance {:objects {uuid/zero root} :refs {}}]
|
||||
(t/is (identical? instance (fixture/validate! instance)))
|
||||
(t/is (= [uuid/zero] (mapv :id (fixture/upload-order instance))))))
|
||||
|
||||
(t/deftest upload-order-starts-at-root
|
||||
(let [instance (sample-instance)
|
||||
ids (mapv :id (fixture/upload-order instance))]
|
||||
(t/is (= uuid/zero (first ids)))
|
||||
(t/is (= [uuid/zero (fixture/ref-id instance :rect)] ids))))
|
||||
|
||||
(t/deftest upload-order-is-parent-before-child
|
||||
(let [instance (nested-instance)
|
||||
order (fixture/upload-order instance)
|
||||
names (mapv :name order)
|
||||
position (into {} (map-indexed (fn [index shape] [(:id shape) index]) order))]
|
||||
(t/is (= ["Root Frame" "A" "A1" "A2" "B" "B1"] names))
|
||||
(t/is (every? (fn [shape]
|
||||
(or (= uuid/zero (:id shape))
|
||||
(< (position (:parent-id shape))
|
||||
(position (:id shape)))))
|
||||
order))))
|
||||
|
||||
(t/deftest upload-order-follows-flex-reverse
|
||||
(let [frame-id (uuid/custom 8 1)
|
||||
a-id (uuid/custom 8 2)
|
||||
b-id (uuid/custom 8 3)
|
||||
frame (cts/setup-shape {:id frame-id :type :frame :name "Flex"
|
||||
:x 0 :y 0 :width 200 :height 100
|
||||
:parent-id uuid/zero :frame-id uuid/zero
|
||||
:layout :flex :layout-flex-dir :row-reverse
|
||||
:shapes [a-id b-id]})
|
||||
shape-a (cts/setup-shape {:id a-id :type :rect :name "A"
|
||||
:x 0 :y 0 :width 10 :height 10
|
||||
:parent-id frame-id :frame-id frame-id})
|
||||
shape-b (cts/setup-shape {:id b-id :type :rect :name "B"
|
||||
:x 20 :y 0 :width 10 :height 10
|
||||
:parent-id frame-id :frame-id frame-id})
|
||||
root (cts/setup-shape {:id uuid/zero :type :frame :name "Root Frame"
|
||||
:x 0 :y 0 :width 1920 :height 1080
|
||||
:parent-id uuid/zero :frame-id uuid/zero
|
||||
:shapes [frame-id]})
|
||||
instance {:objects {uuid/zero root
|
||||
frame-id frame
|
||||
a-id shape-a
|
||||
b-id shape-b}
|
||||
:refs {}}]
|
||||
(t/is (identical? instance (fixture/validate! instance)))
|
||||
(t/is (= ["Root Frame" "Flex" "B" "A"]
|
||||
(mapv :name (fixture/upload-order instance))))))
|
||||
|
||||
(t/deftest ref-id-resolves-labels
|
||||
(let [instance (sample-instance)]
|
||||
(t/is (= (uuid/custom 1 1) (fixture/ref-id instance :rect)))
|
||||
(t/is (nil? (fixture/ref-id instance :missing)))))
|
||||
|
||||
(t/deftest missing-root-is-rejected
|
||||
(let [instance (sample-instance)
|
||||
data (validation-error (update instance :objects dissoc uuid/zero))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= uuid/zero (:id data)))))
|
||||
|
||||
(t/deftest non-frame-root-is-rejected
|
||||
(let [instance (sample-instance)
|
||||
data (validation-error (assoc-in instance [:objects uuid/zero :type] :rect))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= uuid/zero (:id data)))))
|
||||
|
||||
(t/deftest root-with-non-zero-parent-id-is-rejected
|
||||
(let [instance (sample-instance)
|
||||
data (validation-error
|
||||
(assoc-in instance [:objects uuid/zero :parent-id] (uuid/custom 9 9)))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= uuid/zero (:id data)))))
|
||||
|
||||
(t/deftest object-key-must-match-shape-id
|
||||
(let [instance (sample-instance)
|
||||
rect (get-in instance [:objects (fixture/ref-id instance :rect)])
|
||||
data (validation-error (assoc-in instance [:objects (uuid/custom 7 7)] rect))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= (:id rect) (:shape-id data)))))
|
||||
|
||||
(t/deftest missing-child-is-rejected
|
||||
(let [instance (sample-instance)
|
||||
bogus (uuid/custom 3 3)
|
||||
data (validation-error
|
||||
(assoc-in instance [:objects uuid/zero :shapes] [bogus]))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= bogus (:child data)))))
|
||||
|
||||
(t/deftest child-not-listed-by-parent-is-rejected
|
||||
(let [instance (sample-instance)
|
||||
rect-id (fixture/ref-id instance :rect)
|
||||
data (validation-error (assoc-in instance [:objects uuid/zero :shapes] []))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= "object is not reachable from the root" (:hint data)))
|
||||
(t/is (= rect-id (:id data)))))
|
||||
|
||||
(t/deftest mismatched-parent-id-is-rejected
|
||||
(let [instance (sample-instance)
|
||||
rect-id (fixture/ref-id instance :rect)
|
||||
data (validation-error
|
||||
(assoc-in instance [:objects rect-id :parent-id] (uuid/custom 9 7)))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= "shape :shapes lists a child with a different :parent-id" (:hint data)))
|
||||
(t/is (= rect-id (:child data)))))
|
||||
|
||||
(t/deftest duplicate-child-ids-are-rejected
|
||||
(let [instance (sample-instance)
|
||||
rect-id (fixture/ref-id instance :rect)
|
||||
data (validation-error
|
||||
(assoc-in instance [:objects uuid/zero :shapes] [rect-id rect-id]))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= "an object is reachable more than once" (:hint data)))
|
||||
(t/is (= rect-id (:id data)))))
|
||||
|
||||
(t/deftest shape-listing-itself-is-rejected
|
||||
(let [instance (sample-instance)
|
||||
data (validation-error
|
||||
(assoc-in instance [:objects uuid/zero :shapes] [uuid/zero]))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= "root must not be a descendant of itself" (:hint data)))
|
||||
(t/is (= uuid/zero (:id data)))))
|
||||
|
||||
(t/deftest detached-cycle-is-rejected
|
||||
(let [c1-id (uuid/custom 5 1)
|
||||
c2-id (uuid/custom 5 2)
|
||||
shape1 (cts/setup-shape {:id c1-id :type :rect :name "C1"
|
||||
:x 0 :y 0 :width 10 :height 10
|
||||
:parent-id c2-id :frame-id c1-id
|
||||
:shapes [c2-id]})
|
||||
shape2 (cts/setup-shape {:id c2-id :type :rect :name "C2"
|
||||
:x 0 :y 0 :width 10 :height 10
|
||||
:parent-id c1-id :frame-id c2-id
|
||||
:shapes [c1-id]})
|
||||
data (validation-error
|
||||
(assoc (sample-instance) :objects
|
||||
(assoc (get (sample-instance) :objects)
|
||||
c1-id shape1
|
||||
c2-id shape2)))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= "object is not reachable from the root" (:hint data)))
|
||||
(t/is (contains? #{c1-id c2-id} (:id data)))))
|
||||
|
||||
(t/deftest reachable-cycle-is-rejected
|
||||
(let [a-id (uuid/custom 4 1)
|
||||
b-id (uuid/custom 4 2)
|
||||
shape-a (cts/setup-shape {:id a-id :type :rect :name "A"
|
||||
:x 0 :y 0 :width 10 :height 10
|
||||
:parent-id uuid/zero :frame-id uuid/zero
|
||||
:shapes [b-id]})
|
||||
shape-b (cts/setup-shape {:id b-id :type :rect :name "B"
|
||||
:x 0 :y 0 :width 10 :height 10
|
||||
:parent-id a-id :frame-id uuid/zero
|
||||
:shapes [a-id]})
|
||||
instance (-> (sample-instance)
|
||||
(assoc-in [:objects uuid/zero :shapes] [a-id])
|
||||
(assoc-in [:objects a-id] shape-a)
|
||||
(assoc-in [:objects b-id] shape-b))
|
||||
data (validation-error instance)]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= "shape :shapes lists a child with a different :parent-id" (:hint data)))
|
||||
(t/is (= a-id (:child data)))))
|
||||
|
||||
(t/deftest non-vector-children-are-rejected
|
||||
(let [instance (sample-instance)
|
||||
rect-id (fixture/ref-id instance :rect)
|
||||
data (validation-error (assoc-in instance [:objects rect-id :shapes] 5))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= "shape :shapes must be a vector" (:hint data)))
|
||||
(t/is (= rect-id (:id data)))))
|
||||
|
||||
(t/deftest frame-id-mismatch-is-rejected
|
||||
(let [instance (nested-instance)
|
||||
a1-id (fixture/ref-id instance :a1)
|
||||
data (validation-error
|
||||
(assoc-in instance [:objects a1-id :frame-id] uuid/zero))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= :invalid-frame (:code data)))
|
||||
(t/is (= a1-id (:shape-id data)))))
|
||||
|
||||
(t/deftest missing-ref-target-is-rejected
|
||||
(let [instance (sample-instance)
|
||||
data (validation-error
|
||||
(assoc-in instance [:refs :rect] (uuid/custom 6 6)))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= :rect (:label data)))))
|
||||
|
||||
(t/deftest non-uuid-ref-value-is-rejected
|
||||
(let [data (validation-error (assoc-in (sample-instance) [:refs :rect] "not-a-uuid"))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (some? (::sm/explain data)))))
|
||||
|
||||
(t/deftest missing-refs-key-is-rejected
|
||||
(let [data (validation-error (dissoc (sample-instance) :refs))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (some? (::sm/explain data)))))
|
||||
|
||||
(t/deftest non-canonical-shape-is-rejected
|
||||
(let [instance (sample-instance)
|
||||
rect-id (fixture/ref-id instance :rect)
|
||||
data (validation-error (assoc-in instance [:objects rect-id] {:id rect-id}))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (some? (::sm/explain data)))))
|
||||
|
||||
(t/deftest malformed-envelopes-are-rejected
|
||||
(t/testing "empty objects map"
|
||||
(let [data (validation-error {:objects {} :refs {}})]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (= uuid/zero (:id data)))))
|
||||
|
||||
(t/testing "nil instance"
|
||||
(let [data (validation-error nil)]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (some? (::sm/explain data)))))
|
||||
|
||||
(t/testing "nil shape value"
|
||||
(let [instance (sample-instance)
|
||||
rect-id (fixture/ref-id instance :rect)
|
||||
data (validation-error (assoc-in instance [:objects rect-id] nil))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (some? (::sm/explain data)))))
|
||||
|
||||
(t/testing "nil ref value"
|
||||
(let [data (validation-error (assoc-in (sample-instance) [:refs :rect] nil))]
|
||||
(t/is (= ::fixture/invalid-fixture (:type data)))
|
||||
(t/is (some? (::sm/explain data))))))
|
||||
@ -175,6 +175,7 @@
|
||||
|
||||
(def test-namespaces
|
||||
['frontend-tests.basic-shapes-test
|
||||
'frontend-tests.benches.fixture-test
|
||||
'frontend-tests.code-gen-style-test
|
||||
'frontend-tests.composable-tests.comp.sync-test
|
||||
'frontend-tests.copy-as-svg-test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user