diff --git a/.serena/memories/frontend/testing.md b/.serena/memories/frontend/testing.md index e1249e2800..ceffa3af2d 100644 --- a/.serena/memories/frontend/testing.md +++ b/.serena/memories/frontend/testing.md @@ -8,8 +8,10 @@ Frontend unit tests live under `frontend/test/frontend_tests/` and use `cljs.tes From `frontend/`: - Full unit test run: `pnpm run test`. -- Focused unit tests: edit `test/frontend_tests/runner.cljs` to narrow the suite, then run `pnpm run test`. +- Focus a frontend CLJS test namespace: `pnpm run test -- --focus frontend-tests.logic.components-and-tokens`. +- Focus one frontend CLJS test var: `pnpm run test -- --focus frontend-tests.logic.components-and-tokens/change-spacing-token-in-main-updates-copy-layout`. - Build test target only: `pnpm run build:test`. +- After `pnpm run build:test`, direct compiled runner focus is faster: `node target/tests/test.js --focus frontend-tests.logic.components-and-tokens/change-spacing-token-in-main-updates-copy-layout`. - Watch tests: `pnpm run watch:test`. ## Playwright integration tests diff --git a/docs/technical-guide/developer/common.md b/docs/technical-guide/developer/common.md index 2ad8821d78..4115b51d85 100644 --- a/docs/technical-guide/developer/common.md +++ b/docs/technical-guide/developer/common.md @@ -281,8 +281,17 @@ for how to define custom metadata and other ways of selecting tests. it, but for now we use shadow-cljs with package.json scripts: ```bash +# To run all frontend tests once pnpm run test -pnpm run test:watch + +# To run all frontend tests and keep watching for changes +pnpm run watch:test + +# To run a single frontend tests module +pnpm run test -- --focus frontend-tests.logic.components-and-tokens + +# To run a single frontend test +pnpm run test -- --focus frontend-tests.logic.components-and-tokens/change-token-in-main ``` #### Test output diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 8935cd286a..152d4466bc 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -1,6 +1,8 @@ (ns frontend-tests.runner (:require [cljs.test :as t] + [clojure.string :as str] + [clojure.tools.cli :refer [parse-opts]] [frontend-tests.basic-shapes-test] [frontend-tests.copy-as-svg-test] [frontend-tests.data.nitrate-test] @@ -38,7 +40,8 @@ [frontend-tests.util-range-tree-test] [frontend-tests.util-simple-math-test] [frontend-tests.util-webapi-test] - [frontend-tests.worker-snap-test])) + [frontend-tests.worker-snap-test] + [goog.object :as gobj])) (enable-console-print!) @@ -47,44 +50,176 @@ (.exit js/process 0) (.exit js/process 1))) +(def test-namespaces + '[frontend-tests.basic-shapes-test + frontend-tests.copy-as-svg-test + frontend-tests.data.nitrate-test + frontend-tests.data.repo-test + frontend-tests.errors-test + frontend-tests.main-errors-test + frontend-tests.data.uploads-test + frontend-tests.data.viewer-test + frontend-tests.data.workspace-colors-test + frontend-tests.data.workspace-media-test + frontend-tests.data.workspace-shortcuts-test + frontend-tests.data.workspace-texts-test + frontend-tests.data.workspace-thumbnails-test + frontend-tests.helpers-shapes-test + frontend-tests.logic.comp-remove-swap-slots-test + frontend-tests.logic.components-and-tokens + frontend-tests.logic.copying-and-duplicating-test + frontend-tests.logic.frame-guides-test + frontend-tests.logic.groups-test + frontend-tests.logic.pasting-in-containers-test + frontend-tests.plugins.context-shapes-test + frontend-tests.plugins.parser-test + frontend-tests.plugins.tokens-test + frontend-tests.plugins.utils-test + frontend-tests.svg-fills-test + frontend-tests.tokens.import-export-test + frontend-tests.tokens.logic.token-actions-test + frontend-tests.tokens.logic.token-data-test + frontend-tests.tokens.logic.token-remapping-test + frontend-tests.tokens.style-dictionary-test + frontend-tests.tokens.token-errors-test + frontend-tests.tokens.workspace-tokens-remap-test + frontend-tests.ui.ds-controls-numeric-input-test + frontend-tests.util-object-test + frontend-tests.util-range-tree-test + frontend-tests.util-simple-math-test + frontend-tests.util-webapi-test + frontend-tests.worker-snap-test]) + +(assert (every? find-ns-obj test-namespaces) + "test-namespaces contains a namespace that isn't required in runner.cljs") + +(def cli-options + [["-f" "--focus FOCUS" "Run one test namespace or one test var, e.g. frontend-tests.logic.components-and-tokens/change-token-in-main"] + ["-h" "--help"]]) + +(defn- argv + [] + (let [args (->> (.-argv js/process) + (array-seq) + (drop 2))] + ;; `pnpm run test -- --focus ...` forwards the separator to the node + ;; process, so drop one leading `--` before handing args to tools.cli. + (cond-> args + (= "--" (first args)) rest))) + +(defn- usage + [summary] + (str "Usage: pnpm run test -- [options]\n\n" + "Options:\n" + summary "\n\n" + "Focus examples:\n" + " pnpm run test -- --focus frontend-tests.logic.components-and-tokens\n" + " pnpm run test -- --focus frontend-tests.logic.components-and-tokens/change-token-in-main")) + +(defn- fail! + [message] + (js/console.error message) + (.exit js/process 1)) + +(defn- parse-focus + [focus] + (let [[ns-name test-name & extra] (str/split focus #"/")] + (cond + (or (str/blank? ns-name) (seq extra)) + (fail! (str "Invalid --focus value: " focus)) + + (some? test-name) + {:ns (symbol ns-name) :test test-name} + + :else + {:ns (symbol ns-name)}))) + +(defn- fixture-value + [ns-obj fixture-name] + (let [value (gobj/get ns-obj (munge fixture-name))] + (when-not (undefined? value) + value))) + +(defn- ns-test-vars + [ns-sym] + (when-let [ns-obj (find-ns-obj ns-sym)] + (->> (js-keys ns-obj) + (keep (fn [key] + (some-> (gobj/get ns-obj key) + (.-cljs$lang$var)))) + (filter (comp :test meta)) + (sort-by (comp :line meta))))) + +(defn- ns-fixtures + [ns-sym vars] + (when-let [ns-obj (find-ns-obj ns-sym)] + (let [ns-key (or (some-> vars first meta :ns) ns-sym) + once-fixtures (fixture-value ns-obj "cljs-test-once-fixtures") + each-fixtures (fixture-value ns-obj "cljs-test-each-fixtures")] + {:once (when once-fixtures {ns-key once-fixtures}) + :each (when each-fixtures {ns-key each-fixtures})}))) + +(defn- selected-tests + [{:keys [ns test]}] + (when-not (some #{ns} test-namespaces) + (fail! (str "Unknown test namespace: " ns))) + (let [vars (vec (ns-test-vars ns))] + (when (empty? vars) + (fail! (str "No tests found in namespace: " ns))) + (if test + (let [test-sym (symbol test) + test-var (some #(when (= test-sym (:name (meta %))) %) vars)] + (if test-var + {:vars [test-var] + :fixtures (ns-fixtures ns [test-var])} + (fail! (str "Unknown test var: " ns "/" test)))) + {:vars vars + :fixtures (ns-fixtures ns vars)}))) + +(defn- merge-fixtures + [fixtures] + {:once (apply merge (keep :once fixtures)) + :each (apply merge (keep :each fixtures))}) + +(defn- run-test-vars! + [tests] + (let [vars (vec (mapcat :vars tests)) + fixtures (merge-fixtures (map :fixtures tests)) + env (assoc (t/empty-env) + :once-fixtures (:once fixtures) + :each-fixtures (:each fixtures)) + summary (volatile! {:test 0 :pass 0 :fail 0 :error 0 :type :summary})] + (t/run-block + (concat [(fn [] (t/set-env! env))] + (t/test-vars-block vars) + [(fn [] + (vswap! summary + (partial merge-with +) + (:report-counters (t/get-and-clear-env!)))) + (fn [] + (t/set-env! env) + (t/do-report @summary) + (t/report (assoc @summary :type :end-run-tests)) + (t/clear-env!))])))) + +(defn- run-focused-test! + [focus] + (run-test-vars! [(selected-tests (parse-focus focus))])) + (defn init [] - (t/run-tests - 'frontend-tests.basic-shapes-test - 'frontend-tests.copy-as-svg-test - 'frontend-tests.data.nitrate-test - 'frontend-tests.data.repo-test - 'frontend-tests.errors-test - 'frontend-tests.main-errors-test - 'frontend-tests.data.uploads-test - 'frontend-tests.data.viewer-test - 'frontend-tests.data.workspace-colors-test - 'frontend-tests.data.workspace-media-test - 'frontend-tests.data.workspace-shortcuts-test - 'frontend-tests.data.workspace-texts-test - 'frontend-tests.data.workspace-thumbnails-test - 'frontend-tests.helpers-shapes-test - 'frontend-tests.logic.comp-remove-swap-slots-test - 'frontend-tests.logic.components-and-tokens - 'frontend-tests.logic.copying-and-duplicating-test - 'frontend-tests.logic.frame-guides-test - 'frontend-tests.logic.groups-test - 'frontend-tests.logic.pasting-in-containers-test - 'frontend-tests.plugins.context-shapes-test - 'frontend-tests.plugins.parser-test - 'frontend-tests.plugins.tokens-test - 'frontend-tests.plugins.utils-test - 'frontend-tests.svg-fills-test - 'frontend-tests.tokens.import-export-test - 'frontend-tests.tokens.logic.token-actions-test - 'frontend-tests.tokens.logic.token-data-test - 'frontend-tests.tokens.logic.token-remapping-test - 'frontend-tests.tokens.style-dictionary-test - 'frontend-tests.tokens.token-errors-test - 'frontend-tests.tokens.workspace-tokens-remap-test - 'frontend-tests.ui.ds-controls-numeric-input-test - 'frontend-tests.util-object-test - 'frontend-tests.util-range-tree-test - 'frontend-tests.util-simple-math-test - 'frontend-tests.util-webapi-test - 'frontend-tests.worker-snap-test)) + (let [{:keys [options errors summary]} (parse-opts (argv) cli-options)] + (cond + (seq errors) + (fail! (str/join "\n" errors)) + + (:help options) + (do + (println (usage summary)) + (.exit js/process 0)) + + (:focus options) + (run-focused-test! (:focus options)) + + :else + (run-test-vars! (map #(selected-tests {:ns %}) test-namespaces)))))