diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 1cec7db935..6b1de52126 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -193,7 +193,9 @@ :enable-inspect-styles :enable-feature-fdata-objects-map :enable-feature-render-wasm - :enable-token-import-from-library]) + :enable-token-import-from-library + :enable-render-switch + :enable-render-wasm-info]) (defn parse [& flags] diff --git a/frontend/playwright/data/logged-in-user/get-profile-wasm-renderer.json b/frontend/playwright/data/logged-in-user/get-profile-wasm-renderer.json new file mode 100644 index 0000000000..03fa91aff4 --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-profile-wasm-renderer.json @@ -0,0 +1,24 @@ +{ + "~:email": "foo@example.com", + "~:is-demo": false, + "~:auth-backend": "penpot", + "~:fullname": "Princesa Leia", + "~:modified-at": "~m1713533116365", + "~:is-active": true, + "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:is-muted": false, + "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116365", + "~:is-blocked": false, + "~:props": { + "~:nudge": { + "~:big": 10, + "~:small": 1 + }, + "~:v2-info-shown": true, + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false, + "~:renderer": "~:wasm" + } +} diff --git a/frontend/playwright/ui/pages/WasmWorkspacePage.js b/frontend/playwright/ui/pages/WasmWorkspacePage.js index 8b5139e9a0..583d25f5f9 100644 --- a/frontend/playwright/ui/pages/WasmWorkspacePage.js +++ b/frontend/playwright/ui/pages/WasmWorkspacePage.js @@ -1,16 +1,21 @@ import { expect } from "@playwright/test"; import { WorkspacePage } from "./WorkspacePage"; +export const WASM_PROFILE = "logged-in-user/get-profile-wasm-renderer.json"; + export const WASM_FLAGS = [ "enable-feature-render-wasm", "enable-render-wasm-dpr", "enable-feature-text-editor-v2", + // Default flags enable render-wasm-info; keep screenshots stable in e2e. + "disable-render-wasm-info", ]; export class WasmWorkspacePage extends WorkspacePage { static async init(page) { await super.init(page); await WasmWorkspacePage.mockConfigFlags(page, WASM_FLAGS); + await WasmWorkspacePage.mockRPC(page, "get-profile", WASM_PROFILE); await page.addInitScript(() => { document.addEventListener("penpot:wasm:loaded", () => { @@ -99,4 +104,9 @@ export class WasmWorkspacePage extends WorkspacePage { options, ); } + + async setupEmptyFile() { + await super.setupEmptyFile(); + await this.mockRPC("get-profile", WASM_PROFILE); + } } diff --git a/frontend/src/app/main/features.cljs b/frontend/src/app/main/features.cljs index 4ee041fd4d..7bfc9a8a45 100644 --- a/frontend/src/app/main/features.cljs +++ b/frontend/src/app/main/features.cljs @@ -28,19 +28,42 @@ (defn setup-wasm-features [features state] - (let [params (rt/get-params state) - wasm (get params :wasm) - renderer (when (contains? cf/flags :render-switch) - (-> state :profile :props :renderer)) - enable-wasm (or (= "true" wasm) (and (= renderer :wasm) (not= "false" wasm))) - disable-wasm (or (= "false" wasm) (and (= renderer :svg) (not= "true" wasm))) - features (cond-> features - enable-wasm (conj "render-wasm/v1") - disable-wasm (disj "render-wasm/v1"))] - ;; If wasm render is enabled text-editor/v2 must be used - (cond-> features - (contains? features "render-wasm/v1") - (conj "text-editor/v2")))) + (let [wasm-override (-> (rt/get-params state) + (get :wasm)) + + ;; When the :render-switch feature flag is active, read the user's + ;; preferred renderer from their profile settings (default :svg). + renderer (when (contains? cf/flags :render-switch) + (dm/get-in state [:profile :props :renderer] :svg)) + + features (cond + ;; Priority 1 — URL query param (?wasm=true|false) + ;; overrides everything: user profile and team flags. + (= "true" wasm-override) + (conj features "render-wasm/v1") + + (= "false" wasm-override) + (disj features "render-wasm/v1") + + ;; Priority 2 — User profile preference. + ;; If renderer is non-nil, the :render-switch flag is + ;; active and profile data has loaded. Respect the + ;; user's saved choice (:wasm or :svg). + (some? renderer) + (if (= :wasm renderer) + (conj features "render-wasm/v1") + (disj features "render-wasm/v1")) + + ;; Priority 3 — Fall back to the team-level + ;; feature set unchanged (no override). + :else + features)] + + ;; The WASM renderer requires the v2 text editor (hard dependency). + ;; Ensure it's always enabled whenever render-wasm/v1 is active. + (if (contains? features "render-wasm/v1") + (conj features "text-editor/v2") + (disj features "text-editor/v2")))) (defn get-enabled-features "An explicit lookup of enabled features for the current team" @@ -52,65 +75,15 @@ (set/union (get team :features)) (setup-wasm-features state)))) -(defn enabled-by-flags? - [{:keys [features-runtime features]} feature] - (or (contains? features-runtime feature) - (contains? features feature))) - -(defn enabled-without-migration? - [{:keys [features-runtime features]} feature] - (or (contains? features-runtime feature) - (contains? global-enabled-features feature) - (contains? features feature))) - -(defn wasm-url-override - [state] - (case (get (rt/get-params state) :wasm) - "true" true - "false" false - nil)) - -(def wasm-url-override-ref - (l/derived wasm-url-override st/state)) - -(defn- wasm-enabled? - [state] - (let [override (wasm-url-override state) - renderer (when (contains? cf/flags :render-switch) - (-> state :profile :props :renderer))] - (cond - (some? override) - override - - (contains? cf/flags :render-switch) - (case renderer - :wasm true - :svg false - ;; SVG renderer as default until profile data arrives OR if render-switch - ;; flag is disabled. - false) - - (contains? cfeat/no-migration-features "render-wasm/v1") - (enabled-without-migration? state "render-wasm/v1") - - :else - (enabled-by-flags? state "render-wasm/v1")))) - (defn active-feature? - "Given a state and feature, check if feature is enabled." + "Given a state and feature, check if feature is enabled. + Relies on the pre-computed :features set in state, which already + incorporates URL overrides, user profile preferences, team flags, + and runtime toggles via setup-wasm-features." [state feature] (assert (contains? cfeat/supported-features feature) "feature not supported") - - (cond - (= feature "render-wasm/v1") - (wasm-enabled? state) - - (contains? cfeat/no-migration-features feature) - (enabled-without-migration? state feature) - - :else - (enabled-by-flags? state feature))) + (contains? (:features state) feature)) (defn active-features? "Given a state and a set of features, check if the features are all enabled." @@ -134,26 +107,12 @@ (l/derived (l/key :features) st/state)) (defn use-feature - "A react hook that checks if feature is currently enabled" + "A react hook that checks if feature is currently enabled. + Uses the pre-computed :features set from state — the same set that + setup-wasm-features has already resolved." [feature] - (let [enabled-features (mf/deref features-ref) - wasm-override (mf/deref wasm-url-override-ref) - renderer (mf/deref (l/derived #(-> % :profile :props :renderer) st/state)) - wasm-enabled (cond - (some? wasm-override) - wasm-override - - (contains? cf/flags :render-switch) - (= renderer :wasm) - - :else - (contains? enabled-features "render-wasm/v1"))] - (cond - (= feature "render-wasm/v1") - wasm-enabled - - :else - (contains? enabled-features feature)))) + (let [enabled-features (mf/deref features-ref)] + (contains? enabled-features feature))) (defn toggle-feature "An event constructor for runtime feature toggle. @@ -204,7 +163,7 @@ ptk/EffectEvent (effect [_ state _] (let [features (get state :features)] - (if (active-feature? state "render-wasm/v1") + (if (contains? features "render-wasm/v1") (wasm/initialize true) (wasm/initialize false)) @@ -225,7 +184,7 @@ ptk/EffectEvent (effect [_ state _] (let [features (get state :features)] - (if (active-feature? state "render-wasm/v1") + (if (contains? features "render-wasm/v1") (wasm/initialize true) (wasm/initialize false)) diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index 920a79605f..50de0423fa 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -12,6 +12,7 @@ [app.config :as cf] [app.main.data.team :as dtm] [app.main.errors :as errors] + [app.main.features :as features] [app.main.repo :as rp] [app.main.router :as rt] [app.main.store :as st] @@ -147,6 +148,14 @@ [] (ptk/reify ::init-routes ptk/WatchEvent - (watch [_ _ _] - (rx/of (rt/initialize-router routes) - (rt/initialize-history on-navigate))))) + (watch [_ _ stream] + (rx/merge + (rx/of (rt/initialize-router routes) + (rt/initialize-history on-navigate)) + (->> stream + (rx/filter (ptk/type? ::rt/navigated)) + (rx/map deref) + (rx/map #(dm/get-in % [:query-params :wasm])) + (rx/buffer 2 1) + (rx/filter (fn [[v1 v2]] (not= v1 v2))) + (rx/map features/recompute-features)))))) diff --git a/frontend/test/frontend_tests/helpers/state.cljs b/frontend/test/frontend_tests/helpers/state.cljs index 9a4f4b7f2c..0582b9a41a 100644 --- a/frontend/test/frontend_tests/helpers/state.cljs +++ b/frontend/test/frontend_tests/helpers/state.cljs @@ -10,6 +10,7 @@ [app.common.schema :as sm] [app.common.test-helpers.files :as cthf] [app.main.data.workspace.layout :as layout] + [app.main.features :as features] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -18,7 +19,10 @@ :workspace-global layout/default-global :current-file-id nil :current-page-id nil - :features-team #{"components/v2"}}) + :features-team #{"components/v2"} + ;; With :render-switch enabled by default, render-wasm/v1 follows the + ;; profile renderer preference instead of the global feature flag alone. + :profile {:props {:renderer :wasm}}}) (defn- on-error [cause] @@ -34,6 +38,9 @@ :permissions {:can-edit true} :files {(:id file) file})) store (ptk/store {:state state :on-error on-error})] + ;; Unit tests skip team/workspace bootstrap; mirror team init so + ;; :features is populated the same way as features/initialize does in app. + (ptk/emit! store (features/initialize #{})) store)) (defn run-store diff --git a/frontend/test/frontend_tests/helpers/wasm.cljs b/frontend/test/frontend_tests/helpers/wasm.cljs index 5a75f65dc0..497b4c22f1 100644 --- a/frontend/test/frontend_tests/helpers/wasm.cljs +++ b/frontend/test/frontend_tests/helpers/wasm.cljs @@ -8,8 +8,9 @@ "Test helpers for mocking WASM API boundary functions. In the Node.js test environment the WASM binary is not available, - but the `render-wasm/v1` feature flag is enabled by default, so - every geometry-modifying event takes the WASM code path. + but `render-wasm/v1` is active when the test store uses a profile + with `:renderer :wasm` (see `frontend-tests.helpers.state`), so + geometry/text events take the WASM code path. This namespace provides lightweight mock implementations that let the Clojure-side logic execute normally while stubbing out every call that would touch the WASM heap.