diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index c642ac1b8..531c15aad 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -8,7 +8,8 @@ nextjournal.view/defview clojure.core/defn nextjournal.devcards/defcard clj-kondo.lint-as/def-catch-all taoensso.encore/defonce clojure.core/defonce - taoensso.encore/defalias clojure.core/def} + taoensso.encore/defalias clojure.core/def + promesa.core/let clojure.core/let} :skip-comments true :linters {:consistent-alias {:aliases {datomic.api datomic nextjournal.log log diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 16a57b142..bda2e46f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,8 +3,51 @@ name: Continuous Delivery on: push jobs: + + build-and-upload-viewer-resources: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch_depth: 0 + + - name: ๐Ÿ”ง Install java + uses: actions/setup-java@v1 + with: + java-version: '11.0.7' + + - name: ๐Ÿ”ง Install clojure + uses: DeLaGuardo/setup-clojure@master + with: + cli: '1.10.3.943' + + - name: Setup Babashka + uses: turtlequeue/setup-babashka@v1.3.0 + with: + babashka-version: 0.7.6 + + - name: ๐Ÿ— maven cache + uses: actions/cache@v2 + with: + path: | + ~/.m2 + ~/.gitlibs + key: ${{ runner.os }}-maven-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: ๐Ÿ” Google Auth + uses: google-github-actions/auth@v0 + with: + credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY }} + + - name: ๐Ÿ”ง Setup Google Cloud SDK + uses: google-github-actions/setup-gcloud@v0.3.0 + + - name: Build and upload viewer resources + run: bb build+upload-viewer-resources + test: - needs: [build-and-upload-viewer-resources] runs-on: ${{matrix.sys.os}} strategy: @@ -33,18 +76,24 @@ jobs: with: cli: '1.10.3.943' + - name: Setup Babashka + uses: turtlequeue/setup-babashka@v1.3.0 + with: + babashka-version: 0.7.6 + - name: ๐Ÿ— maven cache uses: actions/cache@v2 with: path: | ~/.m2 ~/.gitlibs + ~/.deps.clj key: ${{ runner.os }}-maven-${{ github.sha }} restore-keys: | ${{ runner.os }}-maven- - name: ๐Ÿงช Run tests - run: clojure -X:test + run: bb clojure -X:test static-build: runs-on: ubuntu-latest @@ -109,45 +158,19 @@ jobs: sha: ${{github.event.pull_request.head.sha || github.sha}} target_url: https://snapshots.nextjournal.com/clerk/build/${{ github.sha }} - build-and-upload-viewer-resources: + ui-tests: + needs: [static-build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch_depth: 0 - - name: ๐Ÿ”ง Install java - uses: actions/setup-java@v1 - with: - java-version: '11.0.7' - - - name: ๐Ÿ”ง Install clojure - uses: DeLaGuardo/setup-clojure@master - with: - cli: '1.10.3.943' - - name: Setup Babashka uses: turtlequeue/setup-babashka@v1.3.0 with: babashka-version: 0.7.6 - - name: ๐Ÿ— maven cache - uses: actions/cache@v2 - with: - path: | - ~/.m2 - ~/.gitlibs - key: ${{ runner.os }}-maven-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-maven- - - - name: ๐Ÿ” Google Auth - uses: google-github-actions/auth@v0 - with: - credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY }} - - - name: ๐Ÿ”ง Setup Google Cloud SDK - uses: google-github-actions/setup-gcloud@v0.3.0 - - - name: Build and upload viewer resources - run: bb build+upload-viewer-resources + - name: Run Playwright tests against static assets + run: | + bb test:static-app ${{ github.sha }} diff --git a/.gitignore b/.gitignore index a5db64f57..47887431b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ .clerk/ .cpcache/ .lsp/ -node_modules/ +node_modules /public/js/ /.shadow-cljs/ /target/ @@ -16,3 +16,4 @@ yarn.lock .work /build /public/build +.nrepl-port diff --git a/bb.edn b/bb.edn index 32f85178f..7ec6e12ab 100644 --- a/bb.edn +++ b/bb.edn @@ -8,10 +8,13 @@ [babashka.deps :as deps] [babashka.fs :as fs] [babashka.process :as p]) - :init (defn viewer-css-path [] - (let [cp (str/trim (with-out-str (deps/clojure ["-A:sci" "-Spath"])))] - (str/trim (:out (shell {:out :string} (str "bb -cp " cp " -e '(println (.getPath (clojure.java.io/resource \"css/viewer.css\")))'")))))) + :init (do + (defn viewer-css-path [] + (let [cp (str/trim (with-out-str (deps/clojure ["-A:sci" "-Spath"])))] + (str/trim (:out (shell {:out :string} (str "bb -cp " cp " -e '(println (.getPath (clojure.java.io/resource \"css/viewer.css\")))'")))))) + (defn latest-sha [] + (str/trim (:out (p/sh "git rev-parse HEAD"))))) print-viewer-css {:task (println (viewer-css-path))} copy-viewer-css {:task (fs/copy (viewer-css-path) "resources/stylesheets/viewer.css" #{:replace-existing})} @@ -41,6 +44,13 @@ :depends [release:js] :task (clojure "-X:demo nextjournal.clerk/build-static-app!")} + test:static-app {:doc "Run UI tests for current SHA. Provide SHA as command line arg." + :task (do + (shell {:dir "ui_tests"} "yarn install") + (apply shell {:dir "ui_tests"} + "yarn nbb -m playwright-tests" + (or *command-line-args* [(latest-sha)])))} + release:jar {:doc "Builds the jar" :task (let [rev-count (-> (p/process ["git" "rev-list" "HEAD" "--count"] {:out :string}) p/check :out str/trim Integer/parseInt) version (format "0.6.%d" (inc rev-count))] diff --git a/bb/viewer_resources_hashing.clj b/bb/viewer_resources_hashing.clj index a88a81662..3516cddbe 100644 --- a/bb/viewer_resources_hashing.clj +++ b/bb/viewer_resources_hashing.clj @@ -11,8 +11,8 @@ ;; Example link in bucket: ;; "https://storage.googleapis.com/nextjournal-cas-eu/data/8VwKauX6JACEP3K6ahNmP5p1w7rWdhKzeGXCDrHMnJiVrUxHVxcm3Xj84K2r3fcAKWxMQKzqoFe92osgFEHCuKCtZC" -(def gs-bucket "gs://nextjournal-cas-eu/data") -(def base-url "https://storage.googleapis.com/nextjournal-cas-eu/data") +(def gs-bucket "gs://nextjournal-cas-eu") +(def base-url "https://storage.googleapis.com/nextjournal-cas-eu") (defn sha512s [] (let [files (map str (mapcat #(fs/glob % "**.{js,css}") output-dirs)) @@ -48,13 +48,11 @@ (let [front-end-hash (str (djv/file-set-hash (file-set)))] (spit viewer-js-hash-file front-end-hash))) -(def gs-url-prefix "https://storage.googleapis.com/nextjournal-cas-eu/data") - (defn lookup-url [lookup-hash] (str gs-bucket "/lookup/" lookup-hash)) (defn cas-link [hash] - (str gs-url-prefix "/" hash)) + (str base-url "/data/" hash)) (defn build+upload-viewer-resources [] (let [front-end-hash (str/trim (slurp viewer-js-hash-file)) @@ -63,9 +61,9 @@ (when (= res ::djv/not-found) (tasks/run 'build:js) (let [content-hash (djv/sha512 (slurp "build/viewer.js")) - viewer-js-http-link (str (cas-link content-hash) "?cache=false")] + viewer-js-http-link (str (cas-link content-hash))] (spit manifest {"/js/viewer.js" viewer-js-http-link}) (println "Manifest:" (slurp manifest)) (println "Coping manifest to" (lookup-url front-end-hash)) (djv/gs-copy manifest (lookup-url front-end-hash)) - (djv/gs-copy "build/viewer.js" (str gs-bucket "/" content-hash)))))) + (djv/gs-copy "build/viewer.js" (str gs-bucket "/data/" content-hash)))))) diff --git a/deps.edn b/deps.edn index 4bebc2ce1..4b6b0e6d1 100644 --- a/deps.edn +++ b/deps.edn @@ -39,7 +39,8 @@ :test {:extra-deps {nubank/matcher-combinators {:mvn/version "3.3.1"} io.github.cognitect-labs/test-runner {:git/tag "v0.5.0" :git/sha "b3fd0d2"}} :extra-paths ["test"] - :exec-fn cognitect.test-runner.api/test} + :exec-fn cognitect.test-runner.api/test + :jvm-opts ["-Dclerk.resource_manifest={\"/js/viewer.js\" \"http://localhost:7778/js/viewer.js\"}"]} :demo {:extra-deps {com.github.seancorfield/next.jdbc {:mvn/version "1.2.659"} org.xerial/sqlite-jdbc {:mvn/version "3.34.0"} diff --git a/resources/viewer-js-hash b/resources/viewer-js-hash index 10039aae7..0cf483d29 100644 --- a/resources/viewer-js-hash +++ b/resources/viewer-js-hash @@ -1 +1 @@ -yh4majqZ75XpcUpjEFqRovMNjpk \ No newline at end of file +2qvjUtnvEguryFhLN9ZpY4VRcKRo \ No newline at end of file diff --git a/src/nextjournal/clerk/config.clj b/src/nextjournal/clerk/config.clj index fe6744eb9..d74831311 100644 --- a/src/nextjournal/clerk/config.clj +++ b/src/nextjournal/clerk/config.clj @@ -11,7 +11,7 @@ (when-let [prop (System/getProperty "clerk.disable_cache")] (not= "false" prop))) -(def gs-url-prefix "https://storage.googleapis.com/nextjournal-cas-eu/data") +(def gs-url-prefix "https://storage.googleapis.com/nextjournal-cas-eu") (def lookup-hash (str/trim (slurp (io/resource "viewer-js-hash")))) (def lookup-url (str gs-url-prefix "/lookup/" lookup-hash)) @@ -21,9 +21,9 @@ (read-string prop)))) (defonce !resource->url + ;; contains asset manifest in the form: + ;; {"/js/viewer.js" "https://..."} (atom (or resource-manifest-from-props - ;; assume that CI will have published a CAS-link under this lookup, - ;; prior to hitting this code-path (edn/read-string (slurp lookup-url))))) #_(swap! !resource->url assoc "/css/viewer.css" "https://storage.googleapis.com/nextjournal-cas-eu/data/8VvAV62HzsvhcsXEkHP33uj4cV9UvdDz7DU9qLeVRCfEP9kWLFAzaMKL77trdx898DzcVyDVejdfxvxj5XB84UpWvQ") diff --git a/src/nextjournal/clerk/sci_viewer.cljs b/src/nextjournal/clerk/sci_viewer.cljs index 34e708ade..61c041556 100644 --- a/src/nextjournal/clerk/sci_viewer.cljs +++ b/src/nextjournal/clerk/sci_viewer.cljs @@ -10,8 +10,8 @@ [nextjournal.markdown.transform :as md.transform] [nextjournal.sci-configs.js-interop :as sci-configs.js-interop] [nextjournal.sci-configs.reagent :as sci-configs.reagent] - [nextjournal.ui.components.icon :as icon] [nextjournal.ui.components.d3-require :as d3-require] + [nextjournal.ui.components.icon :as icon] [nextjournal.view.context :as view-context] [nextjournal.viewer.code :as code] [nextjournal.viewer.katex :as katex] @@ -33,7 +33,6 @@ :label-color (if selected? "white-90" "black-60") :badge-background-color (if selected? "bg-white-20" "bg-black-10")}) - (declare inspect) (defn value-of @@ -125,7 +124,7 @@ (map (partial str/join "=")) (str/join "&"))) -#_(opts->query {:s 10 :num 42}) +#_(opts->query {:s 12 :num 42}) (defn unreadable-edn [edn] (html [:span.inspected-value.whitespace-nowrap.cmt-default edn])) @@ -1023,7 +1022,6 @@ black")}]))} sci-configs.js-interop/namespaces sci-configs.reagent/namespaces)}))) - (defn eval-form [f] (sci/eval-form @!sci-ctx f)) diff --git a/ui_tests/package.json b/ui_tests/package.json new file mode 100644 index 000000000..0416287d3 --- /dev/null +++ b/ui_tests/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "nbb": "^0.1.9", + "playwright": "^1.19.1" + } +} diff --git a/ui_tests/playwright_tests.cljs b/ui_tests/playwright_tests.cljs new file mode 100644 index 000000000..393819583 --- /dev/null +++ b/ui_tests/playwright_tests.cljs @@ -0,0 +1,132 @@ +(ns playwright-tests + (:require [clojure.string :as str] + [clojure.test :as t :refer [deftest is async use-fixtures]] + [promesa.core :as p])) + +(def sha (first *command-line-args*)) +(def index (str/replace "https://snapshots.nextjournal.com/clerk/build/{{sha}}/index.html" + "{{sha}}" sha)) + +(def chromium + (.-chromium (js/require "playwright"))) + +(def browser (atom nil)) + +(def default-launch-options + (clj->js {:args ["--no-sandbox"] + #_#_:headless false + #_#_:devtools true})) + +(def headless (boolean (.-CI js/process.env))) + +(defn launch-browser [] + (p/let [b (.launch chromium #js {:headless headless}) + _ (reset! browser b)])) + +(def close-browser true) + +(use-fixtures :once + {:before + (fn [] + (async done + (-> + (launch-browser) + (.catch js/console.log) + (.finally done)))) + :after + (fn [] + (async done + (if close-browser + (p/let [_ (.close @browser)] + (done)) + (done))))}) + +(defn goto [page url] + (.goto page url #js{:waitUntil "networkidle"})) + +(defn sleep [ms] + (js/Promise. + (fn [resolve] + (js/setTimeout resolve ms)))) + +;; https://snapshots.nextjournal.com/clerk/build/549f9956870c69ef0951ca82d55a8e5ec2e49ed4/index.html + +(defn test-notebook [page link] + (println "Visiting" link) + (p/let [_ (goto page link) + _t (-> (.locator page "div.viewer") + (.allInnerTexts))] + (is true))) + +(def console-errors (atom [])) + +(deftest index-page-test + (async done + (-> (p/let [page (.newPage @browser) + _ (.on page "console" + (fn [msg] + (when (and (= "error" (.type msg)) + (not (str/ends-with? + (.-url (.location msg)) "favicon.ico"))) + (swap! console-errors conj msg)))) + _ (goto page index) + elt (-> (.locator page "h1:has-text(\"Clerk\")") + (.elementHandle #js {:timeout 10000})) + _ (is elt) + links (-> (.locator page "text=/.*\\.clj$/i") + (.allInnerTexts)) + links (map (fn [link] + (str index "#/" link)) links)] + (p/run! #(test-notebook page %) links) + (is (zero? (count @console-errors)) + (str/join "\n" (map (fn [msg] + [(.text msg) (.location msg)]) + @console-errors)))) + (.catch (fn [err] + (js/console.log err) + (is false))) + (.finally + (fn [] + (p/let [#_#__ (sleep 15000)] + (done))))))) + +(defmethod t/report [:cljs.test/default :begin-test-var] [m] + (println "===" (-> m :var meta :name)) + (println)) + +(defn print-summary [] + (t/report (assoc (:report-counters (t/get-current-env)) :type :summary))) + +(defmethod t/report [:cljs.test/default :end-test-vars] [_] + (let [env (t/get-current-env) + counters (:report-counters env) + failures (:fail counters) + errors (:error counters)] + (when (or (pos? failures) + (pos? errors)) + (set! (.-exitCode js/process) 1)) + (print-summary))) + +(defn get-test-vars [] + (->> (ns-publics 'playwright-tests) + vals + (filter (comp :test meta)))) + +(defn -main [& _args] + (t/test-vars (get-test-vars))) + +(defmacro defp [name & body] + `(-> (p/let [res (do ~@body)] + (def ~name res)) + (.catch js/console.log))) + +(comment + (launch-browser) + (defp p (.newPage @browser)) + (.on p "console" (fn [msg] + (when (= "error" (.type msg)) + (swap! console-errors conj msg)))) + (goto p "https://snapshots.nextjournal.com/clerk/build/549f9956870c69ef0951ca82d55a8e5ec2e49ed4/index.html") + (defp loc (.locator p "text=/.*\\.clj$/i")) + (defp elt (.elementHandles loc #js {:timeout 1000})) + )