mirror of
https://github.com/penpot/penpot.git
synced 2026-09-18 18:06:14 +00:00
Read the route from state in the navigated effect instead of the closed-over match, and build the href base from cf/public-uri instead of location.pathname, via the new pure mirrored-href helper. Drop the legacy path-param fallback from match->context-params and remove the legacy hash routes (viewer-legacy, workspace-legacy, dashboard-legacy-*) with their redirect components; old URLs now resolve to not-found. render-sprite is kept. Update the link-preview subsystem doc and extend router tests (subpath base, query clearing, state-as-source). AI-assisted-by: muse-spark-1.3-contributor
159 lines
6.5 KiB
Clojure
159 lines
6.5 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.router-test
|
|
(:require
|
|
[app.common.uri :as u]
|
|
[app.config :as cf]
|
|
[app.main.router :as rt]
|
|
[app.util.globals :as globals]
|
|
[cljs.test :as t :include-macros true]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
(t/deftest match-context-params-file-link
|
|
;; Workspace and viewer links only mirror the file-id.
|
|
(let [match {:query-params {:team-id "team-1"
|
|
:file-id "file-1"
|
|
:page-id "page-1"}}]
|
|
(t/is (= {:file-id "file-1"}
|
|
(rt/match->context-params match)))))
|
|
|
|
(t/deftest match-context-params-project-link
|
|
(let [match {:query-params {:team-id "team-1"
|
|
:project-id "project-1"}}]
|
|
(t/is (= {:team-id "team-1"
|
|
:project-id "project-1"}
|
|
(rt/match->context-params match)))))
|
|
|
|
(t/deftest match-context-params-team-link
|
|
(let [match {:query-params {:team-id "team-1"}}]
|
|
(t/is (= {:team-id "team-1"}
|
|
(rt/match->context-params match)))))
|
|
|
|
(t/deftest match-context-params-no-context
|
|
(let [match {:query-params {:token "some-token"}}]
|
|
(t/is (nil? (rt/match->context-params match)))))
|
|
|
|
(t/deftest match-context-params-project-link-without-team
|
|
;; Without a team-id the raw map keeps a nil team-id; the nil is dropped
|
|
;; later by the query-string serialization, not here.
|
|
(let [match {:query-params {:project-id "project-1"}}]
|
|
(t/is (= {:team-id nil
|
|
:project-id "project-1"}
|
|
(rt/match->context-params match)))))
|
|
|
|
(t/deftest match-context-params-file-beats-project-and-team
|
|
;; With file, project and team ids present, only the file-id is mirrored.
|
|
(let [match {:query-params {:team-id "team-1"
|
|
:project-id "project-1"
|
|
:file-id "file-1"}}]
|
|
(t/is (= {:file-id "file-1"}
|
|
(rt/match->context-params match)))))
|
|
|
|
(t/deftest match-context-params-repeated-key
|
|
;; A repeated query key arrives as a vector; the last value wins.
|
|
(let [match {:query-params {:file-id ["file-old" "file-1"]}}]
|
|
(t/is (= {:file-id "file-1"}
|
|
(rt/match->context-params match)))))
|
|
|
|
(defn- with-stubbed-browser
|
|
"Run `thunk` with the `globals/location` mock props and a recording
|
|
`js/history.replaceState`. Restores both originals afterwards: the
|
|
location mock is shared across tests and `js/history` may not exist
|
|
in the test environment at all."
|
|
[{:keys [pathname search hash href]} replace-calls thunk]
|
|
(let [loc globals/location
|
|
old-pathname (.-pathname loc)
|
|
old-search (.-search loc)
|
|
old-hash (.-hash loc)
|
|
old-href (.-href loc)
|
|
old-history (.-history js/globalThis)]
|
|
(set! (.-pathname loc) pathname)
|
|
(set! (.-search loc) search)
|
|
(set! (.-hash loc) hash)
|
|
(set! (.-href loc) href)
|
|
(set! (.-history js/globalThis)
|
|
#js {:replaceState (fn [_ _ url] (swap! replace-calls conj url))})
|
|
(try
|
|
(thunk)
|
|
(finally
|
|
(set! (.-pathname loc) old-pathname)
|
|
(set! (.-search loc) old-search)
|
|
(set! (.-hash loc) old-hash)
|
|
(set! (.-href loc) old-href)
|
|
(set! (.-history js/globalThis) old-history)))))
|
|
|
|
(t/deftest mirrored-href-subpath-base
|
|
;; The base comes from cf/public-uri, so subpath deployments keep
|
|
;; their prefix.
|
|
(t/is (= "/penpot/?file-id=file-1#/workspace?file-id=file-1"
|
|
(rt/mirrored-href {:file-id "file-1"} "#/workspace?file-id=file-1" "/penpot/"))))
|
|
|
|
(t/deftest mirrored-href-no-context
|
|
;; Routes without context clear the pre-fragment query.
|
|
(t/is (= "/#/auth/login"
|
|
(rt/mirrored-href nil "#/auth/login" "/"))))
|
|
|
|
(defn- emit-navigated
|
|
"Run the `navigated` effect with `match` stored as the state route,
|
|
pinning `cf/public-uri` so the test does not depend on the test-env
|
|
globals. The closed-over match is deliberately empty to prove the
|
|
effect reads the route from the state, not from the closure."
|
|
[match public-uri]
|
|
(with-redefs [cf/public-uri (u/uri public-uri)]
|
|
(ptk/effect (rt/navigated {} false) {:route match} nil)))
|
|
|
|
(t/deftest navigated-mirrors-context-on-change
|
|
;; New context in the state route triggers exactly one mirrored write.
|
|
(let [calls (atom [])]
|
|
(with-stubbed-browser
|
|
{:pathname "/" :search "" :hash "#/workspace?file-id=file-1" :href "http://localhost/"}
|
|
calls
|
|
(fn []
|
|
(emit-navigated {:query-params {:file-id "file-1"}} "http://localhost/")
|
|
(t/is (= ["/?file-id=file-1#/workspace?file-id=file-1"] @calls))))))
|
|
|
|
(t/deftest navigated-skips-write-when-mirrored
|
|
;; When the URL already carries the mirrored context, nothing is written.
|
|
(let [calls (atom [])]
|
|
(with-stubbed-browser
|
|
{:pathname "/" :search "?file-id=file-1" :hash "#/workspace?file-id=file-1" :href "http://localhost/?file-id=file-1#/workspace?file-id=file-1"}
|
|
calls
|
|
(fn []
|
|
(emit-navigated {:query-params {:file-id "file-1"}} "http://localhost/")
|
|
(t/is (= [] @calls))))))
|
|
|
|
(t/deftest navigated-strips-stale-context
|
|
;; A stale pre-fragment query is replaced with the current context.
|
|
(let [calls (atom [])]
|
|
(with-stubbed-browser
|
|
{:pathname "/" :search "?file-id=old" :hash "#/dashboard/recent?team-id=team-1" :href "http://localhost/?file-id=old#/dashboard/recent?team-id=team-1"}
|
|
calls
|
|
(fn []
|
|
(emit-navigated {:query-params {:team-id "team-1"}} "http://localhost/")
|
|
(t/is (= ["/?team-id=team-1#/dashboard/recent?team-id=team-1"] @calls))))))
|
|
|
|
(t/deftest navigated-clears-query-without-context
|
|
;; Routes without context clear a stale pre-fragment query.
|
|
(let [calls (atom [])]
|
|
(with-stubbed-browser
|
|
{:pathname "/" :search "?file-id=old" :hash "#/auth/login" :href "http://localhost/?file-id=old#/auth/login"}
|
|
calls
|
|
(fn []
|
|
(emit-navigated {:query-params {:token "some-token"}} "http://localhost/")
|
|
(t/is (= ["/#/auth/login"] @calls))))))
|
|
|
|
(t/deftest navigated-keeps-subpath-base
|
|
;; Under a subpath deployment the mirrored href keeps the prefix
|
|
;; from cf/public-uri.
|
|
(let [calls (atom [])]
|
|
(with-stubbed-browser
|
|
{:pathname "/penpot/" :search "" :hash "#/workspace?file-id=file-1" :href "http://localhost/penpot/"}
|
|
calls
|
|
(fn []
|
|
(emit-navigated {:query-params {:file-id "file-1"}} "http://localhost/penpot/")
|
|
(t/is (= ["/penpot/?file-id=file-1#/workspace?file-id=file-1"] @calls))))))
|