diff --git a/test/reagenttest/testreagent.cljs b/test/reagenttest/testreagent.cljs index 2b998f42..e82ef76e 100644 --- a/test/reagenttest/testreagent.cljs +++ b/test/reagenttest/testreagent.cljs @@ -22,7 +22,7 @@ (set! rv/debug false))}) (u/deftest ^:dom really-simple-test - (let [ran (r/atom 0) + (let [ran (atom 0) really-simple (fn [] (swap! ran inc) [:div "div in really-simple"])] @@ -32,7 +32,7 @@ (is (= "div in really-simple" (.-innerText div))))))) (u/deftest ^:dom test-simple-callback - (let [ran (r/atom 0) + (let [ran (atom 0) comp (r/create-class {:component-did-mount #(swap! ran inc) :render @@ -46,13 +46,12 @@ [:div (str "hi " (:foo props) ".")]))})] (u/async (u/with-render [div [comp {:foo "you"} 1]] - ;; FIXME: Why? - (swap! ran inc) (is (= "hi you." (.-innerText div))) - (is (= 3 @ran)))))) + ;; 1 from render, 1 from component did mount + (is (= 2 @ran)))))) (u/deftest ^:dom test-state-change - (let [ran (r/atom 0) + (let [ran (atom 0) self (r/atom nil) comp (r/create-class {:get-initial-state (fn [] {:foo "initial"}) @@ -77,7 +76,7 @@ (u/deftest ^:dom test-ratom-change (let [compiler u/*test-compiler* - ran (r/atom 0) + ran (atom 0) runs (rv/running) val (r/atom 0) secval (r/atom 0) @@ -131,8 +130,6 @@ (u/with-render [div [c1]] (is (= 2 @ran)) (u/act (swap! v2 inc)) - ;; FIXME: ??? - ;; (is (= 2 @ran)) (is (= 3 @ran)) (u/act (swap! v1 inc)) (is (= 5 @ran)) @@ -149,7 +146,7 @@ )))) (u/deftest ^:dom init-state-test - (let [ran (r/atom 0) + (let [ran (atom 0) really-simple (fn [] (let [this (r/current-component)] (swap! ran inc) @@ -334,20 +331,18 @@ parent (fn [] [comp @prop 1])] (u/async (u/with-render [div [parent]] - ;; FIXME: Why? - (swap! ran inc) (is (= "hi you." (.-innerText div))) (is (= 1 @top-ran)) - (is (= 3 @ran)) + (is (= 2 @ran)) (u/act (swap! prop assoc :foo "me")) (is (= "hi me." (.-innerText div))) (is (= 1 @top-ran)) - (is (= 4 @ran)))))) + (is (= 3 @ran)))))) (u/deftest ^:dom test-return-class-fn - (let [ran (r/atom 0) - top-ran (r/atom 0) + (let [ran (atom 0) + top-ran (atom 0) comp (fn [] (swap! top-ran inc) (r/create-class @@ -361,16 +356,14 @@ parent (fn [] [comp @prop 1])] (u/async (u/with-render [div [parent]] - ;; FIXME: Why? - (swap! ran inc) (is (= "hi you." (.-innerText div))) (is (= 1 @top-ran)) - (is (= 3 @ran)) + (is (= 2 @ran)) (u/act (swap! prop assoc :foo "me")) (is (= "hi me." (.-innerText div))) (is (= 1 @top-ran)) - (is (= 4 @ran)))))) + (is (= 3 @ran)))))) (u/deftest test-create-element (let [ae r/as-element @@ -478,31 +471,28 @@ ;; TODO: Test create-element with key prop (u/async - (p/let [w (u/track-warnings-promise - #(u/with-render [div [:div - (list - [:> "div" {:key 1} "a"] - [:> "div" {:key 2} "b"])]] - nil))] - (is (empty? (:warn w)))) - - (p/let [w (u/track-warnings-promise - #(u/with-render [div [:div - (list - [:r> "div" #js {:key 1} "a"] - [:r> "div" #js {:key 2} "b"])]] - nil))] - (is (empty? (:warn w)))) - - (p/let [f (fn [props c] - [:div props c]) - w (u/track-warnings-promise - #(u/with-render [div [:div - (list - [:f> f {:key 1} "a"] - [:f> f {:key 2} "b"])]] - nil))] - (is (empty? (:warn w)))))) + (u/with-render [div [:div + (list + [:> "div" {:key 1} "a"] + [:> "div" {:key 2} "b"])]] + {:capture-errors true} + (is (empty? (:warn @reagent.debug/warnings)))) + + (u/with-render [div [:div + (list + [:r> "div" #js {:key 1} "a"] + [:r> "div" #js {:key 2} "b"])]] + {:capture-errors true} + (is (empty? (:warn @reagent.debug/warnings)))) + + (let [f (fn [props c] + [:div props c])] + (u/with-render [div [:div + (list + [:f> f {:key 1} "a"] + [:f> f {:key 2} "b"])]] + {:capture-errors true} + (is (empty? (:warn @reagent.debug/warnings))))))) (u/deftest test-reactize-component (let [ae r/as-element @@ -539,27 +529,22 @@ (for [i (range 3)] ^{:key i} [:p i (some-> a deref)]) (for [i (range 3)] - [:p {:key i} i (some-> a deref)])]) - w (u/track-warnings-promise - (fn [] - (u/with-render [_div [c]] - nil)))] - (is (empty? (:warn w)))) + [:p {:key i} i (some-> a deref)])])] + (u/with-render [_div [c]] + {:capture-errors true} + (is (empty? (:warn @reagent.debug/warnings))))) (testing "Check warning text can be produced even if hiccup contains function literals" (p/let [c (fn key-tester [] [:div (for [i (range 3)] ^{:key nil} - [:button {:on-click #(js/console.log %)}])]) - w (u/track-warnings-promise - (u/wrap-capture-console-error-promise - (fn [] - (u/with-render [_div [c]] - nil))))] - (if (dev?) - (is (re-find #"Warning: Every element in a seq should have a unique :key: \(\[:button \{:on-click #object\[Function\]\}\] \[:button \{:on-click #object\[Function\]\}\] \[:button \{:on-click #object\[Function\]\}\]\)\n \(in reagenttest.testreagent.key_tester\)" - (first (:warn w))))))))) + [:button {:on-click #(js/console.log %)}])])] + (u/with-render [_div [c]] + {:capture-errors true} + (when (dev?) + (is (re-find #"Warning: Every element in a seq should have a unique :key: \(\[:button \{:on-click #object\[Function\]\}\] \[:button \{:on-click #object\[Function\]\}\] \[:button \{:on-click #object\[Function\]\}\]\)\n \(in reagenttest.testreagent.key_tester\)" + (first (:warn @reagent.debug/warnings)))))))))) (u/deftest test-extended-syntax (is (= "

foo

" @@ -993,43 +978,29 @@ (u/async ;; Error is orginally caused by comp1, so only that is shown in the error - ;; FIXME: - #_ - (p/let [e (u/track-warnings-promise - (u/wrap-capture-window-error-promise - (u/wrap-capture-console-error-promise - (fn [] - (is (thrown-with-msg? - :default #"Invalid tag: 'div.' \(in reagenttest.testreagent.comp1\)" - (-> (u/with-render [div [comp2 [:div. "foo"]]] - nil) - (p/catch (fn [_e] nil)))))))))] + (u/with-render [div [comp2 [:div. "foo"]]] + {:capture-errors true} + ;; The render call itself throws the exception + (is (re-find #"Invalid tag: 'div.' \(in reagenttest.testreagent.comp1\)" (.-message u/*render-error*))) (is (re-find #"The above error occurred in the component:" - (first (:error e))))) - - ;; FIXME: - #_ - (p/let [e (u/track-warnings-promise - (u/wrap-capture-window-error-promise - (u/wrap-capture-console-error-promise - (fn [] - (is (thrown-with-msg? - :default #"Invalid tag: 'div.' \(in reagenttest.testreagent.comp1\)" - (-> (u/with-render [div [comp1 [:div. "foo"]]] - nil) - (p/catch (fn [_e] nil)))))))))] + (first (:error @reagent.debug/warnings)))) + nil) + + (u/with-render [div [comp1 [:div. "foo"]]] + {:capture-errors true} + (is (re-find #"Invalid tag: 'div.' \(in reagenttest.testreagent.comp1\)" (.-message u/*render-error*))) (is (re-find #"The above error occurred in the component:" - (first (:error e))))) + (first (:error @reagent.debug/warnings)))) + nil) (let [e (debug/track-warnings #(r/as-element [nat] compiler))] (is (re-find #"Using native React classes directly" (-> e :warn first)))) - (p/let [e (u/track-warnings-promise - #(u/with-render [div [comp3]] - nil))] + (u/with-render [div [comp3]] + {:capture-errors true} (is (re-find #"Reactive deref not supported" - (-> e :warn first)))) + (-> @reagent.debug/warnings :warn first)))) (let [e (debug/track-warnings #(r/as-element (comp4) compiler))] @@ -1055,21 +1026,19 @@ comp2 (fn comp2 [] [comp1])] (u/async - (u/track-warnings-promise - (u/wrap-capture-window-error-promise - (u/wrap-capture-console-error-promise - #(u/with-render [div [error-boundary [comp2]]] - (is (= "Test error" (.-message @error))) - (is (re-find #"Something went wrong\." (.-innerHTML div))) - (if (dev?) - ;; FIXME: Firefox formats the stack traces differently, and perhaps there is a real problem that - ;; the component function names aren't being used correctly, and all components are named "cmp"? - (when-not (.. js/navigator -userAgent toLowerCase (includes "firefox")) - (is (re-find #"^\n at reagenttest.testreagent.comp1 \([^)]*\)\n at reagenttest.testreagent.comp2 \([^)]*\)\n at reagent[0-9]+ \([^)]*\)\n at reagenttest.testreagent.error_boundary \([^)]*\)" - (.-componentStack ^js @info)))) - ;; Names are completely manged on adv compilation - (is (re-find #"^\n at .* \([^)]*\)\n at .* \([^)]*\)\n at .* \([^)]*\)\n at .+ \([^)]*\)" - (.-componentStack ^js @info))))))))))) + (u/with-render [div [error-boundary [comp2]]] + {:capture-errors true} + (is (= "Test error" (.-message @error))) + (is (re-find #"Something went wrong\." (.-innerHTML div))) + (if (dev?) + ;; FIXME: Firefox formats the stack traces differently, and perhaps there is a real problem that + ;; the component function names aren't being used correctly, and all components are named "cmp"? + (when-not (.. js/navigator -userAgent toLowerCase (includes "firefox")) + (is (re-find #"^\n at reagenttest.testreagent.comp1 \([^)]*\)\n at reagenttest.testreagent.comp2 \([^)]*\)\n at reagent[0-9]+ \([^)]*\)\n at reagenttest.testreagent.error_boundary \([^)]*\)" + (.-componentStack ^js @info)))) + ;; Names are completely manged on adv compilation + (is (re-find #"^\n at .* \([^)]*\)\n at .* \([^)]*\)\n at .* \([^)]*\)\n at .+ \([^)]*\)" + (.-componentStack ^js @info)))))))) #_{:clj-kondo/ignore [:deprecated-var]} (u/deftest ^:dom test-dom-node @@ -1103,7 +1072,9 @@ exp (atom 0) node (atom nil) state (r/atom 0) + component-instance (atom nil) comp (fn [] + (reset! component-instance (r/current-component)) (let [old @spy] (r/after-render (fn [] @@ -1111,28 +1082,39 @@ (swap! spy inc))) (is (= @spy old)) (is (= @exp @val)) - [:div {:ref #(reset! node %)} @state]))] + [:div + {:ref #(reset! node %)} + @state]))] (u/async (u/with-render [div [comp]] - ;; FIXME: Rewrite this to use act? - - (is (= 1 @spy)) - (swap! state inc) + ;; after-render was already called after the initial render (is (= 1 @spy)) + + (u/act (swap! state inc)) + + (is (= 2 @spy)) + + ;; Update some non reactive values (r/next-tick #(swap! val inc)) (reset! exp 1) - (is (= 0 @val)) - (r/flush) + + ;; After waiting for render, the next-tick update has also happened + (u/act nil) + (is (= 1 @val)) (is (= 2 @spy)) - ;; FIXME: c is nil because render call doesn't return anything - ; (r/force-update c) - ; (is (= 3 @spy)) - ; (r/next-tick #(reset! spy 0)) - ; (is (= 3 @spy)) - ; (r/flush) - ; (is (= 0 @spy)) - ) + + ;; Now if we force render directly, spy is also updated from after-render callback + (r/force-update @component-instance) + (is (= 3 @spy)) + + ;; next-tick callback isn't called right away, + (r/next-tick #(reset! spy 0)) + (is (= 3 @spy)) + + ;; the update is run when waiting for the component to finish rendering + (u/act nil) + (is (= 0 @spy))) (is (= nil @node))))) (u/deftest style-property-names-are-camel-cased @@ -1266,18 +1248,18 @@ (u/async (when (dev?) - (p/let [e (u/track-warnings-promise - #(u/with-render [div [component]] - (u/act (reset! prop (sorted-map 1 2))) - ; (try - ; (r/flush) - ; (catch :default e - ; (reset! error-thrown-after-updating-props true))) - - (is (not @component-was-updated)) - (is (not @error-thrown-after-updating-props))))] + (u/with-render [div [component]] + {:capture-errors true} + (u/act (reset! prop (sorted-map 1 2))) + ; (try + ; (r/flush) + ; (catch :default e + ; (reset! error-thrown-after-updating-props true))) + + (is (not @component-was-updated)) + (is (not @error-thrown-after-updating-props)) (is (re-find #"Warning: Exception thrown while comparing argv's in shouldComponentUpdate:" - (first (:warn e))))))))) + (first (:warn @reagent.debug/warnings))))))))) (u/deftest ^:dom get-derived-state-from-props-test (let [prop (r/atom 0) @@ -1317,14 +1299,12 @@ (if (= 0 @prop) [:div "Ok"] (throw (js/Error. "foo"))))] - ;; FIXME: These assertions aren't being run? (u/async - (u/wrap-capture-window-error-promise - (u/wrap-capture-console-error-promise - #(u/with-render [div [component [bad-component]]] - (is (= "Ok" (.-innerText div))) - (u/act (swap! prop inc)) - (is (= "Error" (.-innerText div))))))))) + (u/with-render [div [component [bad-component]]] + {:capture-errors true} + (is (= "Ok" (.-innerText div))) + (u/act (swap! prop inc)) + (is (= "Error" (.-innerText div))))))) (u/deftest ^:dom get-snapshot-before-update-test (let [ref (react/createRef) diff --git a/test/reagenttest/testwrap.cljs b/test/reagenttest/testwrap.cljs index 5a5c1990..2bdaf777 100644 --- a/test/reagenttest/testwrap.cljs +++ b/test/reagenttest/testwrap.cljs @@ -95,8 +95,8 @@ (u/deftest ^:dom test-wrap (let [compiler u/*test-compiler* state (r/atom {:foo {:bar {:foobar 1}}}) - ran (r/atom 0) - grand-state (clojure.core/atom nil) + ran (atom 0) + grand-state (atom nil) grand-child (fn [v] (swap! ran inc) (reset! grand-state v) diff --git a/test/reagenttest/utils.clj b/test/reagenttest/utils.clj index cac506ea..ccb2f83b 100644 --- a/test/reagenttest/utils.clj +++ b/test/reagenttest/utils.clj @@ -62,4 +62,4 @@ (let [[opts body] (if (map? (first body)) [(first body) (rest body)] [nil body])] - `(reagenttest.utils/with-render* ~comp ~(:compiler opts) (fn [~sym] (promesa.core/do ~@body))))) + `(reagenttest.utils/with-render* ~comp ~opts (fn [~sym] (promesa.core/do ~@body))))) diff --git a/test/reagenttest/utils.cljs b/test/reagenttest/utils.cljs index 356aa7d9..4cac15fb 100644 --- a/test/reagenttest/utils.cljs +++ b/test/reagenttest/utils.cljs @@ -51,46 +51,31 @@ (finally (set! js/console.error org)))))) -;; Promise versions - -(defn wrap-capture-console-error-promise [f] - (fn [] - (let [org js/console.error] - (set! js/console.error log-error) - (-> (f) - (p/finally - (fn [] - (set! js/console.error org))))))) - -(defn track-warnings-promise [f] - (set! debug/tracking true) - (reset! debug/warnings nil) - (-> (f) - (p/then (fn [] - @debug/warnings)) - (p/finally - (fn [] - (reset! debug/warnings nil) - (set! debug/tracking false))))) - -(defn wrap-capture-window-error-promise [f] - (if (exists? js/window) - (fn [] - (let [org js/console.onerror] - (set! js/window.onerror (fn [e] - (log-error e) - true)) - (-> (f) - (p/finally - (fn [] (set! js/window.onerror org)))))) +(defn init-capture [] + (let [org-console js/console.error + org-window js/window.onerror + l (fn [e] + (log-error e))] + ;; console.error + (set! js/console.error log-error) + ;; reagent.debug + (set! debug/tracking true) + (reset! debug/warnings nil) + ;; window.error + (if (exists? js/window) + (set! js/window.onerror (fn [e] + (log-error e) + true)) + (let [process (js/require "process")] + (.on process "uncaughtException" l))) (fn [] - (let [process (js/require "process") - l (fn [e] - (log-error e))] - (.on process "uncaughtException" l) - (-> (f) - (p/finally - (fn [] (.removeListener process "uncaughtException" l)))))))) + (set! js/console.error org-console) + (reset! debug/warnings nil) + (set! debug/tracking false) + (if (exists? js/window) + (set! js/window.onerror org-window) + (let [process (js/require "process")] + (.removeListener process "uncaughtException" l)))))) (defn act* "Run f to trigger Reagent updates, @@ -104,6 +89,8 @@ (p/resolve! p))) p)) +(def ^:dynamic *render-error* nil) + (defn with-render* "Render the given component to a DOM node, after the the component is mounted to DOM, @@ -112,23 +99,44 @@ before unmounting the component from DOM." ([comp f] (with-render* comp *test-compiler* f)) - ([comp compiler f] + ([comp options f] (let [div (.createElement js/document "div") - p (p/deferred) + first-render (p/deferred) callback (fn [] - (p/resolve! p))] - (if compiler - (rdom/render comp div {:compiler compiler - :callback callback}) - (rdom/render comp div callback)) - (.then p - (fn [] - (-> (js/Promise.resolve (f div)) - (.then (fn [] - (rdom/unmount-component-at-node div) - ;; Need to wait for reagent tick after unmount - ;; for the ratom watches to be removed? - (let [p (p/deferred)] - (r/next-tick (fn [] - (p/resolve! p))) - p))))))))) + (p/resolve! first-render)) + compiler (:compiler options) + restore-error-handlers (when (:capture-errors options) + (init-capture)) + ;; Magic setup to make exception from render available to the + ;; with-render body. + render-error (atom nil)] + (try + (if compiler + (rdom/render comp div {:compiler compiler + :callback callback}) + (rdom/render comp div callback)) + (catch :default e + (reset! render-error e) + nil)) + (-> first-render + ;; The callback is called even if render throws an error, + ;; so this is always resolved. + (p/then (fn [] + (p/do + (set! *render-error* @render-error) + (f div) + (set! *render-error* nil)))) + ;; If f throws more errors, just ignore them? + ;; Not sure if this makes sense. + (p/catch (fn [] nil)) + (p/then (fn [] + (rdom/unmount-component-at-node div) + ;; Need to wait for reagent tick after unmount + ;; for the ratom watches to be removed? + (let [ratoms-cleaned (p/deferred)] + (r/next-tick (fn [] + (p/resolve! ratoms-cleaned))) + ratoms-cleaned))) + (p/finally (fn [] + (when restore-error-handlers + (restore-error-handlers))))))))