diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c6a455c6..66809b529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ We use [Break Versioning][breakver]. The version numbers follow a `. value` request coercion function") - (-response-coercer [this model] "Returns a `value format => value` response coercion function")) + (-response-coercer [this model] "Returns a `value format => value` response coercion function") + (-query-string-coercer [this model] "Returns a `value => value` query string coercion function")) #?(:clj (defmethod print-method ::coercion [coercion ^Writer w] @@ -219,3 +221,33 @@ [match] (if-let [coercers (-> match :result :coerce)] (coerce-request coercers match))) + +(defn coerce-query-params + "Uses an input schema and coercion implementation from the given match to + encode query-parameters map. + + If no match, no input schema or coercion implementation, just returns the + original parameters map." + [match query-params] + (when query-params + (let [coercion (-> match :data :coercion) + schema (when coercion + (-compile-model coercion (-> match :data :parameters :query) nil)) + coercer (when (and schema coercion) + (-query-string-coercer coercion schema))] + (if coercer + (let [result (coercer query-params :default)] + (if (error? result) + (throw (ex-info (str "Query parameters coercion failed") + result)) + result)) + query-params)))) + +(defn match->path + "Create routing path from given match and optional query-parameters map. + + Query-parameters are encoded using the input schema and coercion implementation." + ([match] + (r/match->path match)) + ([match query-params] + (r/match->path match (coerce-query-params match query-params)))) diff --git a/modules/reitit-core/src/reitit/core.cljc b/modules/reitit-core/src/reitit/core.cljc index 95633aecc..58893e775 100644 --- a/modules/reitit-core/src/reitit/core.cljc +++ b/modules/reitit-core/src/reitit/core.cljc @@ -68,10 +68,12 @@ (:template match) (:required match) path-params))))) (defn match->path + "Create routing path from given match and optional query-parameters map." ([match] (match->path match nil)) ([match query-params] - (some-> match :path (cond-> (seq query-params) (str "?" (impl/query-string query-params)))))) + (some-> match :path (cond-> (seq query-params) + (str "?" (impl/query-string query-params)))))) ;; ;; Different routers diff --git a/modules/reitit-frontend/src/reitit/frontend.cljs b/modules/reitit-frontend/src/reitit/frontend.cljs index bc5ed8514..f6acaef9b 100644 --- a/modules/reitit-frontend/src/reitit/frontend.cljs +++ b/modules/reitit-frontend/src/reitit/frontend.cljs @@ -40,14 +40,16 @@ (defn ^{:see-also ["reitit.core/match->path"]} match->path - "Create routing path from given match and optional query-string map and - optional fragment string." + "Create routing path from given match and optional query-parameters map and + optional fragment string. + + Query-parameters are encoded using the input schema and coercion implementation." ([match] (match->path match nil nil)) ([match query-params] (match->path match query-params nil)) ([match query-params fragment] - (when-let [path (r/match->path match query-params)] + (when-let [path (coercion/match->path match query-params)] (cond-> path (and fragment (seq fragment)) (str "#" (impl/form-encode fragment)))))) diff --git a/modules/reitit-frontend/src/reitit/frontend/easy.cljs b/modules/reitit-frontend/src/reitit/frontend/easy.cljs index b50bbb48a..c68781947 100644 --- a/modules/reitit-frontend/src/reitit/frontend/easy.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/easy.cljs @@ -48,9 +48,10 @@ The URL is formatted using Reitit frontend history handler, so using it with anchor element href will correctly trigger route change event. - Note: currently collections in query-parameters are encoded as field-value - pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them - differently, convert the collections to strings first." + By default currently collections in query parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can + either use Malli coercion to encode values, or just turn the values to strings + before calling the function." ([name] (rfh/href @history name nil nil nil)) ([name path-params] @@ -69,9 +70,10 @@ Will also trigger on-navigate callback on Reitit frontend History handler. - Note: currently collections in query parameters are encoded as field-value - pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them - differently, convert the collections to strings first. + By default currently collections in query parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can + either use Malli coercion to encode values, or just turn the values to strings + before calling the function. See also: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState" @@ -93,9 +95,10 @@ Will also trigger on-navigate callback on Reitit frontend History handler. - Note: currently collections in query-parameters are encoded as field-value - pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them - differently, convert the collections to strings first. + By default currently collections in query parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can + either use Malli coercion to encode values, or just turn the values to strings + before calling the function. See also: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" @@ -122,9 +125,10 @@ Will also trigger on-navigate callback on Reitit frontend History handler. - Note: currently collections in query-parameters are encoded as field-value - pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them - differently, convert the collections to strings first. + By default currently collections in query parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can + either use Malli coercion to encode values, or just turn the values to strings + before calling the function. See also: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState @@ -142,8 +146,11 @@ New query params can be given as a map, or a function taking the old params and returning the new modified params. - Note: The query parameter values aren't coereced, so the - update fn will see string values for all query params." + The current path is matched against the routing tree, and the match data + (schema, coercion) is used to encode the query parameters. + If the current path doesn't match any route, the query parameters + are parsed from the path without coercion and new values + are also stored without coercion encoding." ([new-query-or-update-fn] (rfh/set-query @history new-query-or-update-fn)) ([new-query-or-update-fn {:keys [replace] :as opts}] diff --git a/modules/reitit-frontend/src/reitit/frontend/history.cljs b/modules/reitit-frontend/src/reitit/frontend/history.cljs index 86043ddc3..adb446f9a 100644 --- a/modules/reitit-frontend/src/reitit/frontend/history.cljs +++ b/modules/reitit-frontend/src/reitit/frontend/history.cljs @@ -187,9 +187,10 @@ The URL is formatted using Reitit frontend history handler, so using it with anchor element href will correctly trigger route change event. - Note: currently collections in query parameters are encoded as field-value - pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them - differently, convert the collections to strings first." + By default currently collections in query parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can + either use Malli coercion to encode values, or just turn the values to strings + before calling the function." ([history name] (href history name nil)) ([history name path-params] @@ -208,9 +209,10 @@ Will also trigger on-navigate callback on Reitit frontend History handler. - Note: currently collections in query-parameters are encoded as field-value - pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them - differently, convert the collections to strings first. + By default currently collections in query parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can + either use Malli coercion to encode values, or just turn the values to strings + before calling the function. See also: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState" @@ -236,9 +238,10 @@ Will also trigger on-navigate callback on Reitit frontend History handler. - Note: currently collections in query-parameters are encoded as field-value - pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them - differently, convert the collections to strings first. + By default currently collections in query parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can + either use Malli coercion to encode values, or just turn the values to strings + before calling the function. See also: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState" @@ -264,9 +267,10 @@ Will also trigger on-navigate callback on Reitit frontend History handler. - Note: currently collections in query-parameters are encoded as field-value - pairs separated by &, i.e. \"?a=1&a=2\", if you want to encode them - differently, convert the collections to strings first. + By default currently collections in query parameters are encoded as field-value + pairs separated by &, i.e. \"?a=1&a=2\". To encode them differently, you can + either use Malli coercion to encode values, or just turn the values to strings + before calling the function. See also: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState @@ -289,13 +293,22 @@ New query params can be given as a map, or a function taking the old params and returning the new modified params. - Note: The query parameter values aren't coereced, so the - update fn will see string values for all query params." + The current path is matched against the routing tree, and the match data + (schema, coercion) is used to encode the query parameters. + If the current path doesn't match any route, the query parameters + are parsed from the path without coercion and new values + are also stored without coercion encoding." ([history new-query-or-update-fn] (set-query history new-query-or-update-fn nil)) ([history new-query-or-update-fn {:keys [replace] :as opts}] (let [current-path (-get-path history) - new-path (rf/set-query-params current-path new-query-or-update-fn)] + match (rf/match-by-path (:router history) current-path) + new-path (if match + (let [query-params (if (fn? new-query-or-update-fn) + (new-query-or-update-fn (:query (:parameters match))) + new-query-or-update-fn)] + (rf/match->path match query-params (:fragment (:parameters match)))) + (rf/set-query-params current-path new-query-or-update-fn))] (if replace (.replaceState js/window.history nil "" (-href history new-path)) (.pushState js/window.history nil "" (-href history new-path))) diff --git a/modules/reitit-malli/src/reitit/coercion/malli.cljc b/modules/reitit-malli/src/reitit/coercion/malli.cljc index b94b17b30..813bbc200 100644 --- a/modules/reitit-malli/src/reitit/coercion/malli.cljc +++ b/modules/reitit-malli/src/reitit/coercion/malli.cljc @@ -9,7 +9,8 @@ [malli.swagger :as swagger] [malli.transform :as mt] [malli.util :as mu] - [reitit.coercion :as coercion])) + [reitit.coercion :as coercion] + [clojure.string :as string])) ;; ;; coercion @@ -76,6 +77,22 @@ (assoc error :transformed transformed)))) value)))))))) +(defn- -query-string-coercer + "Create coercer for query-parameters, always allows extra params and does + encoding using string-transformer." + [schema string-transformer-provider options] + (let [;; Always allow extra paramaters on query-parameters encoding + open-schema (mu/open-schema schema) + ;; Do not remove extra keys + string-transformer (if (satisfies? TransformationProvider string-transformer-provider) + (-transformer string-transformer-provider (assoc options :strip-extra-keys false)) + string-transformer-provider) + encoder (m/encoder open-schema options string-transformer)] + (fn [value format] + (if encoder + (encoder value) + value)))) + ;; ;; public api ;; @@ -112,6 +129,9 @@ ([opts] (let [{:keys [transformers lite compile options error-keys encode-error] :as opts} (merge default-options opts) show? (fn [key] (contains? error-keys key)) + ;; Query-string-coercer needs to construct transfomer without strip-extra-keys so it will + ;; use the transformer-provider directly. + string-transformer-provider (:default (:string transformers)) transformers (walk/prewalk #(if (satisfies? TransformationProvider %) (-transformer % opts) %) transformers) compile (if lite (fn [schema options] (compile (binding [l/*options* options] (l/schema schema)) options)) @@ -176,6 +196,8 @@ (-request-coercer [_ type schema] (-coercer schema type transformers :decode opts)) (-response-coercer [_ schema] - (-coercer schema :response transformers :encode opts)))))) + (-coercer schema :response transformers :encode opts)) + (-query-string-coercer [_ schema] + (-query-string-coercer schema string-transformer-provider opts)))))) (def coercion (create default-options)) diff --git a/modules/reitit-schema/src/reitit/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc index b2fd14d1d..d3b37d9a8 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -101,6 +101,8 @@ value)))) (-response-coercer [this schema] (if (coerce-response? schema) - (coercion/-request-coercer this :response schema))))) + (coercion/-request-coercer this :response schema))) + (-query-string-coercer [this schema] + nil))) (def coercion (create default-options)) diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index bf4bed822..004cda93b 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -148,6 +148,8 @@ value)))) (-response-coercer [this spec] (if (coerce-response? spec) - (coercion/-request-coercer this :response spec))))) + (coercion/-request-coercer this :response spec))) + (-query-string-coercer [this spec] + nil))) (def coercion (create default-options)) diff --git a/test/cljc/reitit/coercion_test.cljc b/test/cljc/reitit/coercion_test.cljc index ee18c59ce..0fcfd125b 100644 --- a/test/cljc/reitit/coercion_test.cljc +++ b/test/cljc/reitit/coercion_test.cljc @@ -1,5 +1,8 @@ (ns reitit.coercion-test - (:require [clojure.test :refer [deftest is testing]] + (:require [clojure.spec.alpha :as cs] + [clojure.string :as str] + [clojure.test :refer [deftest is testing]] + [malli.core :as m] [malli.experimental.lite :as l] [reitit.coercion :as coercion] [reitit.coercion.malli] @@ -7,8 +10,8 @@ [reitit.coercion.spec] [reitit.core :as r] [schema.core :as s] - [clojure.spec.alpha :as cs] - [spec-tools.data-spec :as ds]) + [spec-tools.data-spec :as ds] + [malli.transform :as mt]) #?(:clj (:import (clojure.lang ExceptionInfo)))) @@ -150,3 +153,72 @@ {:compile coercion/compile-request-coercers})] (is (= {:path {:user-id 123, :company "metosin"}} (:parameters (match-by-path-and-coerce! router "/metosin/users/123")))))) + +(deftest match->path-parameter-coercion-test + (testing "default handling for query-string collection" + (let [router (r/router ["/:a/:b" ::route])] + (is (= "/olipa/kerran?x=a&x=b" + (-> router + (r/match-by-name! ::route {:a "olipa", :b "kerran"}) + (coercion/match->path {:x [:a :b]})))) + + (is (= "/olipa/kerran?x=a&x=b&extra=extra-param" + (-> router + (r/match-by-name! ::route {:a "olipa", :b "kerran"}) + (coercion/match->path {:x [:a :b] + :extra "extra-param"})))))) + + (testing "custom encode/string for a collection" + (let [router (r/router ["/:a/:b" + {:name ::route + :coercion reitit.coercion.malli/coercion + :parameters {:query [:map + [:x + [:vector + {:encode/string (fn [xs] + (str/join "," (map name xs))) + :decode/string (fn [s] + (mapv keyword (str/split s #",")))} + :keyword]]]}}] + {:compile coercion/compile-request-coercers}) + match (r/match-by-name! router ::route {:a "olipa", :b "kerran"})] + (is (= {:x "a,b"} + (coercion/coerce-query-params match {:x [:a :b]}))) + + ;; NOTE: "," is urlencoded by the impl/query-string step + (is (= "/olipa/kerran?x=a%2Cb" + (coercion/match->path match {:x [:a :b]}))) + + (testing "extra query-string parameters aren't removed by coercion" + (is (= "/olipa/kerran?x=a%2Cb&extra=extra-param" + (-> router + (r/match-by-name! ::route {:a "olipa", :b "kerran"}) + (coercion/match->path {:x [:a :b] + :extra "extra-param"}))))) + + (is (= {:query {:x [:a :b]}} + (-> (r/match-by-path router "/olipa/kerran") + (assoc :query-params {:x "a,b"}) + (coercion/coerce!)))))) + + (testing "encoding and multiple query param values" + (let [router (r/router ["/:a/:b" + {:name ::route + :coercion reitit.coercion.malli/coercion + :parameters {:query [:map + [:x + [:vector + [:keyword + ;; For query strings encode only calls encode, so no need to check if decode if value is encoded or not. + {:decode/string (fn [s] (keyword (subs s 2))) + :encode/string (fn [k] (str "__" (name k)))}]]]]}}] + {:compile coercion/compile-request-coercers})] + (is (= "/olipa/kerran?x=__a&x=__b" + (-> router + (r/match-by-name! ::route {:a "olipa", :b "kerran"}) + (coercion/match->path {:x [:a :b]})))) + + (is (= {:query {:x [:a :b]}} + (-> (r/match-by-path router "/olipa/kerran") + (assoc :query-params {:x ["__a" "__b"]}) + (coercion/coerce!))))))) diff --git a/test/cljs/reitit/frontend/core_test.cljs b/test/cljs/reitit/frontend/core_test.cljs index 0d7a7bc7d..3bb7907f3 100644 --- a/test/cljs/reitit/frontend/core_test.cljs +++ b/test/cljs/reitit/frontend/core_test.cljs @@ -1,13 +1,16 @@ (ns reitit.frontend.core-test - (:require [clojure.test :refer [deftest testing is are]] - [reitit.core :as r] - [reitit.frontend :as rf] + (:require [clojure.string :as str] + [clojure.test :refer [are deftest is testing]] + [malli.core :as m] + [malli.transform :as mt] [reitit.coercion :as rc] - [schema.core :as s] - [reitit.coercion.schema :as rcs] [reitit.coercion.malli :as rcm] + [reitit.coercion.schema :as rcs] + [reitit.core :as r] + [reitit.frontend :as rf] [reitit.frontend.test-utils :refer [capture-console]] - [reitit.impl :as impl])) + [reitit.impl :as impl] + [schema.core :as s])) (deftest query-params-test (is (= {:foo "1"} @@ -297,3 +300,33 @@ (testing "Fragment encoding" (is (= "foo#foo+bar+%25" (rf/match->path {:path "foo"} nil "foo bar %"))))) + +(deftest match->path-coercion-test + (testing "default keyword to string" + (is (str/starts-with? + (rf/match->path {:path "foo"} {:q :x}) + "foo?q=x"))) + + (testing "default string transformer" + (is (= "foo?q=__x" + (rf/match->path {:data {:coercion rcm/coercion + :parameters {:query [[:map + [:q {:decode/string (fn [s] (keyword (subs s 2))) + :encode/string (fn [k] (str "__" (name k)))} + :keyword]]]}} + :path "foo"} + {:q "x"})))) + + (testing "custom string transformer" + (is (= "foo?q=--x" + (rf/match->path {:data {:coercion (rcm/create (assoc-in rcm/default-options + [:transformers :string :default] + (mt/transformer + {:name :foo-string + :encoders {:foo/type {:leave (fn [x] (str "--" x))}}}))) + :parameters {:query [[:map + [:q (m/-simple-schema + {:type :foo/type + :pred string?})]]]}} + :path "foo"} + {:q "x"}))))) diff --git a/test/cljs/reitit/frontend/easy_test.cljs b/test/cljs/reitit/frontend/easy_test.cljs index e36d9eeeb..1e0284ade 100644 --- a/test/cljs/reitit/frontend/easy_test.cljs +++ b/test/cljs/reitit/frontend/easy_test.cljs @@ -1,16 +1,24 @@ (ns reitit.frontend.easy-test - (:require [clojure.test :refer [deftest testing is are async]] + (:require [clojure.test :refer [are async deftest is testing]] + [goog.events :as gevents] + [reitit.coercion.malli :as rcm] [reitit.core :as r] [reitit.frontend.easy :as rfe] - [reitit.frontend.history :as rfh] - [goog.events :as gevents])) + [reitit.frontend.history :as rfh])) (def browser (exists? js/window)) (def router (r/router ["/" ["" ::frontpage] ["foo" ::foo] - ["bar/:id" ::bar]])) + ["bar/:id" + {:name ::bar + :coercion rcm/coercion + :parameters {:query [:map + [:q {:optional true} + [:keyword + {:decode/string (fn [s] (keyword (subs s 2))) + :encode/string (fn [k] (str "__" (name k)))}]]]}}]])) ;; TODO: Only tests fragment history, also test HTML5? @@ -26,61 +34,75 @@ (fn on-navigate [match history] (let [url (rfh/-get-path history)] (case (swap! n inc) - 1 (do (is (some? (:popstate-listener history))) + 1 (rfh/push-state history ::frontpage) + 2 (do (is (some? (:popstate-listener history))) (is (= "/" url) "start at root") (rfe/push-state ::foo nil {:a 1} "foo bar")) ;; 0. / ;; 1. /foo?a=1#foo+bar - 2 (do (is (= "/foo?a=1#foo+bar" url) + 3 (do (is (= "/foo?a=1#foo+bar" url) "push-state") (.back js/window.history)) ;; 0. / - 3 (do (is (= "/" url) + 4 (do (is (= "/" url) "go back") - (rfe/navigate ::bar {:path-params {:id 1}})) + (rfe/navigate ::bar {:path-params {:id 1} + :query-params {:q "x"}})) ;; 0. / ;; 1. /bar/1 - 4 (do (is (= "/bar/1" url) + 5 (do (is (= "/bar/1?q=__x" url) "push-state 2") (rfe/replace-state ::bar {:id 2})) ;; 0. / ;; 1. /bar/2 - 5 (do (is (= "/bar/2" url) + 6 (do (is (= "/bar/2" url) "replace-state") (rfe/set-query {:a 1})) ;; 0. / ;; 1. /bar/2 ;; 2. /bar/2?a=1 - 6 (do (is (= "/bar/2?a=1" url) + 7 (do (is (= "/bar/2?a=1" url) "update-query with map") - (rfe/set-query #(assoc % :b "foo") {:replace true})) + (rfe/set-query #(assoc % :q "x") {:replace true})) ;; 0. / ;; 1. /bar/2 ;; 2. /bar/2?a=1&b=foo - 7 (do (is (= "/bar/2?a=1&b=foo" url) + 8 (do (is (= "/bar/2?a=1&q=__x" url) "update-query with fn") (.go js/window.history -2)) + + ;; Go to non-matching path and check set-query works + ;; (without coercion) without a match + 9 (do (is (= "/" url) "go back two events") + (.pushState js/window.history nil "" "#/non-matching-path")) + + 10 (do (is (= "/non-matching-path" url)) + (rfe/set-query #(assoc % :q "x"))) + + 11 (do (is (= "/non-matching-path?q=x" url)) + (.go js/window.history -2)) + ;; 0. / - 8 (do (is (= "/" url) - "go back two events") + 12 (do (is (= "/" url) + "go back two events") - ;; Reset to ensure old event listeners aren't called - (rfe/start! router - (fn on-navigate [match history] - (let [url (rfh/-get-path history)] - (case (swap! n inc) - 9 (do (is (= "/" url) - "start at root") - (rfe/push-state ::foo)) - 10 (do (is (= "/foo" url) - "push-state") - (rfh/stop! @rfe/history) - (done)) - (do - (is false (str "extra event 2" {:n @n, :url url})) - (done))))) - {:use-fragment true})) + ;; Reset to ensure old event listeners aren't called + (rfe/start! router + (fn on-navigate [match history] + (let [url (rfh/-get-path history)] + (case (swap! n inc) + 13 (do (is (= "/" url) + "start at root") + (rfe/push-state ::foo)) + 14 (do (is (= "/foo" url) + "push-state") + (rfh/stop! @rfe/history) + (done)) + (do + (is false (str "extra event 2" {:n @n, :url url})) + (done))))) + {:use-fragment true})) (do (is false (str "extra event 1" {:n @n, :url url})) (done))))) diff --git a/test/cljs/reitit/frontend/history_test.cljs b/test/cljs/reitit/frontend/history_test.cljs index 4d3951be4..55aea67a0 100644 --- a/test/cljs/reitit/frontend/history_test.cljs +++ b/test/cljs/reitit/frontend/history_test.cljs @@ -3,14 +3,22 @@ [reitit.core :as r] [reitit.frontend.history :as rfh] [reitit.frontend.test-utils :refer [capture-console]] - [goog.events :as gevents])) + [goog.events :as gevents] + [reitit.coercion.malli :as rcm])) (def browser (exists? js/window)) (def router (r/router ["/" ["" ::frontpage] ["foo" ::foo] - ["bar/:id" ::bar]])) + ["bar/:id" + {:name ::bar + :coercion rcm/coercion + :parameters {:query [:map + [:q {:optional true} + [:keyword + {:decode/string (fn [s] (keyword (subs s 2))) + :encode/string (fn [k] (str "__" (name k)))}]]]}}]])) (deftest fragment-history-test (when browser @@ -24,9 +32,12 @@ (rfh/href history ::foo))) (is (= "#/bar/5" (rfh/href history ::bar {:id 5}))) - (is (= "#/bar/5?q=x" + (testing "query string coercion doesn't strip extra keys" + (is (= "#/bar/5?extra=a" + (rfh/href history ::bar {:id 5} {:extra "a"})))) + (is (= "#/bar/5?q=__x" (rfh/href history ::bar {:id 5} {:q "x"}))) - (is (= "#/bar/5?q=x#foo" + (is (= "#/bar/5?q=__x#foo" (rfh/href history ::bar {:id 5} {:q "x"} "foo"))) (let [{:keys [value messages]} (capture-console (fn [] @@ -58,11 +69,11 @@ (.back js/window.history)) 4 (do (is (= "/" url) "go back") - (rfh/push-state history ::bar {:id 1})) - 5 (do (is (= "/bar/1" url) + (rfh/push-state history ::bar {:id 1} {:extra "a"})) + 5 (do (is (= "/bar/1?extra=a" url) "push-state 2") - (rfh/replace-state history ::bar {:id 2})) - 6 (do (is (= "/bar/2" url) + (rfh/replace-state history ::bar {:id 2} {:q "x"})) + 6 (do (is (= "/bar/2?q=__x" url) "replace-state") (.back js/window.history)) 7 (do (is (= "/" url) @@ -84,7 +95,7 @@ (rfh/href history ::foo))) (is (= "/bar/5" (rfh/href history ::bar {:id 5}))) - (is (= "/bar/5?q=x" + (is (= "/bar/5?q=__x" (rfh/href history ::bar {:id 5} {:q "x"}))) (let [{:keys [value messages]} (capture-console (fn [] @@ -119,8 +130,8 @@ (rfh/push-state history ::bar {:id 1})) 5 (do (is (= "/bar/1" url) "push-state 2") - (rfh/replace-state history ::bar {:id 2})) - 6 (do (is (= "/bar/2" url) + (rfh/replace-state history ::bar {:id 2} {:q "x"})) + 6 (do (is (= "/bar/2?q=__x" url) "replace-state") (.back js/window.history)) 7 (do (is (= "/" url)