diff --git a/.serena/memories/common/test-setup.md b/.serena/memories/common/test-setup.md index d5aebfea52..32f87710e9 100644 --- a/.serena/memories/common/test-setup.md +++ b/.serena/memories/common/test-setup.md @@ -12,10 +12,13 @@ clojure -M:dev:test pnpm run test:jvm --focus common-tests.logic.variants-switch-test clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch pnpm run test:js +pnpm run test:quiet +pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test +pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn pnpm run watch:test ``` -Focused JS tests are selected by editing `test/common_tests/runner.cljs`, then running `pnpm run test:js`. Multiple JVM `--focus` flags compose as a union. +Use `test:quiet` for non-interactive JS runs; it buffers `build:test` output and forwards runner args. Common JS runner args support `--focus ` and `--log-level trace|debug|info|warn|error`. After `pnpm run build:test`, direct compiled runner focus is faster: `node target/tests/test.js --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn`. New common JS test namespaces must be required/listed in `common_tests/runner.cljc`; new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags compose as a union. ## Test helpers diff --git a/.serena/memories/frontend/testing.md b/.serena/memories/frontend/testing.md index c5edd61f87..30c32e2fef 100644 --- a/.serena/memories/frontend/testing.md +++ b/.serena/memories/frontend/testing.md @@ -15,6 +15,8 @@ From `frontend/`: - 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`. +New frontend test namespaces must be required/listed in `frontend_tests/runner.cljs`; new vars in existing namespaces need no runner change. + ## Playwright integration tests Do not add, modify, or run Playwright integration tests under `frontend/playwright` unless explicitly asked. When explicitly asked, use `pnpm run test:e2e` or `pnpm run test:e2e --grep "pattern"` from `frontend/`; ensure dependencies are installed through `./scripts/setup` if the environment is not prepared. diff --git a/common/package.json b/common/package.json index 300c768dcb..1335f71d61 100644 --- a/common/package.json +++ b/common/package.json @@ -30,6 +30,7 @@ "watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests/ --exec 'node target/tests/test.js'\"", "build:test": "clojure -M:dev:shadow-cljs compile test", "test:js": "pnpm run build:test && node target/tests/test.js", + "test:quiet": "node ./scripts/test-quiet.js", "test:jvm": "clojure -M:dev:test" } } diff --git a/common/scripts/test-quiet.js b/common/scripts/test-quiet.js new file mode 100644 index 0000000000..6b614c74c4 --- /dev/null +++ b/common/scripts/test-quiet.js @@ -0,0 +1,25 @@ +import { spawnSync } from "node:child_process"; + +const progress = (msg) => process.stderr.write(`${msg}\n`); + +progress("Building test bundle..."); +const build = spawnSync("pnpm", ["run", "build:test"], { + stdio: ["ignore", "pipe", "pipe"], + maxBuffer: 64 * 1024 * 1024, +}); + +if (build.status !== 0) { + progress("Building test bundle failed"); + if (build.stdout?.length) process.stdout.write(build.stdout); + if (build.stderr?.length) process.stderr.write(build.stderr); + process.exit(build.status ?? 1); +} + +progress("Running tests..."); +const result = spawnSync( + "node", + ["target/tests/test.js", ...process.argv.slice(2)], + { stdio: "inherit" }, +); + +process.exit(result.status ?? 1); diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 902f1d1aef..9245d062f7 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -7,6 +7,10 @@ (ns common-tests.runner (:require #?(:clj [common-tests.fressian-test]) + #?(:cljs [app.common.logging :as l]) + #?(:cljs [clojure.string :as str]) + #?(:cljs [clojure.tools.cli :refer [parse-opts]]) + #?(:cljs [goog.object :as gobj]) [clojure.test :as t] [common-tests.attrs-test] [common-tests.buffer-test] @@ -78,22 +82,12 @@ [common-tests.undo-stack-test] [common-tests.uuid-test])) -#?(:cljs (enable-console-print!)) - -#?(:cljs - (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] - (if (cljs.test/successful? m) - (.exit js/process 0) - (.exit js/process 1)))) - -(defn -main - [& args] - (t/run-tests +(def test-namespaces + [#?(:clj 'common-tests.fressian-test) 'common-tests.attrs-test 'common-tests.buffer-test 'common-tests.colors-test 'common-tests.data-test - #?(:clj 'common-tests.fressian-test) 'common-tests.files-changes-test 'common-tests.files-builder-test 'common-tests.files-migrations-test @@ -157,4 +151,169 @@ 'common-tests.types.token-test 'common-tests.types.tokens-lib-test 'common-tests.undo-stack-test - 'common-tests.uuid-test)) + 'common-tests.uuid-test]) + +#?(:cljs + (assert (every? find-ns-obj test-namespaces) + "test-namespaces contains a namespace that isn't required in runner.cljc")) + +#?(:cljs (enable-console-print!)) + +#?(:cljs + (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] + (if (cljs.test/successful? m) + (.exit js/process 0) + (.exit js/process 1)))) + +#?(:cljs + (do + ;; This runner intentionally mirrors frontend-tests.runner. Both runners need + ;; forwarded CLI args, focused namespace/var execution, fixture preservation, + ;; and app log-level setup. A shared helper could own those mechanics, but we + ;; keep the logic local while there are only two test targets because sharing + ;; it would add cross-module test classpath coupling. + (def ^:private log-levels + #{:trace :debug :info :warn :error}) + + (def cli-options + [["-f" "--focus FOCUS" "Run one test namespace or one test var, e.g. common-tests.logic.comp-sync-test/test-sync-when-changing-attribute"] + ["-l" "--log-level LEVEL" "Set app logger level: trace|debug|info|warn|error" + :parse-fn keyword + :validate [log-levels "must be one of trace, debug, info, warn, error"]] + ["-h" "--help"]]) + + (defn- argv + [] + (let [args (->> (.-argv js/process) + (array-seq) + (drop 2))] + (cond-> args + (= "--" (first args)) rest))) + + (defn- usage + [summary] + (str "Usage: pnpm run test:js -- [options]\n\n" + "Options:\n" + summary "\n\n" + "Focus examples:\n" + " pnpm run test:js -- --focus common-tests.logic.comp-sync-test\n" + " pnpm run test:js -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute\n\n" + "Log level example (quiets app logging during the run):\n" + " pnpm run test:js -- --focus common-tests.logic.comp-sync-test --log-level warn")) + + (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- run-all-tests! + [] + (run-test-vars! (map #(selected-tests {:ns %}) test-namespaces))))) + +(defn -main + [& _args] + #?(:cljs + (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)) + + :else + (do + (when-let [level (:log-level options)] + (l/setup! {:app level})) + (if (:focus options) + (run-focused-test! (:focus options)) + (run-all-tests!))))) + :clj + (apply t/run-tests test-namespaces))) diff --git a/docs/technical-guide/developer/common.md b/docs/technical-guide/developer/common.md index 6393bdacd6..e9bf7bda43 100644 --- a/docs/technical-guide/developer/common.md +++ b/docs/technical-guide/developer/common.md @@ -277,6 +277,31 @@ You can also mark tests in the code by adding metadata: Please refer to the [kaocha manual](https://cljdoc.org/d/lambdaisland/kaocha/1.91.1392/doc/6-focusing-and-skipping) for how to define custom metadata and other ways of selecting tests. +Common tests can also be run in the JavaScript runtime with shadow-cljs. From +common: + +```bash +# To run all common JavaScript tests once +pnpm run test:js + +# Quiet run for non-interactive output +pnpm run test:quiet + +# To run a single common JavaScript tests module +pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test + +# To run a single common JavaScript test and quiet app-level logging +pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn +``` + +The common JavaScript runner accepts the same forwarded runner arguments through +pnpm run test:js -- ..., +pnpm run test:quiet -- ..., or directly through +node target/tests/test.js ... after +pnpm run build:test. Supported runner arguments +are --focus namespace-or-var and +--log-level trace|debug|info|warn|error. + **NOTE**: in frontend we still can't use kaocha to run the tests. We are on it, but for now we use shadow-cljs with package.json scripts: @@ -284,17 +309,20 @@ it, but for now we use shadow-cljs with package.json # To run all frontend tests once pnpm run test +# Quiet run for non-interactive output +pnpm run test:quiet + # 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 +pnpm run test:quiet -- --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 +pnpm run test:quiet -- --focus frontend-tests.logic.components-and-tokens/change-token-in-main # To quiet app-level logging during the run (trace|debug|info|warn|error) -pnpm run test -- --focus frontend-tests.logic.components-and-tokens --log-level warn +pnpm run test:quiet -- --focus frontend-tests.logic.components-and-tokens --log-level warn ``` For non-interactive runs (CI, scripted invocations, agent loops), use the quiet @@ -304,11 +332,6 @@ test-runner output streams through normally. Short progress hints (`Building wasm...`, `Running tests...`) go to `stderr`, so capturing `stdout` gives you just the test results. -```bash -# Quiet run (same arguments as `pnpm run test`) -pnpm run test:quiet -- --focus frontend-tests.logic.components-and-tokens -``` - #### Test output The default kaocha reporter outputs a summary for the test run. There is a pair diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 3112ed0a73..d7048409a8 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -94,6 +94,11 @@ (assert (every? find-ns-obj test-namespaces) "test-namespaces contains a namespace that isn't required in runner.cljs") +;; This runner intentionally mirrors common-tests.runner. Both runners need +;; forwarded CLI args, focused namespace/var execution, fixture preservation, +;; and app log-level setup. A shared helper could own those mechanics, but we +;; keep the logic local while there are only two test targets because sharing +;; it would add cross-module test classpath coupling. (def ^:private log-levels #{:trace :debug :info :warn :error})