Improve common JS test runner

Add focused common JavaScript test execution with log-level control and a quiet test wrapper matching the frontend workflow.

Update developer docs and testing memories to reflect the common/frontend test split, and document why runner helper extraction is deferred.

Signed-off-by: Codex <codex@openai.com>
This commit is contained in:
Michael Panchenko 2026-05-20 11:16:10 +00:00 committed by Alonso Torres
parent e252bcf901
commit cec90416c2
7 changed files with 240 additions and 22 deletions

View File

@ -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 <namespace-or-var>` 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

View File

@ -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.

View File

@ -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"
}
}

View File

@ -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);

View File

@ -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)))

View File

@ -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
<code class="language-text">common</code>:
```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
<code class="language-text">pnpm run test:js -- ...</code>,
<code class="language-text">pnpm run test:quiet -- ...</code>, or directly through
<code class="language-text">node target/tests/test.js ...</code> after
<code class="language-text">pnpm run build:test</code>. Supported runner arguments
are <code class="language-text">--focus namespace-or-var</code> and
<code class="language-text">--log-level trace|debug|info|warn|error</code>.
**NOTE**: in <code class="language-text">frontend</code> we still can't use kaocha to run the tests. We are on
it, but for now we use shadow-cljs with <code class="language-text">package.json</code> scripts:
@ -284,17 +309,20 @@ it, but for now we use shadow-cljs with <code class="language-text">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

View File

@ -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})