Allow running a single frontend test via --focus

Previously `pnpm run test` always ran the full frontend-tests suite,
which made tight iteration on a single namespace or var painful. The
runner now accepts `--focus <ns>` or `--focus <ns>/<var>` and executes
only the matching tests, preserving each namespace's `:once` and `:each`
fixtures so behavior matches a full-suite run.

  pnpm run test -- --focus frontend-tests.logic.groups-test
  pnpm run test -- --focus frontend-tests.logic.groups-test/some-test

Also updates the developer guide and the testing memory so the flag is
discoverable from both docs and agent context.
This commit is contained in:
Michael Panchenko 2026-05-19 18:41:14 +02:00 committed by Alonso Torres
parent 63e7df5fda
commit 17041b53a7
3 changed files with 188 additions and 42 deletions

View File

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

View File

@ -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 <code class="language-text">package.json</code> 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

View File

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